@pond-ts/charts 0.41.0 → 0.43.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,49 +1,53 @@
1
1
  import { useContext, useEffect, useMemo } from 'react';
2
2
  import { ValueSeries } from 'pond-ts';
3
- import { barsFromTimeSeries, barsFromValueSeries } from './data.js';
4
- import { barAt, barExtent, barIndexAtTime, drawBars, resolveBarBaseline, } from './bars.js';
3
+ import { barsFromTimeSeries, barsFromValueSeries, categoryStack, stacksFromBins, stacksFromColumns, stacksFromGroups, } from './data.js';
4
+ import { barAt, barExtent, barIndexAtTime, drawBars, drawStacks, resolveBarBaseline, stackAt, stackBinExtent, stackValueExtent, } from './bars.js';
5
5
  import { ContainerContext, LayersContext, } from './context.js';
6
6
  import { useSlotKey } from './use-slot-key.js';
7
7
  /**
8
- * A bar draw layer: one rectangle per event, spanning the key's `[begin, end]`
9
- * (inset by `gap`) from the axis baseline to a numeric `column`'s value. Reads
10
- * the key endpoints + column into a {@link BarSeries}, registers into the
11
- * enclosing {@link Layers} (scaling against its `axis`), and renders nothing to
12
- * the DOM the row draws it. A gap (missing value) is skipped (no bar).
8
+ * A bar / histogram draw layer. In its simplest form, one rectangle per event
9
+ * spanning the key's `[begin, end]` from the axis baseline to a numeric
10
+ * `column`'s value (see below). It also draws **stacked** bars (a group-by
11
+ * dimension segments, `columns` / a `Map` series / `bins`) and **horizontal**
12
+ * bars (`orientation='horizontal'`, bins on the y axis) first-class histogram
13
+ * support. Registers into the enclosing {@link Layers} and renders nothing to the
14
+ * DOM; the row draws it.
13
15
  *
14
- * **Baseline.** Bars rest on the zero line when the axis domain spans zero (the
15
- * common all-positive auto-fit case {@link barExtent} pulls `0` into the
16
- * domain), or on the axis floor when an explicit `<YAxis min={…}>` sits above
17
- * zero (see {@link resolveBarBaseline}).
16
+ * **Data sources.** A time / value `TimeSeries` or `ValueSeries` (`column`), a
17
+ * wide series or `bins` array (`columns`), or a `Map<group, TimeSeries>`
18
+ * (`column`) the last three stack. Every shape composes from pond's own
19
+ * aggregation (`aggregate` / `byColumn` / `partitionBy`); the histogram guide
20
+ * has the recipes.
18
21
  *
19
- * **Interaction.** Hover joins the tracker (`sampleAt` the value of the bar
20
- * **under the cursor**) and lights that bar (hover-highlight). Click selects the
21
- * hit bar (`hitTest`); the matching bar — same key **and** this series' `label`,
22
- * so two series sharing a timestamp don't both light up — draws highlighted
23
- * (outlined for the committed select, fill-only for the transient hover). Both
24
- * resolve by **containment**: the tracker by the bar's `[begin, end]` time span
25
- * (`barIndexAtTime`), the click by the bar's pixel rect (`barAt`) — so the
26
- * readout reads the same bar you click, even across a wide bucket (they differ
27
- * only by the `gap` inset, where the pixel rect is narrower than the span).
22
+ * **Baseline (single, vertical).** Bars rest on the zero line when the axis
23
+ * domain spans zero, or on the axis floor when an explicit `<YAxis min>` sits
24
+ * above zero (see {@link resolveBarBaseline}).
28
25
  *
29
- * Both channels are also **controllable from outside** the chart via the
30
- * container: `selected`/`onSelect` (committed) and `hovered`/`onHover` (transient)
31
- * pass either to pin the lit/selected bar from a legend or list row, and read
32
- * the callback to mirror a bar-originated hover/click out-of-band. Symmetric pair,
33
- * keyed by the same {@link SelectInfo} identity.
26
+ * **Baseline (stacked).** A stack is **cumulative from value 0** the segments
27
+ * sum upward from the zero line, so its value axis **must include 0**. The
28
+ * auto-fit guarantees this: {@link stackValueExtent} always returns `[0, maxTotal]`.
29
+ * An explicit `<YAxis min>` **above** 0 is therefore unsupported for a stack — it
30
+ * would hide the bottom of the cumulative column; only the portion above the floor
31
+ * draws (clipped cleanly at the plot floor, as any bar below an explicit floor is).
32
+ * Segment values are assumed **non-negative** (a negative or zero segment is
33
+ * skipped — diverging stacks are out of scope).
34
34
  *
35
- * **Value axis** bars also scale on a value axis when fed a `ValueSeries`
36
- * (`series.byValue('dist')`): estela's distance-domain splits/laps, one bar per
37
- * segment over a monotonic axis. A `ValueSeries` is point-keyed, so the span is
38
- * neighbour-derived like a point `TimeSeries` (see {@link barsFromValueSeries}).
35
+ * **Interaction (opt-in via `id`).** Hover lights the bar / segment under the
36
+ * cursor (hit-tested by pixel rect, so it works in both orientations); click
37
+ * selects it (outlined). A stacked segment's identity is `(id, key = bin begin,
38
+ * label = group)`. Both channels are controllable from outside via the container
39
+ * (`selected`/`onSelect`, `hovered`/`onHover`). The in-chart `flag`/`crosshair`
40
+ * value cursor is single-series-vertical only.
39
41
  *
40
42
  * ```tsx
41
43
  * <Layers>
42
44
  * <BarChart series={hourlyVolume} column="count" />
45
+ * <BarChart series={byHost} column="n" colors={{ web1: '#…' }} />
46
+ * <BarChart bins={powerDist} column="seconds" orientation="horizontal" ordinal />
43
47
  * </Layers>
44
48
  * ```
45
49
  */
