@pond-ts/charts 0.53.1 → 0.55.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.
@@ -1,22 +1,10 @@
1
1
  import { ValueSeries } from 'pond-ts';
2
2
  import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
3
+ import type { NumericColumn, ValueNumericColumn } from './column-names.js';
3
4
  import type { DecimateOption } from './decimate.js';
4
5
  import { type Curve } from './curve.js';
5
6
  import { type GapMode } from './gaps.js';
6
- export interface AreaChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
7
- /**
8
- * The source series. A `TimeSeries` fills against the time axis; a
9
- * `ValueSeries` (`series.byValue('dist')`) against its value axis — the
10
- * container infers which from the data, no axis-type prop (mirrors
11
- * `<LineChart>`). Either way `column` names the numeric value to fill from.
12
- *
13
- * **Live charts:** `series.byValue(…)` mints a *fresh* projection each call, so
14
- * an inline `series={s.byValue('dist')}` re-registers this layer every render —
15
- * on a frequently re-rendering chart, memoize the projection (`useMemo`).
16
- */
17
- series: TimeSeries<S> | ValueSeries<VS>;
18
- /** Name of the numeric value column to fill from. */
19
- column: string;
7
+ export interface AreaChartCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
20
8
  /**
21
9
  * The series' semantic identifier — what the data _is_ / how it should read
22
10
  * (e.g. `elevation`, or a signed-traffic role like `in` / `out`). The theme
@@ -88,6 +76,32 @@ export interface AreaChartProps<S extends SeriesSchema = SeriesSchema, VS extend
88
76
  */
89
77
  index?: number;
90
78
  }
79
+ /**
80
+ * AreaChart's source + column props, a **union over the series kind** so the
81
+ * column names are checked against the schema that was actually passed
82
+ * ([PND-CHARTAPI]). A single member carrying `NumericColumn<S> |
83
+ * ValueNumericColumn<VS>` would silently widen to `string`: only one of the
84
+ * two generics is ever inferred, and the other falls back (measured in
85
+ * `spikes/charts-type-seam/`). Loosely-typed series still accept any name.
86
+ */
87
+ type AreaChartSource<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = {
88
+ /**
89
+ * The source series. **Live charts:** `series.byValue(…)` mints a
90
+ * *fresh* projection each call, so an inline `series={s.byValue('d')}`
91
+ * re-registers this layer every render — on a frequently re-rendering
92
+ * (e.g. scrub-driven) chart, memoize the projection (`useMemo`) so the
93
+ * layer isn't rebuilt each frame.
94
+ */
95
+ series: TimeSeries<S>;
96
+ column: NumericColumn<S>;
97
+ readout?: NumericColumn<S>;
98
+ } | {
99
+ series: ValueSeries<VS>;
100
+ column: ValueNumericColumn<VS>;
101
+ readout?: ValueNumericColumn<VS>;
102
+ };
103
+ /** `<AreaChart>`'s props: the shared knobs plus one series-kind source shape. */
104
+ export type AreaChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = AreaChartCommon<S, VS> & AreaChartSource<S, VS>;
91
105
  /**
92
106
  * An area draw layer: fills between a value `column` and a `baseline`, with a
93
107
  * graded (gradient) shade — opaque at the line, transparent at the baseline —
@@ -108,5 +122,6 @@ export interface AreaChartProps<S extends SeriesSchema = SeriesSchema, VS extend
108
122
  * </Layers>
109
123
  * ```
110
124
  */
111
- export declare function AreaChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, as: semantic, axis, baseline, curve, gaps, decimate, legend, index, }: AreaChartProps<S, VS>): null;
125
+ export declare function AreaChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, readout, as: semantic, axis, baseline, curve, gaps, decimate, legend, index, }: AreaChartProps<S, VS>): null;
126
+ export {};
112
127
  //# sourceMappingURL=AreaChart.d.ts.map
package/dist/AreaChart.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useContext, useEffect, useMemo } from 'react';
2
2
  import { ValueSeries } from 'pond-ts';
3
- import { fromTimeSeries, fromValueSeries } from './data.js';
3
+ import { assertNumericColumn, fromTimeSeries, fromValueSeries, } from './data.js';
4
4
  import { areaExtent, drawArea } from './area.js';
