Use a custom mark when a visualization fits the shared scene model but is not expressible as a useful composition of built-in Cartesian, polar, or geographic marks.
Use a custom renderer when the same chart scene needs a different mounted surface. Use a custom SVG serializer when only SVG markup or resources differ.
Neither extension should reach into private scene compiler state.
Before creating a mark, check whether the result is a combination of:
Composition retains built-in type inference, focus metadata, animation, and subpath bundle boundaries. The chart examples show boxplots, candlesticks, networks, and annotations built this way.
createMark is the normal extension boundary:
import { createMark } from '@tanstack/charts'
interface ThresholdDatum {
id: string
value: number
}
const threshold = createMark<ThresholdDatum, never, number>(({ markIndex }) => {
const id = `threshold-${markIndex}`
const datum: ThresholdDatum = { id: 'target', value: 75 }
return {
id,
channels: {
y: {
scale: 'y',
values: [datum.value],
},
},
render({ chart, scales, theme }) {
const y = scales.y.map(datum.value)
return {
nodes: [
{
kind: 'rule',
key: datum.id,
x1: chart.x,
x2: chart.x + chart.width,
y1: y,
y2: y,
style: {
stroke: theme.foreground,
strokeOpacity: 0.55,
},
},
],
}
},
}
})initialize materializes channels for one scene build. render receives the resolved plot bounds, scales, theme, color resolver, and text layout tools.
Available scene nodes:
Every node requires a deterministic key.
The threshold above is decorative, so it emits no points. Return ChartPoint records when custom geometry should participate in focus, tooltips, keyboard navigation, or selection.
Each point should retain:
Omit points for decorative geometry. Do not invent fake interactive data for a frame, grid, or threshold that should not receive focus.
Most marks use the same value type for interaction and scale domains. When they intentionally differ, import the advanced factory:
import { createMarkWithScaleValues } from '@tanstack/charts/mark/scale-values'
createMarkWithScaleValues<Datum, PointX, PointY, ScaleX, ScaleY>(initialize)This is useful for interval endpoints or custom layouts whose interactive anchor is not the complete set of values materialized on an axis.
Use ChartMarkPointX and ChartMarkPointY for the interaction contract and ChartMarkScaleX and ChartMarkScaleY for the positional contract. Ordinary chart code should rely on definition inference instead.
Configured callable D3 scales are the normal path. ChartScale, ChartColorScale, and ChartColorLegend exist for context-aware adapters that need chart range, theme, or responsive legend geometry.
Keep specialized scale dependencies in the module that uses them. A line-only bundle must not pay for a custom scale registered elsewhere.
See Scales and D3 and Legends and Color.
A full renderer implements ChartRenderer and returns a ChartSurface:
import { mountChartRenderer } from '@tanstack/charts/renderer'
const host = mountChartRenderer(container, {
definition,
renderer: myRenderer,
ariaLabel: 'Threshold history',
})The renderer owns server shell markup, its mounted element, scene painting, coordinate conversion, focus painting, and cleanup. The host retains sizing, runtime, keyboard, tooltip, selection, and focus-strategy behavior. Keep prerender deterministic and make mount adopt compatible server markup.
Use ChartRendererRenderContext.surface instead of assuming onRender exposes an SVG element. Framework consumers pass renderer through @tanstack/react-charts/core or @tanstack/octane-charts/core.
A ChartSvgRenderer accepts the complete ChartScene and accessible SVG options:
const renderSvg: ChartSvgRenderer<Row, Date, number> = (scene, options) => {
return serializeMySvg(scene, options)
}Pass it through renderSvg on the vanilla host or any default SVG framework adapter. Preserve:
Use renderChartSvgWithResources from @tanstack/charts/svg/resources when the only missing behavior is gradients or clipping.
A ChartFocusStrategy owns pointer resolution, grouping, and keyboard navigation. Its generic types must remain identical to the chart points it receives.
A ChartSpatialIndexFactory builds optional nearest-point acceleration from scene points. Return original typed points from the index. Do not erase them to unknown and cast them back in callbacks.