@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.
@@ -0,0 +1,127 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { SeriesSchema, ValueSeriesSchema } from 'pond-ts';
3
+ import { type BarListColumn, type ListCellSpec, type ListMarker, type ListRow, type ListSortDirection } from './list.js';
4
+ import { type ListRowsSource, type ListSeriesSource } from './list-source.js';
5
+ import { type ChartTheme } from './theme.js';
6
+ /** The props both BarList source doors share. */
7
+ export interface BarListCommon<R extends ListRow = ListRow> {
8
+ /**
9
+ * The bar lines, **top→bottom within each row** — each names a `values`
10
+ * entry for its length and optionally a theme role (`as`). Several columns
11
+ * stack as parallel lines per row (the to-site / from-site pairing), all on
12
+ * the **one shared scale**, so lengths compare across lines and rows alike.
13
+ */
14
+ columns: readonly BarListColumn[];
15
+ /**
16
+ * The shared scale's `[min, max]`. **Omitted ⇒ resolved from the data**:
17
+ * `[min(0, data min), data max]` over every bar column of every row. Set it
18
+ * to pin the scale across live updates (a re-sorting traffic list whose max
19
+ * changes every tick) or across sibling lists.
20
+ *
21
+ * Bars are **length-encoded from the domain minimum** — the component
22
+ * assumes non-negative values. A negative value stays in-domain (the auto
23
+ * fit widens below zero) but draws as a short left-anchored bar, not a
24
+ * diverging one; diverging bar lists are out of scope (transform upstream,
25
+ * or use {@link BoxList}, whose marks are positional).
26
+ */
27
+ domain?: readonly [number, number];
28
+ /**
29
+ * Name of the `values` entry that **ranks the list** — with several bar
30
+ * columns, this is the decision of which one drives the order. Missing /
31
+ * non-finite values sort last either direction. **Omitted ⇒ input order**
32
+ * (the chronological splits case).
33
+ */
34
+ sortBy?: string;
35
+ /** `'desc'` (default — largest on top, the ranked-list convention) or `'asc'`. */
36
+ sortDirection?: ListSortDirection;
37
+ /** Full custom comparator — **overrides** `sortBy`/`sortDirection`. */
38
+ sort?: (a: R, b: R) => number;
39
+ /** Data cell columns rendered **between the label and the bars**, in order. */
40
+ before?: readonly ListCellSpec<R>[];
41
+ /** Data cell columns rendered **after the bars**, in order (a split's
42
+ * speed / climb readouts). */
43
+ after?: readonly ListCellSpec<R>[];
44
+ /**
45
+ * A row's expanded detail (any node — a stats grid, a nested chart).
46
+ * **Providing it adds the chevron column**; expansion is per-row,
47
+ * uncontrolled, keyed on `row.key` (so it survives a re-sort), seeded by
48
+ * `defaultExpanded`. Omitted ⇒ no expander UI at all.
49
+ */
50
+ renderExpanded?: (row: R) => ReactNode;
51
+ /** Row keys expanded on first render. */
52
+ defaultExpanded?: readonly string[];
53
+ /** Observe a toggle (`expanded` is the row's **new** state). */
54
+ onExpandToggle?: (key: string, expanded: boolean) => void;
55
+ /**
56
+ * The selected row's `key`, marked with an inset edge in the annotation
57
+ * (marks) register — selection is a user's mark, not data. Consumer-owned
58
+ * state: pair with `onRowClick`. `null` / omitted ⇒ none.
59
+ */
60
+ selected?: string | null;
61
+ /** Row click (rows show hover + pointer affordances only when provided). */
62
+ onRowClick?: (row: R) => void;
63
+ /** Each bar line's height in px. **Omitted ⇒ `8`.** */
64
+ barHeight?: number;
65
+ /** Rule between rows (`theme.axis.grid`). **Omitted ⇒ `true`.** */
66
+ divided?: boolean;
67
+ /**
68
+ * Reference **markers** on the shared scale — each draws a dotted vertical
69
+ * rule through every row (annotation-register ink) with its `label` printed
70
+ * above the list, centred on the rule. An SLA line, a capacity, the fleet
71
+ * average. Marker values **join the auto domain fit** (a threshold above
72
+ * the data max widens the scale); under an explicit `domain` they clamp.
73
+ */
74
+ markers?: readonly ListMarker[];
75
+ /**
76
+ * The vertical **baseline rule** at the scale origin (the glyph cell's left
77
+ * edge, the row dividers' `axis.grid` ink). **Omitted ⇒ `false`** — a bar's track
78
+ * already shows where zero is; opt in when the tracks are visually quiet.
79
+ * (`<BoxList>` defaults it **on**: its lines float at `lower`, so the shared
80
+ * origin is what relates rows to each other.)
81
+ */
82
+ baseline?: boolean;
83
+ /** Styling — the same {@link ChartTheme} the canvas charts read; bars
84
+ * resolve `theme.bar[as]`. **Omitted ⇒ {@link defaultTheme}.** */
85
+ theme?: ChartTheme;
86
+ }
87
+ /**
88
+ * `<BarList>`'s props: the shared knobs ({@link BarListCommon}) plus **exactly
89
+ * one** source door ([PND-CHARTAPI]). Passing both `rows` and `series`, or
90
+ * neither, is now a **compile** error rather than a render-time throw.
91
+ *
92
+ * The two members differ in their row type on purpose. Through `rows`, a
93
+ * caller's `R` flows into every callback. Through `series` the rows are read
94
+ * internally and are plain {@link ListRow}s, so that member pins the callbacks
95
+ * to `ListRow` — annotating a callback with a custom row type while passing
96
+ * `series` no longer compiles, where before it silently lied (#590 review).
97
+ */
98
+ export type BarListProps<R extends ListRow = ListRow, S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = (BarListCommon<R> & ListRowsSource<R>) | (BarListCommon<ListRow> & ListSeriesSource<S, VS>);
99
+ /**
100
+ * A **ranked bar list** — the DOM sister of `<BarChart orientation="horizontal">`,
101
+ * for the table-shaped cases: one row per *entity* (interface, split, symbol),
102
+ * a label cell, one proportional bar line per configured column, optional data
103
+ * cells before/after, optional per-row expander. react-timeseries-charts'
104
+ * `HorizontalBarChart`, reconceived as what it always was: a table.
105
+ *
106
+ * **Standalone** — no `<ChartContainer>`; there is no time axis here. It takes
107
+ * a `theme` directly and renders a plain `<table>` (label cells can be links,
108
+ * cells align by table layout, the expander is a `colSpan` row).
109
+ *
110
+ * - **One shared scale.** Every bar of every row maps through one
111
+ * `[min, max]` (see `domain`), because cross-row comparison is the point.
112
+ * - **Gaps.** A missing / non-numeric value renders an empty track and sorts
113
+ * last — absence reads as absence, never as zero-drawn-long.
114
+ * - **Sorting.** `sortBy` + `sortDirection` for the common case, `sort` for
115
+ * anything else, input order otherwise.
116
+ *
117
+ * ```tsx
118
+ * <BarList
119
+ * rows={listRowsFromTimeSeries(splits, { label: (i) => `${i + 1}` })}
120
+ * columns={[{ column: 'speed' }]}
121
+ * after={[{ key: 'speed', align: 'right', render: (r) => fmtMph(r.values.speed) }]}
122
+ * renderExpanded={(r) => <SplitDetail row={r} />}
123
+ * />
124
+ * ```
125
+ */
126
+ export declare function BarList<R extends ListRow = ListRow, S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>(props: BarListProps<R, S, VS>): import("react/jsx-runtime").JSX.Element;
127
+ //# sourceMappingURL=BarList.d.ts.map
@@ -0,0 +1,84 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { ValueSeries } from 'pond-ts';
4
+ import { listFraction, listRowsFromTimeSeries, listRowsFromValueSeries, resolveListDomain, sortListRows, } from './list.js';
5
+ import { isSeriesSource, } from './list-source.js';
6
+ import { ListTable } from './ListTable.js';
7
+ import { defaultTheme } from './theme.js';
8
+ /**
9
+ * A **ranked bar list** — the DOM sister of `<BarChart orientation="horizontal">`,
10
+ * for the table-shaped cases: one row per *entity* (interface, split, symbol),
11
+ * a label cell, one proportional bar line per configured column, optional data
12
+ * cells before/after, optional per-row expander. react-timeseries-charts'
13
+ * `HorizontalBarChart`, reconceived as what it always was: a table.
14
+ *
15
+ * **Standalone** — no `<ChartContainer>`; there is no time axis here. It takes
16
+ * a `theme` directly and renders a plain `<table>` (label cells can be links,
17
+ * cells align by table layout, the expander is a `colSpan` row).
18
+ *
19
+ * - **One shared scale.** Every bar of every row maps through one
20
+ * `[min, max]` (see `domain`), because cross-row comparison is the point.
21
+ * - **Gaps.** A missing / non-numeric value renders an empty track and sorts
22
+ * last — absence reads as absence, never as zero-drawn-long.
23
+ * - **Sorting.** `sortBy` + `sortDirection` for the common case, `sort` for
24
+ * anything else, input order otherwise.
25
+ *
26
+ * ```tsx
27
+ * <BarList
28
+ * rows={listRowsFromTimeSeries(splits, { label: (i) => `${i + 1}` })}
29
+ * columns={[{ column: 'speed' }]}
30
+ * after={[{ key: 'speed', align: 'right', render: (r) => fmtMph(r.values.speed) }]}
31
+ * renderExpanded={(r) => <SplitDetail row={r} />}
32
+ * />
33
+ * ```
34
+ */
35
+ export function BarList(props) {
36
+ // One normalized view of the union — `isSeriesSource` is the runtime
37
+ // narrowing; the doors are mutually exclusive by construction now.
38
+ const source = props;
39
+ const { rows, series, label, columns, domain, sortBy, sortDirection = 'desc', sort, before, after, renderExpanded, defaultExpanded, onExpandToggle, selected, onRowClick, markers, barHeight = 8, divided, baseline, theme = defaultTheme, } = source;
40
+ // A runtime guard for JS consumers and `any`-typed call sites — the
41
+ // props union makes both branches unreachable from typed TS, but a
42
+ // silently-ignored source prop is a worse failure than a throw.
43
+ if (isSeriesSource(props) === (rows !== undefined)) {
44
+ throw new Error('<BarList>: provide exactly one of `rows` (records) or `series` (one row per event)');
45
+ }
46
+ // The series door reads internally — starting from a pond series there is
47
+ // no shaping step (with `series`, R stays the default ListRow).
48
+ const allRows = useMemo(() => rows ??
49
+ (series instanceof ValueSeries
50
+ ? listRowsFromValueSeries(series, label !== undefined ? { label } : {})
51
+ : listRowsFromTimeSeries(series, label !== undefined ? { label } : {})), [rows, series, label]);
52
+ const sorted = useMemo(() => sortListRows(allRows, sortBy, sortDirection, sort), [allRows, sortBy, sortDirection, sort]);
53
+ const scale = useMemo(() => resolveListDomain(allRows, columns.map((c) => c.column), domain, markers?.map((m) => m.value)), [allRows, columns, domain, markers]);
54
+ const resolvedMarkers = useMemo(() => markers?.map((m) => ({
55
+ frac: listFraction(m.value, scale),
56
+ ...(m.label !== undefined ? { label: m.label } : {}),
57
+ })), [markers, scale]);
58
+ return (_jsx(ListTable, { rows: sorted, kind: "bar", markers: resolvedMarkers, before: before, after: after, renderExpanded: renderExpanded, defaultExpanded: defaultExpanded, onExpandToggle: onExpandToggle, selected: selected, onRowClick: onRowClick, divided: divided, baseline: baseline, theme: theme, renderGlyphs: (row) => (_jsx(_Fragment, { children: columns.map((col, ci) => {
59
+ const style = theme.bar[col.as ?? 'default'] ?? theme.bar.default;
60
+ const frac = listFraction(row.values[col.column], scale);
61
+ return (_jsxs("div", { "data-list-track": col.column, style: {
62
+ position: 'relative',
63
+ height: barHeight,
64
+ margin: '3px 0',
65
+ borderRadius: barHeight / 2,
66
+ overflow: 'hidden',
67
+ }, children: [_jsx("div", { style: {
68
+ position: 'absolute',
69
+ inset: 0,
70
+ background: style.fill,
71
+ opacity: 0.15,
72
+ } }), frac !== null && frac > 0 && (_jsx("div", { "data-list-bar": col.column, style: {
73
+ position: 'absolute',
74
+ top: 0,
75
+ bottom: 0,
76
+ left: 0,
77
+ width: `${frac * 100}%`,
78
+ background: style.fill,
79
+ opacity: style.opacity,
80
+ borderRadius: barHeight / 2,
81
+ } }))] }, `${ci} ${col.column}`));
82
+ }) })) }));
83
+ }
84
+ //# sourceMappingURL=BarList.js.map
@@ -0,0 +1,108 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { SeriesSchema, ValueSeriesSchema } from 'pond-ts';
3
+ import { type BoxListColumn, type ListCellSpec, type ListMarker, type ListRow, type ListSortDirection } from './list.js';
4
+ import { type ListRowsSource, type ListSeriesSource } from './list-source.js';
5
+ import { type ChartTheme } from './theme.js';
6
+ /** The props both BoxList source doors share. */
7
+ export interface BoxListCommon<R extends ListRow = ListRow> {
8
+ /**
9
+ * The box lines, **top→bottom within each row** — each names the `values`
10
+ * entries for its five-number summary (`lower`/`upper` required; `q1`+`q3`
11
+ * both-or-neither; `median` optional — the `<BoxPlot>` vocabulary), plus an
12
+ * optional current-value tick (`value`) with an inline `format`ted label.
13
+ * All lines share the **one scale** so distributions compare across the
14
+ * whole list.
15
+ */
16
+ columns: readonly BoxListColumn[];
17
+ /**
18
+ * The shared scale's `[min, max]`. **Omitted ⇒ resolved from the data**
19
+ * over every box's `lower`/`upper`/`value`: `[min(0, data min), data max]`.
20
+ */
21
+ domain?: readonly [number, number];
22
+ /**
23
+ * Name of the `values` entry that ranks the list. The box columns are
24
+ * plain `values` names, so any stat sorts — the current value
25
+ * (`sortBy="in_now"`), a p95, a median — with no stat-picking rule to
26
+ * remember. Missing sorts last. **Omitted ⇒ input order.**
27
+ */
28
+ sortBy?: string;
29
+ /** `'desc'` (default) or `'asc'`. */
30
+ sortDirection?: ListSortDirection;
31
+ /** Full custom comparator — **overrides** `sortBy`/`sortDirection`. */
32
+ sort?: (a: R, b: R) => number;
33
+ /** Data cell columns between the label and the boxes. */
34
+ before?: readonly ListCellSpec<R>[];
35
+ /** Data cell columns after the boxes. */
36
+ after?: readonly ListCellSpec<R>[];
37
+ /** A row's expanded detail; providing it adds the chevron column (see
38
+ * `BarListProps.renderExpanded`). */
39
+ renderExpanded?: (row: R) => ReactNode;
40
+ /** Row keys expanded on first render. */
41
+ defaultExpanded?: readonly string[];
42
+ /** Observe a toggle (`expanded` is the row's new state). */
43
+ onExpandToggle?: (key: string, expanded: boolean) => void;
44
+ /** The selected row's `key` (inset accent edge). Pair with `onRowClick`. */
45
+ selected?: string | null;
46
+ /** Row click (also gates the hover affordance). */
47
+ onRowClick?: (row: R) => void;
48
+ /** Each box line's height in px. **Omitted ⇒ `10`.** */
49
+ barHeight?: number;
50
+ /** Rule between rows. **Omitted ⇒ `true`.** */
51
+ divided?: boolean;
52
+ /**
53
+ * Reference **markers** on the shared scale — a dotted vertical rule
54
+ * through every row with the `label` printed above the list (see
55
+ * `BarListProps.markers`; identical semantics, including joining the auto
56
+ * domain fit).
57
+ */
58
+ markers?: readonly ListMarker[];
59
+ /**
60
+ * The vertical **baseline rule** at the scale origin (the glyph cell's left
61
+ * edge, the row dividers' `axis.grid` ink). **Omitted ⇒ `true`** — box lines float
62
+ * at their `lower` quantile, so the shared origin is what lets the eye
63
+ * relate rows to each other. Pass `false` to drop it.
64
+ */
65
+ baseline?: boolean;
66
+ /** Styling — boxes resolve `theme.box[as]`. **Omitted ⇒ {@link defaultTheme}.** */
67
+ theme?: ChartTheme;
68
+ }
69
+ /**
70
+ * `<BoxList>`'s props: the shared knobs ({@link BoxListCommon}) plus **exactly
71
+ * one** source door ([PND-CHARTAPI]). Passing both `rows` and `series`, or
72
+ * neither, is now a **compile** error rather than a render-time throw.
73
+ *
74
+ * The two members differ in their row type on purpose. Through `rows`, a
75
+ * caller's `R` flows into every callback. Through `series` the rows are read
76
+ * internally and are plain {@link ListRow}s, so that member pins the callbacks
77
+ * to `ListRow` — annotating a callback with a custom row type while passing
78
+ * `series` no longer compiles, where before it silently lied (#590 review).
79
+ */
80
+ export type BoxListProps<R extends ListRow = ListRow, S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = (BoxListCommon<R> & ListRowsSource<R>) | (BoxListCommon<ListRow> & ListSeriesSource<S, VS>);
81
+ /**
82
+ * A **distribution row list** — {@link BarList}'s sister, drawing a horizontal
83
+ * five-number box per configured column instead of a value bar: the light
84
+ * `lower→upper` range band, the stronger `q1→q3` body, the `median` line, and
85
+ * an optional **current-value tick** with a printed label (the esnet
86
+ * traffic-by-interface look: where traffic *ranges* vs where it *is now*).
87
+ *
88
+ * Same table contract as {@link BarList} (standalone, cells, sort, expander,
89
+ * one shared scale, gap-aware), same quantile vocabulary as the canvas
90
+ * `<BoxPlot>` (`lower`/`q1`/`median`/`q3`/`upper`, both-or-neither body,
91
+ * quantiles **pre-computed upstream** — `series.reduce` facts; the chart never
92
+ * computes them).
93
+ *
94
+ * ```tsx
95
+ * <BoxList
96
+ * rows={ifaceRows}
97
+ * columns={[
98
+ * { lower: 'in_p5', q1: 'in_p25', median: 'in_p50', q3: 'in_p75',
99
+ * upper: 'in_p95', value: 'in_now', format: fmtBps },
100
+ * { lower: 'out_p5', q1: 'out_p25', median: 'out_p50', q3: 'out_p75',
101
+ * upper: 'out_p95', value: 'out_now', format: fmtBps, as: 'secondary' },
102
+ * ]}
103
+ * sortBy="in_now"
104
+ * />
105
+ * ```
106
+ */
107
+ export declare function BoxList<R extends ListRow = ListRow, S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>(props: BoxListProps<R, S, VS>): import("react/jsx-runtime").JSX.Element;
108
+ //# sourceMappingURL=BoxList.d.ts.map
@@ -0,0 +1,125 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { ValueSeries } from 'pond-ts';
4
+ import { listFraction, listRowsFromTimeSeries, listRowsFromValueSeries, resolveListDomain, sortListRows, validateBoxListColumn, } from './list.js';
5
+ import { isSeriesSource, } from './list-source.js';
6
+ import { ListTable, listInk } from './ListTable.js';
7
+ import { defaultTheme } from './theme.js';
8
+ /**
9
+ * A **distribution row list** — {@link BarList}'s sister, drawing a horizontal
10
+ * five-number box per configured column instead of a value bar: the light
11
+ * `lower→upper` range band, the stronger `q1→q3` body, the `median` line, and
12
+ * an optional **current-value tick** with a printed label (the esnet
13
+ * traffic-by-interface look: where traffic *ranges* vs where it *is now*).
14
+ *
15
+ * Same table contract as {@link BarList} (standalone, cells, sort, expander,
16
+ * one shared scale, gap-aware), same quantile vocabulary as the canvas
17
+ * `<BoxPlot>` (`lower`/`q1`/`median`/`q3`/`upper`, both-or-neither body,
18
+ * quantiles **pre-computed upstream** — `series.reduce` facts; the chart never
19
+ * computes them).
20
+ *
21
+ * ```tsx
22
+ * <BoxList
23
+ * rows={ifaceRows}
24
+ * columns={[
25
+ * { lower: 'in_p5', q1: 'in_p25', median: 'in_p50', q3: 'in_p75',
26
+ * upper: 'in_p95', value: 'in_now', format: fmtBps },
27
+ * { lower: 'out_p5', q1: 'out_p25', median: 'out_p50', q3: 'out_p75',
28
+ * upper: 'out_p95', value: 'out_now', format: fmtBps, as: 'secondary' },
29
+ * ]}
30
+ * sortBy="in_now"
31
+ * />
32
+ * ```
33
+ */
34
+ export function BoxList(props) {
35
+ // One normalized view of the union — `isSeriesSource` is the runtime
36
+ // narrowing; the doors are mutually exclusive by construction now.
37
+ const source = props;
38
+ const { rows, series, label, columns, domain, sortBy, sortDirection = 'desc', sort, before, after, renderExpanded, defaultExpanded, onExpandToggle, selected, onRowClick, markers, barHeight = 10, divided, baseline = true, theme = defaultTheme, } = source;
39
+ // A runtime guard for JS consumers and `any`-typed call sites — the
40
+ // props union makes both branches unreachable from typed TS, but a
41
+ // silently-ignored source prop is a worse failure than a throw.
42
+ if (isSeriesSource(props) === (rows !== undefined)) {
43
+ throw new Error('<BoxList>: provide exactly one of `rows` (records) or `series` (one row per event)');
44
+ }
45
+ for (const col of columns)
46
+ validateBoxListColumn(col);
47
+ // The series door reads internally — starting from a pond series there is
48
+ // no shaping step (with `series`, R stays the default ListRow).
49
+ const allRows = useMemo(() => rows ??
50
+ (series instanceof ValueSeries
51
+ ? listRowsFromValueSeries(series, label !== undefined ? { label } : {})
52
+ : listRowsFromTimeSeries(series, label !== undefined ? { label } : {})), [rows, series, label]);
53
+ const sorted = useMemo(() => sortListRows(allRows, sortBy, sortDirection, sort), [allRows, sortBy, sortDirection, sort]);
54
+ const scale = useMemo(() => resolveListDomain(allRows, columns.flatMap((c) => c.value !== undefined
55
+ ? [c.lower, c.upper, c.value]
56
+ : [c.lower, c.upper]), domain, markers?.map((m) => m.value)), [allRows, columns, domain, markers]);
57
+ const resolvedMarkers = useMemo(() => markers?.map((m) => ({
58
+ frac: listFraction(m.value, scale),
59
+ ...(m.label !== undefined ? { label: m.label } : {}),
60
+ })), [markers, scale]);
61
+ return (_jsx(ListTable, { rows: sorted, kind: "box", markers: resolvedMarkers, before: before, after: after, renderExpanded: renderExpanded, defaultExpanded: defaultExpanded, onExpandToggle: onExpandToggle, selected: selected, onRowClick: onRowClick, divided: divided, baseline: baseline, theme: theme, renderGlyphs: (row) => (_jsx(_Fragment, { children: columns.map((col, ci) => (_jsx(BoxLine
62
+ // Index-qualified so two lines over the same quantile names
63
+ // (say, styled differently) never collide.
64
+ , { row: row, col: col, scale: scale, height: barHeight, style: theme.box[col.as ?? 'default'] ?? theme.box.default, ink: listInk(theme), fontSize: theme.font.size }, `${ci} ${col.lower} ${col.upper}`))) })) }));
65
+ }
66
+ /** One horizontal box line: range band → body → median → current tick + label. */
67
+ function BoxLine({ row, col, scale, height, style, ink, fontSize, }) {
68
+ const at = (name) => name === undefined ? null : listFraction(row.values[name], scale);
69
+ const lo = at(col.lower);
70
+ const hi = at(col.upper);
71
+ const q1 = at(col.q1);
72
+ const q3 = at(col.q3);
73
+ const med = at(col.median);
74
+ const tick = at(col.value);
75
+ const raw = col.value !== undefined ? row.values[col.value] : undefined;
76
+ const label = col.format !== undefined && typeof raw === 'number' && Number.isFinite(raw)
77
+ ? col.format(raw)
78
+ : null;
79
+ const pct = (f) => `${f * 100}%`;
80
+ // The row keeps its slot height even when everything is missing — a gap
81
+ // reads as an empty line, not a collapsed row.
82
+ return (_jsxs("div", { "data-list-boxline": "", style: { position: 'relative', height: height + 4, margin: '2px 0' }, children: [lo !== null && hi !== null && (_jsx("div", { "data-list-range": "", style: {
83
+ position: 'absolute',
84
+ top: 2,
85
+ bottom: 2,
86
+ left: pct(lo),
87
+ width: pct(Math.max(hi - lo, 0)),
88
+ background: style.whisker,
89
+ opacity: 0.55,
90
+ borderRadius: height / 2,
91
+ } })), q1 !== null && q3 !== null && (_jsx("div", { "data-list-body": "", style: {
92
+ position: 'absolute',
93
+ top: 2,
94
+ bottom: 2,
95
+ left: pct(q1),
96
+ width: pct(Math.max(q3 - q1, 0)),
97
+ background: style.fill,
98
+ opacity: Math.min(style.fillOpacity * 2, 1),
99
+ borderRadius: 1,
100
+ } })), med !== null && (_jsx("div", { "data-list-median": "", style: {
101
+ position: 'absolute',
102
+ top: 2,
103
+ bottom: 2,
104
+ left: `calc(${pct(med)} - ${style.medianWidth / 2}px)`,
105
+ width: style.medianWidth,
106
+ background: style.median,
107
+ } })), tick !== null && (_jsxs(_Fragment, { children: [_jsx("div", { "data-list-tick": "", style: {
108
+ position: 'absolute',
109
+ top: 0,
110
+ bottom: 0,
111
+ left: `calc(${pct(tick)} - 1.5px)`,
112
+ width: 3,
113
+ background: style.stroke,
114
+ borderRadius: 1,
115
+ } }), label !== null && (_jsx("span", { "data-list-value": "", style: {
116
+ position: 'absolute',
117
+ left: `calc(${pct(tick)} + 8px)`,
118
+ top: '50%',
119
+ transform: 'translateY(-50%)',
120
+ whiteSpace: 'nowrap',
121
+ fontSize: fontSize + 1,
122
+ color: ink,
123
+ }, children: label }))] }))] }));
124
+ }
125
+ //# sourceMappingURL=BoxList.js.map
package/dist/BoxPlot.d.ts CHANGED
@@ -1,37 +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 BoxShape } from './box.js';
4
5
  import type { DecimateOption } from './decimate.js';