5
5
  import { resolveCurve } from './curve.js';
6
6
  import { DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
@@ -36,7 +36,7 @@ function domainFloor(yScale) {
36
36
  * </Layers>
37
37
  * ```
38
38
  */
39
- export function AreaChart({ series, column, as: semantic, axis, baseline, curve, gaps = DEFAULT_GAP_MODE, decimate = true, legend, index = 0, }) {
39
+ export function AreaChart({ series, column, readout, as: semantic, axis, baseline, curve, gaps = DEFAULT_GAP_MODE, decimate = true, legend, index = 0, }) {
40
40
  const container = useContext(ContainerContext);
41
41
  if (container === null) {
42
42
  throw new Error('<AreaChart> must be rendered inside a <ChartContainer>');
@@ -48,6 +48,22 @@ export function AreaChart({ series, column, as: semantic, axis, baseline, curve,
48
48
  const cs = useMemo(() => series instanceof ValueSeries
49
49
  ? fromValueSeries(series, column)
50
50
  : fromTimeSeries(series, column), [series, column]);
51
+ // Readout column values for a value-axis series (time path reads it off the
52
+ // event) — the tracker reports it alongside the plotted fill so an off-chart
53
+ // readout can show a source value. See AreaChartProps.readout.
54
+ //
55
+ // The time path buffers nothing (it has an event, not an index), so it
56
+ // validates the name here instead, so a mistyped `readout` fails the same way
57
+ // on both axis kinds rather than throwing on one and silently doing nothing
58
+ // on the other. Mirrors `<LineChart>`.
59
+ const readoutY = useMemo(() => {
60
+ if (readout === undefined)
61
+ return undefined;
62
+ if (series instanceof ValueSeries)
63
+ return fromValueSeries(series, readout).y;
64
+ assertNumericColumn(series, readout);
65
+ return undefined;
66
+ }, [series, readout]);
51
67
  // Styling: semantic identifier → theme area style. The single styling channel.
52
68
  const { area } = container.theme;
53
69
  const style = (semantic !== undefined ? area[semantic] : undefined) ?? area.default;
@@ -77,8 +93,19 @@ export function AreaChart({ series, column, as: semantic, axis, baseline, curve,
77
93
  if (i < 0)
78
94
  return [];
79
95
  const v = cs.y[i];
96
+ const rv = readoutY?.[i];
80
97
  return Number.isFinite(v)
81
- ? [{ x: cs.x[i], value: v, color: style.color, label }]
98
+ ? [
99
+ {
100
+ x: cs.x[i],
101
+ value: v,
102
+ color: style.color,
103
+ label,
104
+ ...(rv !== undefined && Number.isFinite(rv)
105
+ ? { readout: rv }
106
+ : {}),
107
+ },
108
+ ]
82
109
  : [];
83
110
  }
84
111
  const e = series.nearest(x);
@@ -87,11 +114,23 @@ export function AreaChart({ series, column, as: semantic, axis, baseline, curve,
87
114
  // get() wants a literal key; column is a runtime string. Cast the
88
115
  // *event* (not the method — that would detach `this`) to a
89
116
  // string-keyed get; runtime-safe read + guard.
90
- const v = e.get(column);
117
+ const ev = e;
118
+ const v = ev.get(column);
119
+ const rv = readout !== undefined ? ev.get(readout) : undefined;
91
120
  // The readout dot rides the value line (not the baseline), coloured by
92
121
  // the outline stroke. A gap yields no readout (like the fill).
93
122
  return typeof v === 'number' && Number.isFinite(v)
94
- ? [{ x: e.begin(), value: v, color: style.color, label }]
123
+ ? [
124
+ {
125
+ x: e.begin(),
126
+ value: v,
127
+ color: style.color,
128
+ label,
129
+ ...(typeof rv === 'number' && Number.isFinite(rv)
130
+ ? { readout: rv }
131
+ : {}),
132
+ },
133
+ ]
95
134
  : [];
96
135
  },
97
136
  draw: (ctx, xScale, yScale) => drawArea(ctx, cs, xScale, yScale, style,
@@ -106,6 +145,8 @@ export function AreaChart({ series, column, as: semantic, axis, baseline, curve,
106
145
  cs,
107
146
  series,
108
147
  column,
148
+ readout,
149
+ readoutY,
109
150
  style,
110
151
  label,
111
152
  baseline,
@@ -1,24 +1,9 @@
1
1
  import { ValueSeries } from 'pond-ts';
2
2
  import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
3
+ import type { NumericColumn, ValueNumericColumn } from './column-names.js';
3
4
  import { type Curve } from './curve.js';
4
5
  import type { DecimateOption } from './decimate.js';
5
- export interface BandChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
6
- /**
7
- * The source series. A `TimeSeries` fills the envelope against the time axis;
8
- * a `ValueSeries` (`series.byValue('dist')`) against its value axis — the
9
- * container infers which from the data, no axis-type prop (mirrors
10
- * `<LineChart>` / `<AreaChart>`). Either way `lower`/`upper` name the numeric
11
- * edge columns.
12
- *
13
- * **Live charts:** `series.byValue(…)` mints a *fresh* projection each call, so
14
- * an inline `series={s.byValue('dist')}` re-registers this layer every render —
15
- * on a frequently re-rendering chart, memoize the projection (`useMemo`).
16
- */
17
- series: TimeSeries<S> | ValueSeries<VS>;
18
- /** Name of the numeric column for the band's lower edge (e.g. `p25`). */
19
- lower: string;
20
- /** Name of the numeric column for the band's upper edge (e.g. `p75`). */
21
- upper: string;
6
+ export interface BandChartCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
22
7
  /**
23
8
  * The band's semantic identifier — what the spread _is_ (e.g. `outer` for a
24
9
  * p5/p95 envelope, `inner` for p25/p75). The theme maps it to a
@@ -64,6 +49,32 @@ export interface BandChartProps<S extends SeriesSchema = SeriesSchema, VS extend
64
49
  */
65
50
  index?: number;
66
51
  }
52
+ /**
53
+ * BandChart's source + column props, a **union over the series kind** so the
54
+ * column names are checked against the schema that was actually passed
55
+ * ([PND-CHARTAPI]). A single member carrying `NumericColumn<S> |
56
+ * ValueNumericColumn<VS>` would silently widen to `string`: only one of the
57
+ * two generics is ever inferred, and the other falls back (measured in
58
+ * `spikes/charts-type-seam/`). Loosely-typed series still accept any name.
59
+ */
60
+ type BandChartSource<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = {
61
+ /**
62
+ * The source series. **Live charts:** `series.byValue(…)` mints a
63
+ * *fresh* projection each call, so an inline `series={s.byValue('d')}`
64
+ * re-registers this layer every render — on a frequently re-rendering
65
+ * (e.g. scrub-driven) chart, memoize the projection (`useMemo`) so the
66
+ * layer isn't rebuilt each frame.
67
+ */
68
+ series: TimeSeries<S>;
69
+ lower: NumericColumn<S>;
70
+ upper: NumericColumn<S>;
71
+ } | {
72
+ series: ValueSeries<VS>;
73
+ lower: ValueNumericColumn<VS>;
74
+ upper: ValueNumericColumn<VS>;
75
+ };
76
+ /** `<BandChart>`'s props: the shared knobs plus one series-kind source shape. */
77
+ export type BandChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = BandChartCommon<S, VS> & BandChartSource<S, VS>;
67
78
  /**
68
79
  * A variance-band draw layer: fills the envelope between the `lower` and `upper`
69
80
  * columns of `series` (typically `rollingByColumn` percentiles), gap-aware, and
@@ -82,4 +93,5 @@ export interface BandChartProps<S extends SeriesSchema = SeriesSchema, VS extend
82
93
  * ```
83
94
  */
84
95
  export declare function BandChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, lower, upper, as: semantic, axis, curve, decimate, legend, index, }: BandChartProps<S, VS>): null;
