Accepted answer

Setting the end ticks and specifying the precise space of the ticks in a linear scale is a pain in the neck. The reason is that D3 axis generator was created in such a way that the ticks are automatically generated and spaced. So, what is handy for someone who doesn't care too much for customisation can be a nuisance for those that want a precise customisation.

My solution here is a hack: create two scales, one linear scale that you'll use to plot your data, and a second scale, that you'll use only to make the axis and whose values you can set at your will. Here, I choose a scalePoint() for the ordinal scale.

Something like this:

var realScale = d3.scaleLinear()
    .domain([-2*Math.PI, 2*Math.PI]);

var axisScale = d3.scalePoint()
    .domain(["-2 \u03c0", "-1.5 \u03c0", "-\u03c0", "-0.5 \u03c0", "0",
        "0.5 \u03c0", "\u03c0", "1.5 \u03c0", "2 \u03c0"]);

Don't mind the \u03c0, that's just π (pi) in Unicode.

Check this demo, hover over the circles to see their positions:

var width = 500,
    height = 150;
var data = [-2, -1, 0, 0.5, 1.5];

var realScale = d3.scaleLinear()
    .range([10, width - 10])
    .domain([-2 * Math.PI, 2 * Math.PI]);

var axisScale = d3.scalePoint()
    .range([10, width - 10])
    .domain(["-2 \u03c0", "-1.5 \u03c0", "-\u03c0", "-0.5 \u03c0", "0", "0.5 \u03c0", "\u03c0", "1.5 \u03c0", "2 \u03c0"]);

var svg ="body").append("svg")
    .attr("width", width)
    .attr("height", height);

var circles = svg.selectAll("circle").data(data)
    .attr("r", 8)
    .attr("fill", "teal")
    .attr("cy", 50)
    .attr("cx", function(d) {
        return realScale(d * Math.PI)
    .text(function(d) {
        return "this circle is at " + d + " \u03c0"

var axis = d3.axisBottom(axisScale);

var gX = svg.append("g")
    .attr("transform", "translate(0,100)")
<script src=""></script>


I was able to implement an x axis in units of PI/2, under program control (not manually laid out), by targetting the D3 tickValues and tickFormat methods. The call to tickValues sets the ticks at intervals of PI/2. The call to tickFormat generates appropriate tick labels. You can view the complete code on GitHub:


My solution is to customise tickValues and tickFormat. Only 1 scale is needed, and delegate d3.ticks function to give me the new tickValues that are proportional to Math.PI.

const piChar = String.fromCharCode(960);
const tickFormat = val => {
    const piVal = val / Math.PI;
    return piVal + piChar;

const convertSIToTrig = siDomain => {
    const trigMin = siDomain[0] / Math.PI;
    const trigMax = siDomain[1] / Math.PI;
    return d3.ticks(trigMin, trigMax, 10).map(v => v * Math.PI);

const xScale = d3.scaleLinear().domain([-Math.PI * 2, Math.PI * 2]).range([0, 600]);

const xAxis = d3.axisBottom(xScale)

This way if your xScale's domain were changed via zoom/pan, the new tickValues are nicely generated with smaller/bigger interval

Related Query

More Query from same tag