API · Annotations
Annotations API
On this page
ChartGPU's annotations feature provides a powerful system for adding visual markers, reference lines, text notes, and point annotations to charts. The annotations API includes both declarative configuration and interactive authoring tools.
Table of Contents
- Overview
- Annotation Types
- Configuration
- Interactive Authoring
- Hit Testing
- Drag and Reposition
- Configuration Dialog
- Usage Examples
- Advanced Use Cases
- Troubleshooting
Overview
Annotations are visual overlays that can be:
- Declaratively configuredvia
ChartGPUOptions.annotations - Interactively created and editedusing
createAnnotationAuthoring(...) - Programmatically managedwith full undo/redo support
- Exported and importedas JSON for persistence
Key capabilities:
- Vertical lines (lineX), horizontal lines (lineY), text notes, point markers, filled vertical bands (bandX)
- Two coordinate systems: data-space (tracks with zoom/pan) and plot-space (pinned HUD-style)
- Drag-to-reposition with type-specific constraints
- Right-click context menu for creation, editing, and deletion
- Undo/redo history (50 entries)
- JSON export/import
- Layer control (above or below series)
- Rich styling with colors, line styles, opacity, and labels
Annotation Types
LineX (Vertical Line)
A vertical line at a specific x-coordinate in data-space.
interface AnnotationLineX {
readonly type: 'lineX';
readonly x: number; // Data-space x coordinate
readonly yRange?: readonly [minY: number, maxY: number]; // Optional y range
}
Use cases: Milestones, event markers, phase boundaries, time markers
Drag behavior: Constrained to horizontal movement only
LineY (Horizontal Line)
A horizontal line at a specific y-coordinate in data-space.
interface AnnotationLineY {
readonly type: 'lineY';
readonly y: number; // Data-space y coordinate
readonly xRange?: readonly [minX: number, maxX: number]; // Optional x range
}
Use cases: Thresholds, baselines, reference levels, target values
Drag behavior: Constrained to vertical movement only
Point Marker
A marker at a specific (x, y) coordinate in data-space.
interface AnnotationPoint {
readonly type: 'point';
readonly x: number; // Data-space x coordinate
readonly y: number; // Data-space y coordinate
readonly marker?: AnnotationPointMarker;
}
interface AnnotationPointMarker {
readonly symbol?: 'circle' | 'rect' | 'triangle';
readonly size?: number; // Marker size in CSS pixels
readonly style?: AnnotationStyle;
}
Use cases: Peak/trough markers, special events, outliers, highlights
Drag behavior: Free 2D movement in data-space
Text Note
A text annotation at a specific position in either data-space or plot-space.
interface AnnotationText {
readonly type: 'text';
readonly position: AnnotationPosition;
readonly text: string;
}
type AnnotationPosition =
| Readonly<{ space: 'data'; x: number; y: number }> // Tracks with zoom/pan
| Readonly<{ space: 'plot'; x: number; y: number }>; // Pinned to plot area (0-1 fractions)
Use cases:
- Data-spaceAnnotations that track with data (e.g., "peak value", "minimum")
- Plot-spaceHUD-style notes that stay pinned (e.g., "Q1 2024", watermarks)
Drag behavior: Free 2D movement preserving original coordinate space
BandX (Filled Vertical Region)
A filled, full-height vertical band spanning a data-space x-range. Useful for highlighting a regime, outage window, or any span along the x-axis.
interface AnnotationBandX {
readonly type: 'bandX';
readonly from: number; // Data-space x start
readonly to: number; // Data-space x end
}
{
type: 'bandX',
from: outageStart,
to: outageEnd,
layer: 'belowSeries',
style: { color: '#ef4444', opacity: 0.15 },
}
Use cases: Outage windows, market regimes, weekends/holidays, highlighted ranges
Notes:
from/toorder doesn't matter; the band is drawn between whichever is smaller/larger.- Use
layer: 'belowSeries'so the band sits behind the data. style.color/style.opacitycontrol the fill;lineWidth/lineDashare ignored (the band is a solid fill, not a stroked line).labelis not supported onbandX— pair it with a separatetextannotation if you need a caption.
Drag behavior: Not draggable via interactive authoring (declarative-only for now)
Configuration
All annotations share a common base configuration:
interface AnnotationConfigBase {
readonly id?: string; // Optional stable identifier
readonly layer?: 'belowSeries' | 'aboveSeries';
readonly style?: AnnotationStyle;
readonly label?: AnnotationLabel;
}
type AnnotationConfig = (
AnnotationLineX | AnnotationLineY | AnnotationPoint | AnnotationText | AnnotationBandX
) & AnnotationConfigBase;
Style Configuration
interface AnnotationStyle {
readonly color?: string; // CSS color string
readonly lineWidth?: number; // Line width in CSS pixels (1-8 typical)
readonly lineDash?: ReadonlyArray<number>; // Dash pattern (e.g., [4, 4] for dashed)
readonly opacity?: number; // 0-1 (default: varies by type)
}
Common patterns:
- Solid
lineDash: undefined - Dashed
lineDash: [4, 4] - Dotted
lineDash: [2, 2]
Label Configuration
Labels provide automatic text rendering with templating support:
interface AnnotationLabel {
readonly text?: string; // Explicit label text
readonly template?: string; // Template string (e.g., 'x={x}, y={y}')
readonly decimals?: number; // Decimal places for numeric formatting
readonly offset?: readonly [dx: number, dy: number]; // Pixel offset from anchor
readonly anchor?: 'start' | 'center' | 'end';
readonly background?: AnnotationLabelBackground;
}
interface AnnotationLabelBackground {
readonly color?: string; // Background color
readonly opacity?: number; // Background opacity (0-1)
readonly padding?: AnnotationLabelPadding;
readonly borderRadius?: number; // Border radius in CSS pixels
}
type AnnotationLabelPadding =
| number // Uniform padding
| readonly [top: number, right: number, bottom: number, left: number];
Template variables:
{x}x-coordinate value{y}y-coordinate value{value}y-coordinate value (alias)
Examples:
// Simple label
label: { text: 'Milestone' }
// Template with decimals
label: { template: 'ref y={y}', decimals: 3 }
// Label with background
label: {
text: 'Peak',
offset: [10, -10],
anchor: 'start',
background: {
color: '#000000',
opacity: 0.7,
padding: [2, 6, 2, 6],
borderRadius: 6
}
}
Layer Control
Annotations can be rendered above or below series data:
layer: 'belowSeries' | 'aboveSeries' // Default: 'aboveSeries'
Use cases:
'belowSeries'Reference lines, grids, background markers'aboveSeries'Highlights, labels, annotations that must be visible
Interactive Authoring
The createAnnotationAuthoring(...) function provides a complete annotation editing UI:
function createAnnotationAuthoring(
container: HTMLElement,
chart: ChartGPUInstance,
options?: AnnotationAuthoringOptions
): AnnotationAuthoringInstance;
interface AnnotationAuthoringOptions {
readonly menuZIndex?: number; // Context menu z-index (default: 1000)
readonly toolbarZIndex?: number; // Toolbar z-index (default: 10)
readonly showToolbar?: boolean; // Enable toolbar (default: true)
readonly enableContextMenu?: boolean; // Enable right-click menu (default: true)
}
AnnotationAuthoringInstance
The returned instance provides programmatic control:
interface AnnotationAuthoringInstance {
addVerticalLine(x: number): void;
addTextNote(x: number, y: number, text: string, space?: 'data' | 'plot'): void;
undo(): boolean;
redo(): boolean;
exportJSON(): string;
getAnnotations(): readonly AnnotationConfig[];
dispose(): void;
}
Interactive Features
Right-click context menu:
- Right-click on empty space → Add vertical line / horizontal line / text note
- Right-click on annotation → Edit / Delete
- Automatic coordinate conversion (data-space and plot-space)
- Escape key closes menu
Drag-to-reposition:
- Click and drag any annotation to move it
- Visual feedback: cursor changes, 70% opacity during drag
- Type-specific constraints (lineX: horizontal only, lineY: vertical only)
- Escape cancels drag and reverts position
- smooth interactive frame rates performance
Undo/redo system:
- Toolbar buttons in top-right corner
- Single history entry per operation (create, drag, edit, delete)
- 50 entry limit
- Full state restoration
JSON export:
- Export button copies to clipboard
- Fallback modal if clipboard unavailable
- Compatible with
ChartGPUOptions.annotations
Lifecycle Management
// Create authoring helper
const authoring = createAnnotationAuthoring(container, chart, {
showToolbar: true,
enableContextMenu: true,
});
// Use programmatic API
authoring.addVerticalLine(Date.now());
authoring.addTextNote(50, 75, 'Important', 'data');
// Cleanup (IMPORTANT: dispose before chart)
authoring.dispose();
chart.dispose();
Important: Always call authoring.dispose() before chart.dispose() to clean up event listeners and DOM elements.
Hit Testing
Hit testing detects which annotation (if any) the user clicked or hovered over. This is handled internally by createAnnotationAuthoring(...), but the underlying API is available for custom implementations.
Hit Test API
interface AnnotationHitTestResult {
readonly annotationIndex: number; // Index in annotations array
readonly annotation: AnnotationConfig; // The annotation that was hit
readonly hitType: 'line' | 'text' | 'point' | 'label';
readonly distanceCssPx: number; // Distance from pointer in CSS pixels
}
interface AnnotationHitTesterOptions {
readonly lineTolerance?: number; // Default: 20px
readonly textTolerance?: number; // Default: 8px
readonly pointTolerance?: number; // Default: 16px
readonly labelTolerance?: number; // Default: 2px
readonly spatialGridThreshold?: number; // Default: 20 annotations
}
Hit priority order: Labels > Points > Text > Lines
Coordinate systems:
- Inputcanvas-space CSS pixels (relative to canvas element)
- Internalconverts to data-space or plot-space as needed
- Handles grid margins, device pixel ratio, zoom/pan transformations
Drag and Reposition
Dragging is constraint-based and optimized for smooth interactive frame rates performance:
Constraints
| Type | Movement Constraint | Coordinate Space |
|---|---|---|
| lineX | Horizontal only | Data-space |
| lineY | Vertical only | Data-space |
| point | Free 2D | Data-space |
| text | Free 2D | Original space |
Performance Optimizations
- Optimistic updatesduring drag (no history entries)
- Single history entryon drag end
- Window-level pointer eventsfor smooth dragging outside canvas
- Visual feedbackwith cursor changes and opacity reduction
Cancellation
- Escape keycancels drag and reverts to original position
- Pointer cancelevents handled gracefully
Configuration Dialog
The configuration dialog provides a modal UI for creating and editing annotations. It's created internally by createAnnotationAuthoring(...) but can be customized:
Dialog Features
Color picker:
- 12-color high-contrast palette
- Grid layout with visual swatches
- Hover effects and selection state
Form controls:
- Text input (label, text content)
- Textarea (multi-line text)
- Dropdown (line stylesolid/dashed/dotted)
- Slider (line width1-8px, marker size: 4-16px)
Keyboard shortcuts:
Escapecloses dialog (cancel)Enterin text fields submits form (save)- Auto-focuson first input
Palette Customization
interface AnnotationConfigDialogOptions {
readonly palette?: readonly string[]; // Default: high-contrast 12-color palette
readonly zIndex?: number; // Dialog z-index (default: 1000)
}
const HIGH_CONTRAST_PALETTE = [
'#ef4444', // Red (critical)
'#f97316', // Orange (warning)
'#eab308', // Yellow (caution)
'#22c55e', // Green (success)
'#06b6d4', // Cyan (info)
'#3b82f6', // Blue (primary)
'#8b5cf6', // Purple (accent)
'#ec4899', // Pink (highlight)
'#ffffff', // White (high contrast)
'#94a3b8', // Gray (neutral)
'#64748b', // Dark gray (subtle)
'#1e293b', // Near-black (background)
];
Usage Examples
Basic Declarative Annotations
const options: ChartGPUOptions = {
series: [
{ type: 'line', name: 'Temperature', data: [...] }
],
annotations: [
// Vertical line at specific timestamp
{
type: 'lineX',
x: 1704067200000,
layer: 'belowSeries',
style: { color: '#40d17c', lineWidth: 2 },
label: { text: 'Q1 Start' }
},
// Horizontal threshold line
{
type: 'lineY',
y: 75,
layer: 'belowSeries',
style: {
color: '#ffd166',
lineWidth: 2,
lineDash: [8, 6]
},
label: { template: 'threshold={y}', decimals: 1 }
},
// Peak marker
{
type: 'point',
x: 1704153600000,
y: 98.6,
layer: 'aboveSeries',
marker: {
symbol: 'circle',
size: 10,
style: { color: '#ff4ab0' }
},
label: {
template: 'peak={y}°F',
decimals: 1,
offset: [10, -10]
}
},
// Plot-space HUD text
{
type: 'text',
position: { space: 'plot', x: 0.05, y: 0.05 },
text: 'Preliminary Data',
layer: 'aboveSeries',
style: { color: '#ffffff', opacity: 0.7 }
}
]
};
const chart = await ChartGPU.create(container, options);
Interactive Annotation Authoring
import { ChartGPU, createAnnotationAuthoring } from '@chartgpu/chartgpu';
// Create chart
const chart = await ChartGPU.create(container, {
series: [{ type: 'line', data: [...] }],
annotations: [] // Start with no annotations
});
// Enable annotation authoring
const authoring = createAnnotationAuthoring(container, chart, {
showToolbar: true,
enableContextMenu: true,
menuZIndex: 1000,
toolbarZIndex: 10
});
// Users can now:
// - Right-click to add annotations
// - Drag annotations to reposition
// - Right-click annotations to edit/delete
// - Use undo/redo buttons
// - Export JSON
// Programmatic operations
authoring.addVerticalLine(Date.now());
authoring.addTextNote(50, 75, 'Critical Point', 'data');
const annotations = authoring.getAnnotations();
const json = authoring.exportJSON();
// Cleanup
authoring.dispose();
chart.dispose();
Programmatic Annotation Management
// Add annotations dynamically
chart.setOption({
...chart.options,
annotations: [
...chart.options.annotations ?? [],
{
type: 'lineX',
x: newEventTimestamp,
style: { color: '#00ff00', lineWidth: 2 },
label: { text: 'New Event' }
}
]
});
// Update existing annotation
const annotations = chart.options.annotations ?? [];
const updated = annotations.map((a, i) =>
i === targetIndex
? { ...a, style: { ...a.style, color: '#ff0000' } }
: a
);
chart.setOption({ ...chart.options, annotations: updated });
// Remove annotation
const filtered = annotations.filter((_, i) => i !== removeIndex);
chart.setOption({ ...chart.options, annotations: filtered });
JSON Export and Import
// Export annotations
const authoring = createAnnotationAuthoring(container, chart);
const json = authoring.exportJSON();
localStorage.setItem('chart-annotations', json);
// Import annotations
const storedJson = localStorage.getItem('chart-annotations');
if (storedJson) {
const annotations = JSON.parse(storedJson) as AnnotationConfig[];
chart.setOption({ ...chart.options, annotations });
}
Advanced Use Cases
Dynamic Thresholds with Labels
function addThreshold(y: number, label: string, color: string) {
const annotations = chart.options.annotations ?? [];
chart.setOption({
...chart.options,
annotations: [
...annotations,
{
type: 'lineY',
y,
layer: 'belowSeries',
style: { color, lineWidth: 2, lineDash: [4, 4], opacity: 0.9 },
label: {
template: `${label}={y}`,
decimals: 2,
offset: [8, -8],
anchor: 'start',
background: {
color: '#000000',
opacity: 0.6,
padding: [2, 6, 2, 6],
borderRadius: 4
}
}
}
]
});
}
addThreshold(100, 'Max', '#ef4444');
addThreshold(50, 'Target', '#22c55e');
addThreshold(0, 'Min', '#3b82f6');
Event Timeline with Vertical Lines
const events = [
{ timestamp: 1704067200000, label: 'Release 1.0', color: '#22c55e' },
{ timestamp: 1704153600000, label: 'Bug Fix', color: '#f97316' },
{ timestamp: 1704240000000, label: 'Release 2.0', color: '#3b82f6' },
];
const annotations: AnnotationConfig[] = events.map(evt => ({
type: 'lineX',
x: evt.timestamp,
layer: 'belowSeries',
style: { color: evt.color, lineWidth: 2 },
label: {
text: evt.label,
offset: [8, 10],
anchor: 'start',
background: { color: '#000000', opacity: 0.7, padding: [2, 6, 2, 6], borderRadius: 4 }
}
}));
chart.setOption({ ...chart.options, annotations });
Peak and Trough Markers
function findExtrema(data: DataPoint[]): { max: DataPoint; min: DataPoint } {
let max = data[0]!;
let min = data[0]!;
for (const point of data) {
const y = Array.isArray(point) ? point[1] : point.y;
const maxY = Array.isArray(max) ? max[1] : max.y;
const minY = Array.isArray(min) ? min[1] : min.y;
if (y > maxY) max = point;
if (y < minY) min = point;
}
return { max, min };
}
const { max, min } = findExtrema(seriesData);
const maxX = Array.isArray(max) ? max[0] : max.x;
const maxY = Array.isArray(max) ? max[1] : max.y;
const minX = Array.isArray(min) ? min[0] : min.x;
const minY = Array.isArray(min) ? min[1] : min.y;
const annotations: AnnotationConfig[] = [
{
type: 'point',
x: maxX,
y: maxY,
layer: 'aboveSeries',
marker: { symbol: 'circle', size: 10, style: { color: '#22c55e' } },
label: { template: 'Peak: {y}', decimals: 2, offset: [10, -10] }
},
{
type: 'point',
x: minX,
y: minY,
layer: 'aboveSeries',
marker: { symbol: 'circle', size: 10, style: { color: '#ef4444' } },
label: { template: 'Trough: {y}', decimals: 2, offset: [10, 10] }
}
];
chart.setOption({ ...chart.options, annotations });
Plot-Space HUD Overlays
const hudAnnotations: AnnotationConfig[] = [
// Top-left watermark
{
type: 'text',
position: { space: 'plot', x: 0.05, y: 0.05 },
text: '© 2024 Company',
layer: 'aboveSeries',
style: { color: '#ffffff', opacity: 0.3 }
},
// Bottom-right status
{
type: 'text',
position: { space: 'plot', x: 0.95, y: 0.95 },
text: 'Live',
layer: 'aboveSeries',
style: { color: '#22c55e', opacity: 1 }
},
// Center banner
{
type: 'text',
position: { space: 'plot', x: 0.5, y: 0.5 },
text: 'DRAFT',
layer: 'belowSeries',
style: { color: '#ef4444', opacity: 0.2 }
}
];
chart.setOption({ ...chart.options, annotations: hudAnnotations });
Custom Right-Click Handler
const canvas = container.querySelector('canvas')!;
canvas.addEventListener('contextmenu', (e) => {
e.preventDefault();
const hit = chart.hitTest(e);
if (hit.isInGrid) {
// Show custom menu at hit.gridX, hit.gridY (CSS pixels relative to canvas)
// Use hit.match for snap-to-data behavior
if (hit.match) {
console.log('Right-clicked near data point:', hit.match.value);
// Add annotation at exact data point
const [x, y] = hit.match.value;
// ... add annotation programmatically
} else {
console.log('Right-clicked in plot area at grid position:', hit.gridX, hit.gridY);
// ... convert grid coordinates to data-space and add annotation
}
}
});
Custom visuals beyond built-in annotations
If you need a custom, non-standard visual element “inside the chart”, ChartGPU offers a few practical paths depending on how deep you need to go:
- Use built-in annotations (recommended)
lineX,lineY,point,text, andbandXcover most “mark it / label it / highlight it” use cases and integrate with zoom/pan automatically (data-space) or stay pinned (plot-space). SeeChartGPUOptions.annotationsand the examples above. - Model it as dataif your custom element can be represented as points/lines/bars, encode it as a normal series (often a
scatterseries for custom glyphs + labels via annotations). - Overlay your own layeradd an absolutely-positioned DOM/canvas/WebGPU overlay on top of the chart’s canvas. This gives you full rendering freedom without modifying ChartGPU, but you’ll need to handle coordinate transforms, DPR, and clipping to the plot area (use
options.gridand listen to'zoomRangeChange'to re-render). - Fork for true WebGPU injectionChartGPU does not currently expose a public “custom render pass” plugin hook into its internal WebGPU render pass. If you need arbitrary shaders/draw calls in the same pipeline, fork ChartGPU and add a renderer under
src/renderers/, wiring it intosrc/core/createRenderCoordinator.ts. Start with Internals (GitHub) and the renderer map.
If you’re building your own WebGPU visualization from scratch (outside ChartGPU), the low-level exports in GPU context (GitHub) and Render scheduler (GitHub) are the intended public starting points.
Troubleshooting
Common Issues
Annotations not visible:
- Check
layerproperty - ensure it's set appropriately for your use case - Verify coordinates are within the visible data range
- Check
style.opacity- ensure it's not 0 or very low - For plot-space text: verify x and y are in [0, 1] range
Drag not working:
- Ensure
createAnnotationAuthoring(...)has been called - Check that pointer events are not being blocked by other elements
- Verify canvas is not inside a container with
pointer-events: none
Context menu not appearing:
- Ensure
enableContextMenu: truein options - Check that browser's default context menu isn't blocking it
- Verify z-index is high enough (
menuZIndexoption)
Poor performance with many annotations:
- Limit to ~20-30 visible annotations for optimal performance
- Use
layer: 'belowSeries'for background annotations - Avoid very wide
lineTolerancein hit testing
Undo/redo not working:
- Ensure you're using the programmatic API through
AnnotationAuthoringInstance - Direct calls to
chart.setOption(...)bypass the history system - Check that
dispose()hasn't been called
Labels overlapping:
- Adjust
label.offsetto shift label position - Use
label.anchorto control alignment - Consider reducing the number of labels or using conditional rendering
Best Practices
Performance:
- Keep annotation count reasonable (<50 for smooth smooth interactive frame rates)
- Use
layer: 'belowSeries'for decorative elements - Batch annotation updates in a single
setOption(...)call
Coordinate systems:
- Use data-space for annotations that track with zoom/pan
- Use plot-space for HUD elements that stay pinned
- Test with different zoom levels to verify coordinate behavior
Styling:
- Use high-contrast colors for visibility
- Set appropriate
opacityfor layering (0.8-0.95 typical) - Use
lineDashpatterns for distinction from series lines - Add label backgrounds for readability over complex charts
Lifecycle:
- Always call
authoring.dispose()beforechart.dispose() - Remove event listeners if implementing custom interactions
- Clear large annotation arrays when switching datasets
Accessibility:
- Provide meaningful label text for screen readers
- Use sufficient color contrast (consider colorblind users)
- Include keyboard navigation for custom implementations
See Also
- Interaction API - Event handling, zoom, and pan
- Options API - Chart configuration reference
- Chart API - ChartGPU instance methods
- Example: Annotation Authoring - Interactive demo
- TypeScript Types - Full type definitions
API reference for the linked @chartgpu/chartgpu package (v0.4.0). Source on GitHub.