The top of the chart layout (react-timeseries-charts-style). Owns the shared
x geometry: it collects each row's per-slot gutter widths, reserves each
slot's max across rows (so the innermost axis aligns column-by-column and
every row's plot left-aligns), and from the slot sums derives plotWidth and
the shared time xScale. It renders its rows (separated by rowGap) then one
TimeAxis at the bottom, aligned under the plots. Y axes are per-row
(<YAxis>).
A width in pixels renders straight through; 'auto' (or an omitted
width) measures the available width first — see ChartContainerProps.width and AutoWidthContainer.
Props
axisPanZoomboolean | 'none' | 'x' | 'y' | 'xy'Which axis strips take gestures — the opt-in for grabbing an axis, and
'none' by default so no existing chart changes behaviour:
'none'(orfalse, the default) — the strips are inert chrome.'x'— the<XAxis>strip pans on drag and zooms on wheel, exactly as the plot's own gestures do (same maths, same sign, samebounds/minDurationfences). Double-click returns to the declaredrange. A category axis has no continuous domain and stays inert.'y'— each<YAxis>gutter zooms that one axis on drag or wheel, double-click releasing it back to its fit. Report it to a scale UI withYAxisProps.onBoundsChange.'xy'(ortrue) — both.
Deliberately independent of panZoom, in both directions. A chart
can scale its y axes without letting the plot capture vertical drags (which
would fight a selection sweep), and an interactive plot does not hand its
axes gestures nobody asked for. The one thing they share is the view itself:
the x strip moves the same range the plot's pan does, and reports through
onTimeRangeChange the same way.
The pairing to reach for on a time-series chart is
panZoom="panZoom" axisPanZoom="xy" — drag the plot to pan, drag the x strip
to pan, wheel either to zoom, and drag a y gutter to override its fit.
bandAlign'center' | 'end' | 'start'Where the capped category block sits in the plot when maxBandWidth
binds. Default 'start' — pack from the left, leaving the far side
empty. 'center' and 'end' place it otherwise.
A no-op without maxBandWidth, or when the cap doesn't bind: the block
fills the plot and there is no slack to place. (There is deliberately no
'fill' member — "fill" is what omitting maxBandWidth means, and a
fill value alongside a pitch cap would be a contradiction rather than a
choice.)
boundsreadonly [number, number]Outer pan/zoom extent — [min, max] (same units as range) the
view can never move outside. Panning into an edge stops there (the window
keeps its span); zooming out is capped at this width, so bounds is the
zoom-out ceiling that pairs with the minDuration zoom-in
floor. Omit for no limit (pan/zoom is unbounded). Constrains gestures
(and any range routed through the container); seed range within it.
On a trading-time axis the clamp is in wall-clock ms (a sensible outer
limit; the per-session pan/zoom math already holds the trading span at each
calendar edge).
calendarTradingCalendarLikeThe high-level sugar for discontinuities: a trading calendar the
container derives the provider from itself (calendar.discontinuities({ spacing })), so you don't wire the low-level prop. A @pond-ts/financial
TradingCalendar satisfies the structural TradingCalendarLike shape
(charts never imports that package). Combine with spacing. For the
full option matrix (a bar period, a scoped range) use the low-level
discontinuities prop instead. Only affects a time axis.
The provider is memoized on (calendar, spacing), so pass a stable
calendar reference (build it once, not inline in JSX).
categoriesreadonly string[]Make the x axis ordinal at the container level — one equal-width slot per name, in this order ([PND-IGNITECAT]).
Note this is a list of names (string[]), unlike <BarChart categories>, which takes { label, value } data. The container names the
slots; the bar layer fills them.
Until this prop, the band scale was reachable only through a layer:
<BarChart categories> (and a horizontal heat map) reported
xKind: 'category',
every other layer reported 'time' or 'value', and the container throws
on a mix — so a line, a point or an envelope over categorical bars was
not expressible at all. The workaround was to key every layer to a
synthetic integer index and hand-supply the tick labels, which forfeits two
features the ordinal axis already implements: <XAxis> label thinning
(gated on a category axis with no custom ticks) and the
maxBandWidth / bandAlign slot packing.
Declaring the categories here inverts that. The container owns the ordinal domain, so any value-keyed layer can live on it and both of those features keep working.
Keying a layer to the slots
The band scale's domain is numeric — slot i occupies [i, i+1], so
its centre is i + 0.5. Key a ValueSeries there and the mark lands
on the slot centre, which is also where <XAxis> puts the tick:
const line = ValueSeries.from(
tickers.map((t, i) => ({ x: i + 0.5, target: t.target })),
{ key: 'x' },
);
<ChartContainer categories={tickers.map((t) => t.label)} width="auto">
<ChartRow height={220}>
<YAxis id="v" />
<Layers>
<BarChart categories={bars} />
<LineChart series={line} column="target" axis="v" />
</Layers>
</ChartRow>
</ChartContainer>;
What still errors
- A time-keyed layer. A
TimeSerieshas no slot to sit in; mixing one into an ordinal container is a hard error, as a mixed x-kind always was. - A category layer that disagrees.
<BarChart categories>in an ordinal container must name the same list in the same order — this prop is authoritative, and a silent mismatch would draw bars under the wrong labels.
What declaring it costs
Setting this makes the x axis ordinal, and two container capabilities are defined only on a continuous x. Both were already true of an inferred category axis; they are stated here because this prop lets you opt a previously-continuous container into them:
- x pan and zoom stop.
panZoomkeeps working on y (panY/zoomY), but the x half is gated off — sliding between named slots is not a gesture the axis has a meaning for. rangestops applying to x. The domain is[0, n], derived from the slot count, so an x range is a no-op rather than an error. Show a subset by passing fewer categories.xScalestops applying.'log'/'symlog'describe how a continuous x spaces its values; ordinal slots are evenly spaced by definition, so the kind is ignored (as it already is on a time axis).
The hazard this cannot catch
A value-keyed layer is taken at its word. Anything reporting 'value'
is read as slot coordinates, so a layer whose x means something else
will draw — in the wrong place, silently. The sharpest instance is a
horizontal categorical <BarChart>: its x is bar length, not a
coordinate, so on an ordinal x it plots magnitudes as slot positions.
Don't mix one into an ordinal container.
This is documented rather than enforced, and the reason is worth keeping:
a guard was written for it, testing binCategories. That is the generic
"my y is ordinal" channel, and a vertical heat map sets it too — so
the guard rejected a ValueSeries grid with named columns on x, which is
a wanted layout (ordinal rows plus ordinal columns is just a 2-D grid),
with an error naming a <BarChart> that wasn't in the tree. Nothing on a
layer source distinguishes "my x is a coordinate" from "my x is a
magnitude", so there is no contradiction to detect — and a flag invented
to carry it would buy a false sense of coverage while every other misuse
stayed silent.
One more edge
categories={[]} is an ordinal axis with no slots yet, not a fallback
to time. That is the useful reading for a loading state: the kind stays
put when the data arrives, instead of flipping and rebuilding every scale
mid-session.
Omit for the inferred behaviour: a container with only category layers still resolves its slots from them, exactly as before.
childrenReactNodecreatingAnnotationKindAnnotationKindtype@pond-ts/charts'region' | 'marker' | 'baseline'The kind of an annotation, and of a creation tool.
| nullThe armed annotation creation tool (the consumer's toolbar sets it), or
null/omitted for idle. When set, the plot captures a create gesture — a
preview tracks the pointer, and on release onCreate fires. The consumer
then adds the mark, disarms (back to null), and selects it (spring-loaded);
keep it set to place several. Requires editAnnotations.
crosshairSnapbooleancursor="crosshair" reticle y snapping. Default true — the
crosshair centres on the nearest data point (the horizontal line snaps to
that sample's value). false — the horizontal line + centre follow the
pointer y freely, the value read as yScale.invert(pointerY). Either way
the vertical line snaps its x to the data grid (so the time readout is
clean), and both draw a full-height dashed vertical + full-width dashed
horizontal line.
cursorCursorModeCursorModetype@pond-ts/charts'none' | 'line' | 'point' | 'inline' | 'flag' | 'crosshair' | 'region'The in-chart cursor presentation for a row (the synced vertical line is shared
across rows). Exclusive modes — pick one:
In-chart cursor presentation — the default for all rows (a row may override
via <ChartRow cursor>). Default 'line' — the synced vertical line,
with values surfaced outside the chart via onTrackerChanged.
'point' / 'inline' / 'flag' add per-series marks; 'none' hides it.
'region' shades the bucket under the pointer (needs cursorSequence).
See CursorMode.
cursorFormatCursorFormatCursorFormattype@pond-ts/chartsstring | (value: number, ctx: { defaultText: string; grain: TimeGrain | undefined }) => stringHow to format the cursor / marker readout on the x axis
(ChartContainerProps.cursorFormat) — time or value kind. Either:
The cursor / marker readout format — the crosshair x pill, marker
axis indicators, and annotation auto-labels — independent of the tick
labels on both axis kinds: it does not disqualify the dateStyle
ladder (time), and it never moves the tick labels (value). It beats an
explicit <XAxis format> for the readout only — pill precedence is
cursorFormat → axis format → container — so terse ticks can pair with a
precise readout (+2.0σ labels, +1.83σ pill).
Omitted ⇒ the axis's own formatter. On a time axis that default is
grain-aware: the readout formats at the axis's granularity, so a
day-or-coarser axis reads a date (never a time-of-day) and a sub-day
axis reads date + clock — a daily bar at a foreign-tz midnight no longer
renders as 02 AM. On a value axis it is the tick formatter
(timeFormat-shaped, else the d3 default).
A d3 specifier string formats uniformly (time specifier on a time
axis, number specifier on a value axis); a function
(value, { grain, defaultText }) => string receives the axis's resolved
coarse TimeGrain (undefined on a value axis) and the default
readout text, so it can branch on the zoom level and pass defaultText
through for grains it doesn't override (no re-deriving the grain from the
range). See CursorFormat. This is the independent readout channel;
timeFormat owns the labels. (A category axis reads names, and a
transformed axis's pill speaks its derived unit — neither consults
cursorFormat.)
cursorSequenceSequenceSequenceclasspond-tsAn unbounded fixed-step grid definition used for alignment or aggregation.
| BoundedSequenceBoundedSequenceclasspond-tsA finite ordered list of Interval buckets.
The bucketing for cursor="region" — the interval highlighted under the
pointer. A pond Sequence (duration or calendar-aware —
Sequence.every('1d'), Sequence.calendar('month')) is realized over the
current view; a BoundedSequence (e.g. a TradingCalendar's
sessionSequence() / barSequence()) is used as-is, so the band can track
whole sessions. Either way the band maps through xScale, so on a
trading-time axis the closed part of the bucket collapses. Ignored unless
cursor="region".
Time axis only. A bucket is a time interval, so the region cursor is gated to a time x-axis — on a value axis (a horizontal histogram, a value-keyed chart) it's a no-op (highlighting a value band on a horizontal histogram would be a different, y-oriented cursor).
Pass a stable reference. The buckets are memoized on this value + the
view range; a Sequence/BoundedSequence rebuilt inline every render
re-realizes the buckets on each pointer move (harmless for a coarse
day/session sequence, wasteful for a fine one over a wide view) — hoist it or
useMemo it.
cursorTimebooleanShow the cursor's time atop the in-chart readout (when a row's cursor draws
one). Default false. Formatted by timeFormat to match the time
axis.
discontinuitiesDiscontinuityProviderDiscontinuityProviderinterface@pond-ts/charts{ boundaries?: unknown; clampDown: unknown; clampUp: unknown; copy: unknown; distance: unknown; offset: unknown }The structural discontinuity-provider surface scaleTradingTime consumes to
collapse closed-market time. Charts declares this shape itself and never
imports @pond-ts/financial — a TradingCalendar.discontinuities() provider
satisfies it structurally, so the packages stay decoupled (trading-calendar
RFC §6.1). Domain values are epoch-milliseconds.
A trading-calendar discontinuity provider — closed-market time
(weekends, holidays, overnight, lunch breaks) collapsed. Supply it to turn
the shared x axis into a trading-time axis: gaps disappear and time
stays proportional within each session. A @pond-ts/financial
TradingCalendar.discontinuities() satisfies this structurally (charts
never imports that package). The low-level primitive: pass
calendar.discontinuities() (or a { spacing, period } variant) directly.
Only affects a time axis (ignored on a value axis). Takes precedence
over calendar if both are given.
Pass a stable reference. The scale (and container frame) rebuild when
this prop's identity changes, so memoize it — const disc = useMemo(() => calendar.discontinuities(), [calendar]) — rather than calling
.discontinuities() inline in JSX, which would rebuild every render.
Accepts an explicit undefined (a cond ? provider : undefined toggle
under exactOptionalPropertyTypes), same as omitting it.
editAnnotationsbooleanEnter annotation-edit mode: suppresses the data cursor and makes editable
annotations (those given an onChange) interactive — hovering one reveals its
handles + highlights it, and dragging edits it. Default false. Pairs
with each annotation's onChange (where the edit goes); this is the mode that
turns the affordances on and gets the cursor out of the way.
gridbooleanDraw the reference gridlines behind the data. On a calendar (time) axis
the verticals are the full grain populations — every day / month /
aligned clock instant in view, each grain fading by its calendar density
— not just the labelled ticks (the labels decorate the grid; they don't
define it). Default true. Set false for a clean backdrop —
session dividers (below) are independent and still draw when enabled.
heightnumber | 'auto'Total height in CSS pixels, or 'auto' to fill the available height —
the container-owned vertical layout ([PND-HEIGHT]). Omitted means the
classic mode: rows declare pixel heights and the container's height is
their sum.
With a height, the container renders as a flex column — the rows
block flexes, the x-axis strip keeps its natural height at the bottom —
and <ChartRow flex> rows (a bare <ChartRow> is flex={1}) divide
whatever the browser says is left. That "whatever the browser says" is
the point: the axis strip's height depends on its label, the theme's
font size, whether the tick ladder is showing its calendar band row at
the current grain, and how many marker pills stack — it is not a constant
a caller could subtract, and every consumer who tried carried a wrong
number (20, 24, and the recipe's 22 were all in the wild for one strip).
CSS does the subtraction, so there is no number to know.
A single full-bleed chart is therefore zero arithmetic:
<ChartContainer width="auto" height="auto">
<ChartRow>
<YAxis id="v" />
<Layers>…</Layers>
</ChartRow>
</ChartContainer>
Fixed-height rows keep their pixels inside a managed container, and
non-row children (a draggable splitter between two rows) take their
natural space — so the resizable-panels shape becomes one flex row
absorbing slack over one fixed row the drag resizes, with no reserved
strip constant and no measuring hook.
'auto' measures with the same ResizeObserver as width="auto", gates
the first paint until both needed dimensions exist, latches the last
non-zero size while hidden, and — because a flex-column child's
height defaults to its content — warns in dev when a measured dimension
stays 0: the parent needs a definite height, or the deadlock is the
default.
maxBandWidthnumberCap the slot pitch on a category x axis, in CSS pixels ([PND-BANDPACK]). A band scale otherwise spreads its categories across the full plot width, so three categories in a 900px panel become three 300px bars and thirty become thirty 30px ones — the same chart in the same panel reading as two different charts depending on how many categories the data happened to return.
That is fine for a static chart with a known domain and wrong for a live one: when the category count moves over a session, bar width becomes a meaningless variable that moves on its own, and a reader can't compare the chart to what it looked like a minute ago or to the same chart on another screen. Capping the pitch keeps bar width constant and comparable, and the empty space left over is itself information — it shows the set is small.
Omitted ⇒ slots fill the plot (unchanged). When n × maxBandWidth exceeds
the plot, the cap can't bind and the slots fill as before, so this degrades
correctly as categories accumulate. Use bandAlign to say where the
capped block sits.
This caps the slot, not the bar. <BarChart gap> still insets the bar
within its slot, and the two compose — one knob for pitch, one for ink,
neither doing the other's job. (Inverting gap against a measured plot
width was the workaround this replaces for the width half; the packing half
had no workaround at all.)
Vertical / x-axis categories only. A orientation="horizontal"
categorical chart puts its categories on the y axis as unit slots,
which is a different mechanism and is not capped by this.
minDurationnumberZoom-in floor — the minimum visible duration in ms. Default 1.
onCreate(spec: CreateSpecCreateSpectype@pond-ts/charts{ at: number; kind: 'marker' } | { axis: string; kind: 'baseline'; value: number } | { from: number; kind: 'region'; to: number }What a completed create gesture reports to ContainerFrame.onCreate —
the new mark's kind + position in axis units (+ the y-axis id for a baseline).
(Which row a mark lands on is the consumer's call for now; multi-row routing is
a follow-up.)
) => voidFired when a create gesture completes (on release). See CreateSpec.
onDrawStats(frame: DrawStatsFrameDrawStatsFrameinterface@pond-ts/charts{ layers: readonly LayerDrawInfo[]; rowKey: symbol; totalDrawMs: number }The per-repaint draw-stats frame handed to ContainerProps.onDrawStats.
Fires once per row-canvas repaint (rows repaint independently, so a
multi-row container fires one frame per row that painted), carrying that row's
layers newest-drawn. The seam the dashboard A/B asked for (2026-07-21): read
drawnCount vs sourceCount to see whether M4 engaged, an…
) => voidDraw-cost + decimation observability. Fires once per row-canvas repaint
with a DrawStatsFrame — one LayerDrawInfo per layer in that
row carrying its as, drawMs, and (for a decimating layer) sourceCount
/ drawnCount / decimated. Compare drawnCount to sourceCount to see
whether M4 engaged; read drawMs for per-layer render cost. Omitted ⇒ no
measurement — the render loop skips per-layer timing entirely, so this is
zero-overhead when unused. Keep the callback cheap (it runs inside the draw
frame); route it to a ref/store rather than doing React state work per frame.
onEditAnnotation(id: string) => voidFired when a mark is double-clicked — the request to edit just that one
(set its editing prop in response). Single click selects (inspect); double
click edits. Works in any mode.
onHoverAnnotation(id: string | null) => voidFired when the pointer enters an annotation (its id) or leaves it (null).
Mirror it to a controlled hovered prop on each mark to sync hover both ways
(e.g. a legend row ↔ the mark). Fires in any mode.
onRegionSelect(range: readonly [number, number]) => voidMakes the region cursor draggable: drag across the plot and the band
extends bucket by bucket (snapping to cursorSequence points); on
release this fires once with the selected [lo, hi] span, and the cursor
reverts to the single-bucket highlight (it does not keep the range). Typical
use — zoom the view to the returned span (the container doesn't zoom itself;
that's the consumer's call), or map it onto a data subscription's range params.
The span is a neutral numeric pair in axis units — epoch ms on a time
axis, the axis value (strike, distance, …) on a value axis — mirroring the
polymorphic range input. A time consumer that wants a TimeRange builds one
from the pair.
With no cursorSequence the region cursor is the degenerate case — it
renders as a line on hover and the drag is freeform (raw [lo, hi], no
bucket snapping); the same callback fires on release. Bucket snapping needs a
cursorSequence, which is time-axis only (a time interval over a value
domain is meaningless), so a value axis is always freeform. No-op unless
cursor="region" on a time or value x-axis (a category axis is
excluded — an ordinal-slot select is a different gesture).
onSelectAnnotation(id: string | null) => voidFired when an annotation is clicked (its id), the plot is clicked empty
(null), or a region is double-clicked (the shortcut into edit). The consumer
holds the selected id and sets each mark's selected={id === sel}.
onTimeRangeChange(range: [number, number]) => voidControlled view range — fires on pan/zoom with the new [start, end]. Wire
it back to range for a controlled chart; omit for uncontrolled (the
container holds the view internally). Uncontrolled + panZoom seeds the
internal view from range whenever it isn't actively holding one — so
toggling panZoom on, or a controlled→uncontrolled switch, starts from the
current range, not the mount-time one. Once uncontrolled, later range
changes are ignored so they can't fight the user's pan. To drive the range
externally — or to follow a live sliding window — use controlled mode (this
callback).
onTrackerChanged(info: TrackerInfoTrackerInfointerface@pond-ts/charts{ time: number; values: readonly TrackerSample[] }The hover snapshot handed to onTrackerChanged — the cursor time + every
series' value there, so a consumer can render the readout outside the chart.
| null) => voidFires on pointer move with the hovered time + every series' value there (so
you can render a readout outside the chart), and null on leave.
originnumber | 'data'Label the x axis as offsets from a zero point instead of absolute
values — the duration (elapsed-time) axis. A time axis reads
00:00 00:05 00:10 where it read 10:35 10:40 10:45; a value axis reads
distance-from-the-origin (0 500 1000) where it read absolute distance.
'data'— the start of the data (the union of the layers' x extents), so the labels are "since the beginning of the series" and stay put as you pan.- a number — an explicit zero point in axis units: a race gun, a trigger
instant, a lap marker. Ticks before it read negative (
-00:05— the T-minus case).
Ticks are placed at round durations measured from the origin, not at the
wall-clock boundaries the calendar ladder would pick — that's the difference
between 00:00 00:05 00:10 and 00:01:43 00:06:43. Gridlines follow them,
and so does the cursor pill (one grain finer, as ever: 00:05:12).
This is a labelling mode, not a data transform: range, an annotation's
at, an onRegionSelect span, trackerPosition are all still absolute
axis units. Ignored on a category axis. An explicit timeFormat /
<XAxis format> still wins — on a time axis a d3 time specifier can only
describe an instant, so it labels the underlying wall clock (the lever for
stacking a wall-clock strip under a duration strip, on shared ticks); on a
value axis a number specifier formats the offset. On a trading-calendar
axis the durations are wall-clock, so ticks spanning a collapsed session
gap sit unevenly — elapsed trading time is not implemented.
panZoomboolean | 'none' | 'pan' | 'panZoom' | 'panZoomX' | 'panZoomY' | 'panZoomXY'Which pan/zoom gestures the plot captures:
'none'(orfalse, the default) — neither; the plot doesn't capture drag or scroll.'pan'— drag to pan the time range, no wheel-zoom (scroll still scrolls the page).'panZoom'(ortrue) — drag to pan and wheel to zoom around the cursor.
The boolean form is the back-compat shorthand (true ⇒ 'panZoom',
false ⇒ 'none'). Bound the reachable range with bounds
(zoom-out / pan extent) and minDuration (zoom-in floor).
This prop is about the plot only. Gestures on the axis strips are a
separate opt-in — see axisPanZoom — so turning pan/zoom on here does
not silently make the axes grabbable.
rangeTimeRangeTimeRangeclasspond-tsA time interval event key with inclusive start and end boundaries. Example: new TimeRange({ start, end }).
| readonly [number, number]The shared x domain [begin, end] — a tuple, or a TimeRange
(series.timeRange()). Units follow the data: epoch-ms for a time axis,
the value units (distance, …) for a value axis. Omit to auto-fit to the
rows' extents. The axis kind is never taken from here — it's inferred from
the data — so a tuple stays a time domain on a time chart.
regionSelectModifier'shift'Which modifier a region-drag needs — set 'shift' when you also enable
pan (panZoom="pan" or "panZoom") and want plain drag to pan,
shift-drag to select. It's only enforced while pan is enabled (with pan
off there's no gesture conflict, so shift is optional — either drag
selects). Omitted ⇒ a region-drag
preempts pan (drag always selects; document that precedence for users).
Wheel-zoom is unaffected in every case.
rowGapnumberVertical space between rows in CSS pixels (not under the axis). Default 0.
sessionDividers'none' | 'labeled' | 'all'Where to draw session dividers — the solid verticals at a trading
calendar's collapse seams: boundaries that removed (closed-market)
time actually precedes, not every session roll (only with a
discontinuities / calendar provider). On a real exchange calendar
every session open follows an overnight gap, so seams = session opens; a
calendar of contiguous full-day sessions has seams only where days were
excised (the weekend). Default 'none' — the hierarchical grid
already marks the calendar structure at every zoom, so dividers are
opt-in emphasis: 'all' draws one at every seam in view (the
TradingView session-separator look, crowding lines fading out),
'labeled' only at seams the axis also labels. Dividers are independent
of grid — 'all' + grid={false} is the
separators-on-a-clean-plot look.
showAxisbooleanAuto-render the shared x axis under the rows. Default true. Set
false for a bare plot (a sparkline), or when you place your own <XAxis>
child (e.g. with a label, custom ticks, or on side="top"). Named
showAxis (not axis) to avoid clashing with a layer's axis prop, which
picks which <YAxis> it scales against — a different axis entirely.
snapbooleanSnap mode (the toolbar's "Snap"). Default true. When on, a dragged
mark snaps to other marks' guidelines (their x-positions, within a few
px) so spans align; off = free placement. (Snapping to the nearest data
sample is not implemented — guideline alignment only.)
spacing'proportional' | 'uniform'The trading axis metric, when a calendar is supplied
(trading-calendar RFC Q7). 'proportional' (default) keeps time
proportional within and across sessions — a half-day is half as wide.
'uniform' gives every session equal width (the TradingView ordinal look).
Ignored without calendar (a low-level discontinuities provider already
carries its own metric).
themeChartThemeChartThemeinterface@pond-ts/charts{ annotation?: { color: string; dash?: readonly number[]; depth: readonly [number, number, number]; fillOpacity: number; roles?: {}; spanEdge?: string }; area: { default: AreaStyle }; axis: { band?: { divider?: string; fill: string; label?:…Visual styling for a chart, threaded through ChartContainer via
context. Canvas has no CSS cascade into drawn pixels, so this typed object is
the single styling channel for the drawn layers; DOM chrome (axis labels)
derives from it too.
Visual theme for all rows; defaults to defaultTheme.
timeFormatAxisFormatAxisFormattype@pond-ts/chartsstring | (value: number) => stringHow to format an axis's values — a d3 [format specifier]
(https://github.com/d3/d3-format#locale_format) string, or a custom
(value) => string function. Omit for the scale's d3 default.
Time-axis label formatting — a d3 time specifier string (e.g. '%H:%M')
or a (epochMs) => string function (AxisFormat). A custom format
owns the labels, so it opts the axis out of the dateStyle ladder
(flat / stacked) by design. Omitted ⇒ the flat/stacked date style. To
shape only the cursor readout while keeping a date style, use
cursorFormat instead. (For back-compat this also shapes the readout
when cursorFormat is absent.)
trackerPositionnumber | nullControlled tracker position (epoch ms) — where to show the synced crosshair
when this chart isn't the one under the pointer. A live local hover
always wins over it, so this is a followed position, not a hard pin:
supply it to drive the cursor from outside (a scrubber, a playback head, or
— the main use — cross-chart sync). Maps through this chart's own
xScale, so it lands at the right pixel even under a different zoom.
Multi-chart sync falls out of this plus onTrackerChanged: give
every <ChartContainer> the same trackerPosition={sharedTime} and set
sharedTime from each one's onTrackerChanged. The hovered chart favors its
own pointer (and reports the time out); the others follow. Clear sharedTime
to null on the group's onPointerLeave so the crosshair lifts when the
pointer leaves every chart. (See the "Synced cursors across charts" story /
dashboard guide.)
Omit or pass null (equivalent) for no controlled position — a hovered
chart still tracks its pointer, a non-hovered one shows nothing. To force a
chart to never show a cursor, use cursor="none", not trackerPosition.
See onTrackerChanged.
widthnumber | 'auto'Total width in CSS pixels (plot + axis gutters), or 'auto' to fill the
available width — which is also what an omitted width means.
The canvas renderer needs real pixels to lay out ticks and slots before it
draws, so 'auto' does not hand the canvas a percentage: the container
renders a plain full-width box, measures it with a ResizeObserver, and
mounts the chart at that pixel width, re-rendering as the box resizes.
Nothing paints until a real width exists — a zero-width chart is
degenerate, not empty — so an auto container renders an empty box for the
first layout pass.
This is the responsive-width recipe moved inside the library, and it closes that recipe's sharpest edge by construction: the measured box is one the library owns, so it can never be the caller's padded or bordered box (whose border-box width overflows the chart by exactly the padding). Style your own wrapper around the container as freely as you like.
The parent needs a definite width. 'auto' measures a width: 100%
box, so a parent whose own width comes from its content — a float, an
inline-block, a grid auto track, a flex child without min-width: 0 —
measures 0, and the chart is the content that would have given it a width.
That is a standing deadlock, not a slow start: the chart stays blank with
no error. Give the parent a width, a flex basis, or min-width: 0, or
pass a number.
A container hidden by an ancestor's display: none is fine — it keeps the
last width it measured and stays mounted, so a tab switch does not discard
pan/zoom position, selection or hover.
Pass a number whenever the width is already known — a fixed-size panel, a print layout, a test. It skips the measure pass and paints on the first render.
xScale'linear' | 'log' | 'symlog'How the value x axis maps data to pixels. Omitted ⇒ 'linear'.
'log' for a quantity spanning orders of magnitude — a power–duration
curve is watts against 1s · 5s · 1m · 20m · 3h, which is unreadable on a
linear x. 'symlog' is the same but linear through zero, for data that
crosses it.
Ignored on a time or category axis, which have their own spacing rules.
Why this lives on the container and not on <XAxis scale>, which is
where <YAxis scale>'s mirror would put it: the rows are stacked
vertically, so a given pixel column has to mean the same x in every one of
them — otherwise the stack doesn't line up and a cursor at one pixel
reads a different value per row. The x scale and its domain are therefore
shared by requirement, not by convention, and a shared thing is declared
once by the thing that contains them. <YAxis> is the opposite for the
same reason: each row carries its own quantity, so its scale must be
per-row, which is why min / max / pad / scale belong to the axis.
That gives the test for what belongs here rather than on <XAxis>: does
it define the mapping or the domain? origin, spacing, calendar and
the viewport props all do, and sit here for the same reason. Every
<XAxis> prop (format, label, side, ticks, align, …) does not —
they style a scale the axis only draws, and putting a scale-defining prop
among them would mean a registration round-trip to the component that
already owns it.
(Had <XAxis> been mandatory in the declaration, the props would more
naturally have lived there and x would mirror y — see [PND-XLOG].)