Quantities in units that have no scale in common get an axis each. Pass yAxis
an array, give each entry beyond the first an id, and point a mark at one with
yAxis: '<id>' — a mark that names none is measured against the first.
Guides are drawn down the left, and axes sharing a side stack outward in the
order they were declared. position: 'right' moves one opposite, which on a
chart with two is usually what you want: it puts each guide beside the marks it
belongs to instead of leaving the reader to work out which of two left-hand
guides a line is measured against. An axis keeps the side it named however few it
ends up sharing the plot with, so a legend hiding the last series off another axis
leaves this one where it was — wherever the vertical scale is Pluma's to resolve.
A chart that brought its own scale, or whose vertical scale isn't linear, gets
one guide down the left whatever position says.
Here Apple's monthly traded volume and its closing price: shares and dollars, one chart.
Give each mark a seriesName, as above. Without one they take the same palette
slot, and nothing on the plot would say which line belongs to which side.
Every vertical axis is drawn at the same tick heights, over one grid — several grids on one plot leaves the reader working out which lines belong to which axis. So a few things are decided for the set rather than per axis:
tickCount on any axis
pins it for all of them.scale belongs to the first axis, and the rest cover the
vertical range it lays out. Use domain on any of them for a range that has to
be exact — or with one end 'auto' to hold an axis at zero and still let the
set choose where it stops.Everything else is per axis: position, label, format, notation,
isHidden. An axis nothing is drawn against isn't drawn at all.
Adding an axis is adding an entry and a mark that names it. Below, Apple's monthly return joins the volume and the price — shares, dollars and percent, each read against its own guide.
A bar group can span the set: groupWith divides one category's band between
several bar marks, so a column of counts can stand beside one of rates with each
measured against its own guide. See Grouping across value
axes.
Each extra axis costs the plot some width, and the reader some work: nothing about a line says which guide it belongs to. Two is usually the most a chart earns. Past that, or where the quantities are separate subjects rather than one qualifying another, stacked charts sharing an x axis read better.
An area fills down to zero, and normally says so — it hands its baseline to the axis, which then has to show zero. That is right when the fill is the quantity: a filled area whose bottom the reader can't see isn't one they can measure.
It is wrong when the fill is a wash under a line. There the line carries the
reading and the fill is only density, so withDomain: false takes the area out of
the range entirely — not just its baseline. Every value the fill covers is one the
line over the same rows has already declared, zero included, so the range is
unchanged by the area's silence. The axis becomes whatever the other marks imply,
the same one the chart would have without the area on it, while the fill still
runs down to zero, off the bottom of the plot, where its gradient finishes:
Pair it with withGradient too. An area clips itself to the plot, so the overhang
stays out of sight either way, but a flat fill running off the bottom edge is
rarely what's wanted.
domain is the blunter instrument for the same end: it sets the range an axis
covers instead of taking the one the data implies, whatever the marks say. Reach
for it when you want an exact window — a fixed scale across several charts, or
headroom the data doesn't ask for — rather than to work around an area. On a time
axis the same option takes dates; Holding a time axis to a
window covers that.
Either end can be 'auto', which holds the other and leaves that one to the data.
[0, 'auto'] is the one to reach for when a line should start at zero: without it
the axis starts wherever the values do, and pinning both ends means inventing a
maximum the data may outgrow.
A held end is where the axis stops, not just a value it has to reach — values past it fall outside the plot — and it is rounded outward to a height the axis' own step lands on, so the ticks stay round whatever number you write. Zero is the exception nobody has to think about: every step divides it, so a floor of zero is always exactly the foot of the axis.
On a chart whose axes share a zero — which they do as soon as an axis past the first carries bars or an area — the ends are whatever puts zero at one height, so a held end is only guaranteed to be covered. A floor of zero still lands exactly, but a floor anywhere else is passed rather than landed on. Name both ends when an axis has to stop somewhere specific on a chart like that.
An axis counting things can end up written in fractions. Nothing about a count
says its values are whole, so a series reaching 1 on a plot with room for seven
ticks is labelled 0, 0.2, 0.4 and up — and a reader who sees 0.5 messages
takes it as a fact about the data rather than about the axis.
tickStep: 'integer' holds the ticks to whole numbers. It constrains the step
rather than fixing it, so an axis that already steps in whole numbers is
unchanged, and one reaching into the thousands still steps by 200 rather than
by 1:
A number is a step exactly — the ticks land on its multiples, and how many there are follows from the range:
tickCount still asks for roughly how many ticks to aim for. Where the two
disagree the step wins, since a count is an approximation and a step is not.
On a time axis a step is a duration in milliseconds, counted from the start of the axis' range. A time axis otherwise ticks on round calendar units, and its weeks start on Sunday — so a series bucketed on Mondays is labelled between its buckets rather than on them. A week-long step lands the ticks on the buckets:
Uneven units can't be written as a duration. A series sitting one row per
calendar month is already ticked a month at a time without a step, and anything
more particular wants a d3-time interval through scale. 'integer' is a
numeric-axis idea and a time axis ignores it.
Numbers get thousands separators, notation: 'compact' shortens them to 50k,
and a date is written short — Mar 1. Rows sitting one per calendar month are
labelled by month instead, with the year abbreviated onto every tick: Jan '26.
That is also the one case where a time axis is given a fixed format rather than
labelling its own ticks, and it comes with the ticks pinned to the months
themselves. A time scale picks its interval from how wide its domain is and how
many ticks it is asked for, never from how far apart the rows sit — over three
monthly points on a wide plot it ticks weekly, and a month label on a weekly tick
would write Jan '26 four times. tickCount still asks for fewer labels; it
thins those months rather than choosing a finer interval.
All of it is US English wherever the chart is opened, rather than the reader's own
locale. A chart writes the same quantity in several places at once — a tick, the
tooltip pointing at it, a label on the line — and those only agree if none of them
moves; a 1.600 tick above a 1,519 tooltip is worse than either convention on
its own. format is the way out when you want something else, and it is called
exactly as you wrote it, including whatever locale you format in.
More categories than the plot has room for means only some of them are labelled —
evenly spaced, starting at the first. tickCount asks for fewer than that, and
tickLabelRotate turns the labels so more of them fit, which is what to reach for
on long category names.
A Date field gets a time axis: real elapsed-time spacing, and ticks that step by
hour, day, week or month depending on the range. tickCount nudges that
granularity.
Five years of Apple's daily closing price — 1,258 points on one line, with the axis choosing its own tick interval as the range changes:
Several lines share one time axis the same way — one series field, one mark:
A time axis covers the range its rows reach. That is wrong whenever the reader picked the range themselves: a report over the last quarter whose series only starts halfway through spans half a quarter, and the chart quietly answers a different question than the one the date picker asked.
domain takes Dates on a time axis, and holds it to the window instead. The
stretch with nothing in it is drawn as an empty margin, which is the reading the
reader asked for — a young segment is young, and a chart that hides that by
starting at its first row says the opposite.
Here Apple's 2017 closing price on an axis held to the whole calendar year, so the two months the series stops short of are visible as the gap they are:
Either end can be 'auto', which holds the other and leaves that one to the rows
— [startOfRange, 'auto'] pins where a report begins and lets it end wherever the
data does.
Dates are held exactly, with no rounding at either end. A time axis' step is a calendar interval, so rounding a bound outward to one would move a window that was picked deliberately, and by as much as a month. Numeric bounds are still rounded out to a round tick, since a round number is not a date anybody chose.
The two kinds don't mix. A number written on a time axis, or a Date written on a
numeric one, is ignored along with the end it was written at — read literally it
would put the axis at the epoch, which is nothing anyone meant.