@pond-ts/charts 0.58.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/BarChart.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { useContext, useEffect, useMemo } from 'react';
2
2
  import { Interval, ValueSeries } from 'pond-ts';
3
- import { barsFromTimeSeries, barsFromBins, barsFromValueSeries, categoryStack, stacksFromBins, stacksFromColumns, stacksFromGroups, } from './data.js';
4
- import { barAt, barExtent, barIndexAtTime, drawBars, drawStacks, normalizeThresholds, resolveBarBaseline, stackAt, stackBinExtent, stackValueExtent, } from './bars.js';
3
+ import { barsFromTimeSeries, barsFromBins, barsFromValueSeries, categoryStack, categoryStacks, stacksFromBins, stacksFromColumns, stacksFromGroups, } from './data.js';
4
+ import { barAt, barExtent, barIndexAtTime, drawBars, drawStacks, resolveBarBaseline, stackAt, stackBinExtent, stackValueExtent, } from './bars.js';
5
5
  import { spansForLayer } from './span.js';
6
+ import { useBandLadder } from './use-band-ladder.js';
6
7
  import { isDev } from './dev.js';
7
8
  import { ContainerContext, LayersContext, } from './context.js';
8
9
  import { sweep1D } from './sweep.js';
@@ -60,7 +61,7 @@ const EMPTY_MARKS = [];
60
61
  * </Layers>
