TanStack
API Reference

Custom Extensions

TanStack Charts exposes narrow inversion-of-control boundaries around its scene compiler. Prefer composition with built-in marks first. Add an extension when the chart requires geometry or behavior that cannot be expressed without distorting its data model.

Composite marks

compositeMark groups ordinary marks behind one stable parent identity:

ts
import { compositeMark } from '@tanstack/charts/mark/composite'

function compositeMark<
  TMarks extends readonly ChartMark<any, any, any, any, any>[],
>(
  marks: TMarks,
  options?: CompositeMarkOptions<ChartMarkDatum<TMarks[number]>>,
): ChartMark<
  ChartMarkDatum<TMarks[number]>,
  ChartMarkPointX<TMarks[number]>,
  ChartMarkPointY<TMarks[number]>,
  ChartMarkScaleX<TMarks[number]>,
  ChartMarkScaleY<TMarks[number]>
>

CompositeMarkOptions contains optional id and motion fields. The result preserves the union of child datum and positional types. Initialization merges the children's semantic channels under parent and child namespaces. Rendering keeps child order, namespaces scene keys and mark IDs, and retains each child point as a separate interaction target. Parent and child motion definitions merge under the resolved child namespace, with child fields taking precedence.

Every child must have a unique ID and an ordinary initialized render method. A child that owns resolveLayout is rejected; keep one resolved-layout owner instead of nesting scheduling lifecycles. See Custom Marks and Renderers for composition guidance.

Custom marks

ts
import { createMark } from '@tanstack/charts'
ts
function createMark<
  TDatum,
  TXValue extends ChartValue = ChartValue,
  TYValue extends ChartValue = ChartValue,
>(
  initialize: (
    context: MarkInitializeContext,
  ) => MarkInitialization<TDatum, TXValue, TYValue>,
  motion?: ChartMotionDefinition<TDatum>,
): ChartMark<TDatum, TXValue, TYValue>

Initialization runs once per scene compilation and receives the mark's layer index:

ts
interface MarkInitializeContext {
  markIndex: number
}

interface InitializedMark<
  TDatum = unknown,
  TXValue extends ChartValue = ChartValue,
  TYValue extends ChartValue = ChartValue,
> {
  id: string
  channels: Readonly<Record<string, MaterializedChannel>>
  motion?: ChartMotionDefinition<any>
  viewport?: Readonly<Partial<Record<'x' | 'y', 'content' | 'fixed'>>>
  focusGuideOnly?: boolean
  layoutLabels?(context: MarkRenderContext): readonly SceneLabel[]
  render(context: MarkRenderContext): MarkScene<TDatum, TXValue, TYValue>
  resolveLayout?(
    context: MarkResolvedLayoutContext,
  ): ResolvedMarkLayout<TDatum, TXValue, TYValue>
}

MarkInitialization also accepts a ResolvedLayoutMarkInitialization, which has resolveLayout instead of an initial render. createMark normalizes both forms to InitializedMark, so wrappers around ordinary marks retain a callable render.

The optional factory motion is copied onto each initialized mark. An initializer may return its own motion when a composite or resolved layout needs a scene-local policy; that value takes precedence over the factory fallback.

Materialized channels declare semantic values before scale resolution:

ts
interface MaterializedChannel {
  scale?: string
  values: readonly unknown[]
  includeZero?: boolean
}

Use scale: 'x', scale: 'y', or scale: 'color' for shared chart scales. includeZero is a hint available to a custom scale resolver. Filter invalid values before materializing them.

viewport overrides presentation ownership independently for x and y. Without an override, a mark is viewport content on an axis when one of its materialized channels uses that axis. 'content' forces the mark into that axis's clipped, translated layer even when no channel establishes the relationship. 'fixed' keeps the mark stationary on that axis even when a channel contributes to its domain. This ownership does not change scale-domain contribution.

The exported ChartContinuousDomain<TValue> type represents viewport state as either readonly [number, number] or readonly [Date, Date], narrowed by the mark and axis value type. Numeric and temporal endpoints cannot be mixed.

Render nodes and points with context.scales[axis].map. The scene compiler applies the mark's viewport translation and remaps its interaction-point references. scale.viewport?.map is for consumers that need the final presented coordinate outside mark rendering.

