Command Palette

Search for a command to run...

Hectal
PHASE 6Intermediate ~20 min· topic 6 of 7

Topic 6.6

Axes

In one line

An axis is the ruler of a chart: a line, tick marks and labels built from a scale. d3.axisBottom(scale) draws it in one call, and a handful of settings control the tick count, the labels, gridlines, colours and updates.

Think of it like this

The ruler on a workbench. Without its marks you cannot say how long a plank is. The chart's bars or line are the plank; the axis is the ruler that lets a reader turn a length back into a number.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

axis
A line with tick marks and labels, drawn from a scale.
tick size
The length of the small tick line.
tick padding
The gap between a tick line and its label.
gridline
A tick drawn across the whole plot so values are easy to follow.
domain line
The long line of the axis, from one end of the range to the other.

Step by step

01Plan the margins first

An axis draws at the origin of its group, so each one needs room. Here the left margin is 56 pixels for labels like 120,000 and the bottom margin 28 pixels. If a label is cut off, the margin is too small, not the axis wrong.

02Add two axes

The stage shows the monthly rides of 2025 as twelve dots: the scales place them, but nothing says what the positions mean. d3.axisBottom(x) and d3.axisLeft(y), each called on a <g> moved into place by its margin, add the rulers. The defaults are crowded: thirteen y labels (0 to 120,000 in steps of 10,000), and full month names that touch each other (September, October, November). The first label is 2025, not January: the default format prints the year at the start of a year.

03Turn the y ticks into gridlines

tickSize(-width) stretches each y tick across the plot. The code makes the lines pale and removes the y domain line, so the axis recedes and the data speaks.

04Name the axis, and let CSS own the colour

A title says what the numbers are. The colour comes from a CSS variable on the SVG, and the axis uses currentColor, so the media query for dark mode recolours axes, labels and title together. In the preview, set your computer to dark mode to see it.

Try it yourself

  1. 1

    Ten ticks instead of five

    In the first example change .ticks(5) to .ticks(10). The Console now says 9 ticks (steps of 500). tickValues([0, 1000, 4380]) gives exactly your three; use it when the numbers carry a meaning (the busiest day).

  2. 2

    Gridlines from the first example

    Change the first axis to d3.axisBottom(x).ticks(5).tickSize(-100). Every tick line, and the two end pieces of the domain line, are now 100 pixels long, pointing up into the empty space (tick line y2: -100; the labels stay put at y: 3): a negative size goes the opposite way. In a chart this draws the gridlines over the plot.

Code & diagrams

What an axis builds, and long labels New tab

d3.create("svg:g") makes a detached <g>, so the example can count an axis's ticks without drawing it. The 0.5 in the tick positions is the axis's half-pixel offset for sharp lines.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

children: path.domain g.tick g.tick g.tick g.tick g.tick
domain path d: M0.5,6V0.5H400.5V6
first two ticks: translate(0.5,0) '0' | translate(91.82420091324201,0) '1,000'
tick line y2: 6  label y: 9  label dy: 0.71em
colours: currentColor currentColor  on the group: 10 sans-serif middle
ticks(5): 5  ticks(10): 9  tickValues([0, 1000, 4380]): 3  tickValues([]): 0
domain [0, 0] -> [ '0.000000' ]
band axis ticks centred in their band: translate(50,0) translate(150,0) translate(250,0)
rotated labels: 6  wrapped lines: 12
Update an axis, and recolour it New tab

transition().end() resolves when the animation finishes, so the printed ticks are the final ones, not a mid-animation guess.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

year:    4 ticks: 2025 April July October
summer:  3 ticks: June July August
narrow:  3 ticks, last tick at translate(205.55494505494505,0)
stroke: currentColor  text fill: currentColor  group colour: rgb(29, 43, 54)
after style(color): group colour: rgb(230, 237, 243)  the attributes are still: currentColor

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Give an axis a scale with a zero-width domain

In the first example change the first scale to d3.scaleLinear().domain([0, 0]).range([0, 400]), as if every ride count were zero.

terminal
$ Console and page (screenshot checked)
── what you'll see ──
children: path.domain g.tick
first two ticks: translate(200.5,0) '0.000000'
ticks(5): 1 ticks(10): 1 tickValues([0, 1000, 4380]): 3 tickValues([]): 0

