@pond-ts/charts 0.57.0 → 0.59.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.
Files changed (85) hide show
  1. package/API.md +576 -0
  2. package/CHANGELOG.md +1213 -1
  3. package/dist/AreaChart.d.ts +12 -1
  4. package/dist/AreaChart.js +131 -13
  5. package/dist/BarChart.d.ts +56 -7
  6. package/dist/BarChart.js +263 -39
  7. package/dist/BarList.d.ts +85 -5
  8. package/dist/BarList.js +25 -4
  9. package/dist/BoxList.d.ts +70 -3
  10. package/dist/BoxList.js +21 -7
  11. package/dist/BoxPlot.d.ts +2 -1
  12. package/dist/BoxPlot.js +101 -9
  13. package/dist/Candlestick.d.ts +13 -1
  14. package/dist/Candlestick.js +89 -3
  15. package/dist/ChartContainer.d.ts +36 -48
  16. package/dist/ChartContainer.js +465 -59
  17. package/dist/ChartRow.d.ts +9 -2
  18. package/dist/ChartRow.js +176 -14
  19. package/dist/HeatMap.d.ts +176 -0
  20. package/dist/HeatMap.js +344 -0
  21. package/dist/Layers.d.ts +5 -1
  22. package/dist/Layers.js +1014 -253
  23. package/dist/Legend.js +8 -4
  24. package/dist/LineChart.d.ts +18 -1
  25. package/dist/LineChart.js +165 -4
  26. package/dist/ListTable.d.ts +30 -3
  27. package/dist/ListTable.js +381 -23
  28. package/dist/ScatterChart.d.ts +3 -2
  29. package/dist/ScatterChart.js +68 -4
  30. package/dist/XAxis.js +40 -22
  31. package/dist/YAxis.d.ts +58 -2
  32. package/dist/YAxis.js +3 -1
  33. package/dist/area.d.ts +34 -1
  34. package/dist/area.js +88 -1
  35. package/dist/bars.d.ts +67 -6
  36. package/dist/bars.js +250 -35
  37. package/dist/box.d.ts +2 -2
  38. package/dist/box.js +158 -40
  39. package/dist/brush.d.ts +142 -0
  40. package/dist/brush.js +179 -0
  41. package/dist/child-index.d.ts +27 -0
  42. package/dist/child-index.js +57 -0
  43. package/dist/context.d.ts +870 -39
  44. package/dist/cursors.d.ts +161 -0
  45. package/dist/cursors.js +503 -0
  46. package/dist/data.d.ts +38 -0
  47. package/dist/data.js +43 -0
  48. package/dist/decimate.d.ts +78 -1
  49. package/dist/decimate.js +157 -0
  50. package/dist/format.d.ts +15 -0
  51. package/dist/format.js +16 -1
  52. package/dist/heat.d.ts +163 -0
  53. package/dist/heat.js +659 -0
  54. package/dist/index.d.ts +13 -4
  55. package/dist/index.js +27 -0
  56. package/dist/line.d.ts +137 -0
  57. package/dist/line.js +328 -0
  58. package/dist/ohlc.d.ts +16 -1
  59. package/dist/ohlc.js +93 -4
  60. package/dist/range.d.ts +14 -1
  61. package/dist/range.js +24 -3
  62. package/dist/scatter.d.ts +17 -9
  63. package/dist/scatter.js +221 -33
  64. package/dist/select.d.ts +13 -5
  65. package/dist/select.js +14 -6
  66. package/dist/selection-fixtures.d.ts +174 -0
  67. package/dist/selection-fixtures.js +569 -0
  68. package/dist/selection-stories.d.ts +73 -0
  69. package/dist/selection-stories.js +301 -0
  70. package/dist/selectors.d.ts +316 -0
  71. package/dist/selectors.js +391 -0
  72. package/dist/span.d.ts +122 -0
  73. package/dist/span.js +203 -0
  74. package/dist/sweep.d.ts +154 -0
  75. package/dist/sweep.js +282 -0
  76. package/dist/theme.d.ts +510 -5
  77. package/dist/theme.js +217 -41
  78. package/dist/tracker.d.ts +6 -0
  79. package/dist/tracker.js +6 -0
  80. package/dist/tradingAxis.fixture.d.ts +78 -0
  81. package/dist/tradingAxis.fixture.js +215 -0
  82. package/dist/useChartLegend.js +18 -3
  83. package/dist/yticks.d.ts +3 -0
  84. package/dist/yticks.js +104 -0
  85. package/package.json +6 -5
