annotation builder pipes them into the spec like any other feature.
Annotations resolve again on every compile. Panel-anchored ones re-flow when the chart resizes; data-anchored ones move with the observation they are pinned to.
Anchoring
An anchor positions every annotation. It 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
PointAnchorSpec is a union of five anchorTypes:
Addressing a cell or a point
An observation anchor normally narrows by group:anchorValue names the position on the main axis and groupValue picks the group. On a heatmap or a scatter one main-axis value names several observations, a whole column of cells or every point at that x, so crossValue names the value on the other axis that picks one out:
crossValue; every other layer ignores it. A tile layer has no groups, so it ignores groupValue. A cell anchor resolves to the center of its tile.
Region anchors
RegionAnchorSpec is a union of three:
padding is 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 around bars does not touch 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'. Give the layer you mean an id and pass it as layerId:
layerId and the anchor resolves against the first layer that matches, skipping layers marked interactive: false. A named layer is searched whatever it is. An id no layer carries drops that annotation with an UNKNOWN_LAYER_ID warning rather than moving it somewhere else.
The kinds differ in what they take: pinnedNumber and comment take a bare
ObservationAnchorSpec, the fields above with no anchorType key, while text, sticker and
arrow take a full PointAnchorSpec, where anchorType is required.
Ids and references
An annotation’sid is unique across every kind, and an annotation anchor’s ref points at it.
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 it never displaces
anything.
The annotation kinds
Each method onannotation adds 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(), a plain arrow between two positions you specify, which reads no data.
A difference arrow’s paint comes from the stylesheet. style.annotation.differenceArrow({ stroke, strokeWidth }) styles every arrow’s line; style.annotation.differenceArrow.label({ fontSize, fontWeight, textColor, fill, stroke, strokeWidth, cornerRadius, paddingInline, paddingBlock, ... }) styles its label; and
{ annotation: id } in the options narrows an entry to one arrow. With nothing declared the line is 2px and the label
an 11.5px boxed number, and the route around the observations scales with strokeWidth. An arrow with no declared
stroke takes the color its two ends share, when both sit on one layer and one group with the same color; otherwise
it is a dark stroke (light under a dark scheme). The label’s box border takes the arrow’s color unless the label
declares its own stroke.
A difference only means something when both ends measure the same thing.
Endpoints on different bases, such as 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 color under the geoms needs 'background' written out, as
above.
A shape’s paint comes from the stylesheet:
style.annotation.shape({ fill, fillAlpha, alpha, stroke, strokeWidth }) 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 takes fill unless stroke says otherwise, strokeWidth: 0
removes it, and fill: 'transparent' leaves only the border.
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 that align names, 'center' by default, so the sample
above centers 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({ fill, fillAlpha, alpha, stroke, strokeWidth, cornerRadius, 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 fill and the box appears, with a border a shade darker than the fill unless stroke says otherwise;
fillAlpha fades the fill and alpha the whole box. The content’s own formatting (a color, a font, a size) paints
over the base type, so a size in the content 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({ stroke, strokeWidth, dashArray }) styles every arrow, and { annotation: id } in the options narrows an entry to
one. With nothing declared an arrow is a solid 4px dark line (light under a dark scheme). strokeWidth
is the width on a wide panel, scaled down on narrow ones, and the arrowheads scale with it. The sticker
look is the arrow’s outline part, style.annotation.arrow.outline({ stroke, strokeWidth, shadow }): an
outline around the line plus a drop shadow. The outline picks a color that reads against the arrow unless
stroke 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', default
'foreground'). fit defaults to 'contain'.
An image’s paint comes from the stylesheet: style.annotation.image({ alpha, cornerRadius, shadow }) styles every image, and { annotation: id } in the options narrows an entry to one. With
nothing declared an image is opaque with square corners and no shadow; cornerRadius rounds the corners, in pixels.
Sticker
A sticker pins a built-in emoji-like image to one observation. Give it asticker id from the catalog and an observation anchor, and it travels with that observation through re-sorts and filters:
'rocket', 'thumbs-up', 'thumbs-down', 'clapping-hands' and
'grinning-face'. Any other id renders nothing.
Pinned number and comment
Both pin a marker dot to one observation and place a small bubble beside it. A pinned number shows the observation’s formatted value behind a swatch; a comment shows its text on one line, cut with an ellipsis when long. Hovering a bubble opens the full tooltip. Both take a bare observation anchor:style.annotation.pinnedNumber({ fill, size, stroke, strokeWidth, shadow }), or
style.annotation.comment(...), and the bubble is their .label(...) part, which takes the dataLabel
properties plus shadow. { annotation: id } in the options narrows an entry to one. A marker with no
declared fill takes the observation’s color, and the swatch inside a pinned-number bubble always keeps
that color.
Editing annotations at runtime
Four commands cover editing: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. It reverts through the inverse patch
rather than the opposite translation, because clamping cannot be inverted.
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 groups and x-values an observation anchor names