46
- export function BarChart({ series, column, as: semantic, axis, gap, index = 0, }) {
50
+ export function BarChart({ series, bins, categories, column, columns, as: semantic, colors, binColors, orientation = 'vertical', ordinal = false, id, axis, gap, index = 0, }) {
47
51
  const container = useContext(ContainerContext);
48
52
  if (container === null) {
49
53
  throw new Error('<BarChart> must be rendered inside a <ChartContainer>');
@@ -52,98 +56,291 @@ export function BarChart({ series, column, as: semantic, axis, gap, index = 0, }
52
56
  if (layers === null) {
53
57
  throw new Error('<BarChart> must be rendered inside a <Layers>');
54
58
  }
55
- const bs = useMemo(() => series instanceof ValueSeries
56
- ? barsFromValueSeries(series, column)
57
- : barsFromTimeSeries(series, column), [series, column]);
58
- // Styling: semantic identifier theme bar style. The single styling channel.
59
+ // Validate the data-source / value-column combination up front (throws are
60
+ // stable across renders, so no need to memoize them).
61
+ const nSources = (series !== undefined ? 1 : 0) +
62
+ (bins !== undefined ? 1 : 0) +
63
+ (categories !== undefined ? 1 : 0);
64
+ if (nSources !== 1) {
65
+ throw new Error('<BarChart> needs exactly one of `series`, `bins`, or `categories`');
66
+ }
67
+ if (categories !== undefined) {
68
+ if (column !== undefined || columns !== undefined) {
69
+ throw new Error('<BarChart categories> takes no `column`/`columns` (each datum carries its own value)');
70
+ }
71
+ if (orientation === 'horizontal') {
72
+ throw new Error('<BarChart categories> is vertical only (categories on x); horizontal category axes are not yet supported');
73
+ }
74
+ }
75
+ const isMap = series instanceof Map;
76
+ if (isMap && columns !== undefined) {
77
+ throw new Error('<BarChart> with a `Map` series stacks its groups — use `column` (the shared value column), not `columns`');
78
+ }
79
+ if (column !== undefined && columns !== undefined) {
80
+ throw new Error('<BarChart> takes `column` or `columns`, not both');
81
+ }
82
+ // The single series' semantic label (its identity for the readout + selection):
83
+ // the `as` role, else the value column. Used only on the single path.
84
+ const label = semantic ?? column ?? id ?? 'value';
85
+ // Build the chart-ready data view. Single-series *vertical* stays on the
86
+ // original BarSeries path (its pixels are unchanged); everything else — any
87
+ // stack, any horizontal — builds a StackedBarSeries (G === 1 for a single
88
+ // horizontal bar) so one oriented draw path covers it.
89
+ const shape = useMemo(() => {
90
+ if (categories !== undefined) {
91
+ // Categorical row-read: one unit-slot bar per category (G === 1), drawn on
92
+ // the container's band scale. The reused stacked geometry — only the axis
93
+ // (band scale + labels) is new.
94
+ return { kind: 'stacked', ss: categoryStack(categories) };
95
+ }
96
+ if (bins !== undefined) {
97
+ const cols = columns ?? (column !== undefined ? [column] : undefined);
98
+ if (cols === undefined) {
99
+ throw new Error('<BarChart bins> needs `column` or `columns`');
100
+ }
101
+ return { kind: 'stacked', ss: stacksFromBins(bins, cols, { ordinal }) };
102
+ }
103
+ if (isMap) {
104
+ if (column === undefined) {
105
+ throw new Error('<BarChart> with a `Map` series needs `column`');
106
+ }
107
+ return {
108
+ kind: 'stacked',
109
+ ss: stacksFromGroups(series, column),
110
+ };
111
+ }
112
+ const s = series;
113
+ if (columns !== undefined) {
114
+ return { kind: 'stacked', ss: stacksFromColumns(s, columns) };
115
+ }
116
+ if (column === undefined) {
117
+ throw new Error('<BarChart> needs `column` or `columns`');
118
+ }
119
+ if (orientation === 'horizontal') {
120
+ // Single horizontal bar: route through the stacked path (G === 1), naming
121
+ // the one group with the series' label so selection matches on it.
122
+ const ss = stacksFromColumns(s, [column]);
123
+ return { kind: 'stacked', ss: { ...ss, groups: [label] } };
124
+ }
125
+ return {
126
+ kind: 'single',
127
+ bs: s instanceof ValueSeries
128
+ ? barsFromValueSeries(s, column)
129
+ : barsFromTimeSeries(s, column),
130
+ };
131
+ }, [
132
+ series,
133
+ bins,
134
+ categories,
135
+ column,
136
+ columns,
137
+ ordinal,
138
+ orientation,
139
+ isMap,
140
+ label,
141
+ ]);
142
+ // The category labels — the ordinal axis's ordered column set (`xCategories`),
143
+ // and the per-bar readout label. `null` unless this is a categorical chart.
144
+ const categoryLabels = useMemo(() => categories?.map((c) => c.label) ?? null, [categories]);
145
+ // The bin axis kind — `'category'` for a categorical chart, else `'time'`/`'value'`
146
+ // (a `TimeSeries`/`Map` bins on time, a `ValueSeries`/`bins`-array on a value
147
+ // axis). For a vertical chart this is the shared x-kind; a horizontal one puts
148
+ // the *value* on x (always 'value') and the bin axis on a linear y.
149
+ const binAxisKind = categories !== undefined
150
+ ? 'category'
151
+ : bins !== undefined
152
+ ? 'value'
153
+ : isMap
154
+ ? 'time'
155
+ : series instanceof ValueSeries
156
+ ? 'value'
157
+ : 'time';
59
158
  const { bar } = container.theme;
60
- const style = (semantic !== undefined ? bar[semantic] : undefined) ?? bar.default;
61
- // Series identity for the readout + selection match (the `as` role, else the
62
- // column name).
63
- const label = semantic ?? column;
64
- // The gap prop overrides the theme default; otherwise the style carries it.
65
- const gapPx = gap ?? style.gap;
66
- // The current selection, narrowed to what the highlight match needs (key +
67
- // label). Read here so a selection change re-registers the layer (in the deps)
68
- // the data canvas repaints with the highlight. Infrequent (a click).
159
+ // Single-series style: the `as` role theme bar style (the single channel).
160
+ const singleStyle = (semantic !== undefined ? bar[semantic] : undefined) ?? bar.default;
161
+ const gapPx = gap ?? bar.default.gap;
162
+ // The stacked path's bar-thickness floor comes from `bar.default` (not the `as`
163
+ // role `as` is single-series only), matching how `gapPx` sources its default.
164
+ const stackMinWidth = bar.default.minWidth;
165
+ // Stacked style: per-group fills (colors override theme role default),
166
+ // plus the shared opacity / outline from the default bar style. Memoized on the
167
+ // groups + colours so a selection change doesn't rebuild it.
168
+ const groups = shape.kind === 'stacked' ? shape.ss.groups : undefined;
169
+ const stackStyle = useMemo(() => {
170
+ const base = bar.default;
171
+ const fills = (groups ?? []).map((g) => colors?.[g] ?? (bar[g] ?? base).fill);
172
+ return {
173
+ fills,
174
+ opacity: base.opacity,
175
+ outlineWidth: base.outlineWidth,
176
+ ...(binColors !== undefined ? { binFills: binColors } : {}),
177
+ };
178
+ }, [bar, groups, colors, binColors]);
179
+ // The current selection / hover, narrowed to the identity the highlight match
180
+ // needs. For a stack that's (id, key, label = group); the single path uses just
181
+ // (id, key). Read here so a change re-registers the layer → the canvas repaints.
69
182
  const selected = container.selected;
70
- const selection = useMemo(() => selected === null ? null : { key: selected.key, label: selected.label }, [selected]);
71
- // The transient hover-highlight, narrowed to the match key (key + label) like
72
- // the selection. Read here so a hover change re-registers the layer → the data
73
- // canvas repaints with the lit bar. Deduped in the container, so this only
74
- // fires on a bar transition (not every pointer move).
75
183
  const hoveredMark = container.hovered;
184
+ const selection = useMemo(() => selected === null
185
+ ? null
186
+ : {
187
+ id: selected.id,
188
+ key: selected.key,
189
+ label: selected.label,
190
+ ...(selected.mark !== undefined ? { mark: selected.mark } : {}),
191
+ }, [selected]);
76
192
  const hover = useMemo(() => hoveredMark === null
77
193
  ? null
78
- : { key: hoveredMark.key, label: hoveredMark.label }, [hoveredMark]);
79
- const entry = useMemo(() => ({
80
- layer: {
81
- yExtent: () => barExtent(bs),
82
- // The container infers the shared x scale's kind from its layers — a
83
- // ValueSeries bars on a value axis, a TimeSeries on time.
84
- xKind: series instanceof ValueSeries ? 'value' : 'time',
85
- xExtent: () => bs.length === 0 ? null : [bs.begin[0], bs.end[bs.length - 1]],
86
- sampleAt: (time) => {
87
- // The flag belongs to the bar **under the cursor** — the bar whose
88
- // span `[begin, end]` contains `time` (barIndexAtTime), NOT
89
- // nearest-by-begin (which flips to the next bar past a wide bar's
90
- // midpoint, landing the flag on the wrong bar). For a point key the
91
- // span is the neighbour-derived Voronoi cell (`barsFromTimeSeries`
92
- // widens `begin === end` into one), so the cells tile the axis and a
93
- // moving cursor always lands in one. Before the first / after the last
94
- // bar no readout, matching the line/area tracker.
95
- if (bs.length === 0)
96
- return [];
97
- const i = barIndexAtTime(bs, time);
98
- if (i < 0)
99
- return [];
100
- const v = bs.y[i];
101
- if (!Number.isFinite(v))
102
- return []; // a gap bar (missing value) reads nothing
103
- // Anchor at the bar's **top-centre** (RFC): the span's centre time
104
- // `(begin + end) / 2` (the bucket mid for an interval key; the Voronoi
105
- // cell centre — ~on the point — for a point key), at `yScale(value)` =
106
- // the bar top. A tall bar (top above the flag stack) drops the staff for
107
- // free (the shared `s.py > stackBottom` rule).
108
- return [
109
- {
110
- x: (bs.begin[i] + bs.end[i]) / 2,
111
- value: v,
112
- color: style.fill,
113
- label,
194
+ : {
195
+ id: hoveredMark.id,
196
+ key: hoveredMark.key,
197
+ label: hoveredMark.label,
198
+ ...(hoveredMark.mark !== undefined
199
+ ? { mark: hoveredMark.mark }
200
+ : {}),
201
+ }, [hoveredMark]);
202
+ const entry = useMemo(() => {
203
+ // ── Single-series, vertical: the original bar path, pixels unchanged. ──
204
+ if (shape.kind === 'single') {
205
+ const bs = shape.bs;
206
+ return {
207
+ layer: {
208
+ yExtent: () => barExtent(bs),
209
+ xKind: binAxisKind,
210
+ xExtent: () => bs.length === 0 ? null : [bs.begin[0], bs.end[bs.length - 1]],
211
+ sampleAt: (time) => {
212
+ if (bs.length === 0)
213
+ return [];
214
+ const i = barIndexAtTime(bs, time);
215
+ if (i < 0)
216
+ return [];
217
+ const v = bs.y[i];
218
+ if (!Number.isFinite(v))
219
+ return [];
220
+ return [
221
+ {
222
+ x: (bs.begin[i] + bs.end[i]) / 2,
223
+ value: v,
224
+ color: singleStyle.fill,
225
+ label,
226
+ },
227
+ ];
114
228
  },
115
- ];
116
- },
117
- hitTest: (px, py, xScale, yScale) => {
118
- const baseline = resolveBarBaseline(yScale);
119
- const hit = barAt(bs, px, py, xScale, yScale, baseline, gapPx, style.minWidth);
120
- if (hit === null)
121
- return null;
122
- const [, begin, value] = hit;
123
- // key = the bar's begin (its stable identity); colour = the resolved
124
- // fill; label = this series' identity (so the highlight targets the
125
- // exact clicked series, not another sharing the timestamp).
126
- return { key: begin, value, color: style.fill, label };
229
+ ...(id === undefined
230
+ ? {}
231
+ : {
232
+ hitTest: (px, py, xScale, yScale) => {
233
+ const baseline = resolveBarBaseline(yScale);
234
+ const hit = barAt(bs, px, py, xScale, yScale, baseline, gapPx, singleStyle.minWidth);
235
+ if (hit === null)
236
+ return null;
237
+ const [, begin, value] = hit;
238
+ return {
239
+ id,
240
+ key: begin,
241
+ value,
242
+ color: singleStyle.fill,
243
+ label,
244
+ };
245
+ },
246
+ }),
247
+ draw: (ctx, xScale, yScale) => drawBars(ctx, bs, xScale, yScale, singleStyle, resolveBarBaseline(yScale), gapPx, id, selection, hover),
248
+ },
249
+ axisId: axis,
250
+ index,
251
+ };
252
+ }
253
+ // ── Stacked (or single horizontal): the oriented, transposed draw path. ──
254
+ const ss = shape.ss;
255
+ const binExtent = () => stackBinExtent(ss);
256
+ const valueExtent = () => stackValueExtent(ss);
257
+ const vertical = orientation === 'vertical';
258
+ return {
259
+ layer: {
260
+ // Horizontal puts the value on the shared x (always 'value'); vertical
261
+ // keeps the bin axis on x. The bin axis on the *other* side is a linear
262
+ // numeric scale either way (time ms label via <YAxis ticks>).
263
+ xKind: vertical ? binAxisKind : 'value',
264
+ xExtent: vertical ? binExtent : valueExtent,
265
+ yExtent: vertical ? valueExtent : binExtent,
266
+ // A categorical chart hands the container its ordered category names — the
267
+ // ordinal axis domain the shared band scale + label formatter build on.
268
+ ...(categoryLabels !== null
269
+ ? { xCategories: () => categoryLabels }
270
+ : {}),
271
+ // No x-scrub flag for a stack / horizontal chart — hover + click read it
272
+ // out instead (the flag is single-series-vertical only).
273
+ sampleAt: () => [],
274
+ ...(id === undefined
275
+ ? {}
276
+ : {
277
+ hitTest: (px, py, xScale, yScale) => {
278
+ const hit = stackAt(ss, px, py, orientation, xScale, yScale, gapPx, stackMinWidth);
279
+ if (hit === null)
280
+ return null;
281
+ const [bi, g, begin, name, value] = hit;
282
+ // A categorical bar carries a stable per-bar `mark` (its column
283
+ // name); the selection keys on `(id, mark)` so it survives a
284
+ // reorder. `bi` is the exact bin index from the hit.
285
+ const stableMark = ss.marks?.[bi];
286
+ return {
287
+ id,
288
+ key: begin,
289
+ value,
290
+ // A per-bin colour override wins over the group fill, so the
291
+ // readout pill reads the bar's own colour.
292
+ color: stackStyle.binFills?.[bi] ?? stackStyle.fills[g],
293
+ // A categorical bar reports its category name; a stack reports
294
+ // the group.
295
+ label: stableMark ?? name,
296
+ ...(stableMark !== undefined ? { mark: stableMark } : {}),
297
+ };
298
+ },
299
+ }),
300
+ draw: (ctx, xScale, yScale) => drawStacks(ctx, ss, orientation, xScale, yScale, stackStyle, gapPx, stackMinWidth, id, selection, hover),
127
301
  },
128
- draw: (ctx, xScale, yScale) => drawBars(ctx, bs, xScale, yScale, style, resolveBarBaseline(yScale), gapPx, label, selection, hover),
129
- },
130
- axisId: axis,
302
+ axisId: axis,
303
+ index,
304
+ };
305
+ }, [
306
+ shape,
307
+ binAxisKind,
308
+ categoryLabels,
309
+ orientation,
310
+ singleStyle,
311
+ stackStyle,
312
+ label,
313
+ id,
314
+ gapPx,
315
+ stackMinWidth,
316
+ selection,
317
+ hover,
318
+ axis,
131
319
  index,
132
- }), [bs, series, column, style, label, gapPx, selection, hover, axis, index]);
133
- // A stable per-instance slot (see useSlotKey) keeps this layer's z-position
134
- // fixed across series/style/selection updates (no jump to the front).
320
+ ]);
321
+ // A stable per-instance slot keeps this layer's z-position fixed across data /
322
+ // style / selection updates (see useSlotKey).
135
323
  const slot = useSlotKey();
136
324
  useEffect(() => () => layers.unregisterLayer(slot), [layers, slot]);
137
325
  useEffect(() => {
138
326
  layers.registerLayer(slot, entry);
139
327
  }, [layers, slot, entry]);
140
- // Also a tracker source: the container fans in this series' value at the
141
- // cursor for the (outside-the-chart) readout.
328
+ // Also a tracker source: the container fans in this layer's value at the cursor
329
+ // for the (outside-the-chart) readout. A stacked / horizontal layer's sampleAt
330
+ // returns nothing, so it contributes no flag but still registers cleanly.
142
331
  const { registerTrackerSource, unregisterTrackerSource } = container;
143
332
  useEffect(() => () => unregisterTrackerSource(slot), [unregisterTrackerSource, slot]);
144
333
  useEffect(() => {
145
334
  registerTrackerSource(slot, entry.layer);
146
335
  }, [registerTrackerSource, slot, entry.layer]);
336
+ // Advertise selectability (only when an `id` was given).
337
+ const { registerSelectable, unregisterSelectable } = container;
338
+ useEffect(() => {
339
+ if (id === undefined)
340
+ return;
341
+ registerSelectable(slot);
342
+ return () => unregisterSelectable(slot);
343
+ }, [registerSelectable, unregisterSelectable, slot, id]);
147
344
  return null;
148
345
  }
149
346
  //# sourceMappingURL=BarChart.js.map
@@ -0,0 +1,16 @@
1
+ import { type XAxisProps } from './XAxis.js';
2
+ /**
3
+ * The category-flavoured preset of {@link XAxis} — `<CategoryAxis />` is
4
+ * `<XAxis />`. The axis kind follows the data: on a **category** container (a
5
+ * layer that plots on the ordinal column-domain axis) it ticks once per category,
6
+ * labelling each band centre with the category name (the container's shared
7
+ * formatter). Kept as the familiar name for categorical charts, mirroring
8
+ * {@link TimeAxis}. Forwards every {@link XAxisProps} (`label`, `side`, …).
9
+ *
10
+ * A high-cardinality axis (many categories) thins + truncates its labels to stay
11
+ * legible (categorical-axis RFC, Phase 1). The labels **come from the data** (the
12
+ * `categories` list), so a d3 `format` prop does not apply here (it can't name a
13
+ * category); customize a label by changing the `categories` datum's `label`.
14
+ */
15
+ export declare function CategoryAxis(props?: XAxisProps): import("react/jsx-runtime").JSX.Element;
16
+ //# sourceMappingURL=CategoryAxis.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { XAxis } from './XAxis.js';
3
+ /**
4
+ * The category-flavoured preset of {@link XAxis} — `<CategoryAxis />` is
5
+ * `<XAxis />`. The axis kind follows the data: on a **category** container (a
6
+ * layer that plots on the ordinal column-domain axis) it ticks once per category,
7
+ * labelling each band centre with the category name (the container's shared
8
+ * formatter). Kept as the familiar name for categorical charts, mirroring
9
+ * {@link TimeAxis}. Forwards every {@link XAxisProps} (`label`, `side`, …).
10
+ *
11
+ * A high-cardinality axis (many categories) thins + truncates its labels to stay
12
+ * legible (categorical-axis RFC, Phase 1). The labels **come from the data** (the
13
+ * `categories` list), so a d3 `format` prop does not apply here (it can't name a
14
+ * category); customize a label by changing the `categories` datum's `label`.
15
+ */
16
+ export function CategoryAxis(props = {}) {
17
+ return _jsx(XAxis, { ...props });
18
+ }
19
+ //# sourceMappingURL=CategoryAxis.js.map
@@ -1,4 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
+ import { type DiscontinuityProvider, type TradingCalendarLike } from './tradingTimeScale.js';
3
+ import { Sequence, BoundedSequence } from 'pond-ts';
2
4
  import type { TimeRange } from 'pond-ts';
3
5
  import { type AnnotationKind, type CreateSpec, type CursorMode, type SelectInfo, type TrackerInfo } from './context.js';
4
6
  import { type AxisFormat } from './format.js';
@@ -12,6 +14,45 @@ export interface ChartContainerProps {
12
14
  * the data — so a tuple stays a time domain on a time chart.
13
15
  */
14
16
  range?: readonly [number, number] | TimeRange;
17
+ /**
18
+ * A **trading-calendar** discontinuity provider — closed-market time
19
+ * (weekends, holidays, overnight, lunch breaks) collapsed. Supply it to turn
20
+ * the shared x axis into a **trading-time** axis: gaps disappear and time
21
+ * stays proportional within each session. A `@pond-ts/financial`
22
+ * `TradingCalendar.discontinuities()` satisfies this structurally (charts
23
+ * never imports that package). The **low-level** primitive: pass
24
+ * `calendar.discontinuities()` (or a `{ spacing, period }` variant) directly.
25
+ * Only affects a **time** axis (ignored on a value axis). Takes precedence
26
+ * over {@link calendar} if both are given.
27
+ *
28
+ * **Pass a stable reference.** The scale (and container frame) rebuild when
29
+ * this prop's identity changes, so memoize it — `const disc = useMemo(() =>
30
+ * calendar.discontinuities(), [calendar])` — rather than calling
31
+ * `.discontinuities()` inline in JSX, which would rebuild every render.
32
+ */
33
+ discontinuities?: DiscontinuityProvider;
34
+ /**
35
+ * The **high-level** sugar for {@link discontinuities}: a trading calendar the
36
+ * container derives the provider from itself (`calendar.discontinuities({
37
+ * spacing })`), so you don't wire the low-level prop. A `@pond-ts/financial`
38
+ * `TradingCalendar` satisfies the structural {@link TradingCalendarLike} shape
39
+ * (charts never imports that package). Combine with {@link spacing}. For the
40
+ * full option matrix (a bar `period`, a scoped `range`) use the low-level
41
+ * `discontinuities` prop instead. Only affects a **time** axis.
42
+ *
43
+ * The provider is memoized on `(calendar, spacing)`, so pass a **stable**
44
+ * calendar reference (build it once, not inline in JSX).
45
+ */
46
+ calendar?: TradingCalendarLike;
47
+ /**
48
+ * The trading axis **metric**, when a {@link calendar} is supplied
49
+ * (trading-calendar RFC Q7). `'proportional'` (default) keeps time
50
+ * proportional within and across sessions — a half-day is half as wide.
51
+ * `'uniform'` gives every session equal width (the TradingView ordinal look).
52
+ * Ignored without `calendar` (a low-level `discontinuities` provider already
53
+ * carries its own metric).
54
+ */
55
+ spacing?: 'proportional' | 'uniform';
15
56
  /** Total width in CSS pixels (plot + axis gutters). */
16
57
  width: number;
17
58
  /** Vertical space between rows in CSS pixels (not under the axis). Default 0. */
@@ -35,9 +76,55 @@ export interface ChartContainerProps {
35
76
  * via `<ChartRow cursor>`). **Default `'line'`** — the synced vertical line,
36
77
  * with values surfaced *outside* the chart via {@link onTrackerChanged}.
37
78
  * `'point'` / `'inline'` / `'flag'` add per-series marks; `'none'` hides it.
79
+ * `'region'` shades the bucket under the pointer (needs {@link cursorSequence}).
38
80
  * See {@link CursorMode}.
39
81
  */
40
82
  cursor?: CursorMode;
83
+ /**
84
+ * The bucketing for `cursor="region"` — the interval highlighted under the
85
+ * pointer. A pond {@link Sequence} (duration or calendar-aware —
86
+ * `Sequence.every('1d')`, `Sequence.calendar('month')`) is realized over the
87
+ * current view; a {@link BoundedSequence} (e.g. a `TradingCalendar`'s
88
+ * `sessionSequence()` / `barSequence()`) is used as-is, so the band can track
89
+ * whole **sessions**. Either way the band maps through `xScale`, so on a
90
+ * trading-time axis the closed part of the bucket collapses. Ignored unless
91
+ * `cursor="region"`.
92
+ *
93
+ * **Time axis only.** A bucket is a *time* interval, so the region cursor is
94
+ * gated to a **time** x-axis — on a **value** axis (a horizontal histogram, a
95
+ * value-keyed chart) it's a no-op (highlighting a value *band* on a horizontal
96
+ * histogram would be a different, y-oriented cursor).
97
+ *
98
+ * **Pass a stable reference.** The buckets are memoized on this value + the
99
+ * view range; a `Sequence`/`BoundedSequence` rebuilt inline every render
100
+ * re-realizes the buckets on each pointer move (harmless for a coarse
101
+ * day/session sequence, wasteful for a fine one over a wide view) — hoist it or
102
+ * `useMemo` it.
103
+ */
104
+ cursorSequence?: Sequence | BoundedSequence;
105
+ /**
106
+ * Makes the `region` cursor **draggable**: drag across the plot and the band
107
+ * extends **bucket by bucket** (snapping to `cursorSequence` points); on
108
+ * release this fires **once** with the selected `[start, end)` `TimeRange`, and
109
+ * the cursor reverts to the single-bucket highlight (it does not keep the
110
+ * range). Typical use — zoom the view to the returned range (the container
111
+ * doesn't zoom itself; that's the consumer's call).
112
+ *
113
+ * With **no `cursorSequence`** the region cursor is the degenerate case — it
114
+ * renders as a **line** on hover and the drag is **freeform** (raw `[start,
115
+ * end)`, no bucket snapping); the same callback fires on release. No-op unless
116
+ * `cursor="region"` (and a **time** x-axis).
117
+ */
118
+ onRegionSelect?: (range: TimeRange) => void;
119
+ /**
120
+ * Which modifier a region-drag needs — set `'shift'` when you also enable
121
+ * `panZoom` and want **plain drag to pan, shift-drag to select**. It's only
122
+ * enforced while `panZoom` is on (with pan off there's no gesture conflict, so
123
+ * shift is optional — either drag selects). **Omitted** ⇒ a region-drag
124
+ * **preempts** pan (drag always selects; document that precedence for users).
125
+ * Wheel-zoom is unaffected in every case.
126
+ */
127
+ regionSelectModifier?: 'shift';
41
128
  /**
42
129
  * Fires on pointer move with the hovered time + every series' value there (so
43
130
  * you can render a readout outside the chart), and `null` on leave.
@@ -46,15 +133,21 @@ export interface ChartContainerProps {
46
133
  /**
47
134
  * Controlled selection — the selected mark (echo the `onSelect` arg back), or
48
135
  * `null`. **Omitted ⇒ uncontrolled** (a click on a selectable layer manages it
49
- * internally; pass `null` to force nothing selected). Selectable layers
50
- * (`BarChart`, `BoxPlot`, `ScatterChart`) highlight the mark matching both its
51
- * key and series so two series sharing a timestamp don't both light up.
136
+ * internally; pass `null` to force nothing selected). A layer is **selectable
137
+ * only when it carries an `id`** (the stable series identity) — `BarChart` /
138
+ * `ScatterChart` highlight the mark matching the selection's `id` (the series)
139
+ * and its `key` (the sample), so two series sharing a timestamp don't both
140
+ * light up, and the selection survives a data update (it keys on the stable
141
+ * `id`, not the sample `key`). A layer with no `id` renders + reads out but
142
+ * can't be selected.
52
143
  */
53
144
  selected?: SelectInfo | null;
54
145
  /**
55
146
  * Fires when a selectable layer's mark is clicked, with the hit mark, or `null`
56
- * when a click misses every mark (clears the selection). Notification only
57
- * works in both controlled and uncontrolled mode.
147
+ * when a click misses every mark (or hits a layer with no `id` display-only,
148
+ * so it reads as empty space). Notification only — works in both controlled and
149
+ * uncontrolled mode. If this or `selected` is set but no layer has an `id`, a
150
+ * dev-warning notes that nothing is selectable.
58
151
  */
59
152
  onSelect?: (hit: SelectInfo | null) => void;
60
153
  /**
@@ -180,5 +273,5 @@ export interface ChartContainerProps {
180
273
  * {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
181
274
  * (`<YAxis>`).
182
275
  */
183
- export declare function ChartContainer({ range, width, rowGap, showAxis, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom, onTimeRangeChange, minDuration, cursor, cursorTime, crosshairSnap, editAnnotations, creating, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap, timeFormat, theme, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
276
+ export declare function ChartContainer({ range, width, rowGap, showAxis, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom, onTimeRangeChange, minDuration, cursor, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime, crosshairSnap, editAnnotations, creating, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap, timeFormat, theme, discontinuities, calendar, spacing, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
184
277
  //# sourceMappingURL=ChartContainer.d.ts.map