61
62
  * ```
62
63
  */
63
- export function BarChart({ series, bins, categories, column, columns, as: semantic, colors, binColors, thresholds, bandColors, orientation = 'vertical', ordinal = false, id, axis, gap, decimate = true, legend, index = 0, }) {
64
+ export function BarChart({ series, bins, categories, column, columns, as: semantic, colors, binColors, thresholds, bandColors, orientation = 'vertical', ordinal = false, id, axis, gap, maxBarWidth, decimate = true, legend, index = 0, }) {
64
65
  const container = useContext(ContainerContext);
65
66
  if (container === null) {
66
67
  throw new Error('<BarChart> must be rendered inside a <ChartContainer>');
@@ -78,8 +79,11 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
78
79
  throw new Error('<BarChart> needs exactly one of `series`, `bins`, or `categories`');
79
80
  }
80
81
  if (categories !== undefined) {
81
- if (column !== undefined || columns !== undefined) {
82
- throw new Error('<BarChart categories> takes no `column`/`columns` (each datum carries its own value)');
82
+ if (column !== undefined) {
83
+ throw new Error('<BarChart categories> takes no `column` a single-value datum carries its own `value`, and a stacked one names its groups with `columns`');
84
+ }
85
+ if (columns !== undefined && columns.length === 0) {
86
+ throw new Error('<BarChart categories> with `columns` needs at least one group name');
83
87
  }
84
88
  }
85
89
  const isMap = series instanceof Map;
@@ -105,10 +109,21 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
105
109
  // horizontal bar) so one oriented draw path covers it.
106
110
  const shape = useMemo(() => {
107
111
  if (categories !== undefined) {
108
- // Categorical row-read: one unit-slot bar per category (G === 1), drawn on
109
- // the container's band scale. The reused stacked geometry — only the axis
112
+ // Categorical row-read: one unit-slot bar per category, drawn on the
113
+ // container's band scale. The reused stacked geometry — only the axis
110
114
  // (band scale + labels) is new.
111
- return { kind: 'stacked', ss: categoryStack(categories) };
115
+ //
116
+ // With `columns` it is a real multi-group stack ([PND-CATSTACK]): same
117
+ // slots, same `marks`, `G > 1`. Because `marks` is indexed by BIN, one
118
+ // selection entry naming `(id, mark)` matches every segment of a bar —
119
+ // which is the property that made the hand-composed workaround's
120
+ // "recedes from the waist up" bug inexpressible here.
121
+ return {
122
+ kind: 'stacked',
123
+ ss: columns !== undefined
124
+ ? categoryStacks(categories, columns)
125
+ : categoryStack(categories),
126
+ };
112
127
  }
113
128
  if (bins !== undefined) {
114
129
  const cols = columns ?? (column !== undefined ? [column] : undefined);
@@ -250,67 +265,24 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
250
265
  // The stacked path's bar-thickness floor comes from `bar.default` (not the `as`
251
266
  // role — `as` is single-series only), matching how `gapPx` sources its default.
252
267
  const stackMinWidth = bar.default.minWidth;
268
+ // Same sourcing as `gapPx`: the prop wins, else the theme token, else
269
+ // uncapped ([PND-BARWIDTH]). `bar.default` rather than the `as` role for the
270
+ // stacked ceiling, for the reason above.
271
+ const maxWidthPx = maxBarWidth ?? bar.default.maxWidth;
272
+ // The single-series draw takes a `BarStyle` straight from the theme, so the
273
+ // prop override is applied by shadowing the token — same relationship `gap`
274
+ // has, and with the prop absent the role's own `maxWidth` (if any) already
275
+ // rides along untouched. Only the INK path gets this: `barSlotRect` (the hit
276
+ // region) stays the whole slot, so a narrow capped bar keeps a full-width
277
+ // target — the deliberate ink/hit split `gapPx` already relies on.
278
+ const singleDrawStyle = useMemo(() => maxBarWidth !== undefined
279
+ ? { ...singleStyle, maxWidth: maxBarWidth }
280
+ : singleStyle, [singleStyle, maxBarWidth]);
253
281
  // ── Threshold ladder ([PND-BANDBAR2]) ────────────────────────────────────
254
- // Resolved once here rather than per bar per frame: normalize the breakpoints
255
- // (sort, drop non-finite), then pair them with `bandColors` → the role's
256
- // `BarStyle.bands`. Everything that can go wrong with the pairing is a
257
- // *silent* wrong-looking chart, so each case dev-warns — this feature exists
258
- // because a quietly-unbanded bar was the workaround's failure mode.
259
- // Value-compare the two array props rather than relying on their identity.
260
- // `thresholds={[1, 2]}` inline is the documented usage and the shape every
261
- // story and doc example uses — and a fresh array each render would rebuild
262
- // the ladder, hence the layer `entry` below, hence a `registerLayer` call
263
- // **every render**. That is a repaint treadmill, not just a noisy warning.
264
- // The same value-compare-on-registration reasoning `<YAxis ticks>` already
265
- // applies.
266
- const thresholdKey = thresholds === undefined ? '' : thresholds.join(',');
267
- const bandColorKey = bandColors === undefined ? '' : bandColors.join(',');
268
- const bandLadder = useMemo(() => {
269
- const steps = normalizeThresholds(thresholds);
270
- if (steps === null) {
271
- if (isDev && thresholds !== undefined && thresholds.length > 0) {
272
- console.warn('<BarChart thresholds>: no usable breakpoints, so no banding was ' +
273
- 'applied — each must be finite and greater than zero. Bars draw ' +
274
- 'in the flat fill.');
275
- }
276
- return undefined;
277
- }
278
- // Some, but not all, entries dropped. Silently banding on a subset of what
279
- // the caller wrote is exactly the class of quiet wrongness this feature is
280
- // meant to remove, so say so.
281
- if (isDev && thresholds !== undefined && steps.length < thresholds.length) {
282
- console.warn(`<BarChart thresholds>: dropped ${thresholds.length - steps.length} ` +
283
- 'breakpoint(s) that were not finite and greater than zero. The ' +
284
- 'ladder is walked on the magnitude and mirrored onto whichever side ' +
285
- 'of zero a bar is on, so a negative breakpoint has no meaning; ' +
286
- `banding on [${steps.join(', ')}].`);
287
- }
288
- const want = steps.length + 1;
289
- const supplied = bandColors ?? singleStyle.bands;
290
- if (supplied === undefined || supplied.length === 0) {
291
- if (isDev) {
292
- console.warn(`<BarChart thresholds>: ${steps.length} breakpoint(s) need ${want} ` +
293
- 'band colours, but neither `bandColors` nor the theme role’s ' +
294
- '`BarStyle.bands` supplies any. Bars draw in the flat fill.');
295
- }
296
- return undefined;
297
- }
298
- if (supplied.length < want && isDev) {
299
- console.warn(`<BarChart thresholds>: ${steps.length} breakpoint(s) need ${want} ` +
300
- `band colours but only ${supplied.length} were supplied; bands ` +
301
- 'above the last colour fall back to the flat fill.');
302
- }
303
- // Pad a short ladder with the flat fill so the draw path can index freely.
304
- const resolved = supplied.length >= want
305
- ? supplied.slice(0, want)
306
- : [
307
- ...supplied,
308
- ...Array.from({ length: want - supplied.length }, () => singleStyle.fill),
309
- ];
310
- return { thresholds: steps, colors: resolved };
311
- // eslint-disable-next-line react-hooks/exhaustive-deps -- `thresholdKey` /
312
- // `bandColorKey` are the value-compared stand-ins for the array props.
313
- }, [thresholdKey, bandColorKey, singleStyle]);
282
+ // Breakpoints normalized + paired with `bandColors` the role's
283
+ // `BarStyle.bands` in the shared {@link useBandLadder} (one contract with
284
+ // `<AreaChart thresholds>`, including every dev warning).
285
+ const bandLadder = useBandLadder('BarChart', thresholds, bandColors, singleStyle.bands, singleStyle.fill);
314
286
  // Conflicts between the ladder and the shapes it can't apply to. In an effect
315
287
  // so a re-render doesn't re-log; each fires once per genuinely new pairing.
316
288
  const multiGroup = shape.kind === 'stacked' && shape.ss.groups.length > 1;
@@ -351,11 +323,38 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
351
323
  (ramp !== undefined ? at(ramp, i) : base.fill));
352
324
  // A ramp entry the call site overrode is no longer the ramp's colour, so
353
325
  // its receded counterpart would be wrong — the whole ramp only means
354
- // anything when it is the ramp that painted it.
326
+ // anything when it is the ramp that painted it. This gates the *derived*
327
+ // companions (`dimmedFills` / `hoverFills`), which need a counterpart per
328
+ // group and cannot invent one for an arbitrary call-site colour.
355
329
  const ramped = ramp !== undefined && colors === undefined;
330
+ // `groupColored` derives nothing — it only says "a selected segment keeps
331
+ // its own fill rather than taking the flat `highlight`", because the colour
332
+ // is meaning-carrying.
333
+ //
334
+ // So the condition is exactly **"do the fills actually differ"**, read off
335
+ // the resolved `fills` rather than inferred from anything upstream of them.
336
+ // Both cheaper inferences are wrong, and each was shipped in turn:
337
+ //
338
+ // - Gating on `ramped` (the ramp painted it) meant a stack with `colors`
339
+ // collapsed BOTH segments of a selected bar to one `highlight` blue —
340
+ // losing the segment distinction exactly where the reader is looking. A
341
+ // call site naming its groups' colours is *more* deliberate than a
342
+ // fallback ramp, not less. Found building [PND-CATSTACK].
343
+ // - Gating on `groups.length > 1` (my fix for that) is wrong in the other
344
+ // direction: a multi-group stack under a theme with **no** group ramp, no
345
+ // `colors` and no per-group roles resolves every fill to `base.fill`, so
346
+ // claiming the colour carries meaning suppresses the highlight and leaves
347
+ // *nothing* — selection becomes invisible. `estelaTheme` is exactly that
348
+ // theme, and it ships. Found by Layer-2 review, which is the only way it
349
+ // could have been: every story and test renders `defaultTheme`, whose ramp
350
+ // makes the two gates indistinguishable.
351
+ //
352
+ // Reading `fills` also handles the degenerate `colors` map that assigns one
353
+ // colour to every group: nothing is distinguished, so the highlight applies.
354
+ const groupColoured = new Set(fills).size > 1;
356
355
  return {
357
356
  fills,
358
- ...(ramped ? { groupColored: true } : {}),
357
+ ...(groupColoured ? { groupColored: true } : {}),
359
358
  ...(ramped && rampDim !== undefined
360
359
  ? {
361
360
  dimmedFills: (groups ?? []).map((_g, i) => at(rampDim, i)),
@@ -380,9 +379,10 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
380
379
  ? { emphasisOpacity: base.emphasisOpacity }
381
380
  : {}),
382
381
  ...(base.dimmed !== undefined ? { dimmed: base.dimmed } : {}),
382
+ ...(maxWidthPx !== undefined ? { maxWidth: maxWidthPx } : {}),
383
383
  ...(binColors !== undefined ? { binFills: binColors } : {}),
384
384
  };
385
- }, [bar, groups, colors, binColors]);
385
+ }, [bar, groups, colors, binColors, maxWidthPx]);
386
386
  // The current selection / hover, narrowed to the identity the highlight match
387
387
  // needs. For a stack that's (id, key, label = group); the single path uses just
388
388
  // (id, key). Read here so a change re-registers the layer → the canvas repaints.
@@ -525,7 +525,7 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
525
525
  },
526
526
  }),
527
527
  }),
528
- draw: (ctx, xScale, yScale) => drawBars(ctx, bs, xScale, yScale, singleStyle, resolveBarBaseline(yScale), gapPx, id, selection, hover, decimate, binColors, bandLadder, layerSpans),
528
+ draw: (ctx, xScale, yScale) => drawBars(ctx, bs, xScale, yScale, singleDrawStyle, resolveBarBaseline(yScale), gapPx, id, selection, hover, decimate, binColors, bandLadder, layerSpans),
529
529
  },
530
530
  axisId: axis,
531
531
  index,
@@ -563,7 +563,20 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
563
563
  ? {}
564
564
  : {
565
565
  hitTest: (px, py, xScale, yScale) => {
566
- const hit = stackAt(ss, px, py, orientation, xScale, yScale, gapPx, stackMinWidth);
566
+ // The cap reaches the hit rect ONLY for a real stack, where the
567
+ // rect is what resolves *which segment* was hit and so must be
568
+ // the drawn one. A single-series chart has one segment per slot
569
+ // and nothing to disambiguate, so narrowing its target buys
570
+ // nothing and costs clickability.
571
+ //
572
+ // This path serves single-series **horizontal** charts as well as
573
+ // stacks (see the branch comment above), which is how the prop's
574
+ // documented guarantee — "a single-series bar hit-tests its whole
575
+ // slot" — was true only of vertical ones. Found by Layer-2 review;
576
+ // the fix is to make the guarantee orientation-independent rather
577
+ // than to narrow the claim, since the reason for the split is
578
+ // segment disambiguation and that is a property of the *stack*.
579
+ const hit = stackAt(ss, px, py, orientation, xScale, yScale, gapPx, stackMinWidth, ss.groups.length > 1 ? maxWidthPx : undefined);
567
580
  if (hit === null)
568
581
  return null;
569
582
  const [bi, g, begin, name, value] = hit;
@@ -656,6 +669,7 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
656
669
  categoryLabels,
657
670
  orientation,
658
671
  singleStyle,
672
+ singleDrawStyle,
659
673
  stackStyle,
660
674
  binColors,
661
675
  bandLadder,
@@ -664,6 +678,7 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
664
678
  gapPx,
665
679
  decimate,
666
680
  stackMinWidth,
681
+ maxWidthPx,
667
682
  selection,
668
683
  hover,
669
684
  layerSpans,
package/dist/BarList.d.ts CHANGED
@@ -69,6 +69,28 @@ export interface BarListCommon<R extends ListRow = ListRow> {
69
69
  selected?: string | readonly string[] | null;
70
70
  /** Row click (rows show the pointer affordance only when provided). */
71
71
  onRowClick?: (row: R) => void;
72
+ /**
73
+ * Per-**row** bar colour, `barColors[i]` aligned to the rows in order — the
74
+ * list's `<BarChart binColors>` ([#650]). An `undefined` or short entry falls
75
+ * back to the column's `as` / theme fill, so a partial list is legal.
76
+ *
77
+ * For the case `binColors` was built for and a list could not do: a zone
78
+ * table where each row's bar carries its own step of a ramp (Z1 deep → Z7
79
+ * pale). Without it a `BarListColumn`'s single `as` paints every row the same
80
+ * colour, and the ramp has to move onto the label — putting it on the wrong
81
+ * element, since the bar is the natural carrier of a magnitude.
82
+ *
83
+ * **The fill becomes load-bearing, so its state treatment stands down.** A
84
+ * per-row-coloured bar keeps its own colour while selected or hovered, and
85
+ * the live state is carried by the band and rail that already say it. This is
86
+ * the same rule the multi-metric case follows for the same reason — see the
87
+ * comment beside the fill resolution — and the same one `binColors` follows
88
+ * on the canvas: recolouring a bar that means something would trade a
89
+ * distinction the reader needs for one they already have.
90
+ *
91
+ * [#650]: https://github.com/pond-ts/pond/issues/650
92
+ */
93
+ barColors?: readonly (string | undefined)[];
72
94
  /**
73
95
  * **Plural select** — the list's answer to `<MultiSelector>`, and how a user
74
96
  * produces a multi-row {@link selected}.
package/dist/BarList.js CHANGED
@@ -36,7 +36,7 @@ export function BarList(props) {
36
36
  // One normalized view of the union — `isSeriesSource` is the runtime
37
37
  // narrowing; the doors are mutually exclusive by construction now.
38
38
  const source = props;
39
- const { rows, series, label, columns, domain, sortBy, sortDirection = 'desc', sort, before, after, renderExpanded, defaultExpanded, onExpandToggle, selected, onRowClick, onRowSelect, hovered, onHover, markers, barHeight = 8, divided, baseline, theme = defaultTheme, } = source;
39
+ const { rows, series, label, columns, domain, sortBy, sortDirection = 'desc', sort, before, after, renderExpanded, defaultExpanded, onExpandToggle, selected, onRowClick, onRowSelect, barColors, hovered, onHover, markers, barHeight = 8, divided, baseline, theme = defaultTheme, } = source;
40
40
  // A runtime guard for JS consumers and `any`-typed call sites — the
41
41
  // props union makes both branches unreachable from typed TS, but a
42
42
  // silently-ignored source prop is a worse failure than a throw.
@@ -49,6 +49,21 @@ export function BarList(props) {
49
49
  (series instanceof ValueSeries
50
50
  ? listRowsFromValueSeries(series, label !== undefined ? { label } : {})
51
51
  : listRowsFromTimeSeries(series, label !== undefined ? { label } : {})), [rows, series, label]);
52
+ // Keyed by row, not indexed by render position. `barColors` aligns to the
53
+ // rows the caller passed — but the table renders `sorted`, so an index would
54
+ // silently repaint the ramp onto the wrong rows the moment `sortBy` is set.
55
+ // The key survives any reordering.
56
+ const barColorOf = useMemo(() => {
57
+ if (barColors === undefined)
58
+ return null;
59
+ const m = new Map();
60
+ allRows.forEach((r, i) => {
61
+ const c = barColors[i];
62
+ if (c !== undefined)
63
+ m.set(r.key, c);
64
+ });
65
+ return m;
66
+ }, [barColors, allRows]);
52
67
  const sorted = useMemo(() => sortListRows(allRows, sortBy, sortDirection, sort), [allRows, sortBy, sortDirection, sort]);
53
68
  const scale = useMemo(() => resolveListDomain(allRows, columns.map((c) => c.column), domain, markers?.map((m) => m.value)), [allRows, columns, domain, markers]);
54
69
  const resolvedMarkers = useMemo(() => markers?.map((m) => ({
@@ -68,17 +83,35 @@ export function BarList(props) {
68
83
  // Which is also why nothing below may be the *only* signal: strip
69
84
  // this block and a selected row still reads as selected.
70
85
  const soleMetric = columns.length === 1;
71
- const fill = state.selected && soleMetric
72
- ? style.highlight
73
- : state.dimmed
74
- ? (style.dimmed ?? style.fill)
75
- : style.fill;
86
+ // A per-row colour makes the fill mean something, which puts it in
87
+ // exactly the position a multi-metric row's fill is already in: the
88
+ // state treatment stands down rather than trading a distinction the
89
+ // reader needs for one the band and rail already give them. Same
90
+ // rule `binColors` follows on the canvas.
91
+ const own = barColorOf?.get(row.key);
92
+ const fill = own !== undefined
93
+ ? own
94
+ : state.selected && soleMetric
95
+ ? style.highlight
96
+ : state.dimmed
97
+ ? (style.dimmed ?? style.fill)
98
+ : style.fill;
76
99
  // A `dimmed` token carries its own alpha (`rgba(…,0.32)`), so
77
100
  // multiplying `opacity` on top of it would dim twice. Fall back to
78
101
  // the raw 0.32 only when the theme names no dimmed colour.
79
- const fillOpacity = state.dimmed && style.dimmed === undefined
80
- ? style.opacity * 0.32
81
- : style.opacity;
102
+ // Opacity is where a coloured bar shows its state, since its hue
103
+ // is spoken for. A `dimmed` THEME token carries its own alpha so
104
+ // multiplying on top would dim twice — but that token is not in
105
+ // play for a per-row colour, so the raw 0.32 applies there too.
106
+ const fillOpacity = own !== undefined
107
+ ? state.dimmed
108
+ ? style.opacity * 0.32
109
+ : state.selected
110
+ ? 1
111
+ : style.opacity
112
+ : state.dimmed && style.dimmed === undefined
113
+ ? style.opacity * 0.32
114
+ : style.opacity;
82
115
  return (_jsxs("div", { "data-list-track": col.column, style: {
83
116
  position: 'relative',
84
117
  height: barHeight,
@@ -14,6 +14,108 @@ export interface ChartContainerProps {
14
14
  * the data — so a tuple stays a time domain on a time chart.
15
15
  */
16
16
  range?: readonly [number, number] | TimeRange;
17
+ /**
18
+ * **Make the x axis ordinal at the container level** — one equal-width slot
19
+ * per name, in this order ([PND-IGNITECAT]).
20
+ *
21
+ * Note this is a list of **names** (`string[]`), unlike `<BarChart
22
+ * categories>`, which takes `{ label, value }` data. The container names the
23
+ * slots; the bar layer fills them.
24
+ *
25
+ * Until this prop, the band scale was reachable only *through a layer*:
26
+ * `<BarChart categories>` (and a **horizontal** heat map) reported
27
+ * `xKind: 'category'`,
28
+ * every other layer reported `'time'` or `'value'`, and the container throws
29
+ * on a mix — so **a line, a point or an envelope over categorical bars was
30
+ * not expressible at all**. The workaround was to key every layer to a
31
+ * synthetic integer index and hand-supply the tick labels, which forfeits two
32
+ * features the ordinal axis already implements: `<XAxis>` label thinning
33
+ * (gated on a category axis with no custom ticks) and the
34
+ * {@link maxBandWidth} / {@link bandAlign} slot packing.
35
+ *
36
+ * Declaring the categories here inverts that. The container owns the ordinal
37
+ * domain, so **any value-keyed layer can live on it** and both of those
38
+ * features keep working.
39
+ *
40
+ * ## Keying a layer to the slots
41
+ *
42
+ * The band scale's domain is **numeric** — slot `i` occupies `[i, i+1]`, so
43
+ * its **centre is `i + 0.5`**. Key a `ValueSeries` there and the mark lands
44
+ * on the slot centre, which is also where `<XAxis>` puts the tick:
45
+ *
46
+ * ```tsx
47
+ * const line = ValueSeries.from(
48
+ * tickers.map((t, i) => ({ x: i + 0.5, target: t.target })),
49
+ * { key: 'x' },
50
+ * );
51
+ *
52
+ * <ChartContainer categories={tickers.map((t) => t.label)} width="auto">
53
+ * <ChartRow height={220}>
54
+ * <YAxis id="v" />
55
+ * <Layers>
56
+ * <BarChart categories={bars} />
57
+ * <LineChart series={line} column="target" axis="v" />
58
+ * </Layers>
59
+ * </ChartRow>
60
+ * </ChartContainer>;
61
+ * ```
62
+ *
63
+ * ## What still errors
64
+ *
65
+ * - **A time-keyed layer.** A `TimeSeries` has no slot to sit in; mixing one
66
+ * into an ordinal container is a hard error, as a mixed x-kind always was.
67
+ * - **A category layer that disagrees.** `<BarChart categories>` in an
68
+ * ordinal container must name the same list in the same order — this prop
69
+ * is authoritative, and a silent mismatch would draw bars under the wrong
70
+ * labels.
71
+ * ## What declaring it costs
72
+ *
73
+ * Setting this makes the x axis ordinal, and two container capabilities are
74
+ * defined only on a continuous x. Both were already true of an *inferred*
75
+ * category axis; they are stated here because this prop lets you opt a
76
+ * previously-continuous container into them:
77
+ *
78
+ * - **x pan and zoom stop.** `panZoom` keeps working on **y** (`panY` /
79
+ * `zoomY`), but the x half is gated off — sliding between named slots is
80
+ * not a gesture the axis has a meaning for.
81
+ * - **{@link range} stops applying to x.** The domain is `[0, n]`, derived
82
+ * from the slot count, so an x range is a no-op rather than an error.
83
+ * Show a subset by passing fewer categories.
84
+ * - **{@link xScale} stops applying.** `'log'` / `'symlog'` describe how a
85
+ * *continuous* x spaces its values; ordinal slots are evenly spaced by
86
+ * definition, so the kind is ignored (as it already is on a time axis).
87
+ *
88
+ * ## The hazard this cannot catch
89
+ *
90
+ * **A value-keyed layer is taken at its word.** Anything reporting `'value'`
91
+ * is read as slot coordinates, so a layer whose x means something *else*
92
+ * will draw — in the wrong place, silently. The sharpest instance is a
93
+ * **horizontal categorical `<BarChart>`**: its x is bar *length*, not a
94
+ * coordinate, so on an ordinal x it plots magnitudes as slot positions.
95
+ * Don't mix one into an ordinal container.
96
+ *
97
+ * This is documented rather than enforced, and the reason is worth keeping:
98
+ * a guard was written for it, testing `binCategories`. That is the generic
99
+ * "my **y** is ordinal" channel, and a *vertical* heat map sets it too — so
100
+ * the guard rejected a `ValueSeries` grid with named columns on x, which is
101
+ * a wanted layout (ordinal rows plus ordinal columns is just a 2-D grid),
102
+ * with an error naming a `<BarChart>` that wasn't in the tree. Nothing on a
103
+ * layer source distinguishes "my x is a coordinate" from "my x is a
104
+ * magnitude", so there is no contradiction to detect — and a flag invented
105
+ * to carry it would buy a false sense of coverage while every other misuse
106
+ * stayed silent.
107
+ *
108
+ * ## One more edge
109
+ *
110
+ * **`categories={[]}` is an ordinal axis with no slots yet**, not a fallback
111
+ * to time. That is the useful reading for a loading state: the kind stays
112
+ * put when the data arrives, instead of flipping and rebuilding every scale
113
+ * mid-session.
114
+ *
115
+ * Omit for the inferred behaviour: a container with only category layers
116
+ * still resolves its slots from them, exactly as before.
117
+ */
118
+ categories?: readonly string[];
17
119
  /**
18
120
  * **Cap the slot pitch** on a **category** x axis, in CSS pixels
19
121
  * ([PND-BANDPACK]). A band scale otherwise spreads its categories across the
@@ -99,6 +201,38 @@ export interface ChartContainerProps {
99
201
  * carries its own metric).
100
202
  */
101
203
  spacing?: 'proportional' | 'uniform';
204
+ /**
205
+ * How the **value** x axis maps data to pixels. **Omitted ⇒ `'linear'`.**
206
+ *
207
+ * `'log'` for a quantity spanning orders of magnitude — a power–duration
208
+ * curve is watts against 1s · 5s · 1m · 20m · 3h, which is unreadable on a
209
+ * linear x. `'symlog'` is the same but linear through zero, for data that
210
+ * crosses it.
211
+ *
212
+ * **Ignored on a time or category axis**, which have their own spacing rules.
213
+ *
214
+ * **Why this lives on the container and not on `<XAxis scale>`,** which is
215
+ * where `<YAxis scale>`'s mirror would put it: **the rows are stacked
216
+ * vertically, so a given pixel column has to mean the same x in every one of
217
+ * them** — otherwise the stack doesn't line up and a cursor at one pixel
218
+ * reads a different value per row. The x scale and its domain are therefore
219
+ * *shared by requirement*, not by convention, and a shared thing is declared
220
+ * once by the thing that contains them. `<YAxis>` is the opposite for the
221
+ * same reason: each row carries its own quantity, so its scale **must** be
222
+ * per-row, which is why `min` / `max` / `pad` / `scale` belong to the axis.
223
+ *
224
+ * That gives the test for what belongs here rather than on `<XAxis>`: **does
225
+ * it define the mapping or the domain?** `origin`, `spacing`, `calendar` and
226
+ * the viewport props all do, and sit here for the same reason. Every
227
+ * `<XAxis>` prop (`format`, `label`, `side`, `ticks`, `align`, …) does not —
228
+ * they style a scale the axis only draws, and putting a scale-defining prop
229
+ * among them would mean a registration round-trip to the component that
230
+ * already owns it.
231
+ *
232
+ * (Had `<XAxis>` been mandatory in the declaration, the props would more
233
+ * naturally have lived there and x would mirror y — see [PND-XLOG].)
234
+ */
235
+ xScale?: 'linear' | 'log' | 'symlog';
102
236
  /**
103
237
  * Draw the reference gridlines behind the data. On a calendar (time) axis
104
238
  * the verticals are the **full grain populations** — every day / month /
@@ -124,8 +258,42 @@ export interface ChartContainerProps {
124
258
  * separators-on-a-clean-plot look.
125
259
  */
126
260
  sessionDividers?: 'labeled' | 'all' | 'none';
127
- /** Total width in CSS pixels (plot + axis gutters). */
128
- width: number;
261
+ /**
262
+ * Total width in CSS pixels (plot + axis gutters), or **`'auto'` to fill the
263
+ * available width** — which is also what an omitted `width` means.
264
+ *
265
+ * The canvas renderer needs real pixels to lay out ticks and slots before it
266
+ * draws, so `'auto'` does not hand the canvas a percentage: the container
267
+ * renders a plain full-width box, measures it with a `ResizeObserver`, and
268
+ * mounts the chart at that pixel width, re-rendering as the box resizes.
269
+ * **Nothing paints until a real width exists** — a zero-width chart is
270
+ * degenerate, not empty — so an auto container renders an empty box for the
271
+ * first layout pass.
272
+ *
273
+ * This is the [responsive-width recipe](https://pond-ts.github.io/pond/docs/recipes/responsive-width)
274
+ * moved inside the library, and it closes that recipe's sharpest edge by
275
+ * construction: the measured box is one the library owns, so it can never be
276
+ * the caller's padded or bordered box (whose border-box width overflows the
277
+ * chart by exactly the padding). Style your own wrapper *around* the
278
+ * container as freely as you like.
279
+ *
280
+ * **The parent needs a definite width.** `'auto'` measures a `width: 100%`
281
+ * box, so a parent whose own width comes from its *content* — a float, an
282
+ * `inline-block`, a grid `auto` track, a flex child without `min-width: 0` —
283
+ * measures 0, and the chart is the content that would have given it a width.
284
+ * That is a standing deadlock, not a slow start: the chart stays blank with
285
+ * no error. Give the parent a width, a `flex` basis, or `min-width: 0`, or
286
+ * pass a number.
287
+ *
288
+ * A container hidden by an ancestor's `display: none` is fine — it keeps the
289
+ * last width it measured and stays mounted, so a tab switch does not discard
290
+ * pan/zoom position, selection or hover.
291
+ *
292
+ * Pass a number whenever the width is already known — a fixed-size panel, a
293
+ * print layout, a test. It skips the measure pass and paints on the first
294
+ * render.
295
+ */
296
+ width?: number | 'auto';
129
297
  /** Vertical space between rows in CSS pixels (not under the axis). Default 0. */
130
298
  rowGap?: number;
131
299
  /**
@@ -449,6 +617,10 @@ export interface ChartContainerProps {
449
617
  * the shared time `xScale`. It renders its rows (separated by `rowGap`) then one
450
618
  * {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
451
619
  * (`<YAxis>`).
620
+ *
621
+ * A `width` in pixels renders straight through; `'auto'` (or an omitted
622
+ * `width`) measures the available width first — see {@link
623
+ * ChartContainerProps.width} and {@link AutoWidthContainer}.
452
624
  */
453
- export declare function ChartContainer({ range, maxBandWidth, bandAlign, width, rowGap, showAxis, trackerPosition, onTrackerChanged, onDrawStats, panZoom, bounds, onTimeRangeChange, minDuration, cursor: cursorProp, cursorSequence: cursorSequenceProp, onRegionSelect, regionSelectModifier, cursorTime: cursorTimeProp, crosshairSnap: crosshairSnapProp, editAnnotations, creating, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap, timeFormat, cursorFormat: cursorFormatProp, origin, theme, discontinuities, calendar, spacing, grid, sessionDividers, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
625
+ export declare function ChartContainer(props: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
454
626
  //# sourceMappingURL=ChartContainer.d.ts.map