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:
d3.axisTop
is top-oriented, ticks are drawn above the horizontal domain line.d3.axisBottom
is bottom-oriented, ticks are drawn below the horizontal domain line.d3.axisLeft
is left-oriented, ticks are drawn to the left of the vertical domain line.d3.axisRight
is right-oriented, ticks are drawn to the right of the vertical domain line.
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.