96
+ export {};
85
97
  //# sourceMappingURL=BandChart.d.ts.map
@@ -2,55 +2,86 @@ import { ValueSeries } from 'pond-ts';
2
2
  import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
3
3
  import { type BinRecord, type CategoryDatum } from './data.js';
4
4
  import { type Orientation } from './bars.js';
5
+ import type { NumericColumn, ValueNumericColumn } from './column-names.js';
5
6
  import type { DecimateOption } from './decimate.js';
6
- export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
7
- /**
8
- * The source series. Provide **exactly one** of `series` or `bins`.
9
- *
10
- * - A **`TimeSeries`** (interval / timeRange-keyed is the primary form each
11
- * event's key `[begin, end]` is a bar's x-span; a point-keyed series derives
12
- * its width from neighbour spacing) → single-series bars via `column`, or
13
- * stacked bars from a **wide** series via `columns`.
14
- * - A **`ValueSeries`** (`series.byValue('dist')`) bars against its value axis.
15
- * - A **`ReadonlyMap<group, TimeSeries>`** — one series per stack group, all on
16
- * the same bin grid, the shape
17
- * `series.partitionBy('host', { groups }).aggregate(seq, m).toMap()` returns.
18
- * Stacked bars, `column` names the shared value column, groups = map order.
19
- *
20
- * **Live charts:** `series.byValue(…)` / `.toMap()` mint fresh objects each
21
- * call, so an inline `series={…}` re-registers this layer every render on a
22
- * frequently re-rendering chart, memoize the projection (`useMemo`).
23
- */
24
- series?: TimeSeries<S> | ValueSeries<VS> | ReadonlyMap<string, TimeSeries<S>>;
25
- /**
26
- * `byColumn` **bin records** `Array<{ start, end, …aggregates }>` from a
27
- * value-band aggregation
28
- * (`series.byColumn('power', { width: 20 }, { seconds: })`). The value-axis
29
- * alternative to `series`: `column` / `columns` name the aggregate field(s) to
30
- * draw. Pair with `ordinal` for a category (band) axis.
31
- */
32
- bins?: readonly BinRecord[];
33
- /**
34
- * **Categorical** data — an ordered `{ label, value }[]`, one bar per category
35
- * on a first-class **ordinal category x-axis** (the container infers
36
- * `xKind:'category'` and builds a band scale over the labels). The transpose
37
- * view's "columns on x": each `label` is a category (ticker / account / zone),
38
- * `value` its bar height. Provide **exactly one** of `series` / `bins` /
39
- * `categories`; `categories` takes no `column`/`columns` and is **vertical only**
40
- * (categories on x). Colour per category via `binColors`. (Categorical-axis RFC,
41
- * Phase 1.)
42
- */
43
- categories?: readonly CategoryDatum[];
44
- /** Name of the numeric value column for the bar height (single series). Provide
45
- * `column` **or** `columns`, not both. */
46
- column?: string;
47
- /**
48
- * Stacked-segment columns, **bottom → top** — one segment per name. Use with a
49
- * **wide** `series` (e.g. `pivotByGroup` output) or with `bins`. Mutually
50
- * exclusive with `column`, and invalid with a `Map` series (there the segments
51
- * are the map's groups; use `column`).
52
- */
53
- columns?: readonly string[];
7
+ /**
8
+ * The **mode union** — the legal (source, columns) combinations, each a
9
+ * separate member so an illegal mix fails to compile instead of throwing at
10
+ * render ([PND-CHARTAPI]; the 2026-08 API review's #1 item). Column names are
11
+ * schema-derived where the source is a series (see `column-names.ts`, which
12
+ * also explains why a loosely-typed series still accepts any string).
13
+ *
14
+ * Members, in the order a reader meets them:
15
+ *
16
+ * - **`series` + `column`** — one bar per event. A `TimeSeries`
17
+ * (interval/timeRange-keyed draws true spans; a point key derives width
18
+ * from neighbour spacing), a `ValueSeries` (`series.byValue('dist')`), or a
19
+ * **`ReadonlyMap<group, TimeSeries>`** (the
20
+ * `partitionBy(…).aggregate(…).toMap()` shape) — for a `Map`, `column` names
21
+ * the shared value column and the map's order is the stack order.
22
+ * - **`series` + `columns`** a **wide** series stacked bottom top (e.g.
23
+ * `pivotByGroup` output). Invalid with a `Map` source: there the segments
24
+ * *are* the groups, so use `column`.
25
+ * - **`bins` + `column` / `columns`** — `byColumn` bin records
26
+ * (`Array<{ start, end, …aggregates }>`); the names are **aggregate
27
+ * fields** of the record, not schema columns, so they stay `string`. Pair
28
+ * with `ordinal` for a band axis.
29
+ * - **`categories`** an ordered `{ label, value }[]`, one bar per category.
30
+ * Takes **no** `column`/`columns` (each datum carries its own value).
31
+ * Vertical puts the categories on the ordinal **x** axis (the container's
32
+ * band scale); `orientation="horizontal"` puts them on **y** as unit slots
33
+ * and the value on x, and a `<YAxis>` with no explicit `ticks` labels one
34
+ * per category automatically ([PND-HCAT]).
35
+ *
36
+ * **Live charts:** `series.byValue(…)` / `.toMap()` mint fresh objects each
37
+ * call, so an inline `series={…}` re-registers this layer every render on a
38
+ * frequently re-rendering chart, memoize the projection (`useMemo`).
39
+ */
40
+ type BarChartSource<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = {
41
+ series: TimeSeries<S> | ReadonlyMap<string, TimeSeries<S>>;
42
+ column: NumericColumn<S>;
43
+ columns?: never;
44
+ bins?: never;
45
+ categories?: never;
46
+ } | {
47
+ series: ValueSeries<VS>;
48
+ column: ValueNumericColumn<VS>;
49
+ columns?: never;
50
+ bins?: never;
51
+ categories?: never;
52
+ } | {
53
+ series: TimeSeries<S>;
54
+ columns: readonly NumericColumn<S>[];
55
+ column?: never;
56
+ bins?: never;
57
+ categories?: never;
58
+ } | {
59
+ series: ValueSeries<VS>;
60
+ columns: readonly ValueNumericColumn<VS>[];
61
+ column?: never;
62
+ bins?: never;
63
+ categories?: never;
64
+ } | {
65
+ bins: readonly BinRecord[];
66
+ column: string;
67
+ columns?: never;
68
+ series?: never;
69
+ categories?: never;
70
+ } | {
71
+ bins: readonly BinRecord[];
72
+ columns: readonly string[];
73
+ column?: never;
74
+ series?: never;
75
+ categories?: never;
76
+ } | {
77
+ categories: readonly CategoryDatum[];
78
+ series?: never;
79
+ bins?: never;
80
+ column?: never;
81
+ columns?: never;
82
+ };
83
+ /** The props every {@link BarChartSource} mode shares. */
84
+ export interface BarChartCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
54
85
  /**
55
86
  * The single series' semantic identifier — what the data _is_. The theme maps
56
87
  * it to a {@link BarStyle} (`theme.bar[as] ?? theme.bar.default`). **Single
@@ -68,14 +99,21 @@ export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends
68
99
  */
