@pond-ts/charts 0.54.0 → 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.
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 —
@@ -1,34 +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 LineChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
7
- /**
8
- * The source series. A `TimeSeries` plots against the time axis; a
9
- * `ValueSeries` (`series.byValue('cumDist')`) against its value axis — the
10
- * container infers which from the data, no axis-type prop. Either way the key
11
- * / axis column supplies x and `column` supplies y.
12
- *
13
- * **Live charts:** `series.byValue(…)` mints a *fresh* projection each call, so
14
- * passing `series={s.byValue('dist')}` inline re-registers this layer every
15
- * render — on a frequently re-rendering (e.g. scrub-driven) chart, memoize the
16
- * projection (`useMemo`) so the layer isn't rebuilt each frame.
17
- */
18
- series: TimeSeries<S> | ValueSeries<VS>;
19
- /** Name of the numeric value column to plot. */
20
- column: string;
21
- /**
22
- * Optional column to **read out** at the cursor instead of the plotted
23
- * `column`. The layer still plots `column`; each tracker sample additionally
24
- * carries this column's value as {@link TrackerSample.readout}, so an
25
- * off-chart readout can show the **source** value while the line draws a
26
- * derived one — a smoothed / transformed / normalized plot with a raw-value
27
- * readout (estela plots pace-space + Gaussian-smoothed, reads the native m/s).
28
- * The plotted `value` (hence the in-chart cursor dot) is unchanged.
29
- * **Omitted ⇒ no readout channel** (`readout` is `undefined` on the sample).
30
- */
31
- readout?: string;
7
+ export interface LineChartCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
32
8
  /**
33
9
  * The series' semantic identifier — what the data _is_ / how it should read
34
10
  * (e.g. `heartrate`, `power`, or a role name like `foam`). The theme maps it
@@ -99,6 +75,32 @@ export interface LineChartProps<S extends SeriesSchema = SeriesSchema, VS extend
99
75
  */
100
76
  index?: number;
101
77
  }
78
+ /**
79
+ * LineChart's source + column props, a **union over the series kind** so the
80
+ * column names are checked against the schema that was actually passed
81
+ * ([PND-CHARTAPI]). A single member carrying `NumericColumn<S> |
82
+ * ValueNumericColumn<VS>` would silently widen to `string`: only one of the
83
+ * two generics is ever inferred, and the other falls back (measured in
84
+ * `spikes/charts-type-seam/`). Loosely-typed series still accept any name.
85
+ */
86
+ type LineChartSource<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = {
87
+ /**
88
+ * The source series. **Live charts:** `series.byValue(…)` mints a
89
+ * *fresh* projection each call, so an inline `series={s.byValue('d')}`
90
+ * re-registers this layer every render — on a frequently re-rendering
91
+ * (e.g. scrub-driven) chart, memoize the projection (`useMemo`) so the
92
+ * layer isn't rebuilt each frame.
93
+ */
94
+ series: TimeSeries<S>;
95
+ column: NumericColumn<S>;
96
+ readout?: NumericColumn<S>;
97
+ } | {
98
+ series: ValueSeries<VS>;
99
+ column: ValueNumericColumn<VS>;
100
+ readout?: ValueNumericColumn<VS>;
101
+ };
102
+ /** `<LineChart>`'s props: the shared knobs plus one series-kind source shape. */
103
+ export type LineChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = LineChartCommon<S, VS> & LineChartSource<S, VS>;
102
104
  /**
103
105
  * A line draw layer. Reads `column` from `series` into a {@link ChartSeries}
104
106
  * (columnar, gaps as NaN), registers itself into the enclosing {@link Layers}
@@ -106,4 +108,5 @@ export interface LineChartProps<S extends SeriesSchema = SeriesSchema, VS extend
106
108
  * it. The line breaks at gaps rather than spanning them.
107
109
  */
108
110
  export declare function LineChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, readout, as: semantic, axis, curve, gaps, sessionBreaks, decimate, legend, index, }: LineChartProps<S, VS>): null;
111
+ export {};
109
112
  //# sourceMappingURL=LineChart.d.ts.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @internal The shared row-table shell behind {@link BarList} / {@link BoxList}.