@@ -14,6 +14,17 @@ export interface AreaChartCommon<S extends SeriesSchema = SeriesSchema, VS exten
14
14
  * single styling channel).
15
15
  */
16
16
  as?: string;
17
+ /**
18
+ * **Opt in to selection** — see `<LineChart id>`; the currency is identical
19
+ * because the premise is ([PND-TRACESEL]): a click commits a **series-scoped**
20
+ * `SelectInfo` (`NaN` key/value plus a stable `mark`), a sweep commits a
21
+ * `SpanSelection` with **no marks**.
22
+ *
23
+ * What differs is only the **hit test**: an area is a filled region, so the
24
+ * pointer counts as on it when it lies **between the trace and the
25
+ * baseline** — the whole shape is the target, not the 1.5px edge.
26
+ */
27
+ id?: string;
17
28
  /**
18
29
  * Which `<YAxis>` (by its `id`) this area scales against — picks the *scale*,
19
30
  * where `as` picks the *style*. **Omitted ⇒ the row's default axis.**
@@ -140,6 +151,6 @@ export declare function resolveAreaBaseline(baseline: number | undefined, yScale
140
151
  * </Layers>
141
152
  * ```
142
153
  */
143
- export declare function AreaChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, readout, as: semantic, axis, baseline, curve, gaps, decimate, legend, index, }: AreaChartProps<S, VS>): null;
154
+ export declare function AreaChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, readout, as: semantic, axis, id, baseline, curve, gaps, decimate, legend, index, }: AreaChartProps<S, VS>): null;
144
155
  export {};
145
156
  //# sourceMappingURL=AreaChart.d.ts.map
package/dist/AreaChart.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { useContext, useEffect, useMemo } from 'react';
2
2
  import { ValueSeries } from 'pond-ts';
3
3
  import { assertNumericColumn, fromTimeSeries, fromValueSeries, } from './data.js';
4
- import { areaExtent, drawArea } from './area.js';
4
+ import { areaExtent, areaHitIndex, areaStateStyle, drawArea } from './area.js';
5
+ import { drawPartitioned, plotExtentOf, strokeSpanEdges, } from './line.js';
6
+ import { sweepSpan } from './sweep.js';
5
7
  import { resolveCurve } from './curve.js';