The render context provides final geometry and shared presentation:

ts
interface MarkRenderContext {
  markIndex: number
  surface: ChartBounds
  chart: ChartBounds
  scales: Readonly<Record<string, ResolvedScale>>
  theme: ChartTheme
  color(value: ChartKey | null | undefined): string
  colors: ResolvedColorScale
  layout: ChartLayoutOptions
}

Final-screen mark layout

Use resolveLayout when binning, collision avoidance, topology, or responsive packing depends on final positional scales and inner bounds:

ts
interface MarkResolvedLayoutContext {
  markIndex: number
  chart: ChartBounds
  scales: Readonly<Record<string, ResolvedScale>>
  theme: ChartTheme
  layout: ChartLayoutOptions
}

interface ResolvedMarkLayout<TDatum, TXValue, TYValue> {
  channels?: Readonly<Record<string, MaterializedChannel>>
  states?: InitializedMark<TDatum, TXValue, TYValue>['states']
  layoutLabels?(context: MarkRenderContext): readonly SceneLabel[]
  render(context: MarkRenderContext): MarkScene<TDatum, TXValue, TYValue>
}

The margin solver may call resolveLayout more than once. Keep it synchronous, pure, deterministic, and free of application state. Initial channels alone establish x/y domains. Resolved channels replace them for final non-positional inference, including color; resolved x/y values never re-domain the positional scales. Derived rows stay inside the returned render closure instead of becoming a cross-mark transform graph.

If a custom mark emits labels that should participate in automatic margins, return the same positioned labels from layoutLabels. The solver may call it more than once with different responsive ranges; keep it pure. render still runs once with the final layout. The built-in Cartesian text mark provides this hook.

Return keyed scene nodes and, when the mark participates in native interaction, typed points:

ts
interface MarkScene<
  TDatum = unknown,
  TXValue extends ChartValue = ChartValue,
  TYValue extends ChartValue = ChartValue,
> {
  nodes: readonly SceneNode[]
  points?: readonly ChartPoint<TDatum, TXValue, TYValue>[]
  focusAnchors?: readonly ChartFocusAnchor[]
  focusGuides?: readonly MarkFocusGuide[]
}

type MarkFocusGuide = Omit<SceneFocusGuide, 'placement'> & {
  placement?: SceneFocusGuide['placement']
}

focusAnchors let whenFocused reveal decorative geometry without making it a pointer or keyboard target. focusGuides describe data-less presentation resolved from current focus or cursor state. MarkFocusGuide has the final SceneFocusGuide fields except that placement is optional. Omit it to let mark order place a guide under or over the first ordinary mark. Supply it only when a composed nested scene must preserve an already resolved placement. Every focus guide supplies a resolve callback that receives SceneFocusGuideResolveContext and returns one transient SceneNode or undefined. The callback stays attached to the optional guide instead of becoming unconditional renderer policy. Import resolveCrosshairGuide from @tanstack/charts/crosshair to reuse the built-in rule, band, label, and marker behavior.

Set focusGuideOnly: true on an initialized mark that contributes only these dynamic guides and no ordinary base-scene content. This explicit classification keeps the mark out of the first-ordinary-mark boundary used by default placement. surface is the required full-surface bounds; chart is the inner plot. Guide rules normally clip to chart, while labels may use surface for clamping.

The practical contracts are in Custom Marks and Renderers.

Mark requirements

  • Give the mark a stable ID. Derive a fallback from markIndex only when layer order is stable.
  • Materialize every value needed to establish positional domains during initialization.
  • Map through context.scales; do not recalculate responsive ranges.
  • Declare viewport ownership when channel inference does not match the mark's content or fixed annotation behavior.
  • Give each scene node and point a deterministic key.
  • Keep presentation-only focus anchors keyed to the nodes they reveal.
  • Emit finite geometry only.
  • Preserve the original datum and index in every interaction point.
  • Use one honest focus coordinate and semantic x/y pair per point.
  • Keep semantic row transforms eager and outside render; use resolveLayout only for work that requires final screen geometry.

The scene node and point shapes are documented in Runtime and scene.

Distinct point and scale values

Interval geometry may materialize endpoint value types that differ from its interaction anchor types. Use the exceptional subpath:

ts
import { createMarkWithScaleValues } from '@tanstack/charts/mark/scale-values'
ts
function createMarkWithScaleValues<
  TDatum,
  TXPointValue extends ChartValue,
  TYPointValue extends ChartValue,
  TXScaleValue extends ChartValue,
  TYScaleValue extends ChartValue,
>(
  initialize: (
    context: MarkInitializeContext,
  ) => MarkInitialization<TDatum, TXPointValue, TYPointValue>,
  motion?: ChartMotionDefinition<TDatum>,
): ChartMark<TDatum, TXPointValue, TYPointValue, TXScaleValue, TYScaleValue>

The subpath also exports ChartMarkPointX, ChartMarkPointY, ChartMarkScaleX, and ChartMarkScaleY. Use it only when the distinction is real; ordinary custom marks should use createMark.

Curves

ChartCurve supplies precomputed path data for line and y-oriented area marks:

ts
interface ChartCurve {
  line(points: readonly (readonly [number, number])[]): string
  area(
    top: readonly (readonly [number, number])[],
    bottom: readonly (readonly [number, number])[],
  ): string
}

AreaXCurve has the transposed contract:

ts
interface AreaXCurve {
  areaX(
    right: readonly (readonly [number, number])[],
    left: readonly (readonly [number, number])[],
  ): string
}

The optional bridges d3Curve from @tanstack/charts/d3/shape and d3AreaXCurve from @tanstack/charts/d3/area-x adapt a supplied curve factory to these contracts. D3 module ownership and granular imports are documented in Scales.

Custom positional scales

A custom ChartScale resolves semantic values and the responsive range into a complete mapping and tick set. This is an unchecked math boundary; prefer a configured callable scale when possible.

See Custom scales for the exact context and return type.

Custom color scales and legends

ChartColorScale maps observed values, domain/range hints, and theme tokens to a ResolvedColorScale. ChartColorLegend independently reserves layout height and emits a scene node.

See Color and Custom legends.

Custom text measurement

ChartTextMeasurer lets nonbrowser rendering, special fonts, or an application-owned typography engine provide painted glyph bounds. It affects guide geometry, not mark text rendering.

See Automatic guide layout for the contract.

Spatial indexes

ChartSpatialIndexFactory replaces the default linear pointer lookup without changing scene compilation. Build a point-only index from its first argument, or use context.scene from its second argument to index resolved primitive bounds. Return the nearest original point within the requested distance. The host recreates the index when the scene or factory changes.

See Spatial indexes. The appropriate granular spatial primitive can be brought through the boundary described in Scales.

Custom focus and gestures

ChartFocusStrategy owns pointer resolution, focus grouping, and keyboard task order. Its resolve(points, context) and group(points, context) methods keep coordinates and the active point in named context bags. Rich gestures can instead disable chart-owned focus and maintain selection or viewport state in the application.

See Focus and interaction.

Custom renderers

A ChartRenderer owns both deterministic server markup and one mounted ChartSurface. The surface renders scenes, converts browser coordinates to scene coordinates, paints focus, and releases renderer-owned resources. mountChartRenderer keeps responsive sizing, runtime updates, focus, keyboard, tooltip, and selection behavior shared across renderers.

Custom surfaces should resolve authored focus layers and data-less guides with resolveFocusPresentation(scene, focus, pointer, cursor), then paint its under nodes, the base scene, and its over nodes in that order.

The scene compiler has already converted every mark-emitted MarkFocusGuide to a SceneFocusGuide with required placement before a renderer receives the scene. resolveFocusPresentation calls each guide's resolver with its local focus, pointer, and cursor context. A custom renderer should consume those resolved nodes and final placement through that helper; it should not call guide resolvers or infer mark order again.

Use @tanstack/charts/renderer directly or the framework /core entries. The optional built-in implementation at @tanstack/charts/canvas demonstrates the boundary without changing the default SVG imports.

For an SVG-only serialization change, pass a ChartSvgRenderer as renderSvg to the compatibility host or adapt it with createSvgChartRenderer from @tanstack/charts/svg/renderer. Preserve the SVG root, stable DOM keys, accessible name, coordinate system, and focus presentation expected by that adapter.

Default SVG serialization already preserves declared gradients and group clips; see Rendering and export.