69
100
  colors?: Readonly<Record<string, string>>;
70
101
  /**
71
- * **Per-bin** colours for a single-series band chart — `binColors[i]` fills
72
- * bar `i` (aligned to the bins / bands in order), overriding the `as`/theme
73
- * fill. This is the way to colour heart-rate / power **zones** or value bands
74
- * each their own colour (the `colors` map above is per-**group**, for stacks).
75
- * An `undefined`/short entry falls back to the theme fill. Meant for a
76
- * single-series chart (`column` + `bins`, or a horizontal single series); on a
102
+ * **Per-bar** colours for a single-series chart — `binColors[i]` fills bar
103
+ * `i` (aligned to the bars / bins in order), overriding the `as`/theme fill.
104
+ * This is the way to colour heart-rate / power **zones** or value bands each
105
+ * their own colour, and the **direction-coloured financial volume row** a
106
+ * time-axis `series` derives `binColors` from its own data (rising / falling
107
+ * off open vs close) so volume bars read green / red under the candles (the
108
+ * `colors` map above is per-**group**, for stacks). An `undefined`/short
109
+ * entry falls back to the theme fill. Works on any **single-series** shape —
110
+ * a time / value `series` (vertical or horizontal) or `bins`; on a
77
111
  * multi-group stack it would tint every segment of a bin alike, so it's not
78
- * the tool there.
112
+ * the tool there. A per-bar-coloured bar keeps its own colour under hover /
113
+ * selection (the highlight pops opacity instead of swapping the fill), and
114
+ * the hover / click readout reports the bar's own colour. **Disables the
115
+ * dense-bar envelope decimation** (see `decimate`) — an envelope rect can't
116
+ * carry many bars' colours, so every visible bar draws.
79
117
  */
