Adding a series plugin
This checklist is for contributors adding a built-in series plugin to Charts.
1. Decide capabilities
Compare existing plugins and their actual behavior. For each capability, document Supported, Limited (with constraints), or Unsupported in the series guide. Specify subfeatures separately and test every support claim.
| Capability | Specify support for |
|---|---|
| Axis types | Axes or no axes; linear, logarithmic, datetime for X and Y separately. |
| Category axes | X/Y; category names and indices; category sorting. |
| Axis bounds and order | Explicit min/max and reversed axes. |
| Multiple Y axes | Series assignment to independent Y axes. |
| Split plots | Series displayed in separate plots. |
| Mixed series | Compatible types and shared tooltips. |
| Zoom | x, y, xy, default direction, and reset. |
| Range slider | Preview, filtering, visibility/style overrides, and combined use with zoom. |
| Missing data | null/undefined and skip/connect/zero modes. |
| Colors | Palette, series/point overrides, gradients, and continuous color scales. |
| Legend | Discrete/continuous, symbols, grouping, and hide/show. |
| Data labels | SVG/HTML, explicit labels, formatters/renderers, point overrides, placement, and overlap. |
| Tooltip | Headers, custom rows/renderers/formatters, sorting/totals, and point visibility overrides. |
| Markers and states | Normal/hover markers, point overrides, hover/inactive styles, and halos. |
| Events | Click, pointermove, and cursor. |
| Annotations and overlays | Point annotations; axis plot lines, bands, and shapes. |
| Crosshair | X/Y, pointer following, and snapping to data. |
2. Define the API
- Match established names, units, defaults, and option precedence.
- Define documented series, point, prepared-data, and tooltip types. Extend
SERIES_TYPE,ChartSeries,ChartSeriesData,ChartSeriesOptions,PreparedSeries,TooltipDataChunk, and public exports as needed. - Keep plugin-only fields out of
Base*. Do not widen the registry toany.
3. Implement the pipeline
- Implement SeriesPlugin in
src/plugins/<type>/: validation, series/shape preparation, rendering, interaction lookup, and tooltip rows. Geometry currently lives insrc/core/shapes/<type>/. - Reuse shared helpers; preserve raw data and callback payloads, cache measurements, and clean up subscriptions.
- Implement domain/baseline rules, clipping, plot offsets, and layer/category order; define fill/stroke behavior where applicable.
- Optional
getXAxisPadding(series)requests label space in pixels at the max end of an autoscaled X domain. Core uses the largest request across series; an explicitxAxis.maxdisables that reserve. Keep label measurement and placement policy in the plugin. - For intervals, handle incomplete points and zoom overlap. For paths, preserve boundary neighbors. Keep null/visibility rules consistent across shapes, domains, gradients, and hit testing.
Axis domains
Implement getAxisDomainValues({series, axis}) when scalar point coordinates do not describe the series domain. It receives an ordered group of this plugin's raw or prepared series and the axis direction. Return an array of unformatted values for core to merge and deduplicate across plugins. Returning [] excludes the group; omitting the callback or returning undefined uses scalar x/y coordinates of axis-related series.
For independent points, flatten their values or interval endpoints. Calculate stacks and cumulative values for the whole group. The core/series/stacked-domain.ts helper accepts coordinate roles and a point selector, grouping stacks and summing each sign separately.
The caller selects visibility, zoom filtering, and axis membership. Preserve that selection, including the all-hidden fallback; do not filter hidden series again or mutate input points or their order.
Declare separate axisBaseline: 'x' or 'y' metadata when the value axis needs a baseline. Core includes zero in free linear bounds if any plugin requests it, considering all contributions on the axis. Explicit bounds and zoom take precedence. Logarithmic Y retains positive headroom; logarithmic X and percent domains follow their scale policies.
4. Define layers and clipping
- Optional
getLayerKey({series, seriesKey})selects a layer for one raw or prepared series occurrence. By default, core usesseries.type, so all series of a plugin share one layer. Equal returned keys share a layer; use stable, chart-unique keys. Core throwsChartErrorif different series types return the same key.seriesKeyidentifies this occurrence: core supplies a type/index key for raw input and the series ID for prepared input. The line plugin returnsseriesKeyfor independent layers, including when the same raw object occurs twice. - Core retains series references and member order, orders layers by their first occurrence, prepares shapes in reverse order to reserve label space through
otherLayers, and renders in forward order. Bars retain one layer per type; see the TODO onSeriesPlugin.getLayerKeyfor future layer splitting. renderShapes(args)draws geometry intoplotand SVG labels intolabelsPlot. Core placeslabelsPlotabove the layer's shapes and normal/hover markers, with the same clipping asplot.- Register asynchronous DOM work with
args.registerCompletionduring this call soreflow()waits for its insertion. Checkargs.signalbefore inserting delayed nodes; it is aborted when the preparation is superseded or unmounted. Keep main geometry synchronous and contain failures of optional nodes inside the renderer. - Optional
getClipPath({isRangeSlider, yAxis, zoomState})returns'bounds'(default),'horizontal'(X bounds with an expanded vertical region), orfalse. Core owns SVG IDs and geometry. Clipping affects the shape group and its SVG labels; separate marker, annotation, and HTML-label layers are unaffected. - Line previews use
'bounds'in the range slider. Main-plot lines use'horizontal'unless explicit Y limits or zoom require'bounds'.
5. Define tooltip values
tooltip.prepareDatareturns hovered chunks or, for X-aligned series,xLookupPoints. Mark at most one own chunkclosest: true. In mixed charts (measureDistance: true), theclosestchunk and every lookup point must setdistance: the pixel distance from the pointer to the drawn shape of its series,0when the pointer is inside it. Core keepsclosestonly on the chunk with the smallest distance (on ties, the series drawn in the upper layer wins; an unmeasured distance loses to any measured one), so the tooltip, click events and crosshair snapping agree on a single active chunk.xLookupPointsof all series compete as one group: in the nearest X column the series with the smallest distance wins, then the Y rule applies. Core stripsdistancebefore chunks reach public callbacks. With a single series type nothing competes, so core passesmeasureDistance: falseand the plugin must skip measuring. Exceptions: plugins that report a constant0because the pointer hits the shape, and plugins that need the measurement to pick their own candidate (scatter measures marker contours so that a large marker containing the pointer beats a nearer center).- Measure the rendered geometry, not the data points: account for interpolation curves, stroke widths, line caps, rounded corners and marker sizes. Reuse the helpers of
src/core/utils/geometry.ts(segments, stroked segments and polylines, rectangles, rounded rectangles, polygons) andgetDistanceToSymbolfromsrc/core/utils/symbol.tsfor marker symbols;getDistanceToMarkersfromsrc/core/shapes/marker.tscovers line and area markers. Joins are treated as round and dash gaps are ignored. Plugins that return a chunk only when the pointer hits a shape use0. Build reusable geometry lazily and cache it per prepared item (as line caches its drawn segments ingetSegments()and area caches its band columns) instead of rebuilding it on every pointer move. - The active chunk and shared hover highlighting are separate: core selects the single active chunk, while each renderer decides how to highlight its hovered chunks. Line and area highlight every hovered chunk of their type, scatter highlights only the
closestone. - Implement
tooltip.getValue({item, xAxis, yAxis})hook for built-in sorting and totals. Shared code delegates each hovered chunk to its plugin, including in mixed charts. The hook is optional for compatibility; omitting it falls back to the point's scalarvalue, or its Y value. - Return an unformatted value and preserve
null/undefined. UsegetTooltipAxisValuefromsrc/core/tooltip/utils.tsfor axis values: it resolves category indices to names and preserves numeric/date values. Non-axis plugins extract their scalar value; interval plugins can return a width (asarea-rangedoes). - Declare
tooltip.header.getValuefor series with a header; omitheaderfor series without one. ReusegetTooltipXValue/getTooltipYValueandgetTooltipScalarValuefor common extraction. In mixed tooltips, the highest header priority wins (ties use the first hovered chunk): X headers use the default0, horizontal series use1, and radar categories use2. Values are resolved and formatted once. - Set
header.axisto the axis supplying the header value and category context. It also selects automatic number/date formatting; date precision uses enabled points from visible series contributing headers to that axis, with each Y axis kept separate. Omitaxisfor non-axis headers; setrequiresFormat: truewhen the header should appear only with an explicittooltip.headerFormat(as radar does). - A missing header value differs from an omitted
header: a custom formatter can replace missing continuous or non-axis values with a placeholder, but unresolved categories and headerless plugins omit the header. ExplicitheaderFormatoverrides automatic formatting and is passed to custom renderers together with the original chunks. - Keep tooltip row values, custom formatter context, and renderer payloads independent of the sorting/totals value. Shared code sorts the plugin values and sums only numeric values.
- Use
source: 'color'for swatches; format labels/endpoints once. Preserve renderer precedence and keep plugin formatting hooks internal.
Interaction lookup
Implement interaction.prepareNearestData for unfiltered {position, item} candidates and interaction.getTargetElements for painted SVG elements. Reuse prepared geometry; shared utilities handle distance comparison, hit testing, clipping, and ordering. Keep tooltip selection in tooltip.prepareData; use interaction.getClosestItem only when its result needs conversion (for example, a Sankey link).
Keep adapters in the plugin's interaction.ts and concrete payload types in its interaction-types.ts. Shared code and the public interaction API use generic contracts from core/types/chart/interaction.ts, without a union or mapping of built-in series. Do not import plugin payload types into shared code or public type barrels, including via type-only imports. Include permanent markers, exclude hover-only decorations, and preserve point identity when several SVG parts represent one item.
When preparing a copy of a source point, register it with withSourcePoint from core/utils/source-point. Public interaction results then resolve the original object without changing the data used by rendering or legacy callbacks. Resolve nested source points in composite payloads with getSourcePoint.
6. Integrate
- Register in plugins/index.ts; add applicable defaults.
- Check shared axis, scale, header, grouping, and zoom assumptions. Extend the plugin contract where needed; never add shared series-name branches or lists.
7. Test
- Cover declared capabilities, limitations, feature combinations, and affected existing plugins. Always check empty prepared data, zero-size plots, and resize/data/visibility updates.
- Run
npm run typecheck, focused unit tests, Docker visual tests, andnpm run test:chart-config. Inspect snapshots, declarations, and JSON Schema; preserve unrelated baselines.
8. Document
- Add a Storybook example, series guide, API-doc export, runnable docs example, and navigation/registry entries.
- Explain defaults, constraints, and tooltip value semantics.