Axes

Creating axes

To create an axis, use one of d3.axisBottom, d3.axisTop, d3.axisLeft or d3.axisRight, which take a scale as their only argument, and then append the axis (usually wrapped in a g element) to the SVG.


const width = 500;
const height = 300;

const svg = d3.select("#basicAxis")
  .attr("width", "100%")
  .attr("height", "100%")
  .attr("viewBox", `-10 -10 ${width + 20} ${height + 20}`)
  .attr("style", "width: 100%; height: auto;");
svg.append("rect")
  .attr("width", width)
  .attr("height", height)
  .attr("fill", "#666");			  

// Create a scale for the x-axis:
const scaleX = d3.scaleLinear()
  .domain([-50, 50])
  .range([0, width]);

// Add an x-axis:
svg.append("g")
  .attr("transform", `translate(0, ${height/2})`)
  .call(d3.axisBottom(scaleX));

// Create a scale for the y-axis
// (SVG y-coordinates go from top to bottom):
const scaleY = d3.scaleLinear()
  .domain([20, -20])
  .range([0, height]);

// Add an y-axis:
svg.append("g")
  .attr("transform", `translate(${width/2}, 0)`)
  .call(d3.axisLeft(scaleY));

Result:

The axis types differ in how the ticks are oriented:

You can pass in most scale types, including linear, log, band, and time scales:


  …

const bandScale = d3.scaleBand()
  .domain(["A", "B", "C", "D", "E"])
  .range([0, width])
  .padding(0.1);
const bandAxis = svg.append("g")
  .attr("transform", "translate(0, 0)")
  .call(d3.axisBottom(bandScale).tickSizeOuter(0));	

// ***

const timeScale = d3.scaleUtc()
  .domain([new Date(2023, 0, 1), new Date(2023, 6, 12)])
  .range([ 0, width ])
  .nice();
const timeAxis = svg.append("g")
  .attr("transform", `translate(0, ${height - 40})`)
  .call(d3.axisTop(timeScale).ticks(5, "%b %y"));
 
// ***
 
const logScale = d3.scaleLog()
  .domain([1, 1e4])
  .range([ 0, width ])
  .nice();
const logAxis = svg.append("g")
  .attr("transform", `translate(0, ${height})`)
  .call(d3.axisBottom(logScale).ticks(10, formatPower));	
	  
function formatPower(x) { // see *)
  const e = Math.log10(x);
  if (e !== Math.floor(e)) return; // Ignore non-exact power of ten.
  return `10${(e + "").replace(/./g, c => "⁰¹²³⁴⁵⁶⁷⁸⁹"[c] || "⁻")}`;
};

Result:

*) Function taken from Observable notebook "axis.ticks", by Mike Bostock.

An axis consists of a path element of class domain, representing the extent of the scale’s domain, and g elements of class tick, each containing one of the scale’s ticks. Next example removes the domain class of an axisTop.


  …

const scale = d3.scaleLinear()
  .domain([0, 100])
  .range([0, width]);

svg.append("g")
  .attr("transform", `translate(0, ${height})`)
  .call(d3.axisTop(scale))
  .call(g => g.select(".domain").remove()); // Removes the axis domain line.

Result:

Axis update

A data visualization may require a data update or even a completely new dataset. In this case the scales are likely to change, and consequently the axes will also need to be updated. To update an axis: call the axis component again.


  …

const scale = d3.scaleLinear()
  .domain([0, 100])
  .range([0, width])
  .nice();

const axis = svg.append("g")
  .attr("transform", `translate(0, ${height})`)
  .call(d3.axisTop(scale));

function update() {
  // change scale:
  const min = Math.random() * 100;
  const max = min + Math.random() * 100;
  scale.domain([min, max]).nice();
  // update axis:
  axis.call(d3.axisTop(scale));
};

d3.select("#updateButton")
.on("click", update);

Or with transition...


  … 

function update() {
  const min = Math.random() * 100;
  const max = min + Math.random() * 100;
  scale.domain([min, max]).nice();
  axis
    .transition()
    .duration(750)
    .call(d3.axisTop(scale));
};
  … 

Result:

Configuring ticks

The scale’s automatic tick generator generates nice, human-readable ticks. It calculates the number of ticks and uses a default format for the values. The number of ticks and their format depend on the scale’s type and domain, but not on its range. The scale’s automatic tick generator can be overruled and the default number of ticks and their format can be changed.

Number of ticks

You can use axis.ticks(number) to explicitly set the number of ticks. The specified number is just a suggested count for the number of ticks. The actual number of ticks is restricted to nicely-rounded values. Next example sets the number of ticks to 20 and 4.


  … 

const scale = d3.scaleLinear()
  .domain([0, 100])
  .range([0, width]);

const axis = d3.axisTop(scale)
  .ticks(20);

svg.append("g")
  .attr("transform", "translate(0, 18)")
  .call(axis)

axis.ticks(4);

svg.append("g")
  .attr("transform", `translate(0, ${height})`)
  .call(axis)

Result:

For time scales you can also specify a time interval. See d3-time for more information on time intervals and interval.every(step).


  …

const scale = d3.scaleUtc()
  .domain([new Date(2023, 0, 2), new Date(2023, 0, 15)])
  .range([ 0, width ])
  .nice();

const timeAxis = d3.axisBottom(scale)
  .ticks(3, "%d %b") // format "%d %b" will be explained later.		  
svg.append("g")
  .attr("transform", "translate(0, 0)")
  .call(timeAxis);		  

