@pond-ts/charts 0.54.0 → 0.56.2

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +451 -1
  2. package/dist/AreaChart.d.ts +47 -22
  3. package/dist/AreaChart.js +24 -1
  4. package/dist/BandChart.d.ts +29 -17
  5. package/dist/BarChart.d.ts +92 -49
  6. package/dist/BarChart.js +65 -8
  7. package/dist/BarList.d.ts +127 -0
  8. package/dist/BarList.js +84 -0
  9. package/dist/BoxList.d.ts +108 -0
  10. package/dist/BoxList.js +125 -0
  11. package/dist/BoxPlot.d.ts +63 -30
  12. package/dist/Candlestick.d.ts +5 -4
  13. package/dist/ChartRow.js +57 -6
  14. package/dist/Layers.js +22 -2
  15. package/dist/LineChart.d.ts +29 -26
  16. package/dist/ListTable.d.ts +51 -0
  17. package/dist/ListTable.js +143 -0
  18. package/dist/ScatterChart.d.ts +27 -17
  19. package/dist/YAxis.d.ts +29 -1
  20. package/dist/YAxis.js +19 -5
  21. package/dist/area.js +46 -15
  22. package/dist/band.js +13 -0
  23. package/dist/bars.d.ts +89 -18
  24. package/dist/bars.js +141 -30
  25. package/dist/column-names.d.ts +74 -0
  26. package/dist/column-names.js +2 -0
  27. package/dist/context.d.ts +40 -2
  28. package/dist/data.d.ts +19 -0
  29. package/dist/data.js +46 -0
  30. package/dist/dev.d.ts +2 -0
  31. package/dist/dev.js +2 -0
  32. package/dist/domain.d.ts +54 -1
  33. package/dist/domain.js +195 -2
  34. package/dist/format.d.ts +20 -0
  35. package/dist/format.js +23 -10
  36. package/dist/gaps.d.ts +33 -0
  37. package/dist/gaps.js +49 -0
  38. package/dist/index.d.ts +19 -13
  39. package/dist/index.js +21 -13
  40. package/dist/line.js +10 -1
  41. package/dist/list-source.d.ts +61 -0
  42. package/dist/list-source.js +5 -0
  43. package/dist/list.d.ts +205 -0
  44. package/dist/list.js +165 -0
  45. package/dist/theme.d.ts +42 -0
  46. package/dist/theme.js +24 -0
  47. package/dist/viewport.d.ts +9 -1
  48. package/dist/viewport.js +40 -4
  49. package/dist/yticks.d.ts +44 -0
  50. package/dist/yticks.js +55 -0
  51. package/package.json +3 -3