3
+ *
4
+ * Renders a real `<table>` — the point of the list family is table semantics
5
+ * (label cells that can be links, aligned data cells, a `colSpan` detail row
6
+ * for the expander, screen-reader-legible rows), which a canvas plot can't
7
+ * carry and which hand-rolled flex rows re-implement badly (per-row cell
8
+ * alignment is exactly what table layout solves). The glyph cell takes
9
+ * `width: 100%` so it absorbs the free width; every text cell shrinks to fit.
10
+ *
11
+ * Not exported from the package: the public surface is the two sisters, so the
12
+ * shared shell can evolve without a compatibility contract.
13
+ */
14
+ import { type ReactNode } from 'react';
15
+ import type { ListCellSpec, ListRow } from './list.js';
16
+ import type { ChartTheme } from './theme.js';
17
+ export interface ListTableProps<R extends ListRow> {
18
+ /** Rows in display order (the caller sorts). */
19
+ readonly rows: readonly R[];
20
+ /** Which sister is rendering — stamped as `data-list` for styling/tests. */
21
+ readonly kind: 'bar' | 'box';
22
+ /** The glyph cell's content for one row (the bar / box lines). */
23
+ readonly renderGlyphs: (row: R) => ReactNode;
24
+ readonly before?: readonly ListCellSpec<R>[] | undefined;
25
+ readonly after?: readonly ListCellSpec<R>[] | undefined;
26
+ readonly renderExpanded?: ((row: R) => ReactNode) | undefined;
27
+ readonly defaultExpanded?: readonly string[] | undefined;
28
+ readonly onExpandToggle?: ((key: string, expanded: boolean) => void) | undefined;
29
+ readonly selected?: string | null | undefined;
30
+ readonly onRowClick?: ((row: R) => void) | undefined;
31
+ readonly divided?: boolean | undefined;
32
+ /** Draw the vertical **baseline rule** at the scale origin (the glyph
33
+ * cell's left edge) — the shared reference the eye aligns rows against. */
34
+ readonly baseline?: boolean | undefined;
35
+ /**
36
+ * Reference markers, **pre-resolved to track fractions** by the caller (the
37
+ * shell knows pixels, not the scale): each draws a dotted vertical rule
38
+ * through every row's glyph area, plus a label strip above the list when
39
+ * any carries a `label`. A `null` fraction (out-of-scale gap) is skipped.
40
+ */
41
+ readonly markers?: ReadonlyArray<{
42
+ readonly frac: number | null;
43
+ readonly label?: string;
44
+ }> | undefined;
45
+ readonly theme: ChartTheme;
46
+ }
47
+ /** The shared text ink: the band-label tone when the theme has one (stronger
48
+ * than tick labels — these cells are primary content), else the tick ink. */
49
+ export declare function listInk(theme: ChartTheme): string;
50
+ export declare function ListTable<R extends ListRow>({ rows, kind, renderGlyphs, before, after, renderExpanded, defaultExpanded, onExpandToggle, selected, onRowClick, divided, baseline, markers, theme, }: ListTableProps<R>): import("react/jsx-runtime").JSX.Element;
51
+ //# sourceMappingURL=ListTable.d.ts.map
@@ -0,0 +1,143 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * @internal The shared row-table shell behind {@link BarList} / {@link BoxList}.
4
+ *
5
+ * Renders a real `<table>` — the point of the list family is table semantics
6
+ * (label cells that can be links, aligned data cells, a `colSpan` detail row
7
+ * for the expander, screen-reader-legible rows), which a canvas plot can't
8
+ * carry and which hand-rolled flex rows re-implement badly (per-row cell
9
+ * alignment is exactly what table layout solves). The glyph cell takes
10
+ * `width: 100%` so it absorbs the free width; every text cell shrinks to fit.
11
+ *
12
+ * Not exported from the package: the public surface is the two sisters, so the
13
+ * shared shell can evolve without a compatibility contract.
14
+ */
15
+ import { Fragment, useState } from 'react';
16
+ /** The turquoise the selected-row edge falls back to when the theme has no
17
+ * annotation register — the same built-in the annotation layer uses. */
18
+ const FALLBACK_ACCENT = '#0d9488';
19
+ /** The shared text ink: the band-label tone when the theme has one (stronger
20
+ * than tick labels — these cells are primary content), else the tick ink. */
21
+ export function listInk(theme) {
22
+ return theme.axis.band?.label ?? theme.axis.label;
23
+ }
24
+ export function ListTable({ rows, kind, renderGlyphs, before = [], after = [], renderExpanded, defaultExpanded, onExpandToggle, selected, onRowClick, divided = true, baseline = false, markers = [], theme, }) {
25
+ // Uncontrolled expansion, keyed on row identity so it survives a re-sort.
26
+ const [expanded, setExpanded] = useState(() => new Set(defaultExpanded ?? []));
27
+ const [hovered, setHovered] = useState(null);
28
+ const interactive = onRowClick !== undefined;
29
+ const toggle = (key) => {
30
+ const open = !expanded.has(key);
31
+ setExpanded((prev) => {
32
+ const next = new Set(prev);
33
+ if (open)
34
+ next.add(key);
35
+ else
36
+ next.delete(key);
37
+ return next;
38
+ });
39
+ onExpandToggle?.(key, open);
40
+ };
41
+ const ink = listInk(theme);
42
+ const accent = theme.annotation?.color ?? FALLBACK_ACCENT;
43
+ const divider = divided ? `1px solid ${theme.axis.grid}` : undefined;
44
+ // Label + before + glyph + after (+ expander) — the detail row spans them all.
45
+ const span = 2 + before.length + after.length + (renderExpanded ? 1 : 0);
46
+ const textCell = (align) => ({
47
+ padding: '6px 12px',
48
+ whiteSpace: 'nowrap',
49
+ textAlign: align ?? 'left',
50
+ verticalAlign: 'middle',
51
+ });
52
+ // The glyph cell's shared horizontal geometry — the label strip must use
53
+ // the SAME left/right padding (and baseline border) as the data rows, or
54
+ // its percentages would resolve against a different content width and the
55
+ // labels would sit off their rules.
56
+ const glyphCellStyle = (vertical) => ({
57
+ width: '100%',
58
+ padding: baseline ? `${vertical} 8px ${vertical} 5px` : `${vertical} 8px`,
59
+ verticalAlign: 'middle',
60
+ borderLeft: baseline ? `1px solid ${theme.axis.grid}` : undefined,
61
+ });
62
+ const drawnMarkers = markers.filter((m) => m.frac !== null);
63
+ return (_jsx("table", { "data-list": kind, style: {
64
+ width: '100%',
65
+ borderCollapse: 'collapse',
66
+ font: `${theme.font.size}px/${1.5} ${theme.font.family}`,
67
+ color: ink,
68
+ background: theme.background,
69
+ }, children: _jsxs("tbody", { children: [drawnMarkers.some((m) => m.label !== undefined) && (
70
+ // The marker label strip: one synthetic row above the data, its
71
+ // glyph cell sharing the data rows' horizontal geometry so each
72
+ // label centres exactly on its rule below.
73
+ _jsxs("tr", { "data-list-marker-labels": "", children: [_jsx("td", { style: textCell() }), before.map((cell) => (_jsx("td", { style: textCell(cell.align) }, cell.key))), _jsx("td", { style: glyphCellStyle('0px'), children: _jsx("div", { style: {
74
+ position: 'relative',
75
+ height: theme.font.size + 6,
76
+ }, children: drawnMarkers.map((m, mi) => m.label !== undefined && (_jsx("span", { "data-list-marker-label": "", style: {
77
+ position: 'absolute',
78
+ left: `${m.frac * 100}%`,
79
+ bottom: 0,
80
+ transform: 'translateX(-50%)',
81
+ whiteSpace: 'nowrap',
82
+ color: accent,
83
+ }, children: m.label }, mi))) }) }), after.map((cell) => (_jsx("td", { style: textCell(cell.align) }, cell.key))), renderExpanded !== undefined && _jsx("td", {})] })), rows.map((row, i) => {
84
+ const isSelected = selected != null && selected === row.key;
85
+ const isOpen = renderExpanded !== undefined && expanded.has(row.key);
86
+ return (_jsxs(Fragment, { children: [_jsxs("tr", { "data-list-row": row.key, ...(isSelected ? { 'data-selected': '' } : {}), onClick: onRowClick === undefined ? undefined : () => onRowClick(row),
87
+ // A clickable row is keyboard-reachable too: focusable, and
88
+ // Enter / Space activate it (Space's default scroll is eaten).
89
+ tabIndex: interactive ? 0 : undefined, onKeyDown: onRowClick === undefined
90
+ ? undefined
91
+ : (e) => {
92
+ if (e.key === 'Enter' || e.key === ' ') {
93
+ e.preventDefault();
94
+ onRowClick(row);
95
+ }
96
+ }, onPointerEnter: interactive ? () => setHovered(row.key) : undefined, onPointerLeave: interactive ? () => setHovered(null) : undefined, style: {
97
+ borderTop: i > 0 ? divider : undefined,
98
+ cursor: interactive ? 'pointer' : undefined,
99
+ background: interactive && hovered === row.key
100
+ ? (theme.legend?.border ?? theme.axis.grid)
101
+ : undefined,
102
+ // The selection accent: an inset edge in the annotation
103
+ // register (a *user's* mark, so it takes the marks colour,
104
+ // not a data hue) — reads on any ground, moves no layout.
105
+ boxShadow: isSelected ? `inset 3px 0 0 ${accent}` : undefined,
106
+ }, children: [_jsx("td", { "data-list-cell": "label", style: textCell(), children: row.label ?? row.key }), before.map((cell) => (_jsx("td", { "data-list-cell": cell.key, style: textCell(cell.align), children: cell.render(row) }, cell.key))), _jsx("td", { "data-list-cell": "glyphs",
107
+ // 100% absorbs the table's free width; every text cell
108
+ // shrinks to its content, staying aligned down the list.
109
+ // The baseline rule marks the scale origin: left padding
110
+ // narrows to a 5px breath so the glyphs start just off the
111
+ // rule, and border-collapse joins the rows' rules into one
112
+ // continuous vertical — the same thin `axis.grid` ink as
113
+ // the row dividers, so the two read as one quiet grid.
114
+ style: glyphCellStyle('6px'), children: _jsxs("div", { style: { position: 'relative' }, children: [renderGlyphs(row), drawnMarkers.map((m, mi) => (
115
+ // One dotted segment per row, bleeding through the
116
+ // row's vertical padding (+ divider) so adjacent rows'
117
+ // segments join into one continuous rule. Annotation
118
+ // register — a reference is a user's mark, not data.
119
+ _jsx("div", { "data-list-marker": "", style: {
120
+ position: 'absolute',
121
+ top: -7,
122
+ bottom: -7,
123
+ left: `calc(${m.frac * 100}% - 0.5px)`,
124
+ width: 0,
125
+ borderLeft: `1px dotted ${accent}`,
126
+ pointerEvents: 'none',
127
+ } }, mi)))] }) }), after.map((cell) => (_jsx("td", { "data-list-cell": cell.key, style: textCell(cell.align), children: cell.render(row) }, cell.key))), renderExpanded !== undefined && (_jsx("td", { style: { padding: '0 4px', verticalAlign: 'middle' }, children: _jsx("button", { type: "button", "data-list-expander": "", "aria-expanded": isOpen, "aria-label": isOpen ? 'Collapse row' : 'Expand row', onClick: (e) => {
128
+ // The chevron toggles; it must not double as a row click.
129
+ e.stopPropagation();
130
+ toggle(row.key);
131
+ }, style: {
132
+ background: 'none',
133
+ border: 'none',
134
+ cursor: 'pointer',
135
+ color: theme.axis.label,
136
+ font: 'inherit',
137
+ padding: '2px 6px',
138
+ transform: isOpen ? 'rotate(90deg)' : undefined,
139
+ transition: 'transform 120ms',
140
+ }, children: "\u25B8" }) }))] }), isOpen && (_jsx("tr", { "data-list-detail": row.key, children: _jsx("td", { colSpan: span, style: { padding: '2px 12px 12px' }, children: renderExpanded(row) }) }))] }, row.key));
141
+ })] }) }));
142
+ }
143
+ //# sourceMappingURL=ListTable.js.map
@@ -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 ColorEncoding, type RadiusEncoding } from './encoding.js';
4
5
  import type { DecimateOption } from './decimate.js';