timeAxis.ticks(d3.utcDay.every(2), "%d %b") // a tick every 2 days. 
svg.append("g")
  .attr("transform", `translate(0, ${height - 20})`)
  .call(timeAxis);

Result:

You can only change the number of ticks on quantitative scales, not on categorical scales (ordinal, band, point scales).

Formatting tick values

The second argument of axis.ticks() takes a format string that can be used to configure the format of the value. If you want the default number of ticks, you can use null as the first argument. See d3-format and d3-time-format for information on format strings.

Alternatively you can use axis.tickFormat(format) to set the tick format explicitly (although they do not always format the exact same way). The format argument takes a function, typically via d3.format or d3.timeFormat, but you can also specify your own format function. Next example shows a few examples. In the previous example we used "%d %b" as a time format string.


  …
  
let scale = d3.scaleLinear().range([0, width]);
let axis;

// Two decimals precision:
scale.domain([1, 10]).nice();
axis = d3.axisBottom(scale)
  .ticks(null, ".2f");
svg.append("g")
  .attr("transform", "translate(0, 0)")
  .call(axis);
axis.tickFormat(d3.format(".2f"));		
svg.append("g")
  .attr("transform", "translate(0, 30)")
  .call(axis);
  
// With SI-prefix:
scale.domain([1e6, 100e6]).nice();
axis = d3.axisBottom(scale)
  .ticks(null, "s");
svg.append("g")
  .attr("transform", "translate(0, 80)")
  .call(axis);
axis.tickFormat(d3.format(".2s"));		
svg.append("g")
  .attr("transform", "translate(0, 110)")
  .call(axis);
  
// Custom format function:  
scale.domain([0, 100]).nice();
axis = d3.axisBottom(scale)
  .ticks(10)
  .tickFormat( (d) => d/10 % 2 === 0 ? d + "%" : null );
svg.append("g")
  .attr("transform", "translate(0, 160)")
  .call(axis);  

Result:

You can also overrule the automatically generated tick values by setting the tick values explicitly using axis.tickValues.


  …
  
const scale = d3.scaleLinear()
  .domain([1, 89])
  .range([0, width]);
const axis = d3.axisBottom(scale)
  .tickValues([1, 2, 3, 5, 8, 13, 21, 34, 55, 89]);
svg.append("g")
  .attr("transform", "translate(0, 0)")
  .call(axis);

Result:

Styling the ticks

The length of the ticks can be set with axis.tickSize(length) (default is 6). The padding between the end of the ticks and the value can be set width axis.tickPadding() (default is 3). Instead of axis.tickSize() for all ticks, you can separate the two outer ticks at the ends of the domain path from the rest of the inner ticks in between by using axis.tickSizeOuter() and axis.tickSizeInner().


  …

svg.append("g")
  .attr("transform", "translate(0, 0)")
  .call(d3.axisBottom(scale)
    .tickPadding(15)
    .tickSize(15)			 
  );

svg.append("g")
  .attr("transform", "translate(0, 60)")
  .call(d3.axisBottom(scale)
    .tickPadding(15)
    .tickSizeInner(5) 
    .tickSizeOuter(15) 			 
  ); 

Result:

As explained earlier, an axis consists of a path element of class domain, representing the extent of the scale’s domain, and g elements of class tick, each with a transform attribute and containing one of the scale’s ticks (an SVG line and text element). We can select (in a CSS style sheet or by JS) the domain path and certain ticks by addressing the domain or tick classes and style them.


const width = 500;
const height = 300;
const margin = {
  top: 20,
  right: 20,
  bottom: 25,
  left: 25,
}

const svg = d3.select("svg")
  .attr("width", "100%")
  .attr("height", "100%")
  .attr("viewBox", `0 0 ${width} ${height}`)
  .attr("style", "width: 100%; height: auto; border:1px solid #666;");

const scaleX = d3.scaleLinear()
  .domain([0, 100])
  .range([margin.left, width - margin.right]);

const scaleY = d3.scaleLinear()
  .domain([0, 100])
  .range([height - margin.bottom, margin.top]);

// x-axis:
svg.append("g")
  .attr("transform", `translate(0, ${height - margin.bottom})`)
  .call(d3.axisTop(scaleX)
    .tickSize(height - margin.bottom - margin.top)
  )
  .call(g => g.select(".domain")
    .remove()
  )
  .call(g => g.selectAll(".tick line")
    .attr("stroke-opacity", 0.5)
    .attr("stroke-dasharray", "2,2")
    .attr("stroke-width", "0.6px")
  )
  .call(g => g.selectAll(".tick text")
    .attr("font-size", "8px")
    .attr("y", 12) // overrides tickPadding()
  );		  

// y-axis:
svg.append("g")
  .attr("transform", `translate(${margin.left}, 0)`)
  .call(d3.axisRight(scaleY)
    .tickSize(width - margin.left - margin.right)
  )
  .call(g => g.select(".domain")
    .remove()
  )
  .call(g => g.selectAll(".tick line")
    .attr("stroke-opacity", 0.5)
    .attr("stroke-dasharray", "2,2")
    .attr("stroke-width", "0.6px")			  
  )
  .call(g => g.selectAll(".tick text")
    .attr("font-size", "8px")
    .attr("text-anchor", "end")
    .attr("x", -6) // overrides tickPadding()
  );

Result:

PS. The above example is an adapted version of the Observable notebook Styled Axes.