80
118
  binColors?: readonly (string | undefined)[];
81
119
  /**
@@ -84,7 +122,10 @@ export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends
84
122
  * - `'vertical'` — bars grow **up** from a value baseline, bins on the **x**
85
123
  * axis (time buckets, value bands). The column / time-histogram look.
86
124
  * - `'horizontal'` — bars grow **right**, bins on the **y** axis (a band axis
87
- * like heart-rate zones). Label the bands with `<YAxis ticks={[{ at, label }]}>`.
125
+ * like heart-rate zones). Label the bands with `<YAxis ticks={[{ at, label }]}>`
126
+ * — or, with `categories`, let the `<YAxis>` derive them ([PND-HCAT]): a
127
+ * horizontal categorical chart hands the axis its names and they land one
128
+ * per slot, so a funnel / ranking needs no hand-built tick list.
88
129
  *
89
130
  * A `'horizontal'` chart puts the **value** on the shared x axis, so its
90
131
  * container's x-kind is `'value'` — it cannot share a `<ChartContainer>` with
@@ -130,7 +171,9 @@ export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends
130
171
  * at that density (a perf knob, not a style); interaction still reads the source
131
172
  * bars. Pass `false` to always draw every bar, or `{ threshold }` to tune the
132
173
  * samples-per-pixel factor. **No-op for a stacked / multi-group histogram** (the
133
- * categorical case is low-count; only the single-series path decimates).
174
+ * categorical case is low-count; only the single-series path decimates) — and
175
+ * **no-op when `binColors` is set** (an envelope rect would repaint its bars
176
+ * one flat colour; a per-bar-coloured layer draws every visible bar).
134
177
  */
