Facets repeat one encoding across groups. Multiple-view compositions place different but related encodings together. Both reduce the amount of data one plot must carry, but they answer different questions.
Use facets when every panel has the same role. Use distinct linked views when one panel provides context, a marginal distribution, controls, or detail for another.
| Reader question | Start with |
|---|---|
| How does the same distribution differ across cohorts? | Shared-scale facets |
| How does a bivariate relationship relate to each marginal shape? | Scatterplot with marginal histograms |
| Which portion of a long time domain should the detail view show? | Focus-plus-context views |
| Must every group use a different scale or mark composition? | Independent named chart views |
| Should selecting one view filter or highlight another? | Linked views with shared application state |
Faceting and Composition owns the implementation boundary. This page helps choose a layout and verify its comparison semantics.
Faceted distributions give each cohort its own panel while preserving a common binning and positional scale. The reader can compare both local shape and absolute position without decoding overlapping fills.
Use shared domains when position should be directly comparable. Independent domains can make local variation easier to see, but they can also exaggerate small differences. If independent scales are intentional, give panels their own guides and state the policy.
Keep the denominator visible by grouping both transforms:
const bins = normalize(
binX(rows, {
value: 'body_mass_g',
by: 'species',
thresholds,
outputs: { count: { reduce: 'count' } },
}),
{
value: 'count',
by: 'species',
basis: 'sum',
as: 'proportion',
},
)binX owns aggregate lineage and normalize divides each species by its own count. Omit the second by only when the global population is the intended denominator.
A scatterplot with marginal histograms combines three roles:
All regions can derive from the same raw observations, but they do not share the same marks or plot rectangle. This example uses public binX and binY transforms inside three ordinary chart definitions. Place them with composeViews and grid, then use shareX and shareY to match the scatterplot's resolved scales and plot ranges. viewGrid is concise syntax for the same non-overlapping layout.
Use separate chart hosts when each view needs independent interaction or an independent accessible label. See View Composition for the single-figure layout contract.
An inset can use a different coordinate system from the chart behind it. This places a donut summary over a Cartesian detail chart without teaching either child about the other:
import { composeViews, fill, inset, layer } from '@tanstack/charts/view'
const definition = composeViews({
views: {
detail: detailDefinition,
summary: donutDefinition,
},
layout: layer(
fill('detail'),
inset('summary', {
relativeTo: 'detail',
anchor: 'top-right',
width: 160,
height: 160,
offset: 12,
}),
),
})The inset is anchored to the complete detail frame. It is clipped to its own frame and shrinks proportionally when the host is too small. Because the child scenes share one outer interaction model, empty inset space and the donut hole leave detail geometry behind them eligible for focus.
A focus-plus-context layout uses a compact overview to control the explicit domain of a larger detail chart. The selection is semantic application state, not a rectangle stored inside one renderer.
The overview should retain the complete domain. The detail should receive the selected start and end as its configured scale domain. Pointer, touch, and keyboard controls should update the same semantic values, and those values should survive data revisions and resizes.
See Interactions and Selections for controlled brushes, zoom, and linked state.
Shared selection, cursor, category, or domain state belongs in the application:
Do not query one SVG for a pixel and apply that pixel directly to another view. Different margins, widths, orientations, and scales can represent the same semantic value at different coordinates.
For focus-driven cursors, createChartCursor provides that application-owned state without an overlay or callback relay:
import { createChartCursor, cursorHost } from '@tanstack/charts/cursor'
const sharedDate = createChartCursor<Date, number>()
const cursor = {
use: cursorHost,
controller: sharedDate,
mode: 'focus' as const,
match: 'x' as const,
pin: true,
}
const current = defineChart(currentSpec, { cursor })
const previous = defineChart(previousSpec, { cursor })Pointer, responder, keyboard, or accessibility focus in either browser or React Native chart stores the semantic date. Every subscriber maps that date through its own x scale, resolves its local focus group, and paints its own crosshair({ y: false }). Widths, margins, and y domains can differ. A pinned cursor remains shared until another activation, an escape action, or a programmatic clear dismisses it.
Use anchor: 'value' when setting a controller programmatically for semantic sync. Normalized or scene anchors intentionally share relative or local pixel coordinates instead.
For the underlying row and channel model, see Data and Channels.