5
- export interface BoxPlotProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
6
- /**
7
- * The source series. A `TimeSeries` plots against the time axis; a `ValueSeries`
8
- * (`series.byValue('strike')`, or `ValueSeries.fromColumns` for natively
9
- * value-keyed data — a per-strike IV distribution) against its value axis — the
10
- * container infers which from the data, no axis-type prop (mirrors `<LineChart>`
11
- * / `<ScatterChart>`). The box x-span is the key's `[begin, end)` for an
12
- * interval-keyed `TimeSeries`, else synthesized from neighbour spacing (a
13
- * point-keyed `TimeSeries`, or a `ValueSeries`) so the box keeps real width.
14
- */
15
- series: TimeSeries<S> | ValueSeries<VS>;
16
- /** Name of the numeric column for the lower whisker end (e.g. `p5` / `min`).
17
- * **Required** — with `upper` it's the whisker reach. */
18
- lower: string;
19
- /**
20
- * Name of the numeric column for the box bottom — first quartile (e.g. `p25`).
21
- * **Optional:** omit `q1` **and** `q3` together for a **range-only** box — a
22
- * whisker-only `lower→upper` segment, no body (a bid→ask IV mark). Giving just
23
- * one of `q1`/`q3` throws.
24
- */
25
- q1?: string;
26
- /** Name of the numeric column for the median line (e.g. `p50`). **Optional** —
27
- * omit for no centre line (independent of the box body). */
28
- median?: string;
29
- /** Name of the numeric column for the box top — third quartile (e.g. `p75`).
30
- * **Optional** — omit with `q1` for a range-only box (see `q1`). */
31
- q3?: string;
32
- /** Name of the numeric column for the upper whisker end (e.g. `p95` / `max`).
33
- * **Required** — with `lower` it's the whisker reach. */
34
- upper: string;
6
+ export interface BoxPlotCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
35
7
  /**
36
8
  * The box series' semantic identifier — what the spread _is_ (e.g. `latency`).
37
9
  * The theme maps it to a {@link BoxStyle} (`theme.box[as] ?? theme.box.default`
@@ -122,6 +94,66 @@ export interface BoxPlotProps<S extends SeriesSchema = SeriesSchema, VS extends
122
94
  */
