Axes

What Pluma infers about scales, ticks and margins, and the three things worth saying yourself — a second value axis, a domain, a time granularity.

Several value axes

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.

Loading editor

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.

What the axes share

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:

  • The tick count. Pluma tries a few and keeps whichever lets every axis land on round numbers without leaving the plot half empty. tickCount on any axis pins it for all of them.
  • Where zero falls. A bar or an area is measured from zero, so as soon as one sits on an axis past the first, every axis is anchored to put zero at the same height. Two bottoms in one plot would be a chart drawing its fills from the wrong one.
  • The scale. 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.

More than two

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.

Loading editor

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.

Keeping zero off the axis

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:

<Chart
	ariaLabel="Open rate per day"
	marks={[
		area({ data: rows, x: 'date', y: 'rate', withGradient: true, fillOpacity: 0.2, withDomain: false }),
		line({ data: rows, x: 'date', y: 'rate' }),
	]}
/>

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 a numeric 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.

<Chart ariaLabel="Open rate per day" marks={marks} yAxis={{ domain: [0, 100] }} />

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.

<Chart ariaLabel="Open rate per day" marks={marks} yAxis={{ domain: [0, 'auto'] }} />

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.

How ticks are written

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.

Not every category gets a label

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.

Time axes

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:

Loading editor

Several lines share one time axis the same way — one series field, one mark:

Loading editor