5
- export interface ScatterChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
6
- /**
7
- * The source series. A `TimeSeries` scatters against the time axis; a
8
- * `ValueSeries` (`series.byValue('cumDist')`, or `ValueSeries.fromColumns`
9
- * for natively value-keyed data — IV marks keyed by strike) against its
10
- * value axis — the container infers which from the data, no axis-type prop
11
- * (mirrors `<LineChart>`). Either way the key / axis column supplies each
12
- * point's x and `column` supplies y.
13
- *
14
- * **Live charts:** `series.byValue(…)` mints a *fresh* projection each call,
15
- * so passing `series={s.byValue('dist')}` inline re-registers this layer
16
- * every render — memoize the projection (`useMemo`) on a frequently
17
- * re-rendering chart.
18
- */
19
- series: TimeSeries<S> | ValueSeries<VS>;
20
- /** Name of the numeric value column — each point's y. */
21
- column: string;
6
+ export interface ScatterChartCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
22
7
  /**
23
8
  * The scatter's semantic identifier — what the marks _are_ / how they should
24
9
  * read. The theme maps it to a {@link ScatterStyle} (`theme.scatter[as] ??
@@ -109,6 +94,30 @@ export interface ScatterChartProps<S extends SeriesSchema = SeriesSchema, VS ext
109
94
  */