Myth vs fact

Myth

Axes are decoration you add at the end.

Fact

Without an axis a reader cannot recover a single number from a chart. Plan the margins first; labels that get cut off are the most common axis bug.

Myth

Rotated labels are fine for long names.

Fact

Rotated text is slow to read. Prefer a sideways bar chart, shorter names or wrapped labels.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The axis joins its ticks to the scale: selectAll(".tick").data(values, scale). The key is the tick's position under the current scale, so a tick whose value still exists is reused, new values enter, old ones exit, and a transition slides the ticks that move.

  • ▸

    The default offset is 0.5 pixels (0 when devicePixelRatio is above 1) so that one-pixel lines fall on a pixel centre and look sharp. Set axis.offset(0) if you align other shapes to the tick lines.

Remember this

  1. 1

    The problem this solves: drawing ticks and labels by hand means a loop, a text element and a transform for every tick, again for each chart (Phase 3 did it). An axis reuses the scale you already have: the ticks, their positions and their labels all come from it.

  2. 2

    **What axisBottom(scale) builds.** (Recall from Topic 6.1: scale.ticks() picks the round numbers and scale.tickFormat() prints them. The axis only calls both and draws the result.) You call it on a <g>: g.call(d3.axisBottom(x)). It adds one <path class="domain"> (the line with small end ticks), and for every tick a <g class="tick"> moved to the tick's position, holding a <line> and a <text>. The ticks come from scale.ticks(), the labels from scale.tickFormat(). The group gets font-size 10, font-family sans-serif and text-anchor. There are four: axisBottom, axisTop, axisLeft and axisRight.

  3. 3

    You place the axis yourself. An axis draws at the origin. A bottom axis needs translate(0, height - margin.bottom), a left axis translate(margin.left, 0). Margins must be big enough for the labels: a y axis with numbers such as 60,000 needs about 50 pixels on the left, or the labels are cut off.

  4. 4

    Ticks and labels. .ticks(5) forwards its argument to the scale (a count for number scales, an interval such as d3.utcMonth for time scales). .tickValues([0, 1000, 4380]) ignores the scale's ticks and uses yours. .tickFormat(d3.format(",")) or d3.utcFormat("%b") sets the label text. .tickSize(n) sets the line length, .tickSizeOuter(0) removes the ends of the domain line, and .tickPadding(n) moves the labels away. A band axis puts each tick in the middle of its band.

  5. 5

    Gridlines are tall ticks. .tickSize(-width) makes every tick line run back across the plot area. Then lighten them (stroke-opacity) and remove the domain line with g.select(".domain").remove(). A gridline is a ruler's mark, so keep it quiet.

  6. 6

    Long labels. 25 station names will not fit under bars. Turn the chart sideways (names on the left axis), rotate them (transform="rotate(-40)" and text-anchor: end), or wrap them into <tspan> lines. Rotation is the hardest to read; wrapping suits two or three words.

  7. 7

    Theme and updates. The axis paints with currentColor (the CSS color of the group or a parent), so one CSS variable recolours a whole axis for dark mode, with no change to the code. To update an axis, change the scale's domain and call the axis again: D3 matches ticks by position, keeps those that still exist, adds new ones and removes old ones. Calling g.transition().call(axis) animates it.

Explain it without notes

01

What does d3.axisBottom(x) add to the page?

02

How do you draw gridlines with an axis?

03

How do you recolour an axis for dark mode?

Practice

01

Make a left axis for rides (0 to 4,380) with 5 ticks, labels like 1,000, gridlines 460 pixels long, and no domain line.

02

Write setAxis(g, axis, domain) that updates a bottom axis to a new domain with a 300 ms transition, and returns a promise that resolves when it is done.

Trade-offs

  • ↔

    Gridlines help reading exact values but add ink. Use them on charts where values are read off, and skip them on shape-focused charts.

Done when you can

  • I can add a bottom and a left axis, placed with a margin.

  • I can set the tick count, values and format.

  • I can draw pale gridlines and remove the domain line.

  • I can wrap or rotate long labels.