Annotations are first-class citizens in Semiotic. Every chart component accepts an annotations prop — an array of annotation objects that are rendered automatically based on theirtype. Built-in types like thresholds, labels, callouts, enclosures, trend lines, and bands all work out of the box. NoframeProps wrapping needed.
Thresholds
Thresholds are the most common annotation type. Usey-threshold for a horizontal reference line at a target value, and x-threshold for a vertical line at a specific data point.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { week: 1, score: 12 }, { week: 2, score: 18 }, { week: 3, score: 15 }, { week: 4, score: 24 }, { week: 5, score: 20 }, { week: 6, score: 28 }, { week: 7, score: 32 }, { week: 8, score: 27 }, { week: 9, score: 35 }, { week: 10, score: 30 } ], /* --- Process --- */ xAccessor: "week", yAccessor: "score", /* --- Customize --- */ xLabel: "Week", yLabel: "Score", /* --- Annotate --- */ annotations: [ { type: "y-threshold", y: 25, label: "Target", color: "#22c55e" }, { type: "x-threshold", week: 7, label: "Launch", color: "#6366f1" } ] } export default () => { return <LineChart {...frameProps} /> }
Labels & Callouts
Point annotations let you call attention to individual data points. SetpointIdAccessor on the chart to identify each point, then use pointId on a label or calloutannotation to anchor it to a specific data point. The annotation only renders when the matching point exists. Offset the label position withdx and dy.
import { Scatterplot } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { x: 10, y: 20, label: "A" }, { x: 25, y: 35, label: "B" }, { x: 40, y: 15, label: "C" }, { x: 55, y: 45, label: "D" }, { x: 70, y: 30, label: "E" }, { x: 85, y: 50, label: "F" } ], /* --- Process --- */ xAccessor: "x", yAccessor: "y", pointIdAccessor: "label", /* --- Customize --- */ xLabel: "X Value", yLabel: "Y Value", /* --- Annotate --- */ annotations: [ { type: "label", pointId: "D", label: "Outlier", dx: 30, dy: -30 }, { type: "callout", pointId: "F", label: "Peak", radius: 15, dx: -50, dy: -20 } ] } export default () => { return <Scatterplot {...frameProps} /> }
Hierarchy & Emphasis
When a chart carries several annotations, give one a clear lead. Any annotation accepts emphasis: "primary" | "secondary" — the same token charts use for dashboard hierarchy. A secondaryannotation dims and yields z-order; a primary one paints at full weight and on top. If emphasis is omitted but annotations carry provenance.confidence, Semiotic infers a reading order from confidence descending, then authored array order, and applies a subtle opacity gradient. Annotations with neither signal render exactly as before, so it is purely additive. The wrapper carries class hooks annotation-emphasis--primary /annotation-emphasis--secondary for further styling, and the dim composes with the freshness treatment fromProvenance & Lifecycle— a stale, secondary note becomes the quietest mark on the chart.
This treatment runs through the shared annotation render pass, so custom SVG rules, stream overlays, and static SVG output all resolve hierarchy the same way. The accessibility audit also credits explicit hierarchy signals and warns when a multi-annotation chart has no declared reading order.
import { Scatterplot } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { x: 10, y: 20, label: "A" }, { x: 25, y: 35, label: "B" }, { x: 40, y: 15, label: "C" }, { x: 55, y: 45, label: "D" }, { x: 70, y: 30, label: "E" }, { x: 85, y: 50, label: "F" } ], /* --- Process --- */ xAccessor: "x", yAccessor: "y", pointIdAccessor: "label", /* --- Customize --- */ xLabel: "X Value", yLabel: "Y Value", /* --- Annotate --- */ annotations: [ { type: "callout", pointId: "B", label: "Secondary context", emphasis: "secondary", radius: 12, dx: 30, dy: 35 }, { type: "callout", pointId: "F", label: "Primary insight", emphasis: "primary", radius: 14, dx: -80, dy: -20 } ] } export default () => { return <Scatterplot {...frameProps} /> }
Auto Placement
Set autoPlaceAnnotations to let Semiotic choose offsets for note-like annotations that do not already declare dx ordy. The pass estimates note bounds, tries adjacent candidate positions, avoids existing notes and point marks, and keeps text inside the plot area when possible. Authored offsets stay authoritative by default, and svgAnnotationRules remains the full escape hatch.
Pass true for the default greedy layout, or an object to tune the recipe: defaultOffset, notePadding,markPadding, edgePadding,preserveManualOffsets, routeLongConnectors, andconnectorThreshold. The same pure function is exported fromsemiotic/recipes as annotationLayout, so authors who need tighter control can run it themselves before passing annotations to a frame.
import { Scatterplot } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { x: 10, y: 20, label: "A" }, { x: 25, y: 35, label: "B" }, { x: 40, y: 15, label: "C" }, { x: 55, y: 45, label: "D" }, { x: 70, y: 30, label: "E" }, { x: 85, y: 50, label: "F" } ], /* --- Process --- */ xAccessor: "x", yAccessor: "y", pointIdAccessor: "label", /* --- Customize --- */ xLabel: "X Value", yLabel: "Y Value", /* --- Annotate --- */ annotations: [ { type: "callout", pointId: "F", label: "Placed away from the edge", radius: 14 }, { type: "label", pointId: "D", label: "Second note chooses another side" }, { type: "label", pointId: "B", label: "Manual offset stays fixed", dx: 26, dy: 26 } ], autoPlaceAnnotations: true } export default () => { return <Scatterplot {...frameProps} /> }
Density & Progressive Disclosure
Too many notes is its own failure mode. Add a density key toautoPlaceAnnotations and Semiotic sheds the lowest-priority note annotations once they outgrow what the plot area carries comfortably. Priority reads the signals you already set: anemphasis: "primary" note is the floor and is never shed, provenance.confidence breaks ties, and a stale orexpiredlifecycle.freshness is dropped first. Reference lines, bands, and statistical overlays never count toward the budget — only explanatory notes do.
Pass density: true for the area-derived budget (about one note per 20 000 px²) or an object to tune it —maxAnnotations for a hard cap, areaPerAnnotation,and minVisible. Add progressiveDisclosure: trueto keep the shed notes in the DOM, hidden until the chart is hovered or focused, instead of dropping them — the persistent set stays the floor a non-hover reader always sees. The pure split is exported fromsemiotic/recipes as annotationDensity (returns{ visible, deferred, budget }) alongsideannotationBudget, and diagnoseConfig raises an advisory ANNOTATION_DENSITY warning when notes exceed the budget.
import { Scatterplot } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { x: 10, y: 20, label: "A" }, { x: 25, y: 35, label: "B" }, { x: 40, y: 15, label: "C" }, { x: 55, y: 45, label: "D" }, { x: 70, y: 30, label: "E" }, { x: 85, y: 50, label: "F" } ], /* --- Process --- */ xAccessor: "x", yAccessor: "y", pointIdAccessor: "label", /* --- Customize --- */ xLabel: "X Value", yLabel: "Y Value", /* --- Annotate --- */ annotations: [ { type: "callout", pointId: "F", label: "Primary insight (always kept)", emphasis: "primary", radius: 14 }, { type: "label", pointId: "A", label: "Note A" }, { type: "label", pointId: "B", label: "Note B" }, { type: "label", pointId: "C", label: "Note C" }, { type: "label", pointId: "D", label: "Note D" }, { type: "label", pointId: "E", label: "Note E" } ], autoPlaceAnnotations: { density: { maxAnnotations: 3 } } } export default () => { return <Scatterplot {...frameProps} /> }
Responsive Annotations
Density sheds by count; responsive behavior sheds byimportance as the plot narrows. SetautoPlaceAnnotations: { responsive: true } and, once the plot width drops to the breakpoint (480px by default, or{ minWidth } to tune), secondary-emphasis notes are dropped while primary and unmarked notes stay. Pair it with responsiveWidth for a chart that adapts live, or with progressiveDisclosure to defer the secondary notes (revealed on hover) instead of dropping them. The two charts below carry the same annotations at two widths.
import { Scatterplot } from "semiotic" const frameProps = { /* --- Data --- */ data: [{ x: 10, y: 20, label: "A" }, { x: 25, y: 35, label: "B" }, ... ], /* --- Size --- */ width: 600, /* --- Process --- */ xAccessor: "x", yAccessor: "y", pointIdAccessor: "label", /* --- Annotate --- */ annotations: [ { type: "callout", pointId: "F", label: "Primary (kept)", emphasis: "primary", radius: 12 }, { type: "label", pointId: "B", label: "Secondary B", emphasis: "secondary" }, { type: "label", pointId: "D", label: "Secondary D", emphasis: "secondary" }, { type: "label", pointId: "H", label: "Unmarked (kept)" } ], autoPlaceAnnotations: { responsive: true } } export default () => { return <Scatterplot {...frameProps} /> }
At a constrained width the same configuration sheds the twosecondary notes, keeping only the primary and unmarked ones:
import { Scatterplot } from "semiotic" const frameProps = { /* --- Data --- */ data: [{ x: 10, y: 20, label: "A" }, { x: 25, y: 35, label: "B" }, ... ], /* --- Size --- */ width: 360, /* --- Process --- */ xAccessor: "x", yAccessor: "y", pointIdAccessor: "label", /* --- Annotate --- */ annotations: [ { type: "callout", pointId: "F", label: "Primary (kept)", emphasis: "primary", radius: 12 }, { type: "label", pointId: "B", label: "Secondary B", emphasis: "secondary" }, { type: "label", pointId: "D", label: "Secondary D", emphasis: "secondary" }, { type: "label", pointId: "H", label: "Unmarked (kept)" } ], autoPlaceAnnotations: { responsive: true } } export default () => { return <Scatterplot {...frameProps} /> }
Curved Connectors
Connectors are straight lines by default. Opt into a swoopy quadratic-bezier connector withconnector: { type: "curve" }. Thecurve value controls how far it bows — a fraction of the connector length, default 0.25; negate it to bow the other way — and the arrowhead re-aligns to the curve's tangent. Below, the same callout is drawn with a straight and a curved connector.
import { Scatterplot } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { x: 10, y: 20, label: "A" }, { x: 25, y: 35, label: "B" }, { x: 40, y: 15, label: "C" }, { x: 55, y: 45, label: "D" }, { x: 70, y: 30, label: "E" }, { x: 85, y: 50, label: "F" } ], /* --- Process --- */ xAccessor: "x", yAccessor: "y", pointIdAccessor: "label", /* --- Customize --- */ xLabel: "X Value", yLabel: "Y Value", /* --- Annotate --- */ annotations: [ { type: "callout", pointId: "C", label: "Straight", radius: 10, dx: 55, dy: 45, connector: { end: "arrow" } }, { type: "callout", pointId: "D", label: "Curved", radius: 10, dx: 60, dy: -45, connector: { type: "curve", end: "arrow" } } ] } export default () => { return <Scatterplot {...frameProps} /> }
Association & Redundant Cues
An annotation that ties to its target by color alone fails the correspondence problem: a color-blind reader can't match the hue, and a screen-reader user never sees it at all. Labels and callouts already draw a connector, and enclosures wrap their subject — but atext note draws no connector, so a colored, offsettext note is the classic color-only case. SetautoPlaceAnnotations: { redundantCues: true } and Semiotic adds a faint leader line from the note back to its anchor: aspatial cue, not another color, so the association survives color-blindness. The accessibility audit'sperceivable.annotation-association check treatsredundantCues as satisfying the requirement.
import { Scatterplot } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { x: 10, y: 20, label: "A" }, { x: 25, y: 35, label: "B" }, { x: 40, y: 15, label: "C" }, { x: 55, y: 45, label: "D" }, { x: 70, y: 30, label: "E" }, { x: 85, y: 50, label: "F" } ], /* --- Process --- */ xAccessor: "x", yAccessor: "y", pointIdAccessor: "label", /* --- Customize --- */ xLabel: "X Value", yLabel: "Y Value", /* --- Annotate --- */ annotations: [ { type: "text", pointId: "B", label: "Echoes the series color", color: "#d7263d", dx: 40, dy: -28 } ], autoPlaceAnnotations: { redundantCues: true } } export default () => { return <Scatterplot {...frameProps} /> }
Cohesion
Should a note blend into the chart or stand apart from it? Cohesion is an explicit choice. cohesion: "blended" (the default look) lets notes adopt the chart's visual language — mark colors, chart typography. cohesion: "layer" presents them as a distinct editorial layer: the theme's --semiotic-annotation-colorand an italic editorial face, so the commentary reads as commentary. Set it per annotation, or chart-wide viaautoPlaceAnnotations: { cohesion: "layer" } (a per-annotation cohesion always wins). The example below puts every note in the editorial layer.
import { Scatterplot } from "semiotic" const frameProps = { /* --- Data --- */ data: [{ x: 10, y: 20, label: "A" }, { x: 25, y: 35, label: "B" }, ... ], /* --- Process --- */ xAccessor: "x", yAccessor: "y", pointIdAccessor: "label", /* --- Annotate --- */ annotations: [ { type: "label", pointId: "F", label: "Editorial note", dx: -70, dy: -20 }, { type: "label", pointId: "B", label: "Reads as commentary", dx: 30, dy: 30 } ], autoPlaceAnnotations: { cohesion: "layer" } } export default () => { return <Scatterplot {...frameProps} /> }
Audience & Defensive Annotations
Charts travel. They get screenshotted, pasted into decks, and circulated far from the page that produced them — stripped of provenance. Adefensive annotation anticipates that reuse: it isnever shed by the density budget or responsive shedding, so it survives into every export, and when it carriesprovenance the layout pass bakes the source and confidencevisibly into its label — so a stray screenshot still says who made the note and how sure they were. Below, a tight density budget sheds the ordinary notes, but the AI-sourced caveat stays and announces itself.
import { Scatterplot } from "semiotic" const frameProps = { /* --- Data --- */ data: [{ x: 10, y: 20, label: "A" }, { x: 25, y: 35, label: "B" }, ... ], /* --- Process --- */ xAccessor: "x", yAccessor: "y", pointIdAccessor: "label", /* --- Annotate --- */ annotations: [ { type: "label", pointId: "A", label: "Note A" }, { type: "label", pointId: "C", label: "Note C" }, { type: "label", pointId: "E", label: "Note E" }, // Defensive: survives the budget; provenance is rendered visibly. { type: "callout", pointId: "F", label: "Spike may be a sensor glitch", radius: 12, dx: -90, dy: -24, defensive: true, provenance: { source: "ai", confidence: 0.62 } } ], autoPlaceAnnotations: { density: { maxAnnotations: 2 } } } export default () => { return <Scatterplot {...frameProps} /> }
The same pass adapts annotation amount to the reader. Pass anAudienceProfile asautoPlaceAnnotations: { density: true, audience } and the density budget scales by the audience's familiarity — a low-familiarity audience keeps more orienting notes, an expert audience fewer:
<Scatterplot data={data} xAccessor="x" yAccessor="y" autoPlaceAnnotations={{ density: true, // Experts get a tighter budget (fewer notes); novices a looser one. audience: { familiarity: { Scatterplot: 5 } }, }} annotations={annotations} />Enclosures
Enclosure annotations group clusters of data points visually. Useenclose to draw a circle around a set of points, orrect-enclose for a rectangular bounding box. Pass acoordinates array with the data points to enclose.
import { Scatterplot } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { x: 10, y: 20, label: "A" }, { x: 25, y: 35, label: "B" }, { x: 40, y: 15, label: "C" }, { x: 55, y: 45, label: "D" }, { x: 70, y: 30, label: "E" }, { x: 85, y: 50, label: "F" } ], /* --- Process --- */ xAccessor: "x", yAccessor: "y", /* --- Customize --- */ xLabel: "X Value", yLabel: "Y Value", /* --- Annotate --- */ annotations: [ { type: "enclose", coordinates: [ { x: 55, y: 45 }, { x: 70, y: 30 }, { x: 85, y: 50 } ], label: "High performers", padding: 10 }, { type: "rect-enclose", coordinates: [ { x: 10, y: 20 }, { x: 25, y: 35 } ], label: "Early stage", padding: 8 } ] } export default () => { return <Scatterplot {...frameProps} /> }
Analytical Annotations
Semiotic includes built-in analytical annotations that compute derived visuals from your data. Use trend for a linear regression line on a scatterplot, or band for a shaded target range on a line chart.
Trend Line
import { Scatterplot } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { x: 10, y: 20, label: "A" }, { x: 25, y: 35, label: "B" }, { x: 40, y: 15, label: "C" }, { x: 55, y: 45, label: "D" }, { x: 70, y: 30, label: "E" }, { x: 85, y: 50, label: "F" } ], /* --- Process --- */ xAccessor: "x", yAccessor: "y", /* --- Customize --- */ xLabel: "X Value", yLabel: "Y Value", /* --- Annotate --- */ annotations: [ { type: "trend", method: "linear", color: "#ef4444", label: "Trend" } ] } export default () => { return <Scatterplot {...frameProps} /> }
Target Band
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { week: 1, score: 12 }, { week: 2, score: 18 }, // ...more data points ], /* --- Process --- */ xAccessor: "week", yAccessor: "score", /* --- Customize --- */ xLabel: "Week", yLabel: "Score", /* --- Annotate --- */ annotations: [ { type: "band", y0: 20, y1: 30, fill: "#22c55e", fillOpacity: 0.15, label: "Target range" } ] } export default () => { return <LineChart {...frameProps} /> }
Anomaly Band
The anomaly-band annotation computes the mean and standard deviation of your y-values, shades the "normal" range (mean ± N×σ), and highlights individual outlier points that fall outside. Adjustthreshold to control how many standard deviations define the boundary — the default is 2.
import { Scatterplot } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { x: 1, y: 22 }, { x: 2, y: 25 }, { x: 3, y: 19 }, { x: 4, y: 23 }, { x: 5, y: 21 }, { x: 6, y: 24 }, { x: 7, y: 20 }, { x: 8, y: 55 }, { x: 9, y: 22 }, { x: 10, y: 26 }, { x: 11, y: 18 }, { x: 12, y: 3 }, { x: 13, y: 24 }, { x: 14, y: 21 }, { x: 15, y: 23 }, { x: 16, y: 60 }, { x: 17, y: 20 }, { x: 18, y: 25 }, { x: 19, y: 19 }, { x: 20, y: 22 } ], /* --- Process --- */ xAccessor: "x", yAccessor: "y", /* --- Customize --- */ xLabel: "Sample", yLabel: "Value", /* --- Annotate --- */ annotations: [ { type: "anomaly-band", threshold: 2, fill: "#6366f1", fillOpacity: 0.1, anomalyColor: "#ef4444", anomalyRadius: 6, label: "±2σ" } ] } export default () => { return <Scatterplot {...frameProps} /> }
LOESS Smoothing
For non-linear data, use method: "loess" on atrend annotation. LOESS (Locally Weighted Scatterplot Smoothing) fits a local weighted regression at each point, producing a smooth curve that follows complex patterns better than a straight line. The bandwidth prop (0–1) controls smoothness — lower values track the data more closely, higher values smooth more aggressively.
import { Scatterplot } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { x: 1, y: 5 }, { x: 2, y: 8 }, { x: 3, y: 14 }, { x: 4, y: 11 }, { x: 5, y: 18 }, { x: 6, y: 22 }, { x: 7, y: 28 }, { x: 8, y: 25 }, { x: 9, y: 30 }, { x: 10, y: 35 }, { x: 11, y: 32 }, { x: 12, y: 28 }, { x: 13, y: 24 }, { x: 14, y: 20 }, { x: 15, y: 18 }, { x: 16, y: 22 }, { x: 17, y: 26 }, { x: 18, y: 30 }, { x: 19, y: 34 }, { x: 20, y: 38 } ], /* --- Process --- */ xAccessor: "x", yAccessor: "y", /* --- Customize --- */ xLabel: "X", yLabel: "Y", /* --- Annotate --- */ annotations: [ { type: "trend", method: "loess", bandwidth: 0.3, color: "#8b5cf6", strokeWidth: 2.5, label: "LOESS" }, { type: "trend", method: "linear", color: "#94a3b8", strokeDasharray: "4,4", label: "Linear" } ] } export default () => { return <Scatterplot {...frameProps} /> }
Forecast Region
The forecast annotation fits a regression to your data and extrapolates a configurable number of steps into the future. A widening confidence envelope communicates increasing uncertainty. Usesteps to control the projection distance andconfidence (0–1) to set the interval width.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { week: 1, score: 12 }, { week: 2, score: 18 }, { week: 3, score: 15 }, { week: 4, score: 24 }, { week: 5, score: 20 }, { week: 6, score: 28 }, { week: 7, score: 32 }, { week: 8, score: 27 }, { week: 9, score: 35 }, { week: 10, score: 30 } ], /* --- Process --- */ xAccessor: "week", yAccessor: "score", /* --- Customize --- */ xLabel: "Week", yLabel: "Score", /* --- Annotate --- */ annotations: [ { type: "forecast", steps: 4, confidence: 0.95, fill: "#6366f1", fillOpacity: 0.15, strokeColor: "#6366f1", label: "Forecast" } ] } export default () => { return <LineChart {...frameProps} /> }
Ordinal Charts
Ordinal chart components like BarChart also accept the annotations prop directly. A common use is adding a goal line with y-threshold.
import { BarChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { product: "Alpha", units: 450 }, { product: "Beta", units: 380 }, { product: "Gamma", units: 520 }, { product: "Delta", units: 290 }, { product: "Epsilon", units: 610 } ], /* --- Process --- */ valueAccessor: "units", categoryAccessor: "product", /* --- Annotate --- */ annotations: [ { type: "y-threshold", y: 400, label: "Goal", color: "#f97316" } ] } export default () => { return <BarChart {...frameProps} /> }
Streaming Charts
Annotations work identically on streaming charts. Thresholds and bands stay fixed while data scrolls past them — useful for monitoring dashboards where reference lines mark normal operating ranges, warning levels, or SLA targets.
Point-Anchored Callouts
Use pointIdAccessor to assign a unique ID to each data point, then reference it with pointId on a callout or label annotation. The annotation appears only when that specific point is in the visible window, slides with it, and disappears when it scrolls off-screen. Below, three callouts are anchored to points #60, #120, and #180 — click Restart to watch them appear, track, and disappear as the stream advances.
Streaming Anomaly Detection
Statistical annotations like anomaly-band work naturally on streaming charts — the band recomputes from the current window buffer each frame, so the "normal" range adapts as data arrives. Outlier points are highlighted in real time as they enter the window.
Custom Rules
For cases where the built-in types are not enough, use theframeProps escape hatch to passsvgAnnotationRules or htmlAnnotationRules. These receive the annotation data, scales, and frame state. Return JSX for custom rendering, or null to fall through to default handling.
JSX
<LineChart data={lineData} xAccessor="week" yAccessor="score" annotations={[ { type: "custom-marker", week: 5, score: 20, note: "Review" } ]} frameProps={{ svgAnnotationRules: ({ d, xScale, yScale }) => { if (d.type === "custom-marker") { const cx = xScale(d.week) const cy = yScale(d.score) return ( <g> <circle cx={cx} cy={cy} r={12} fill="none" stroke="#ef4444" strokeWidth={2} /> <text x={cx} y={cy - 18} textAnchor="middle" fill="#ef4444" fontSize={12}> {d.note} </text> </g> ) } return null // Fall through to default handling } }} />
Built-in Types Reference
The following annotation types are recognized automatically by chart and frame components. Pass them in the annotations array with the corresponding type value.
| Type | Charts | Key Props | Description |
|---|
| y-threshold | All | y, label, color | Horizontal reference line at a value |
| x-threshold | XY | x (or data key), label, color | Vertical reference line at a data point |
| label | All | x, y (or pointId), label, dx, dy | Text annotation with connector line |
| callout | All | x, y (or pointId), label, radius | Callout with circular subject highlight |
| callout-circle | All | x, y, label, radius | Same as callout with explicit circle subject |
| callout-rect | All | x, y, label, width, height | Callout with rectangular subject |
| text | All | x, y, label, dx, dy | Plain text at an anchored data position |
| widget | All | x, y (or nodeId), content, width, height | React content anchored in data space |
| enclose | All | coordinates, label, padding | Circle enclosing a set of data points |
| rect-enclose | All | coordinates, label, padding | Rectangle enclosing a set of data points |
| highlight | All | style | Redraws a mark on annotation layer with custom style |
| bracket | Ordinal | category, label, width, height, depth | Bracket spanning a category or range |
| trend | XY | method, color, bandwidth, label | Regression line (linear, polynomial, or loess) |
| band | XY | y0, y1, fill, fillOpacity, label | Shaded horizontal band between two values |
| envelope | XY | bounds, fill, fillOpacity, label | Envelope around a series or coordinate set |
| anomaly-band | XY | threshold, fill, anomalyColor, label | Mean ± N×σ band with highlighted outliers |
| forecast | XY | steps, confidence, method, fill, label | Extrapolated trend with widening confidence envelope |
| category-highlight | Ordinal | category, color, opacity, label | Highlight an ordinal category band |