Every tooltip is styled as a Popover, with the focused position as its header and one row per series beneath it.
A header naming a date matches how far apart the data sits: daily points are named
by their weekday — Monday, July 20 — points less than a day apart carry the time,
weekly ones stay short, and monthly ones name the month in full with its year —
January 2026. Elsewhere the year appears only where the series crosses one; a
month always carries it, since January alone names a month in no particular
year.
These headers are written in US English wherever the chart is opened, as is every other figure the chart formats for itself — see How ticks are written.
When a tooltip needs figures the chart can't derive, render the inside yourself.
React takes a renderTooltipContent function; Ember takes a :tooltipContent
block. Both receive the focused points - each carrying its original row - and the
defaultBody, so you can add to the default rows instead of replacing them:
To replace the surface as well, use renderTooltip (React) or the :tooltip block
(Ember). The chart still decides when the tooltip is open and where it sits; the
styling becomes yours.
Each point carries the row it was drawn from as datum. In React, name the row's
type on the chart to have it typed rather than unknown:
Annotating the render function with ChartTooltipRenderContext<ClientRow> and
passing it in does the same thing. Left off, datum is unknown; the rest of
the point is typed as before.
Rows read in the order the chart declares them: marks in the order they are drawn, and within a mark, series in the order they first appear — the same order the legend reads in. A row stays where the reader last found it however the numbers move, so a tooltip read at one position can be compared against the next.
These three channels take four different rankings over six months — SMS leads in January and trails by June. Walk the chart and the rows hold still while the figures beside them move:
tooltipSeriesOrder="position" orders by where the focused points sit on the plot
instead — top to bottom on a vertical chart, left to right on a horizontal one.
That ranks the series at the position under the pointer, which is worth having on
a chart read one position at a time. The cost is the rows changing places as the
reader moves along the plot — the same six months, reordering under the pointer
four times:
Hovering a chart — or arrowing through it from the keyboard — focuses the whole
column at the pointer, one point per series. The chart marks it with a
full-height crosshair, a dot on every focused series, and the tooltip, which
follows the pointer. Anywhere in the plot focuses the nearest column, so the
whole width of a bar is a target — and only the plot does: an axis' numbers and the
band the legend sits in are not part of it. withFocus={false} turns all of it off.
focusMode="nearest" focuses the single closest point instead of the column,
for reading one series at a time rather than comparing series at one position.
The dot on a focused line is drawn with a ring outside it, in the line's own color held at a quarter opacity — a halo that makes the marker findable without making the reading look bigger than it is. The dot fades in and the ring grows out of the point, so the marker arrives rather than appearing.
Four per-mark options size it. The whole marker measures focusDotRadius plus
focusDotRingWidth, so the defaults draw a 5px dot inside a 4px band:
focusDotRadius — the dot's radius in screen pixels. Defaults to 5.focusDotRingWidth — the band outside it. Defaults to 4; 0 leaves the
dot on its own.focusDotRingColor — defaults to the line's own color. Set it to the
surface the chart sits on to punch the dot out of whatever it lands on
instead.focusDotRingOpacity — defaults to 0.25, or to 1 where the ring was
given a color of its own.Two per-mark options let a mark answer focus with color. They are independent and combine:
withHoverColor — while the mark is part of the focused group, it repaints
in the hover twin of its color.withUnfocusedDim — everything outside the focused group dims, leaving the
focused column carrying the chart.withHoverColor needs a color that has a hover twin — see
Color.
withLegendHoverFocus lets a legend entry drive those same two options. Pointing
at a name — or tabbing onto it, since every entry is a button — highlights that
series across the whole plot: it swaps to its hover color where the mark asked
for withHoverColor, and every other series recedes where the mark asked for
withUnfocusedDim.
It adds no presentation of its own, so a mark with neither option set does not move. Turn at least one of them on.
No tooltip opens and no crosshair is drawn. An entry names a series rather than a place on the plot, so there is no value to read out; that stays the pointer's job on the plot itself.
Worth it once a chart carries enough series that finding one by color is work. On two or three it adds a response the reader did not need.
withZoom lets a reader drag a range out of the plot and read that range on its
own. The chart redraws to what they selected: the horizontal axis covers it, the
value axes refit to what is inside it, and a Reset zoom control appears in the
top corner to put it back.
It is how a reader gets from ninety days of sending to what happened on the fourteenth, without the chart having to offer a date picker for a question they only have once.
Worth it on a chart of a long period, where the interesting stretch is a small part of the whole and the reader is the one who knows which stretch that is. Leave it off where the range is already the point — a chart of four quarters has nothing inside it to find — and on anything small enough that a drag would be a mis-click.
A time or number axis is held to the range exactly: the plot's edges are where the reader dragged, not where the nearest bucket happens to sit. The rows either side of the window are still drawn and then clipped, so a line runs to the edge rather than stopping short of it — and because part of each of those segments is on the plot, the value axis covers them too. A window whose neighboring bucket is a spike keeps room for that spike.
A category axis takes the categories the drag crossed, since a category is either in or out — a categorical scale covers the categories it was given rather than a range between two of them. That is any chart whose horizontal values are names rather than dates or numbers: bars, and lines or areas over string categories alike.
Nothing zooms on a chart with no horizontal axis a range means something on: a
radial mark, a sankey, and a horizontal bar chart, whose horizontal axis is its
magnitude rather than its categories. withZoom on one of those does nothing
rather than doing something strange.
The chart is already focusable, and with withZoom on it answers three more keys:
| Key | What it does |
|---|---|
+ | Halves the window, about its middle |
- | Widens it again, back to the whole of the data |
0 | The whole of the data, in one press |
Unmodified presses only. Ctrl or Cmd with any of them is the browser's own page
zoom, and a reader shrinking the page should get a smaller page rather than a wider
plot.
The arrow keys still walk the focused point, and Home still goes to the first
one — a reader who has zoomed in wants the first point of what they are looking
at, so the reset is 0 and nothing shares a key with the navigation. The Reset
zoom control is a real button in the tab order too, so a reader who has landed
on somebody else's zoomed link can always get out of it.
A chart that can be zoomed should say so in its ariaDescription, as the example
above does. The scene is one image to a screen reader, so nothing about the plot
announces that dragging it does anything.
The chart tracks what the reader drags. zoomWindow on its own is the range to
open at, and the chart goes on tracking from there. Pass onZoomChange alongside
it to own the window instead — which is what a chart whose range is also a date
picker elsewhere on the page wants:
The values are the axis' own: dates on a time axis, numbers on a linear one, and
the first and last category on a categorical one. null is the whole of the data.
The tooltip works exactly as it did. The overlay a drag paints answers the pointer
only while the drag is running, so pointing at the plot reads a column as it
always has — and focus clears for the length of a drag, which is right: the reader
is selecting a range rather than reading a column. Escape abandons a drag in
progress.