Skip to content
microcharts

Annotations

Declarative reference context for React microcharts — thresholds, target zones, moment markers, and callouts, written as children and rendered identically inside every host.

Annotations answer "what should the reader compare this against?". They're plain children — <Threshold>, <TargetZone>, <Marker>, <Callout> — that the host chart draws in its own coordinate space, so the same child lands identically on any chart that accepts it. Reference ink whispers: the hairlines stroke at 0.7 opacity, labels take the muted --mc-neutral label ink, and a target band uses the same faint --mc-band fill the charts do — so the data never competes with its context.

Annotations
p95 latencySLAdeploy

Install

The annotation layer is its own ~1.5 kB entry. A host that renders no annotations pays only a tiny children walker; the mark renderers ship with this import.

import { Threshold, TargetZone, Marker, Callout } from "@microcharts/react/annotations";

The vocabulary

  • <Threshold y={65} label="SLA" /> — a dashed reference hairline at a data-space y.
  • <TargetZone y={[40, 60]} /> — a normal-range band, always drawn beneath the data ink. It takes a label too.
  • <Marker x={8} label="deploy" /> — a vertical moment mark; x addresses the host's primary axis (the data index on index-based charts like Sparkline and SparkBar).
  • <Marker x={8} celebrate /> — six deterministic particles burst once on entrance, for milestone crossings only; under reduced motion they rest as a quiet starburst.
  • <Callout x={5} y={45} label="dip" /> — one narrated point on an elbow hairline. label is required; y is optional and defaults to the frame's mid-height.

label is optional on the other three, and all four take a color to override the reference ink on that one mark. A fragment works as a container — <><Threshold … /><Marker … /></> — the host's walker unwraps it.

celebrate — a milestone crossing
Signups10k!
callout — one narrated point
Latencycache miss

The same child, any host

The children don't know which chart drew them — they resolve against the host's scale. Move the exact <TargetZone> and <Threshold> from a line onto bars and they land in the same data-space, unchanged.

identical annotations on SparkBar
Deploys per daycap

Every value-series host

Every value-series chart hosts annotations — the charts with a continuous value axis and an index / slot / time x-axis, so a data-space x and y map cleanly onto the frame. That's Sparkline, SparkBar, MiniBar, CyclePlot, CitySkyline, ChangePoint, DualSparkline, SpreadBand, ForecastCone, ControlStrip, QueueDepth, BurnChart, Waterfall, PercentileTrace, RetentionCurve, WinProbWorm, ErrorBudget, NetFlow, Horizon, EnsembleGhosts, PairedBars (vertical), and Slope. The same <Threshold> or <Marker> lands in each chart's own data-space, unchanged. (MiniBar and PairedBars host in their default vertical orientation, where value lives on the y-axis; a horizontal layout flips the axes, so annotation children pass through.)

Charts without a single continuous value-y don't host them, by design — there's no shared y for a <Threshold> to land on:

Why it can't hostExamples
Scalar readout — no axisDelta, FatDigits, StatusDot, Thermometer
Part-to-whole — y is a shareMicroDonut, SegmentedBar, Funnel
Value encoded as colourHeatStrip, ActivityGrid, the calendars
Value against valueQuadrantDot, MicroScatter, BiasStrip
Pure timeline — no value axisthe event/phase timelines

Most still accept children as a plain pass-through, so an annotation placed inside one renders nothing and warns in dev; a few — Delta among them — take no children at all, and it's a type error.

All 22 hosts
SparklineTargetZone · Threshold
SLA
SparkBarTargetZone · Threshold
cap
MiniBarThreshold
target
CyclePlotTargetZone · Marker
Sun
CitySkylineThreshold
cap
ChangePointThreshold
baseline
DualSparklineTargetZone
SpreadBandMarker
+8launch
ForecastConeMarker
42today
ControlStripMarker
excursion
QueueDepthMarker
100214▴breach
BurnChartMarker
deadline
WaterfallThreshold
+300+120−140+60−80target
PercentileTraceThreshold
p81median
RetentionCurveThreshold
38%target
WinProbWormMarker · celebrate
+1798%swing
ErrorBudgetCallout
62%deploy dip
NetFlowThreshold
net+
HorizonThreshold
zero
EnsembleGhostsThreshold
cap
PairedBarsThreshold
bar
SlopeThreshold
mid

Honesty rules

  • A coordinate outside the host's domain clamps to the edge at 0.4 opacity — visibly off-scale, never silently dropped.
  • Annotations never touch the auto summary: the data description stays pure. To narrate an annotation on a static chart, pass an explicit summary string.
  • celebrate particles are seeded from the marker's x and the host's size, never Math.random, so server and client render the same burst.
  • A label you supply is the one text the containment rule can't pre-reserve a gutter for, so it's kept inside the chart by one of two mechanisms. Threshold, TargetZone and Callout truncate: a label longer than its available run is cut and given a trailing . Marker instead flips its anchor — a label near the right edge draws leftward from the mark rather than off the chart — so a moment mark never loses characters.
  • Reference hairlines draw at a fixed 1-unit weight, deliberately exempt from --mc-density, so tightening the density never thickens the context ink along with the data. The one exception is the Callout elbow, drawn a shade lighter at 0.75, because it's a leader line pointing at data rather than a reference the reader measures against.