110
95
  index?: number;
111
96
  }
97
+ /**
98
+ * ScatterChart'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 ScatterChartSource<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
+ column: NumericColumn<S>;
115
+ } | {
116
+ series: ValueSeries<VS>;
117
+ column: ValueNumericColumn<VS>;
118
+ };
119
+ /** `<ScatterChart>`'s props: the shared knobs plus one series-kind source shape. */
120
+ export type ScatterChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = ScatterChartCommon<S, VS> & ScatterChartSource<S, VS>;
112
121
  /**
113
122
  * A scatter draw layer: one mark per finite point at `(x, column-value)`
114
123
  * — x from the series' key / axis column (time or value axis) —
@@ -138,4 +147,5 @@ export interface ScatterChartProps<S extends SeriesSchema = SeriesSchema, VS ext
138
147
  * ```
139
148
  */
140
149
  export declare function ScatterChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, as: semantic, id, axis, radius, color, label, offset, decimate, legend, index, }: ScatterChartProps<S, VS>): null;
150
+ export {};
141
151
  //# sourceMappingURL=ScatterChart.d.ts.map
package/dist/YAxis.js CHANGED
@@ -72,14 +72,25 @@ export function YAxis({ id, side = 'left', label, min, max, format, ticks, tickC
72
72
  // a cursor value read identically. `count` calibrates the default formatter's
73
73
  // precision to the tick density, exactly as the axis is.
74
74
  const fmt = yScale ? resolveAxisFormat(yScale, count, format) : String;
75
+ // A **horizontal categorical** layer on this axis supplies its category
76
+ // names ([PND-HCAT]); with no explicit `ticks`, label one per unit slot at
77
+ // its centre (`i + 0.5`) instead of the scale's numeric ticks — a slot index
78
+ // is not a number anyone wants to read. Explicit `ticks` still win, and a
79
+ // non-categorical row is unaffected (no layer answers, so this is `null`).
80
+ const layerCategories = row.layers
81
+ .filter((e) => (e.axisId ?? row.defaultAxisId) === id)
82
+ .map((e) => e.layer.binCategories?.() ?? null)
83
+ .find((c) => c !== null) ?? null;
75
84
  // Explicit `{ at, label }` ticks render verbatim (each label at its `at`),
76
85
  // overriding the auto-picked d3 ticks; otherwise label the scale's ticks via `fmt`.
77
86
  const tickList = ticks
78
87
  ? ticks.map((t) => ({ value: t.at, label: t.label }))
79
- : (yScale ? yScale.ticks(count) : []).map((t) => ({
80
- value: t,
81
- label: fmt(t),
82
- }));
88
+ : layerCategories !== null
89
+ ? layerCategories.map((label, i) => ({ value: i + 0.5, label }))
90
+ : (yScale ? yScale.ticks(count) : []).map((t) => ({
91
+ value: t,
92
+ label: fmt(t),
93
+ }));
83
94
  // The row reserves a slot per axis column (the widest in that column across
84
95
  // rows). Size the box to the slot and align this axis's own (narrower)
85
96
  // content toward the plot — left axes flush right, right axes flush left — so