annotation builder pipes them into the spec like any other feature.
Because they’re a spec feature, annotations re-resolve on every compile: panel-anchored ones re-flow when the chart resizes, and data-anchored ones move with the observation they’re pinned to.
Anchoring
Every annotation is positioned by an anchor that describes a relationship to the graph, not a fixed pixel. Anchors come in two shapes: a point for the kinds that sit somewhere, and a region for the kinds that fill an area.Point anchors
PointAnchorInput is a union of five anchorTypes:
Addressing a grid cell
An observation anchor normally narrows by series:anchorValue names the position on the main axis, groupValue picks the series. On a heatmap there are no series — anchorValue names a whole column of cells — so crossValue names the band on the other axis that picks one cell out of it:
crossValue is read only by a layer whose observations partition a grid, and ignored everywhere else. A cell anchor resolves to the centre of its tile.
Region anchors
RegionAnchorInput is a union of three:
padding is either a number (both axes, in panel fractions) or an AnchorOffset. Omit it and a
selection region gets { x: 8, y: 8, unit: 'px' }, so a box wrapping bars doesn’t sit flush against
them.
align and offset
align names which point of a box an anchor resolves to:
offset is a nudge applied once the target has resolved: { x?, y?, unit?: 'panel' | 'px' }, with
unit defaulting to 'panel'. Pixel offsets are resolved at render time, against the measured box.
Naming the layer
On a combo chart, more than one layer can hold the same address — bars and a trend line both have a value at'Jun'. Name the layer you mean and pass its id as layerId:
layerId and the anchor resolves against the first layer that matches. An id no layer carries drops that annotation with a warning rather than silently moving it.
Note the asymmetry in what each kind takes: pinnedNumber and comment take a bare
ObservationAnchorInput — the fields above with no anchorType key — while text, sticker and
arrow take a full PointAnchorInput, where anchorType is required.
Ids and references
An annotation’sid is unique across every kind, and it is what an annotation anchor’s ref
points at. The first annotation to declare an id keeps it; a second one declaring the same id gets a
generated one plus an ANNOTATION_DUPLICATE_ID warning. Only ids you write yourself can be
referenced — a ref naming nothing raises ANNOTATION_REF_NOT_FOUND and drops the annotation that
referred to it.
One attachment per observation
An observation carries at most one ofsticker, pinnedNumber, comment or image. Adding a
second displaces the first — a comment replaces the pinned number on its bar rather than joining
it. AddAnnotationCommand reverts by putting the displaced annotation back, so one undo restores
both halves of a replacement. (An image has no observation region anchor of its own, so today it
never displaces anything.)
The annotation kinds
Each method onannotation appends one callout. Multiple calls of the same kind accumulate.
Difference arrow
A difference arrow spans two observations and labels the change between them — you choose what the label measures:annotation.arrow(), which is a plain arrow between two positions you specify — it doesn’t read any data.
A difference arrow’s paint comes from the stylesheet: style.annotation.differenceArrow({ color, strokeWidth }) styles every arrow’s line, style.annotation.differenceArrow.label({ fontSize, fontWeight, textColor, background, borderColor, borderWidth, borderRadius, paddingInline, paddingBlock, ... }) its label, and
{ annotation: id } in the options narrows an entry to one. With nothing declared the line is 2px and the label a
12px boxed number; the route around the observations scales with strokeWidth. An arrow whose colour no entry
names takes the series colour its two ends share, when they share one, else black (white under a dark scheme);
the label’s box border takes the arrow’s colour unless borderColor is declared.
A difference is only meaningful when both ends measure the same thing.
Endpoints on different bases — one on the primary y axis and one on the
secondary, or a revenue value against a margin value in the same reshaped
column — raise an
INCOMPARABLE_ARROW_ENDPOINTS warning. The arrow still
draws, so you can see what it claims.Shaded region
zOrder decides whether a shape or image sits behind the data ('background') or in front of it
('foreground', the default). A wash of colour under the marks wants 'background' written out, as
above.
A shape’s paint comes from the stylesheet:
style.annotation.shape({ color, alpha, borderColor, borderWidth }) styles every shape, and { annotation: id } in the options narrows an entry
to one. With nothing declared a shape is a quarter-strength blue wash inside a 1px border of the same
blue at full strength; the border wears color unless borderColor says otherwise, borderWidth: 0
removes it, and color: 'transparent' makes a stroke-only outline.
Text
A text annotation takes a rich-text node and a point anchor.width is a fraction of the plot width; height follows the content:
at places the point of the text’s own box named by align, which defaults to 'center' — so the
sample above centres the block on (0.4, 0.1). Pass align: 'top-left' to hang it from that corner
instead.
A text annotation’s box and base type come from the stylesheet:
style.annotation.text({ background, alpha, borderColor, borderWidth, borderRadius, paddingInline, paddingBlock, fontFamily, fontSize, fontWeight, lineHeight, textColor }) styles every text annotation, and { annotation: id }
in the options narrows an entry to one. With nothing declared the words sit on no box at all, in 15px medium type;
declare a background and the box appears, with a border a shade darker than it unless borderColor says
otherwise, and alpha fades the fill. The content’s own marks — a colour, a font, a size — paint over the base
type, so a size mark is a multiple of the stylesheet’s fontSize.
Arrow
annotation.arrow() draws a plain arrow between two point anchors. Either end can float in panel
fractions or pin to data:
An arrow’s paint comes from the stylesheet:
style.annotation.arrow({ color, strokeWidth, lineType, borderColor, borderWidth, shadow }) styles every arrow, and { annotation: id }
in the options narrows an entry to one. With nothing declared an arrow is a solid 4px line in black (white
under a dark scheme). strokeWidth is the width on a wide panel, scaled down on narrow ones, and the
arrowheads scale with it. borderWidth and shadow are the sticker look — an outline around the line and
a drop shadow; the outline picks a colour that reads against the arrow unless borderColor is declared.
An endpoint on a data anchor with a directional align and no offset of its own is backed off 4px
from the target’s edge, so an arrow pointing at a bar stops just short of touching it. Supply an
explicit offset to place it yourself.
Image
Place an image in a panel region.fit decides how it scales inside the box — 'fill' stretches, 'contain' letterboxes, 'cover' crops to fill:
zOrder ('background' or 'foreground', defaulting
to 'foreground'). fit defaults to 'contain'.
An image’s paint comes from the stylesheet: style.annotation.image({ alpha, borderRadius }) styles every image, and { annotation: id } in the options narrows an entry to one. With
nothing declared an image is opaque with square corners; borderRadius rounds them, in pixels.
Sticker
A sticker pins a built-in emoji-like image to a single observation. Give it asticker id from the catalogue and an observation anchor — it then travels with its data point through re-sorts and filters:
'rocket', 'thumbs-up', 'thumbs-down',
'clapping-hands' and 'grinning-face'. An id outside it renders nothing.
Pinned number and comment
Both pin a marker dot to one observation and place a mini bubble beside it: a pinned number shows the observation’s formatted value behind a swatch, a comment its text on one line, ellipsized when long. Hovering a bubble opens the full tooltip. Both take a bare observation anchor:style.annotation.pinnedNumber({ color, size, borderColor, borderWidth, shadow }) — likewise
style.annotation.comment(...) — and the bubble is their .label(...) part, on the dataLabel words
plus shadow; { annotation: id } in the options narrows an entry to one. A marker whose color no
entry declares borrows the observation’s colour, which the swatch inside a pinned-number bubble always
keeps.
Editing annotations at runtime
Four commands cover the editing surface:MoveAnnotationCommand is the drag path: a translation in panel fractions, clamped inside the panel
on apply. It is relative, so applying it twice moves twice, and it reverts through the inverse patch
rather than the opposite translation — clamping isn’t invertible.
AddAnnotationCommand takes a fully resolved annotation, so callers spread the kind’s defaults:
{ ...SHAPE_DEFAULTS, region }. Only id is optional, and it is settled in the constructor so a
serialized command replays to the same spec.
Related
- Reference lines — for marking a constant value rather than a region
- Highlights — the predicate language
selectionanchors take - Content — the rich-text node shape used by text and comment annotations
- Mappings & aesthetics — the series and x-values an observation anchor names

