Import view composition from the exact subpath:
import {
alignX,
alignY,
composeViews,
fill,
grid,
inset,
layer,
shareX,
shareY,
viewGrid,
} from '@tanstack/charts/view'composeViews(options) combines named static chart definitions into one static chart definition. Each child remains a complete chart with its own marks, scales, guides, margins, color, clipping, and static legends.
The public types are ComposeViewsOptions, ViewDefinitions, ViewLayout, ViewAnchor, ViewInsetOptions, ViewGridCell, ViewAxis, ViewScaleLinkMode, ViewScaleLink, ViewTrack, ViewLink, ViewGridItem, and ViewGridOptions.
fill, layer, and inset can place a polar chart over a Cartesian chart:
import { defineChart } from '@tanstack/charts'
import { pie, polar, radialArc } from '@tanstack/charts/polar'
import { composeViews, fill, inset, layer } from '@tanstack/charts/view'
const summaryRows = [
{ status: 'Complete', count: 72 },
{ status: 'Remaining', count: 28 },
]
const slices = pie(summaryRows, { value: 'count' })
const summaryDefinition = defineChart({
marks: [
polar({
inset: 8,
marks: [
radialArc(slices, {
innerRadius: ({ radius }) => radius * 0.58,
color: 'status',
key: 'status',
}),
],
}),
],
guides: false,
margin: 0,
})
const definition = composeViews({
views: {
detail: scatterDefinition,
summary: summaryDefinition,
},
layout: layer(
fill('detail'),
inset('summary', {
relativeTo: 'detail',
anchor: 'top-right',
width: 160,
height: 160,
offset: 12,
}),
),
})layer resolves children in paint order, so the summary paints after the detail chart. Every child is clipped to its resolved frame. Transparent summary space and the donut hole add no interaction target, leaving detail geometry behind them eligible for outer-chart focus.
An inset is relative to the complete resolved frame named by relativeTo, not that child's inner plot rectangle. Its preferred width, height, and offset shrink proportionally when the referenced frame is too small. The referenced view must appear earlier in paint order.
Every key in views must be placed exactly once. Unknown, missing, and duplicate view placements fail instead of producing a partial scene.
grid accepts fixed and flexible tracks:
Fixed sizes and minimums shrink deterministically when the host is smaller than their preferred total. One view may occupy each row-and-column cell.
const definition = composeViews({
views: {
main: scatterDefinition,
top: xHistogramDefinition,
right: yHistogramDefinition,
},
layout: grid({
rows: [
{ id: 'top', size: 72 },
{ id: 'main', grow: 1 },
],
columns: [
{ id: 'main', grow: 1 },
{ id: 'right', size: 72 },
],
gap: 8,
cells: {
main: { row: 'main', column: 'main' },
top: { row: 'top', column: 'main' },
right: { row: 'main', column: 'right' },
},
}),
links: [shareX('top', 'main'), shareY('right', 'main')],
})Scale links are separate from layout:
Linked views must have equal allocated frames along the linked axis. For example, x-linked views must have the same frame x position and width.
Sharing does not copy or infer another view's domain. Configure the intended domain on both child definitions so a mismatch fails visibly.
The composed definition has one chart host, accessible chart label, tooltip, keyboard model, focus strategy, and animation lifecycle. Child points keep their datum identity and receive stable view-prefixed keys and mark IDs. One outer default focus layer covers every child; explicit child focus and mark-state layers remain part of their child scenes.
Apply host options to the composed definition:
const interactiveDefinition = defineChart(definition, {
keyboard: true,
maxFocusDistance: 40,
})Children must be static definitions. Child-owned host behavior and resources that cannot be adopted into the outer scene are rejected, including selection, behaviors, interactive controls, tooltips, keyboard options, definition-level motion, gradients, scene backgrounds, and guide motion. Mark-local motion and static legends remain supported. Use an ordinary mark for a child background.
Use separate chart hosts when panels need independent host behavior or independent accessible labels.
viewGrid remains an ergonomic wrapper for a non-overlapping grid. It lowers to the same composition, layout, and scale-link machinery:
const definition = viewGrid({
rows: [
{ id: 'top', size: 72 },
{ id: 'main', grow: 1 },
],
columns: [{ id: 'main', grow: 1 }],
gap: 8,
views: [
{
id: 'top',
row: 'top',
column: 'main',
share: { x: 'main' },
chart: xHistogramDefinition,
},
{
id: 'main',
row: 'main',
column: 'main',
chart: scatterDefinition,
},
],
})Use composeViews when the layout combines grids, layers, or insets. Use viewGrid when one named view per grid cell is the clearest expression.