6
8
  import { DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
7
- import { ContainerContext, LayersContext } from './context.js';
9
+ import { ContainerContext, LayersContext, } from './context.js';
8
10
  import { legendLabelFor, useLegendItems, } from './swatch.js';
9
11
  import { useSlotKey } from './use-slot-key.js';
10
12
  /** Read a d3 linear scale's domain lower bound (the axis floor) from the plain
@@ -54,7 +56,7 @@ function domainFloor(yScale) {
54
56
  * </Layers>
55
57
  * ```
56
58
  */
57
- export function AreaChart({ series, column, readout, as: semantic, axis, baseline, curve, gaps = DEFAULT_GAP_MODE, decimate = true, legend, index = 0, }) {
59
+ export function AreaChart({ series, column, readout, as: semantic, axis, id, baseline, curve, gaps = DEFAULT_GAP_MODE, decimate = true, legend, index = 0, }) {
58
60
  const container = useContext(ContainerContext);
59
61
  if (container === null) {
60
62
  throw new Error('<AreaChart> must be rendered inside a <ChartContainer>');
@@ -91,6 +93,45 @@ export function AreaChart({ series, column, readout, as: semantic, axis, baselin
91
93
  // Faintness of the inferred dashed connectors (dashed / step) — theme-level,
92
94
  // falling back to the shared default so a theme without it still renders faint.
93
95
  const gapConnectorOpacity = container.theme.gap?.connectorOpacity ?? DEFAULT_GAP_CONNECTOR_OPACITY;
96
+ // ── The trace's interaction state ([PND-TRACESEL]) — see `<LineChart>` for
97
+ // the reasoning; this is the same derivation over `AreaStyle`'s channels.
98
+ const selectedEntries = container.selected;
99
+ const hoveredEntries = container.hovered;
100
+ // The committed spans, plus the **live** ones of a sweep in flight. A
101
+ // previewed span draws exactly as a committed one, so releasing changes
102
+ // nothing visually — the preview cannot promise a picture the commit does not
103
+ // deliver. The live channel wins while it is non-empty, because during a drag
104
+ // it IS the current answer.
105
+ const previewing = container.previewSpans.length > 0;
106
+ const allSpans = previewing
107
+ ? container.previewSpans
108
+ : container.selectedSpans;
109
+ const traceState = useMemo(() => {
110
+ if (id === undefined)
111
+ return 'rest';
112
+ if (allSpans.some((sp) => sp.id === id))
113
+ return 'rest';
114
+ const mine = (e) => e.id === id;
115
+ if (selectedEntries.some(mine))
116
+ return 'selected';
117
+ if (hoveredEntries.some(mine))
118
+ return 'hover';
119
+ if (selectedEntries.length > 0 || allSpans.length > 0)
120
+ return 'dimmed';
121
+ return 'rest';
122
+ }, [id, selectedEntries, hoveredEntries, allSpans]);
123
+ // **`spanColor` only when this is the ONLY swept trace.** The hue is
124
+ // justified by identity not being in question inside a single series — but
125
+ // sweep two traces and both would go blue, so inside the window you could no
126
+ // longer tell them apart, which is the very thing the rule exists to prevent.
127
+ // With more than one, the window thickens and every trace keeps its colour.
128
+ const soleSpannedTrace = allSpans.length === 1;
129
+ const spanX = useMemo(() => {
130
+ if (id === undefined)
131
+ return null;
132
+ const mine = allSpans.find((sp) => sp.id === id);
133
+ return mine === undefined ? null : mine.x;
134
+ }, [id, allSpans]);
94
135
  const entry = useMemo(() => ({
95
136
  layer: {
96
137
  as: semantic,
@@ -99,6 +140,36 @@ export function AreaChart({ series, column, readout, as: semantic, axis, baselin
99
140
  // ValueSeries plots on a value axis, a TimeSeries on time.
100
141
  xKind: series instanceof ValueSeries ? 'value' : 'time',
101
142
  xExtent: () => cs.length === 0 ? null : [cs.x[0], cs.x[cs.length - 1]],
143
+ // ── Selection, gated on `id` ([PND-TRACESEL]). Same currency as
144
+ // `<LineChart>`; only `hitTest` differs, because a fill is not a stroke.
145
+ ...(id === undefined
146
+ ? {}
147
+ : {
148
+ sweepsRect: false,
149
+ sweepAxis: 'x',
150
+ sweepSpanOnly: true,
151
+ hitTest: (px, py, xScale, yScale) => {
152
+ const i = areaHitIndex(cs, baseline, px, py, xScale, yScale);
153
+ if (i === null)
154
+ return null;
155
+ return {
156
+ id,
157
+ // Series-scoped, with a stable `mark` for identity — see
158
+ // `<LineChart>`'s hitTest for why both halves are needed.
159
+ key: NaN,
160
+ value: NaN,
161
+ color: style.fill,
162
+ label,
163
+ mark: label,
164
+ };
165
+ },
166
+ beginSweep: () => cs.length === 0
167
+ ? null
168
+ : sweepSpan({
169
+ id,
170
+ bounds: [cs.x[0], cs.x[cs.length - 1]],
171
+ }),
172
+ }),
102
173
  sampleAt: (x) => {
103
174
  // No readout past the data (tracker policy — nearest clamps to an
104
175
  // endpoint outside the span); bounds from the columnar x axis.
@@ -151,16 +222,49 @@ export function AreaChart({ series, column, readout, as: semantic, axis, baselin
151
222
  ]
152
223
  : [];
153
224
  },
154
- draw: (ctx, xScale, yScale) => drawArea(ctx, cs, xScale, yScale, style,
155
- // Omitted baseline rests on the axis floor (resolved late from the
156
- // scale, so it tracks the auto-fit domain); a fixed baseline is used
157
- // verbatim.
158
- // A log axis has no position for zero — or anything at or below
159
- // it — so an explicit out-of-domain `baseline` would scale to
160
- // `NaN` and poison every coordinate in the fill path. Fall back
161
- // to the axis floor, which is exactly what an omitted baseline
162
- // already resolves to.
163
- resolveAreaBaseline(baseline, yScale), curveFactory, gaps, gapConnectorOpacity, decimate),
225
+ draw: (ctx, xScale, yScale) => {
226
+ const fill = (st, alpha) => () => {
227
+ const prior = ctx.globalAlpha;
228
+ if (alpha !== 1)
229
+ ctx.globalAlpha = prior * alpha;
230
+ const out = drawAreaWith(st);
231
+ ctx.globalAlpha = prior;
232
+ return out;
233
+ };
234
+ if (spanX === null) {
235
+ const [st, alpha] = areaStateStyle(style, traceState);
236
+ return fill(st, alpha)();
237
+ }
238
+ // EXPERIMENT: annotation-register rules at the window's edges,
239
+ // underneath the trace ink (drawn first). See `strokeSpanEdges`.
240
+ //
241
+ // **Committed spans only.** While the drag is live the brush band
242
+ // already strokes its own edges at the same two x positions, so
243
+ // drawing these too put two rules a fraction of a pixel apart on each
244
+ // boundary — which read as one muddy smear rather than as either. The
245
+ // handoff is the honest reading anyway: the band is the gesture's
246
+ // mark and belongs to the drag; these preview the annotation you
247
+ // would get, and belong to the result.
248
+ if (!previewing)
249
+ strokeSpanEdges(ctx, [xScale(spanX[0]), xScale(spanX[1])], ctx.canvas.height, container.theme.annotation?.spanEdge ?? '#f0b26b');
250
+ const [outStyle, outAlpha] = areaStateStyle(style, 'dimmed');
251
+ const [inStyle] = areaStateStyle(style, 'selected');
252
+ return drawPartitioned(ctx, [xScale(spanX[0]), xScale(spanX[1])], plotExtentOf(ctx, xScale, yScale).height, fill(outStyle, outAlpha), fill(style.spanColor === undefined || !soleSpannedTrace
253
+ ? inStyle
254
+ : { ...inStyle, color: style.spanColor, fill: style.spanColor }, 1), true, 0, plotExtentOf(ctx, xScale, yScale).width);
255
+ function drawAreaWith(st) {
256
+ return drawArea(ctx, cs, xScale, yScale, st,
257
+ // Omitted baseline rests on the axis floor (resolved late from the
258
+ // scale, so it tracks the auto-fit domain); a fixed baseline is used
259
+ // verbatim.
260
+ // A log axis has no position for zero — or anything at or below
261
+ // it — so an explicit out-of-domain `baseline` would scale to
262
+ // `NaN` and poison every coordinate in the fill path. Fall back
263
+ // to the axis floor, which is exactly what an omitted baseline
264
+ // already resolves to.
265
+ resolveAreaBaseline(baseline, yScale), curveFactory, gaps, gapConnectorOpacity, decimate);
266
+ }
267
+ },
164
268
  },
165
269
  axisId: axis,
166
270
  index,
@@ -178,6 +282,11 @@ export function AreaChart({ series, column, readout, as: semantic, axis, baselin
178
282
  gapConnectorOpacity,
179
283
  decimate,
180
284
  axis,
285
+ id,
286
+ traceState,
287
+ spanX,
288
+ soleSpannedTrace,
289
+ previewing,
181
290
  index,
182
291
  ]);
183
292
  // A stable per-instance slot (see useSlotKey) keeps this layer's z-position
@@ -194,6 +303,15 @@ export function AreaChart({ series, column, readout, as: semantic, axis, baselin
194
303
  useEffect(() => {
195
304
  registerTrackerSource(slot, entry.layer);
196
305
  }, [registerTrackerSource, slot, entry.layer]);
306
+ // Advertise selectability (only when an `id` was given) — see the same block
307
+ // in `LineChart.tsx` for why a trace was missing from this set.
308
+ const { registerSelectable, unregisterSelectable } = container;
309
+ useEffect(() => {
310
+ if (id === undefined)
311
+ return;
312
+ registerSelectable(slot);
313
+ return () => unregisterSelectable(slot);
314
+ }, [registerSelectable, unregisterSelectable, slot, id]);
197
315
  // And a legend row: the readout identity + the resolved area style (top line
198
316
  // over the translucent fill), so a `<Legend>` swatch can never drift.
199
317
  const legendRows = useMemo(() => {
@@ -1,6 +1,6 @@
1
1
  import { ValueSeries } from 'pond-ts';
2
2
  import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
3
- import { type BinRecord, type CategoryDatum } from './data.js';
3
+ import { type BinRecord, type CategoryDatum, type CategoryStackDatum } from './data.js';
4
4
  import { type Orientation } from './bars.js';
5
5
  import type { NumericColumn, ValueNumericColumn } from './column-names.js';
6
6
  import type { DecimateOption } from './decimate.js';
@@ -43,11 +43,17 @@ import type { DecimateOption } from './decimate.js';
43
43
  * `<ChartContainer origin>` does not rescue it: it relabels a value axis but
44
44
  * does not re-ladder it.
45
45
  * - **`categories`** — an ordered `{ label, value }[]`, one bar per category.
46
- * Takes **no** `column`/`columns` (each datum carries its own value).
47
- * Vertical puts the categories on the ordinal **x** axis (the container's
48
- * band scale); `orientation="horizontal"` puts them on **y** as unit slots
49
- * and the value on x, and a `<YAxis>` with no explicit `ticks` labels one
50
- * per category automatically ([PND-HCAT]).
46
+ * Takes no `column`. Vertical puts the categories on the ordinal **x** axis
47
+ * (the container's band scale); `orientation="horizontal"` puts them on **y**
48
+ * as unit slots and the value on x, and a `<YAxis>` with no explicit `ticks`
49
+ * labels one per category automatically ([PND-HCAT]).
50
+ * - **`categories` + `columns`** — a **stacked** category chart
51
+ * ([PND-CATSTACK]): each datum is `{ label, values }` and `columns` names the
52
+ * groups to stack, bottom → top. The same relationship `series` + `columns`
53
+ * already has, so a category stack is now a first-class shape rather than one
54
+ * `categories` layer per cumulative total. One layer means one `mark` per
55
+ * bar, so **one selection entry lights the whole bar** and the composed
56
+ * workaround's per-layer replication is unnecessary.
51
57
  *
52
58
  * **Live charts:** `series.byValue(…)` / `.toMap()` mint fresh objects each
53
59
  * call, so an inline `series={…}` re-registers this layer every render — on a
@@ -95,6 +101,12 @@ type BarChartSource<S extends SeriesSchema = SeriesSchema, VS extends ValueSerie
95
101
  bins?: never;
96
102
  column?: never;
97
103
  columns?: never;
104
+ } | {
105
+ categories: readonly CategoryStackDatum[];
106
+ columns: readonly string[];
107
+ series?: never;
108
+ bins?: never;
109
+ column?: never;
98
110
  };
99
111
  /** The props every {@link BarChartSource} mode shares. */
100
112
  export interface BarChartCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
@@ -230,6 +242,43 @@ export interface BarChartCommon<S extends SeriesSchema = SeriesSchema, VS extend
230
242
  * would invert collapses to the style's `minWidth`.
231
243
  */
232
244
  gap?: number;
245
+ /**
246
+ * Cap on a bar's **ink** width in px — applied after the `gap` inset and
247
+ * centred in its slot ([PND-BARWIDTH]). **Omitted ⇒ the theme's `bar`
248
+ * `maxWidth`, and uncapped if that is unset too** (a bar is `slot - gap`
249
+ * wide, as it always was).
250
+ *
251
+ * This is the *absolute* half of the width vocabulary. `gap` is **relative**,
252
+ * so with it alone bar width is always `slot - gap` and fattens as the plot
253
+ * widens; a fixed ink width is what makes a measure comparable **between**
254
+ * panes, since bars that widen with their pane read as different weights of
255
+ * the same thing.
256
+ *
257
+ * Pairs with `<ChartContainer maxBandWidth>`: that caps the **slot** (how far
258
+ * the bars spread), this caps the **ink** inside whatever slot results. The
259
+ * two are independent, which is the point — neither spelling alone expresses
260
+ * "spread the slots, pin the bar":
261
+ *
262
+ * - `maxBandWidth = barWidth + gap` pins the bar but stops the slots
263
+ * spreading;
264
+ * - `maxBandWidth = slotCap` spreads them but lets the bar grow.
265
+ *
266
+ * `theme.bar[as].minWidth` still wins if the two bounds would invert, so the
267
+ * rect can never flip.
268
+ *
269
+ * **On a stacked chart the cap narrows the hit target too**, because a stack
270
+ * hit-tests its drawn segment rect (it must, to resolve *which* segment).
271
+ * A single-series bar is unaffected: it hit-tests its whole slot, so the ink
272
+ * can be narrow while the target stays full width.
273
+ *
274
+ * That holds in **both orientations**, and costs a deliberate guard to keep:
275
+ * a single-series *horizontal* chart shares the oriented draw/hit path with
276
+ * stacks, so the cap is withheld from its hit rect explicitly (`groups.length >
277
+ * 1`). Vertical charts get it for free — `barSlotRect` takes no cap. Don't
278
+ * "simplify" that guard away: the rule follows from segment disambiguation,
279
+ * which is a property of a stack, not of an axis.
280
+ */
281
+ maxBarWidth?: number;
233
282
  /**
234
283
  * **M4 column decimation** (charts decimator wave). **Omitted ⇒ `true`**: once
235
284
  * the visible bars are denser than ~2 per device pixel (each slot < ~1px), the
@@ -319,6 +368,6 @@ export type BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends Valu
319
368
  * </Layers>
320
369
  * ```
321
370
  */
322
- export declare function BarChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, bins, categories, column, columns, as: semantic, colors, binColors, thresholds, bandColors, orientation, ordinal, id, axis, gap, decimate, legend, index, }: BarChartProps<S, VS>): null;
371
+ export declare function BarChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, bins, categories, column, columns, as: semantic, colors, binColors, thresholds, bandColors, orientation, ordinal, id, axis, gap, maxBarWidth, decimate, legend, index, }: BarChartProps<S, VS>): null;
323
372
  export {};
324
373
  //# sourceMappingURL=BarChart.d.ts.map