135
178
  decimate?: DecimateOption;
136
179
  /**
@@ -150,6 +193,14 @@ export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends
150
193
  */
151
194
  index?: number;
152
195
  }
196
+ /**
197
+ * `<BarChart>`'s props: the shared knobs ({@link BarChartCommon}) plus
198
+ * **exactly one** legal source shape ({@link BarChartSource}). Mixing sources
199
+ * (`series` + `bins`) or column forms (`column` + `columns`) is a **compile**
200
+ * error, and a column name that isn't in the series' schema fails to compile
201
+ * too — both were runtime throws before [PND-CHARTAPI].
202
+ */
203
+ export type BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = BarChartCommon<S, VS> & BarChartSource<S, VS>;
153
204
  /**
154
205
  * A bar / histogram draw layer. In its simplest form, one rectangle per event
155
206
  * spanning the key's `[begin, end]` from the axis baseline to a numeric
@@ -194,4 +245,5 @@ export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends
194
245
  * ```
195
246
  */
196
247
  export declare function BarChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, bins, categories, column, columns, as: semantic, colors, binColors, orientation, ordinal, id, axis, gap, decimate, legend, index, }: BarChartProps<S, VS>): null;
248
+ export {};
197
249
  //# sourceMappingURL=BarChart.d.ts.map
package/dist/BarChart.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useContext, useEffect, useMemo } from 'react';
2
2
  import { Interval, ValueSeries } from 'pond-ts';
3
- import { barsFromTimeSeries, barsFromValueSeries, categoryStack, stacksFromBins, stacksFromColumns, stacksFromGroups, } from './data.js';
3
+ import { barsFromTimeSeries, barsFromBins, barsFromValueSeries, categoryStack, stacksFromBins, stacksFromColumns, stacksFromGroups, } from './data.js';
4
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 { legendLabelFor, useLegendItems, } from './swatch.js';
@@ -69,9 +69,6 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
69
69
  if (column !== undefined || columns !== undefined) {
70
70
  throw new Error('<BarChart categories> takes no `column`/`columns` (each datum carries its own value)');
71
71
  }