@@ -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/ChartRow.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Children, cloneElement, isValidElement, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
3
- import { scaleLinear } from 'd3-scale';
4
- import { resolveYDomain } from './domain.js';
2
+ import { Children, cloneElement, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
3
+ import { scaleLinear, scaleLog } from 'd3-scale';
4
+ import { isDev } from './dev.js';
5
+ import { logAxisWarning, needsExtents, resolveYDomain } from './domain.js';
5
6
  import { resolveAxisFormat } from './format.js';
6
7
  import { resolveYTickCount } from './yticks.js';
7
8
  import { placeAxisSlots } from './slots.js';
@@ -39,6 +40,7 @@ function axisSpecEqual(a, b) {
39
40
  return (a.id === b.id &&
40
41
  a.side === b.side &&
41
42
  a.width === b.width &&
43
+ a.scale === b.scale &&
42
44
  // Object.is (not ===) so a degenerate NaN bound compares equal to itself and
43
45
  // doesn't re-register every render.
44
46
  Object.is(a.min, b.min) &&
@@ -171,6 +173,7 @@ export function ChartRow({ height, cursor, children }) {
171
173
  id: IMPLICIT_AXIS_ID,
172
174
  side: 'left',
173
175
  width: 0,
176
+ scale: 'linear',
174
177
  min: undefined,
175
178
  max: undefined,
176
179
  pad: 0,
@@ -222,20 +225,68 @@ export function ChartRow({ height, cursor, children }) {
222
225
  const yScales = useMemo(() => {
223
226
  const map = new Map();
224
227
  for (const ax of effectiveAxes) {
225
- const extents = ax.min === undefined || ax.max === undefined
228
+ const extents = needsExtents(ax)
226
229
  ? layerList
227
230
  .filter((entry) => (entry.axisId ?? defaultAxisId) === ax.id)
228
231
  .map((entry) => entry.layer.yExtent())
229
232
  : [];
230
- const [lo, hi] = resolveYDomain(ax.min, ax.max, extents, ax.pad);
233
+ const [lo, hi] = resolveYDomain(ax.min, ax.max, extents, ax.pad, ax.scale);
231
234
  // Reserve a header band at the top when any axis draws a `'top'` title,
232
235
  // so the title clears the top tick + plot (the whole row shifts down
233
236
  // uniformly, keeping stacked axes aligned). No top titles ⇒ range top 0,
234
237
  // so nothing changes for existing charts.
235
- map.set(ax.id, scaleLinear().domain([lo, hi]).range([height, topHeader]));
238
+ // `scaleLog` and `scaleLinear` share the call/ticks/tickFormat/invert
239
+ // surface every consumer uses (see `YScale`), so choosing between them
240
+ // here is the whole of log support — no draw layer branches on it.
241
+ const base = ax.scale === 'log' ? scaleLog() : scaleLinear();
242
+ map.set(ax.id, base.domain([lo, hi]).range([height, topHeader]));
236
243
  }
237
244
  return map;
238
245
  }, [effectiveAxes, layerList, height, defaultAxisId, topHeader]);
246
+ // Dev-mode diagnostics for a `scale="log"` axis (see `logAxisWarning`). Three
247
+ // things about *where* this sits are load-bearing, each of them a bug the
248
+ // first version shipped:
249
+ //
250
+ // - **An effect, not the scale memo.** Warning from inside `useMemo` is a
251
+ // side effect in a function React may call speculatively — and does call
252
+ // twice under StrictMode.
253
+ // - **Deduplicated by message, in a ref.** The comment on the original said
254
+ // "warn once per offending axis" and nothing implemented it, so a live
255
+ // chart re-warned on every appended sample. Keying on the message (not a
256
+ // bare "already warned" flag) still reports a *different* complaint if the
257
+ // data changes shape.
258
+ // - **`height` is not a dependency.** It is one for the scales, which is why
259
+ // the warning must not ride along: a drag-resize would otherwise emit a
260
+ // line per animation frame.
261
+ //
262
+ // Gated on `isDev` **and** on some axis actually being logarithmic, so a
263
+ // production build and every linear chart skip the extent walk entirely.
264
+ const warnedRef = useRef(new Map());
265
+ useEffect(() => {
266
+ if (!isDev || !effectiveAxes.some((ax) => ax.scale === 'log'))
267
+ return;
268
+ const warned = warnedRef.current;
269
+ for (const ax of effectiveAxes) {
270
+ // A linear axis in the same row has nothing to say and must not pay the
271
+ // O(points) walk below just because a sibling is logarithmic.
272
+ if (ax.scale !== 'log')
273
+ continue;
274
+ // Always walk the extents here, even for a fully-explicit domain the
275
+ // scale memo skips them for: data that cannot be drawn is worth saying so
276
+ // about whether or not it happened to constrain the bounds — and the
277
+ // both-explicit axis was exactly the case the first version stayed silent
278
+ // about.
279
+ const message = logAxisWarning(ax, layerList
280
+ .filter((entry) => (entry.axisId ?? defaultAxisId) === ax.id)
281
+ .map((entry) => entry.layer.yExtent()));
282
+ if (message === null)
283
+ warned.delete(ax.id);
284
+ else if (warned.get(ax.id) !== message) {
285
+ warned.set(ax.id, message);
286
+ console.warn(message);
287
+ }
288
+ }
289
+ }, [effectiveAxes, layerList, defaultAxisId]);
239
290
  // Resolved auto-tick count per axis — explicit `<YAxis tickCount>` else
240
291
  // height-derived (see resolveYTickCount). The single source the `<YAxis>`
241
292
  // labels, the readout formatter (below), and the `Layers` gridlines all read,
package/dist/Layers.js CHANGED
@@ -5,6 +5,7 @@ import { drawGrid, drawDividers, dividerAlphas, thinPixels } from './grid.js';
5
5
  import { cursorParts, bandRect, regionSpan } from './tracker.js';
6
6
  import { resolveSelection } from './select.js';
7
7
  import { panRange, zoomRange, panRangeTrading, zoomRangeTrading, } from './viewport.js';
8
+ import { yTickValues } from './yticks.js';
8
9
  import { flagChipStyle, flagChipX, axisPillX, axisPillStyle } from './chip.js';
9
10
  import { ContainerContext, CursorContext, LayersContext, RowContext, } from './context.js';
10
11
  /** Fallback **y**-gridline tick count, used only before the row publishes its
@@ -101,6 +102,13 @@ export function Layers({ children }) {
101
102
  // A category axis draws no vertical gridlines — a line through each bar
102
103
  // centre reads as noise; the bars are the structure.
103
104
  const xTickVals = container.xKind === 'category' ? [] : xScale.ticks(xTickCount);
105
+ // The same rule on the other axis: a **horizontal** categorical chart
106
+ // ([PND-HCAT]) puts its categories on y, where the `<YAxis>` labels slot
107
+ // *centres* while d3's auto ticks fall on slot *boundaries* — so drawing
108
+ // them would both mismatch the labels and stripe each bar. Suppressed,
109
+ // exactly as the categorical x axis already is.
110
+ const yIsCategory = layers.some((e) => (e.axisId ?? defaultAxisId) === defaultAxisId &&
111
+ (e.layer.binCategories?.() ?? null) !== null);
104
112
  // The reference grid — behind the data, opt-out via `grid={false}` for
105
113
  // a clean backdrop (session dividers below stay independent of it).
106
114
  if (container.grid) {
@@ -108,8 +116,8 @@ export function Layers({ children }) {
108
116
  // single source, height-derived or an explicit `<YAxis tickCount>`), so
109
117
  // a gridline sits under every `<YAxis>` label and no more.
110
118
  const yCount = tickCounts.get(defaultAxisId) ?? GRID_TICKS;
111
- const yTicks = gridY
112
- ? (explicitY ?? gridY.ticks(yCount)).map((t) => gridY(t))
119
+ const yTicks = gridY && !(yIsCategory && explicitY === undefined)
120
+ ? (explicitY ?? yTickValues(gridY, yCount)).map((t) => gridY(t))
113
121
  : [];
114
122
  // On a calendar axis the verticals are the FULL grain populations —
115
123
  // every day in the month, every month in the year, every aligned
@@ -260,6 +268,18 @@ export function Layers({ children }) {
260
268
  // a full replot per mousemove.
261
269
  container.timeRange,
262
270
  container.reportDrawStats,
271
+ // Read inside `draw`, so they have to invalidate it. Omitting `grid` meant
272
+ // toggling `<ChartContainer grid>` changed nothing until some *other*
273
+ // dep moved — pan the plot a pixel and the gridlines you switched off
274
+ // finally vanished. All primitives, so no per-frame identity churn.
275
+ container.grid,
276
+ container.sessionDividers,
277
+ container.xKind,
278
+ // `container.theme` is read too. It is deliberately NOT listed: it is the
279
+ // caller's prop and an inline object literal would rebuild `draw` every
280
+ // render (a full replot per frame). The values `draw` actually takes from
281
+ // it — `background`, `gridColor`, `gridDash` — are extracted above and
282
+ // listed individually, so a theme swap still invalidates.
263
283
  row.rowKey,
264
284
  ]);
265
285
  // Interaction overlay: the cursor marks live on a DOM/SVG overlay above the
@@ -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