123
95
  index?: number;
124
96
  }
97
+ /**
98
+ * BoxPlot's source + column props, a **union over the series kind** so the
99
+ * column names are checked against the schema that was actually passed
100
+ * ([PND-CHARTAPI]). A single member carrying `NumericColumn<S> |
101
+ * ValueNumericColumn<VS>` would silently widen to `string`: only one of the
102
+ * two generics is ever inferred, and the other falls back (measured in
103
+ * `spikes/charts-type-seam/`). Loosely-typed series still accept any name.
104
+ */
105
+ type BoxPlotSource<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = {
106
+ /**
107
+ * The source series. **Live charts:** `series.byValue(…)` mints a
108
+ * *fresh* projection each call, so an inline `series={s.byValue('d')}`
109
+ * re-registers this layer every render — on a frequently re-rendering
110
+ * (e.g. scrub-driven) chart, memoize the projection (`useMemo`) so the
111
+ * layer isn't rebuilt each frame.
112
+ */
113
+ series: TimeSeries<S>;
114
+ /** Lower whisker end (e.g. `p5` / `min`). **Required** — with `upper`
115
+ * it is the whisker reach. */
116
+ lower: NumericColumn<S>;
117
+ /**
118
+ * Box bottom — first quartile (e.g. `p25`). **Optional:** omit `q1`
119
+ * **and** `q3` together for a **range-only** box (a whisker-only
120
+ * `lower→upper` segment, no body — a bid→ask IV mark). Giving just one
121
+ * of the pair throws.
122
+ */
123
+ q1?: NumericColumn<S>;
124
+ /** Median line (e.g. `p50`). **Optional** — omit for no centre line
125
+ * (independent of the box body). */
126
+ median?: NumericColumn<S>;
127
+ /** Box top — third quartile (e.g. `p75`). **Optional** — omit with `q1`
128
+ * for a range-only box. */
129
+ q3?: NumericColumn<S>;
130
+ /** Upper whisker end (e.g. `p95` / `max`). **Required** — with `lower`
131
+ * it is the whisker reach. */
132
+ upper: NumericColumn<S>;
133
+ } | {
134
+ series: ValueSeries<VS>;
135
+ /** Lower whisker end (e.g. `p5` / `min`). **Required** — with `upper`
136
+ * it is the whisker reach. */
137
+ lower: ValueNumericColumn<VS>;
138
+ /**
139
+ * Box bottom — first quartile (e.g. `p25`). **Optional:** omit `q1`
140
+ * **and** `q3` together for a **range-only** box (a whisker-only
141
+ * `lower→upper` segment, no body — a bid→ask IV mark). Giving just one
142
+ * of the pair throws.
143
+ */
144
+ q1?: ValueNumericColumn<VS>;
145
+ /** Median line (e.g. `p50`). **Optional** — omit for no centre line
146
+ * (independent of the box body). */
147
+ median?: ValueNumericColumn<VS>;
148
+ /** Box top — third quartile (e.g. `p75`). **Optional** — omit with `q1`
149
+ * for a range-only box. */
150
+ q3?: ValueNumericColumn<VS>;
151
+ /** Upper whisker end (e.g. `p95` / `max`). **Required** — with `lower`
152
+ * it is the whisker reach. */
153
+ upper: ValueNumericColumn<VS>;
154
+ };
155
+ /** `<BoxPlot>`'s props: the shared knobs plus one series-kind source shape. */
156
+ export type BoxPlotProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = BoxPlotCommon<S, VS> & BoxPlotSource<S, VS>;
125
157
  /**
126
158
  * A discrete box-and-whisker draw layer — the bar-chart analog of the variance
127
159
  * band. Reads **pre-computed quantile columns** of `series` (typically a
@@ -154,4 +186,5 @@ export interface BoxPlotProps<S extends SeriesSchema = SeriesSchema, VS extends
154
186
  * ```
155
187
  */