72
- if (orientation === 'horizontal') {
73
- throw new Error('<BarChart categories> is vertical only (categories on x); horizontal category axes are not yet supported');
74
- }
75
72
  }
76
73
  const isMap = series instanceof Map;
77
74
  if (isMap && columns !== undefined) {
@@ -82,7 +79,14 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
82
79
  }
83
80
  // The single series' semantic label (its identity for the readout + selection):
84
81
  // the `as` role, else the value column. Used only on the single path.
85
- const label = semantic ?? column ?? id ?? 'value';
82
+ //
83
+ // [PND-BARSEM] normalizes a ONE-ENTRY `columns` onto that path, and its mark
84
+ // is the same mark `column` would name — so the lone entry stands in here,
85
+ // or `columns={['a']}` and `column="a"` would report different `SelectInfo.label`
86
+ // for the identical bar (found in review of #593).
87
+ const soleColumn = column ??
88
+ (columns !== undefined && columns.length === 1 ? columns[0] : undefined);
89
+ const label = semantic ?? soleColumn ?? id ?? 'value';
86
90
  // Build the chart-ready data view. Single-series *vertical* stays on the
87
91
  // original BarSeries path (its pixels are unchanged); everything else — any
88
92
  // stack, any horizontal — builds a StackedBarSeries (G === 1 for a single
@@ -99,6 +103,17 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
99
103
  if (cols === undefined) {
100
104
  throw new Error('<BarChart bins> needs `column` or `columns`');
101
105
  }
106
+ // [PND-BARSEM]: a ONE-column vertical histogram draws the same mark as a
107
+ // `series`+`column` chart, so it takes the same path — and with it the
108
+ // whole-slot hit target, the hover colour, the cursor readout and
109
+ // per-bar decimation. Capabilities follow what is drawn, not which prop
110
+ // produced it. Horizontal keeps the transposed stacked path.
111
+ if (cols.length === 1 && orientation !== 'horizontal') {
112
+ return {
113
+ kind: 'single',
114
+ bs: barsFromBins(bins, cols[0], { ordinal }),
115
+ };
116
+ }
102
117
  return { kind: 'stacked', ss: stacksFromBins(bins, cols, { ordinal }) };
103
118
  }
104
119
  if (isMap) {
@@ -112,6 +127,18 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
112
127
  }
113
128
  const s = series;
114
129
  if (columns !== undefined) {
130
+ // [PND-BARSEM]: a one-entry `columns` is a single series wearing the
131
+ // stack's clothes — same mark, so the same capabilities (see the `bins`
132
+ // branch above).
133
+ if (columns.length === 1 && orientation !== 'horizontal') {
134
+ const only = columns[0];
135
+ return {
136
+ kind: 'single',
137
+ bs: s instanceof ValueSeries
138
+ ? barsFromValueSeries(s, only)
139
+ : barsFromTimeSeries(s, only),
140
+ };
141
+ }
115
142
  return { kind: 'stacked', ss: stacksFromColumns(s, columns) };
116
143
  }
117
144
  if (column === undefined) {
@@ -178,7 +205,29 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
178
205
  }, [shape, orientation, binAxisKind]);
179
206
  const { bar } = container.theme;
180
207
  // Single-series style: the `as` role → theme bar style (the single channel).
181
- const singleStyle = (semantic !== undefined ? bar[semantic] : undefined) ?? bar.default;
208
+ //
209
+ // A shape [PND-BARSEM] normalized onto this path (a one-column `bins`, a
210
+ // one-entry `columns`) used to resolve its fill through the *stacked*
211
+ // channel — `colors[group] ?? theme.bar[group] ?? default` — so resolving
212
+ // only `as` here would silently drop a caller's `colors` map and the
213
+ // `theme.bar[<column>]` role, changing the bars' colour with no error
214
+ // (found in review of #593). The column name is that shape's group name, so
215
+ // the same three-step lookup is applied, `as` still winning when given.
216
+ const singleStyle = useMemo(() => {
217
+ const byRole = semantic !== undefined ? bar[semantic] : undefined;
218
+ if (byRole !== undefined)
219
+ return byRole;
220
+ if (soleColumn !== undefined) {
221
+ const override = colors?.[soleColumn];
222
+ const byColumn = bar[soleColumn];
223
+ if (override !== undefined) {
224
+ return { ...(byColumn ?? bar.default), fill: override };
225
+ }
226
+ if (byColumn !== undefined)
227
+ return byColumn;
228
+ }
229
+ return bar.default;
230
+ }, [bar, semantic, soleColumn, colors]);
182
231
  const gapPx = gap ?? bar.default.gap;
183
232
  // The stacked path's bar-thickness floor comes from `bar.default` (not the `as`
184
233
  // role — `as` is single-series only), matching how `gapPx` sources its default.
@@ -244,7 +293,9 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
244
293
  {
245
294
  x: (bs.begin[i] + bs.end[i]) / 2,
246
295
  value: v,
247
- color: singleStyle.fill,
296
+ // A per-bar colour wins over the flat fill, so the readout
297
+ // pill reads the bar's own colour (as the stacked path does).
298
+ color: binColors?.[i] ?? singleStyle.fill,
248
299
  label,
249
300
  },
250
301
  ];
@@ -254,20 +305,33 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
254
305
  : {
255
306
  hitTest: (px, py, xScale, yScale) => {
256
307
  const baseline = resolveBarBaseline(yScale);
257
- const hit = barAt(bs, px, py, xScale, yScale, baseline, gapPx, singleStyle.minWidth);
308
+ // No `gapPx` the hit region is the bar's whole slot (its
309
+ // interval width, full plot height), not the inset rect the
310
+ // gap draws. See barSlotRect.
311
+ const hit = barAt(bs, px, py, xScale, yScale, baseline, singleStyle.minWidth);
258
312
  if (hit === null)
259
313
  return null;
260
- const [, begin, value] = hit;
314
+ const [bi, begin, value] = hit;
315
+ // The bar's stable `mark` (its own axis key) rides the
316
+ // selection, so the highlight match and a controlled echo key
317
+ // on the *sample* rather than on the `begin` edge — which on a
318
+ // point-keyed series is derived geometry, not the sample's key
319
+ // (see BarSeries.marks). `bi` is the exact bar index from the
320
+ // hit, as the stacked path uses it.
321
+ const stableMark = bs.marks?.[bi];
261
322
  return {
262
323
  id,
263
324
  key: begin,
264
325
  value,
265
- color: singleStyle.fill,
326
+ // The bar's own colour when per-bar coloured (stacked-path
327
+ // parity: the readout pill matches the pixels).
328
+ color: binColors?.[bi] ?? singleStyle.fill,
266
329
  label,
330
+ ...(stableMark !== undefined ? { mark: stableMark } : {}),
267
331
  };
268
332
  },
269
333
  }),
270
- draw: (ctx, xScale, yScale) => drawBars(ctx, bs, xScale, yScale, singleStyle, resolveBarBaseline(yScale), gapPx, id, selection, hover, decimate),
334
+ draw: (ctx, xScale, yScale) => drawBars(ctx, bs, xScale, yScale, singleStyle, resolveBarBaseline(yScale), gapPx, id, selection, hover, decimate, binColors),
271
335
  },
272
336
  axisId: axis,
273
337
  index,
@@ -290,8 +354,13 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
290
354
  ...(binBuckets !== null ? { binIntervals: () => binBuckets } : {}),
291
355
  // A categorical chart hands the container its ordered category names — the
292
356
  // ordinal axis domain the shared band scale + label formatter build on.
357
+ // Categorical labels go to the axis the categories actually land on:
358
+ // x for a vertical chart (the container's band scale), y for a
359
+ // horizontal one, where they label the unit slots ([PND-HCAT]).
293
360
  ...(categoryLabels !== null
294
- ? { xCategories: () => categoryLabels }
361
+ ? vertical
362
+ ? { xCategories: () => categoryLabels }
363
+ : { binCategories: () => categoryLabels }
295
364
  : {}),
296
365
  // No x-scrub flag for a stack / horizontal chart — hover + click read it
297
366
  // out instead (the flag is single-series-vertical only).
@@ -335,6 +404,7 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
335
404
  orientation,
336
405
  singleStyle,
337
406
  stackStyle,
407
+ binColors,
338
408
  label,
339
409
  id,
340
410
  gapPx,