156
188
  export declare function BoxPlot<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, lower, q1, median, q3, upper, as: semantic, axis, gap, shape, showMedian, offset, capWidth, id, decimate, legend, index, }: BoxPlotProps<S, VS>): null;
189
+ export {};
157
190
  //# sourceMappingURL=BoxPlot.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import type { SeriesSchema, TimeSeries } from 'pond-ts';
2
+ import type { NumericColumn } from './column-names.js';
2
3
  import type { DecimateOption } from './decimate.js';
3
4
  import { type CandleVariant, type ColorBy } from './ohlc.js';
4
5
  export interface CandlestickProps<S extends SeriesSchema> {
@@ -12,13 +13,13 @@ export interface CandlestickProps<S extends SeriesSchema> {
12
13
  */
13
14
  series: TimeSeries<S>;
14
15
  /** Opening-price column. **Omitted ⇒ `'open'`.** */
15
- open?: string;
16
+ open?: NumericColumn<S>;
16
17
  /** Session-high column. **Omitted ⇒ `'high'`.** */
17
- high?: string;
18
+ high?: NumericColumn<S>;
18
19
  /** Session-low column. **Omitted ⇒ `'low'`.** */
19
- low?: string;
20
+ low?: NumericColumn<S>;
20
21
  /** Closing-price column. **Omitted ⇒ `'close'`.** */
21
- close?: string;
22
+ close?: NumericColumn<S>;
22
23
  /**
23
24
  * The series' semantic identifier — what the data _is_ (e.g. a ticker). The
24
25
  * theme maps it to a {@link CandleStyle} (`theme.candle[as] ??
package/dist/Layers.js CHANGED
@@ -101,6 +101,13 @@ export function Layers({ children }) {
101
101
  // A category axis draws no vertical gridlines — a line through each bar
102
102
  // centre reads as noise; the bars are the structure.
103
103
  const xTickVals = container.xKind === 'category' ? [] : xScale.ticks(xTickCount);
104
+ // The same rule on the other axis: a **horizontal** categorical chart
105
+ // ([PND-HCAT]) puts its categories on y, where the `<YAxis>` labels slot
106
+ // *centres* while d3's auto ticks fall on slot *boundaries* — so drawing
107
+ // them would both mismatch the labels and stripe each bar. Suppressed,
108
+ // exactly as the categorical x axis already is.
109
+ const yIsCategory = layers.some((e) => (e.axisId ?? defaultAxisId) === defaultAxisId &&
110
+ (e.layer.binCategories?.() ?? null) !== null);
104
111
  // The reference grid — behind the data, opt-out via `grid={false}` for
105
112
  // a clean backdrop (session dividers below stay independent of it).
106
113
  if (container.grid) {
@@ -108,7 +115,7 @@ export function Layers({ children }) {
108
115
  // single source, height-derived or an explicit `<YAxis tickCount>`), so
109
116
  // a gridline sits under every `<YAxis>` label and no more.
110
117
  const yCount = tickCounts.get(defaultAxisId) ?? GRID_TICKS;
111
- const yTicks = gridY
118
+ const yTicks = gridY && !(yIsCategory && explicitY === undefined)
112
119
  ? (explicitY ?? gridY.ticks(yCount)).map((t) => gridY(t))
113
120
  : [];
114
121
  // On a calendar axis the verticals are the FULL grain populations —