@pond-ts/charts 0.41.0 → 0.43.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/CHANGELOG.md +157 -6
- package/dist/BarChart.d.ts +139 -54
- package/dist/BarChart.js +301 -104
- package/dist/CategoryAxis.d.ts +16 -0
- package/dist/CategoryAxis.js +19 -0
- package/dist/ChartContainer.d.ts +99 -6
- package/dist/ChartContainer.js +157 -5
- package/dist/Layers.js +119 -12
- package/dist/ScatterChart.d.ts +15 -3
- package/dist/ScatterChart.js +23 -5
- package/dist/XAxis.js +41 -2
- package/dist/annotations.d.ts +38 -1
- package/dist/annotations.js +68 -25
- package/dist/bandScale.d.ts +57 -0
- package/dist/bandScale.js +67 -0
- package/dist/bars.d.ts +113 -5
- package/dist/bars.js +189 -10
- package/dist/context.d.ts +122 -31
- package/dist/data.d.ts +174 -1
- package/dist/data.js +214 -0
- package/dist/grid.d.ts +14 -0
- package/dist/grid.js +36 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +12 -1
- package/dist/scatter.d.ts +9 -7
- package/dist/scatter.js +12 -8
- package/dist/theme.d.ts +7 -0
- package/dist/theme.js +1 -0
- package/dist/tracker.d.ts +37 -0
- package/dist/tracker.js +77 -6
- package/dist/tradingTimeScale.d.ts +97 -0
- package/dist/tradingTimeScale.js +152 -0
- package/dist/viewport.d.ts +23 -0
- package/dist/viewport.js +51 -0
- package/package.json +3 -3
package/dist/bars.js
CHANGED
|
@@ -82,7 +82,8 @@ export function barRect(cs, i, xScale, yScale, baseline, gapPx, minWidthPx) {
|
|
|
82
82
|
* (inset by `gapPx`) from the resolved `baseline` to the value.
|
|
83
83
|
*
|
|
84
84
|
* A gap (non-finite value) is skipped — no bar, no zero-height sliver. A bar
|
|
85
|
-
* matching the current `selection` (same `
|
|
85
|
+
* matching the current `selection` (same sample `key` **and** the layer's own
|
|
86
|
+
* series `id` — `seriesId`; a no-id layer passes `undefined` and never matches)
|
|
86
87
|
* draws in the style's `highlight` colour **and outlined**, so a click reads back
|
|
87
88
|
* on the canvas; a bar matching `hovered` draws in `highlight` **without** the
|
|
88
89
|
* outline (a lighter "this bar is live" on pointer-over); all others use the flat
|
|
@@ -92,7 +93,7 @@ export function barRect(cs, i, xScale, yScale, baseline, gapPx, minWidthPx) {
|
|
|
92
93
|
* O(N) over the events, one fill (+ optional stroke) per bar, no per-bar
|
|
93
94
|
* allocation beyond the rect tuple.
|
|
94
95
|
*/
|
|
95
|
-
export function drawBars(ctx, cs, xScale, yScale, style, baseline, gapPx,
|
|
96
|
+
export function drawBars(ctx, cs, xScale, yScale, style, baseline, gapPx, seriesId, selection, hovered) {
|
|
96
97
|
ctx.save();
|
|
97
98
|
ctx.globalAlpha = style.opacity;
|
|
98
99
|
for (let i = 0; i < cs.length; i += 1) {
|
|
@@ -100,16 +101,18 @@ export function drawBars(ctx, cs, xScale, yScale, style, baseline, gapPx, label,
|
|
|
100
101
|
if (rect === null)
|
|
101
102
|
continue;
|
|
102
103
|
const [x0, x1, yTop, yBottom] = rect;
|
|
103
|
-
// Match by
|
|
104
|
-
//
|
|
105
|
-
// `
|
|
106
|
-
//
|
|
104
|
+
// Match by the series `id` **and** the sample `key` (begin), so two series
|
|
105
|
+
// sharing a timestamp don't both light up (a no-id, non-selectable layer
|
|
106
|
+
// passes `seriesId === undefined` and never matches). Both the committed
|
|
107
|
+
// selection and the transient hover use the `highlight` fill; only the
|
|
108
|
+
// selection adds the outline, so hover reads as a lighter "this bar is live"
|
|
109
|
+
// and select as the committed pick.
|
|
107
110
|
const selected = selection !== null &&
|
|
108
|
-
selection.
|
|
109
|
-
selection.
|
|
111
|
+
selection.id === seriesId &&
|
|
112
|
+
selection.key === cs.begin[i];
|
|
110
113
|
const isHovered = hovered !== null &&
|
|
111
|
-
hovered.
|
|
112
|
-
hovered.
|
|
114
|
+
hovered.id === seriesId &&
|
|
115
|
+
hovered.key === cs.begin[i];
|
|
113
116
|
ctx.fillStyle = selected || isHovered ? style.highlight : style.fill;
|
|
114
117
|
ctx.fillRect(x0, yTop, x1 - x0, yBottom - yTop);
|
|
115
118
|
if (selected) {
|
|
@@ -168,4 +171,180 @@ export function barAt(cs, px, py, xScale, yScale, baseline, gapPx, minWidthPx) {
|
|
|
168
171
|
}
|
|
169
172
|
return null;
|
|
170
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* The `[min, max]` extent of the **value (stacked) axis**. For a true multi-group
|
|
176
|
+
* stack it is `[0, maxTotal]`, where `maxTotal` is the tallest bin's summed finite
|
|
177
|
+
* non-negative segments. For a **single-group** series (`G === 1` — the plain /
|
|
178
|
+
* categorical bar case) it spans the values' own `[min, max]`, so a **negative**
|
|
179
|
+
* bar's floor is in the domain (segments below the baseline stay visible). `0` is
|
|
180
|
+
* always pulled in so the bars rest on a visible baseline (the bar analog of
|
|
181
|
+
* {@link barExtent}). An empty / all-gap series returns `[0, 1]` so the axis still
|
|
182
|
+
* has a usable domain. Feeds the y auto-fit for a vertical histogram, the x
|
|
183
|
+
* auto-fit for a horizontal one.
|
|
184
|
+
*/
|
|
185
|
+
export function stackValueExtent(ss) {
|
|
186
|
+
const G = ss.groups.length;
|
|
187
|
+
let max = 0;
|
|
188
|
+
let min = 0;
|
|
189
|
+
for (let b = 0; b < ss.length; b += 1) {
|
|
190
|
+
let cum = 0;
|
|
191
|
+
for (let g = 0; g < G; g += 1) {
|
|
192
|
+
const v = ss.values[b * G + g];
|
|
193
|
+
if (!Number.isFinite(v))
|
|
194
|
+
continue;
|
|
195
|
+
if (G === 1) {
|
|
196
|
+
// Single-group: a bar honours its sign, so track both ends.
|
|
197
|
+
if (v > max)
|
|
198
|
+
max = v;
|
|
199
|
+
if (v < min)
|
|
200
|
+
min = v;
|
|
201
|
+
}
|
|
202
|
+
else if (v > 0) {
|
|
203
|
+
cum += v; // True stack: sum the positive segments.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (cum > max)
|
|
207
|
+
max = cum;
|
|
208
|
+
}
|
|
209
|
+
// Empty / all-gap / all-zero → a usable unit domain; otherwise the real extent
|
|
210
|
+
// (with 0 pulled in via the `min`/`max` seeds above).
|
|
211
|
+
if (min === 0 && max === 0)
|
|
212
|
+
return [0, 1];
|
|
213
|
+
return [min, max];
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The `[min, max]` extent of the **bin axis** — the first bin's `begin` to the
|
|
217
|
+
* last bin's `end` (the slots are ascending). `null` for an empty series. Feeds
|
|
218
|
+
* the x auto-fit for a vertical histogram, the y auto-fit for a horizontal one.
|
|
219
|
+
*/
|
|
220
|
+
export function stackBinExtent(ss) {
|
|
221
|
+
if (ss.length === 0)
|
|
222
|
+
return null;
|
|
223
|
+
return [ss.begin[0], ss.end[ss.length - 1]];
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The pixel rect `[x0, x1, yTop, yBottom]` (ascending on both axes) of bin `b`'s
|
|
227
|
+
* segment `g`, stacked so it sits atop `cumBefore` (the summed value of the
|
|
228
|
+
* segments below it, in value units). `null` for a gap (see below). Transposes on
|
|
229
|
+
* `orientation`:
|
|
230
|
+
*
|
|
231
|
+
* - **vertical** — the bin span is horizontal (`barSpanPx` on `xScale`); the
|
|
232
|
+
* segment runs vertically from `yScale(cumBefore)` to `yScale(cumBefore + v)`.
|
|
233
|
+
* - **horizontal** — the bin span is vertical (`barSpanPx` on `yScale`); the
|
|
234
|
+
* segment runs horizontally from `xScale(cumBefore)` to `xScale(cumBefore + v)`.
|
|
235
|
+
*
|
|
236
|
+
* `null` for a **gap** — a non-finite, negative, **or zero** value: none of them
|
|
237
|
+
* draw (a zero segment has no extent), and each contributes nothing to the running
|
|
238
|
+
* total. `minSpanPx` floors the **bin** span (bar thickness); the value direction
|
|
239
|
+
* is unfloored. Shared by {@link drawStacks} and {@link stackAt} so the drawn rect
|
|
240
|
+
* and the hit rect are identical.
|
|
241
|
+
*/
|
|
242
|
+
export function segmentRect(ss, b, g, orientation, xScale, yScale, cumBefore, gapPx, minSpanPx) {
|
|
243
|
+
const G = ss.groups.length;
|
|
244
|
+
const v = ss.values[b * G + g];
|
|
245
|
+
// Skip non-finite (a gap) or zero (a zero-extent rect that can't draw or be
|
|
246
|
+
// hit-tested). A **negative** value is a gap only in a true multi-group stack
|
|
247
|
+
// (`G > 1`) — stacking a negative segment is undefined. A **single-group**
|
|
248
|
+
// series (`G === 1`) is a plain bar: it honours its sign and draws from the
|
|
249
|
+
// baseline *down* to a negative value (the categorical row-read's P&L / delta
|
|
250
|
+
// case), so negatives are kept and the `Math.min/Math.max` below normalizes the
|
|
251
|
+
// below-baseline rect.
|
|
252
|
+
if (!Number.isFinite(v) || v === 0 || (v < 0 && G > 1))
|
|
253
|
+
return null;
|
|
254
|
+
if (orientation === 'vertical') {
|
|
255
|
+
const [x0, x1] = barSpanPx(ss.begin[b], ss.end[b], xScale, gapPx, minSpanPx);
|
|
256
|
+
const yA = yScale(cumBefore);
|
|
257
|
+
const yB = yScale(cumBefore + v);
|
|
258
|
+
return [x0, x1, Math.min(yA, yB), Math.max(yA, yB)];
|
|
259
|
+
}
|
|
260
|
+
const [y0, y1] = barSpanPx(ss.begin[b], ss.end[b], yScale, gapPx, minSpanPx);
|
|
261
|
+
const xA = xScale(cumBefore);
|
|
262
|
+
const xB = xScale(cumBefore + v);
|
|
263
|
+
return [Math.min(xA, xB), Math.max(xA, xB), y0, y1];
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Fill every segment of every bin in `ss`, stacking each bin's groups from the
|
|
267
|
+
* value baseline outward (bottom → top vertical, left → right horizontal). A gap
|
|
268
|
+
* (non-finite, or a negative segment of a true multi-group stack) is skipped and
|
|
269
|
+
* adds nothing to the running total, so the segments above it close the space; a
|
|
270
|
+
* single-group series draws its negative bars below the baseline (see
|
|
271
|
+
* {@link segmentRect}). A segment matching the current
|
|
272
|
+
* `selection` (same series `id`, bin `key` **and** group `label`) draws in its
|
|
273
|
+
* group's `highlight` **and** outlined; one matching `hover` draws in `highlight`
|
|
274
|
+
* without the outline; all others use the flat `fill`. `globalAlpha` carries the
|
|
275
|
+
* shared opacity and is restored.
|
|
276
|
+
*
|
|
277
|
+
* O(N·G) over bins × groups, one fill (+ optional stroke) per drawn segment.
|
|
278
|
+
*/
|
|
279
|
+
export function drawStacks(ctx, ss, orientation, xScale, yScale, style, gapPx, minSpanPx, seriesId, selection, hover) {
|
|
280
|
+
const G = ss.groups.length;
|
|
281
|
+
ctx.save();
|
|
282
|
+
ctx.globalAlpha = style.opacity;
|
|
283
|
+
for (let b = 0; b < ss.length; b += 1) {
|
|
284
|
+
let cum = 0;
|
|
285
|
+
for (let g = 0; g < G; g += 1) {
|
|
286
|
+
const rect = segmentRect(ss, b, g, orientation, xScale, yScale, cum, gapPx, minSpanPx);
|
|
287
|
+
const v = ss.values[b * G + g];
|
|
288
|
+
if (Number.isFinite(v) && v > 0)
|
|
289
|
+
cum += v;
|
|
290
|
+
if (rect === null)
|
|
291
|
+
continue;
|
|
292
|
+
const [x0, x1, yTop, yBottom] = rect;
|
|
293
|
+
// With `marks` (the categorical axis), match on the stable per-bin name so a
|
|
294
|
+
// pinned selection survives a column reorder; otherwise on the sample `key`
|
|
295
|
+
// (begin) + group `label`, as a time / value stack does.
|
|
296
|
+
const stableMark = ss.marks?.[b];
|
|
297
|
+
const matches = (m) => m !== null &&
|
|
298
|
+
m.id === seriesId &&
|
|
299
|
+
(stableMark !== undefined
|
|
300
|
+
? m.mark === stableMark
|
|
301
|
+
: m.key === ss.begin[b] && m.label === ss.groups[g]);
|
|
302
|
+
const selected = matches(selection);
|
|
303
|
+
const isHovered = matches(hover);
|
|
304
|
+
// A hovered / selected segment pops to full opacity in its own colour; a
|
|
305
|
+
// resting one draws at the shared alpha.
|
|
306
|
+
ctx.globalAlpha = selected || isHovered ? 1 : style.opacity;
|
|
307
|
+
// A per-bin colour (the single-series band case) overrides the group fill.
|
|
308
|
+
const fill = style.binFills?.[b] ?? style.fills[g];
|
|
309
|
+
ctx.fillStyle = fill;
|
|
310
|
+
ctx.fillRect(x0, yTop, x1 - x0, yBottom - yTop);
|
|
311
|
+
if (selected) {
|
|
312
|
+
ctx.lineWidth = style.outlineWidth;
|
|
313
|
+
ctx.strokeStyle = fill;
|
|
314
|
+
ctx.strokeRect(x0, yTop, x1 - x0, yBottom - yTop);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
ctx.restore();
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Hit-test plot-pixel `(px, py)` against `ss`'s stacked segments — the **first**
|
|
322
|
+
* segment whose rect contains the point, or `null`. The geometry is
|
|
323
|
+
* {@link segmentRect}, so the hit rect is exactly the drawn rect. The returned
|
|
324
|
+
* tuple is `[bin, group, begin, groupName, value]` for the chart to assemble a
|
|
325
|
+
* `SelectInfo` (it owns the colour). Orientation-agnostic — it reads `(px, py)`,
|
|
326
|
+
* so a horizontal histogram hit-tests the same way a vertical one does.
|
|
327
|
+
*
|
|
328
|
+
* O(N·G) over bins × groups (no spatial index — histogram bin/group counts are
|
|
329
|
+
* small; click / hover are cheap events).
|
|
330
|
+
*/
|
|
331
|
+
export function stackAt(ss, px, py, orientation, xScale, yScale, gapPx, minSpanPx) {
|
|
332
|
+
const G = ss.groups.length;
|
|
333
|
+
for (let b = 0; b < ss.length; b += 1) {
|
|
334
|
+
let cum = 0;
|
|
335
|
+
for (let g = 0; g < G; g += 1) {
|
|
336
|
+
const rect = segmentRect(ss, b, g, orientation, xScale, yScale, cum, gapPx, minSpanPx);
|
|
337
|
+
const v = ss.values[b * G + g];
|
|
338
|
+
if (Number.isFinite(v) && v > 0)
|
|
339
|
+
cum += v;
|
|
340
|
+
if (rect === null)
|
|
341
|
+
continue;
|
|
342
|
+
const [x0, x1, yTop, yBottom] = rect;
|
|
343
|
+
if (px >= x0 && px <= x1 && py >= yTop && py <= yBottom) {
|
|
344
|
+
return [b, g, ss.begin[b], ss.groups[g], v];
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
171
350
|
//# sourceMappingURL=bars.js.map
|
package/dist/context.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ScaleLinear, ScaleTime } from 'd3-scale';
|
|
2
2
|
import type { ChartTheme } from './theme.js';
|
|
3
3
|
import type { AxisFormat } from './format.js';
|
|
4
|
+
import type { Interval, TimeRange } from 'pond-ts';
|
|
5
|
+
import type { TradingTimeScale, DiscontinuityProvider } from './tradingTimeScale.js';
|
|
6
|
+
import type { ScaleBand } from './bandScale.js';
|
|
4
7
|
/**
|
|
5
8
|
* The frame a {@link ChartContainer} provides to its rows and the time axis.
|
|
6
9
|
* The container owns the **shared x geometry**: each side is split into *slots*
|
|
@@ -63,13 +66,45 @@ export interface ContainerFrame {
|
|
|
63
66
|
* (`yScale.invert`). The x always snaps to the data grid either way.
|
|
64
67
|
*/
|
|
65
68
|
readonly crosshairSnap: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* `cursor="region"` buckets — the intervals (from `cursorSequence`) realized
|
|
71
|
+
* over the current view, sorted + non-overlapping. `Layers` finds the one under
|
|
72
|
+
* the pointer and shades it (mapped through `xScale`, so on a trading-time axis
|
|
73
|
+
* the closed part of the bucket collapses). `undefined` when no `cursorSequence`
|
|
74
|
+
* is set.
|
|
75
|
+
*/
|
|
76
|
+
readonly cursorBuckets: readonly Interval[] | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* The `region`-cursor **drag anchor** (epoch ms), or `null` when not dragging.
|
|
79
|
+
* A drag on a region cursor (only when {@link onRegionSelect} is set) records
|
|
80
|
+
* the press time here; the band then spans from the anchor's bucket to the
|
|
81
|
+
* pointer's bucket (extending bucket by bucket). Cleared on release.
|
|
82
|
+
*/
|
|
83
|
+
readonly regionAnchor: number | null;
|
|
84
|
+
/** Set / clear the region-drag anchor (see {@link regionAnchor}). */
|
|
85
|
+
setRegionAnchor(time: number | null): void;
|
|
86
|
+
/**
|
|
87
|
+
* One-shot callback fired when a `region`-cursor **drag** is released, with the
|
|
88
|
+
* selected `[start, end)` `TimeRange` (snapped to the `cursorSequence` buckets).
|
|
89
|
+
* Providing it is what makes the region cursor **draggable**; the cursor does
|
|
90
|
+
* not keep the range (it reverts to the single-bucket highlight). Typical use:
|
|
91
|
+
* zoom the view to the returned range.
|
|
92
|
+
*/
|
|
93
|
+
readonly onRegionSelect: ((range: TimeRange) => void) | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* Require a modifier key held to start a region-drag — set to `'shift'` to make
|
|
96
|
+
* plain drag **pan** and **shift**-drag select, when `panZoom` is on. Only
|
|
97
|
+
* enforced while pan is enabled (with no pan there's no gesture to share, so the
|
|
98
|
+
* modifier is optional). `undefined` ⇒ a region-drag preempts pan.
|
|
99
|
+
*/
|
|
100
|
+
readonly regionSelectModifier: 'shift' | undefined;
|
|
66
101
|
/**
|
|
67
102
|
* The selected mark, or `null`. Shared across rows (single selection). A layer
|
|
68
|
-
* highlights the mark matching
|
|
69
|
-
*
|
|
103
|
+
* highlights the mark matching the selection's series **`id`** and the clicked
|
|
104
|
+
* sample `key` (epoch ms) — the `id` picks the series (so two series sharing a
|
|
105
|
+
* timestamp don't both light up), the `key` picks the mark within it. A
|
|
70
106
|
* controlled `selected` prop pins it; otherwise a click on a selectable layer
|
|
71
|
-
*
|
|
72
|
-
* multi-series Bar/Scatter can target the exact clicked mark.
|
|
107
|
+
* (one with an `id`) sets it.
|
|
73
108
|
*/
|
|
74
109
|
readonly selected: SelectInfo | null;
|
|
75
110
|
/**
|
|
@@ -86,12 +121,13 @@ export interface ContainerFrame {
|
|
|
86
121
|
* from the committed `selected`. A row's pointer-move surface hit-tests its
|
|
87
122
|
* selectable layers and sets it; a layer that supports hover-highlight (Bar)
|
|
88
123
|
* draws the matching mark lit (a lighter treatment than `selected`'s outline).
|
|
89
|
-
* Set-on-change (deduped by key
|
|
90
|
-
* mark transition, not every pointer move.
|
|
124
|
+
* Set-on-change (deduped by the series `id` + sample `key`) so the data canvas
|
|
125
|
+
* repaints only on a mark transition, not every pointer move.
|
|
91
126
|
*/
|
|
92
127
|
readonly hovered: SelectInfo | null;
|
|
93
128
|
/** Set the hovered mark (or `null` to clear) from a pointer-move hit-test;
|
|
94
|
-
* deduped
|
|
129
|
+
* deduped by series `id` + sample `key`, so an unchanged mark is a no-op
|
|
130
|
+
* (no repaint). */
|
|
95
131
|
setHovered(hit: SelectInfo | null): void;
|
|
96
132
|
/** The default in-chart cursor presentation for all rows ({@link CursorMode});
|
|
97
133
|
* a row may override it via its own `cursor`. */
|
|
@@ -116,6 +152,14 @@ export interface ContainerFrame {
|
|
|
116
152
|
*/
|
|
117
153
|
registerTrackerSource(key: symbol, source: TrackerSource): void;
|
|
118
154
|
unregisterTrackerSource(key: symbol): void;
|
|
155
|
+
/**
|
|
156
|
+
* Register this layer as **selectable** — a layer calls this (keyed by its
|
|
157
|
+
* per-instance slot) only when it was given an `id`, so the container knows at
|
|
158
|
+
* least one series can be selected. Powers the dev-warn when `selected` /
|
|
159
|
+
* `onSelect` are wired but no layer carries an `id`. Unregister on unmount.
|
|
160
|
+
*/
|
|
161
|
+
registerSelectable(key: symbol): void;
|
|
162
|
+
unregisterSelectable(key: symbol): void;
|
|
119
163
|
/**
|
|
120
164
|
* Shared x→pixel scale, range `[0, plotWidth]`. A d3 `scaleTime` (default) so
|
|
121
165
|
* ticks land on wall-clock boundaries, or a `scaleLinear` when the data is
|
|
@@ -124,16 +168,28 @@ export interface ContainerFrame {
|
|
|
124
168
|
* scales are callable
|
|
125
169
|
* (`value → px`) and expose `invert`/`ticks`/`tickFormat`; consumers use only
|
|
126
170
|
* that shared surface (the cursor coerces `invert` via `+`, `<TimeAxis>` keys
|
|
127
|
-
* ticks via `+d`), so either kind drops in.
|
|
171
|
+
* ticks via `+d`), so either kind drops in. A **`scaleTradingTime`** (when the
|
|
172
|
+
* container is given `discontinuities`) is the third kind — same callable /
|
|
173
|
+
* `invert` / `ticks` / `tickFormat` surface, but the mapping runs through
|
|
174
|
+
* trading time so closed-market gaps collapse (see {@link discontinuities}).
|
|
128
175
|
*/
|
|
129
|
-
readonly xScale: ScaleTime<number, number> | ScaleLinear<number, number
|
|
176
|
+
readonly xScale: ScaleTime<number, number> | ScaleLinear<number, number> | TradingTimeScale | ScaleBand;
|
|
130
177
|
/**
|
|
131
|
-
* The
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* and
|
|
178
|
+
* The discontinuity provider backing a **trading-time** x axis, if one was
|
|
179
|
+
* supplied to the container — closed-market time (weekends, holidays,
|
|
180
|
+
* overnight, lunch breaks) collapsed. `undefined` for a normal continuous
|
|
181
|
+
* time / value axis. Pan and zoom read it to move the view in *trading* time
|
|
182
|
+
* rather than raw wall-clock ms.
|
|
135
183
|
*/
|
|
136
|
-
readonly
|
|
184
|
+
readonly discontinuities?: DiscontinuityProvider | undefined;
|
|
185
|
+
/**
|
|
186
|
+
* The resolved kind of the shared x scale — `'time'` (a `scaleTime`),
|
|
187
|
+
* `'value'` (a `scaleLinear`), or `'category'` (a {@link ScaleBand}: an ordinal
|
|
188
|
+
* column-domain axis, one slot per category). Inferred from the layers' data.
|
|
189
|
+
* `<XAxis>` reads it to pick its default tick formatter (time / number / the
|
|
190
|
+
* category label), and the cursor readout to format the x position.
|
|
191
|
+
*/
|
|
192
|
+
readonly xKind: 'time' | 'value' | 'category';
|
|
137
193
|
/** Pan/zoom enabled — the plot drag-pans and wheel-zooms the shared time range. */
|
|
138
194
|
readonly panZoom: boolean;
|
|
139
195
|
/** Minimum visible duration (ms) — the zoom-in floor. */
|
|
@@ -293,17 +349,26 @@ export interface RowLayer {
|
|
|
293
349
|
yExtent(): [number, number] | null;
|
|
294
350
|
/**
|
|
295
351
|
* The **kind of x axis** this layer's data lives on — `'time'` for a
|
|
296
|
-
* `TimeSeries`, `'value'` for a `ValueSeries
|
|
297
|
-
*
|
|
298
|
-
*
|
|
352
|
+
* `TimeSeries`, `'value'` for a `ValueSeries`, `'category'` for a categorical
|
|
353
|
+
* (ordinal column-domain) layer. The container infers the one shared x scale
|
|
354
|
+
* from its layers (all must agree — a mix is an error), so the axis kind never
|
|
355
|
+
* needs declaring. See {@link ContainerFrame.xScale}.
|
|
299
356
|
*/
|
|
300
|
-
readonly xKind: 'time' | 'value';
|
|
357
|
+
readonly xKind: 'time' | 'value' | 'category';
|
|
301
358
|
/**
|
|
302
359
|
* This layer's `[min, max]` along the **x** axis (the key / value-axis extent),
|
|
303
360
|
* or `null` if empty. The container unions these to auto-fit the shared x
|
|
304
|
-
* domain when no explicit `range` is given.
|
|
361
|
+
* domain when no explicit `range` is given. For a `'category'` layer this is
|
|
362
|
+
* the slot extent `[0, n]` (n = category count).
|
|
305
363
|
*/
|
|
306
364
|
xExtent(): readonly [number, number] | null;
|
|
365
|
+
/**
|
|
366
|
+
* A `'category'` layer's ordered category names (the ordinal axis domain the
|
|
367
|
+
* container builds a {@link ScaleBand} + label formatter from). `undefined` /
|
|
368
|
+
* absent for a `'time'` or `'value'` layer. Category layers in one container
|
|
369
|
+
* must agree on this list (a mix is an error), the same way {@link xKind} must.
|
|
370
|
+
*/
|
|
371
|
+
xCategories?(): readonly string[] | null;
|
|
307
372
|
/**
|
|
308
373
|
* The layer's value(s) at `time` — the nearest sample — for the scrub tracker:
|
|
309
374
|
* one for a line, two (lower/upper) for a band, empty at a gap. Each carries
|
|
@@ -324,9 +389,12 @@ export interface RowLayer {
|
|
|
324
389
|
/**
|
|
325
390
|
* Hit-test plot-pixel `(px, py)` against this layer's marks for click
|
|
326
391
|
* selection — the select-analog of {@link sampleAt}. Returns the hit mark or
|
|
327
|
-
* `null`. **Optional
|
|
328
|
-
*
|
|
329
|
-
*
|
|
392
|
+
* `null`. **Optional, and gated on the layer's `id`:** a layer only wires
|
|
393
|
+
* `hitTest` when it was given an `id` (the series identity). Layers without an
|
|
394
|
+
* `id` — or without discrete selectable marks (line, band, area) — omit it,
|
|
395
|
+
* so they render + read out but never select/hover (a click on them resolves
|
|
396
|
+
* to empty space ⇒ deselect). `xScale`/`yScale` map data→pixels (the row
|
|
397
|
+
* resolves the layer's axis scale, as for `draw`).
|
|
330
398
|
*/
|
|
331
399
|
hitTest?(px: number, py: number, xScale: (value: number) => number, yScale: (value: number) => number): SelectInfo | null;
|
|
332
400
|
/** Draw into the plot canvas. `xScale`/`yScale` map data→pixels. */
|
|
@@ -370,24 +438,47 @@ export interface CursorFlag {
|
|
|
370
438
|
*/
|
|
371
439
|
export interface TrackerSource {
|
|
372
440
|
sampleAt(time: number): readonly TrackerSample[];
|
|
373
|
-
readonly xKind: 'time' | 'value';
|
|
441
|
+
readonly xKind: 'time' | 'value' | 'category';
|
|
374
442
|
xExtent(): readonly [number, number] | null;
|
|
443
|
+
/** A `'category'` source's ordered category names (see {@link RowLayer.xCategories}). */
|
|
444
|
+
xCategories?(): readonly string[] | null;
|
|
375
445
|
}
|
|
376
446
|
/**
|
|
377
|
-
* One
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
447
|
+
* One selection — what {@link RowLayer.hitTest} returns and `onSelect` reports.
|
|
448
|
+
* Selection identity is the **series `id`**, not the sample: `key`/`value` are
|
|
449
|
+
* click **provenance** (the nearest sample under the pointer, informational);
|
|
450
|
+
* equality, dedup, and the controlled echo all key on `id`. Because `id` is a
|
|
451
|
+
* stable series identity — distinct from the `as` theme role, which can repeat —
|
|
452
|
+
* a selection survives a streaming data update where a sample `key` would go
|
|
453
|
+
* stale. Only layers that carry an `id` are selectable (see {@link RowLayer.hitTest}).
|
|
381
454
|
*/
|
|
382
455
|
export interface SelectInfo {
|
|
383
|
-
/**
|
|
456
|
+
/**
|
|
457
|
+
* The **series identity** — the layer's `id` prop. The selection / dedup /
|
|
458
|
+
* controlled-echo key; stable across data updates (unlike {@link key}).
|
|
459
|
+
*/
|
|
460
|
+
readonly id: string;
|
|
461
|
+
/**
|
|
462
|
+
* The clicked sample's key as epoch ms (its event's `begin`) — click
|
|
463
|
+
* **provenance**, informational. NOT the selection identity (that is {@link id}).
|
|
464
|
+
*/
|
|
384
465
|
readonly key: number;
|
|
385
|
-
/** The
|
|
466
|
+
/** The clicked sample's value (the plotted column) — provenance. */
|
|
386
467
|
readonly value: number;
|
|
387
468
|
/** The mark's resolved style colour. */
|
|
388
469
|
readonly color: string;
|
|
389
|
-
/**
|
|
470
|
+
/** Display label (`as` ?? column ?? id) — labels the selection in a readout. */
|
|
390
471
|
readonly label: string;
|
|
472
|
+
/**
|
|
473
|
+
* An optional **stable per-mark identity within the layer** — a *category's
|
|
474
|
+
* column name* on the categorical axis, where every bar shares the layer's
|
|
475
|
+
* `id` but each column needs its own stable handle. When present, the
|
|
476
|
+
* highlight match + controlled `selected` echo key on `(id, mark)` instead of
|
|
477
|
+
* the sample `key`, so a pinned selection survives a column reorder / data
|
|
478
|
+
* update (the slot index is not stable; the column name is). `undefined` for
|
|
479
|
+
* marks whose sample `key` is already their identity (a time / value bar).
|
|
480
|
+
*/
|
|
481
|
+
readonly mark?: string;
|
|
391
482
|
}
|
|
392
483
|
/** The hover snapshot handed to `onTrackerChanged` — the cursor time + every
|
|
393
484
|
* series' value there, so a consumer can render the readout outside the chart. */
|
|
@@ -411,7 +502,7 @@ export interface TrackerInfo {
|
|
|
411
502
|
* time pinned to the x-axis. The ChartIQ / trading-terminal readout. Values
|
|
412
503
|
* snap to the series (the axis pills read like ticks), not the raw mouse Y.
|
|
413
504
|
*/
|
|
414
|
-
export type CursorMode = 'none' | 'line' | 'point' | 'inline' | 'flag' | 'crosshair';
|
|
505
|
+
export type CursorMode = 'none' | 'line' | 'point' | 'inline' | 'flag' | 'crosshair' | 'region';
|
|
415
506
|
/** A registered layer plus the axis id it draws against. */
|
|
416
507
|
export interface LayerEntry {
|
|
417
508
|
readonly layer: RowLayer;
|
package/dist/data.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ValueSeries } from 'pond-ts';
|
|
2
|
+
import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
2
3
|
/**
|
|
3
4
|
* A chart-ready columnar view of a series: parallel typed arrays for the time
|
|
4
5
|
* (x) and value (y) axes, plus the logical row count.
|
|
@@ -96,6 +97,40 @@ export interface BarSeries {
|
|
|
96
97
|
readonly y: Float64Array;
|
|
97
98
|
readonly length: number;
|
|
98
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* A chart-ready view of a **stacked / histogram** bar series — the multi-segment
|
|
102
|
+
* generalization of {@link BarSeries}. Each of the `length` bins spans
|
|
103
|
+
* `[begin[i], end[i]]` on the **bin axis** (time ms, a value, or a band edge) and
|
|
104
|
+
* carries one value per `group` (a stack segment). `groups` lists the segment
|
|
105
|
+
* identities **bottom → top**; `values` is a flat `length × groups.length` grid
|
|
106
|
+
* in **row-major** order, so bin `b`'s segment `g` is `values[b * groups.length + g]`.
|
|
107
|
+
*
|
|
108
|
+
* A single-series bar (the {@link BarSeries} case) is just `groups.length === 1`.
|
|
109
|
+
* Missing / non-finite segment values are `NaN` — the gap signal a stack skips
|
|
110
|
+
* (no segment, and it contributes nothing to the running total), the same
|
|
111
|
+
* `Number.isFinite` contract as {@link BarSeries}. Segment values are assumed
|
|
112
|
+
* **non-negative** (counts / durations); a negative value is treated as a gap
|
|
113
|
+
* (diverging stacks are out of scope — see the histogram guide).
|
|
114
|
+
*
|
|
115
|
+
* The bin axis is x for a **vertical** histogram (bars grow up) and y for a
|
|
116
|
+
* **horizontal** one (bars grow right); the same grid drives both — the draw
|
|
117
|
+
* layer transposes by orientation, the data does not change.
|
|
118
|
+
*/
|
|
119
|
+
export interface StackedBarSeries {
|
|
120
|
+
readonly begin: Float64Array;
|
|
121
|
+
readonly end: Float64Array;
|
|
122
|
+
readonly groups: readonly string[];
|
|
123
|
+
readonly values: Float64Array;
|
|
124
|
+
readonly length: number;
|
|
125
|
+
/**
|
|
126
|
+
* Optional **stable per-bin identity** — `marks[b]` names bin `b` (a category's
|
|
127
|
+
* column name on the categorical axis). When present, the draw / hit-test /
|
|
128
|
+
* selection key on this name instead of the bin's `begin` slot index, so a
|
|
129
|
+
* pinned selection survives a column reorder (the slot index is not stable).
|
|
130
|
+
* `undefined` for a time / value series whose `begin` is already stable.
|
|
131
|
+
*/
|
|
132
|
+
readonly marks?: readonly string[];
|
|
133
|
+
}
|
|
99
134
|
/** The five quantile column names a {@link boxFromTimeSeries} reads, in order. */
|
|
100
135
|
export interface BoxColumns {
|
|
101
136
|
/** Lower whisker end (e.g. `p5` / `min`). */
|
|
@@ -242,4 +277,142 @@ export declare function barsFromTimeSeries<S extends SeriesSchema>(series: TimeS
|
|
|
242
277
|
* @throws TypeError if `column` is not a numeric column.
|
|
243
278
|
*/
|
|
244
279
|
export declare function barsFromValueSeries<VS extends ValueSeriesSchema>(series: ValueSeries<VS>, column: string): BarSeries;
|
|
280
|
+
/**
|
|
281
|
+
* Build a {@link StackedBarSeries} from a **`Map` of grouped series** — one series
|
|
282
|
+
* per stack group. This is the natural reader for pond's grouped-aggregate output:
|
|
283
|
+
* `series.partitionBy('host', { groups }).aggregate(Sequence.every('5m'), { n: 'count' }).toMap()`
|
|
284
|
+
* yields a `Map<host, TimeSeries>`, one interval-keyed series per host. The stack
|
|
285
|
+
* order (`groups`, bottom → top) is the map's **insertion order** (stable when you
|
|
286
|
+
* pass `partitionBy`'s `{ groups }` option).
|
|
287
|
+
*
|
|
288
|
+
* **Aligned by bucket key, not by index.** Each partition's `aggregate` spans only
|
|
289
|
+
* *its own* events' range, so the groups generally have **different** grids (host A
|
|
290
|
+
* might have buckets 0–8, host B buckets 3–9). This reader takes the **union** of
|
|
291
|
+
* every group's `[begin, end)` slots (ascending) and places each group's `column`
|
|
292
|
+
* value at the matching `begin`; a bucket a group is missing reads as a gap
|
|
293
|
+
* (`NaN`, contributing nothing to that stack). So the segments always line up on
|
|
294
|
+
* the real bucket, never on a positional accident. (Pass `aggregate`'s
|
|
295
|
+
* `{ range }` option if you want every group padded to one dense grid — the union
|
|
296
|
+
* is then that grid.) When two groups carry the **same `begin`**, the first
|
|
297
|
+
* group's `end` sets that slot's width — correct for the uniform-width buckets
|
|
298
|
+
* `aggregate` / `pivotByGroup` produce (all groups share the grid width), which is
|
|
299
|
+
* the intended input.
|
|
300
|
+
*
|
|
301
|
+
* @throws Error if `groups` is empty.
|
|
302
|
+
* @throws RangeError / TypeError (via {@link readNumericColumn}) if `column` is
|
|
303
|
+
* missing or non-numeric in any member.
|
|
304
|
+
*/
|
|
305
|
+
export declare function stacksFromGroups<S extends SeriesSchema>(groups: ReadonlyMap<string, TimeSeries<S>>, column: string): StackedBarSeries;
|
|
306
|
+
/**
|
|
307
|
+
* Build a {@link StackedBarSeries} from a **wide** series — one numeric column
|
|
308
|
+
* per stack group. This is the reader for pond's `pivotByGroup` output (long →
|
|
309
|
+
* wide reshape: each group value becomes its own column), or any series that is
|
|
310
|
+
* already wide (e.g. `in` / `out` traffic). `columns` names the segment columns
|
|
311
|
+
* **bottom → top**; a `ValueSeries` bins on its value axis (neighbour-spaced
|
|
312
|
+
* slots), a `TimeSeries` on its key (interval spans or neighbour-spaced points).
|
|
313
|
+
*
|
|
314
|
+
* @throws RangeError / TypeError if any column is missing or non-numeric.
|
|
315
|
+
*/
|
|
316
|
+
export declare function stacksFromColumns<S extends SeriesSchema, VS extends ValueSeriesSchema>(series: TimeSeries<S> | ValueSeries<VS>, columns: readonly string[]): StackedBarSeries;
|
|
317
|
+
/**
|
|
318
|
+
* A single bin record from `byColumn` — its `[start, end)` range plus the mapped
|
|
319
|
+
* aggregate columns (read by name via {@link stacksFromBins}). Deliberately just
|
|
320
|
+
* the `start`/`end` shape (no index signature) so pond's
|
|
321
|
+
* `byColumn(...): Array<{ start, end } & ReduceResult>` assigns to it structurally
|
|
322
|
+
* — the aggregate fields ride along and are read out by the reader.
|
|
323
|
+
*/
|
|
324
|
+
export type BinRecord = {
|
|
325
|
+
readonly start: number;
|
|
326
|
+
readonly end: number;
|
|
327
|
+
};
|
|
328
|
+
/** Options for {@link stacksFromBins}. */
|
|
329
|
+
export interface StacksFromBinsOptions {
|
|
330
|
+
/**
|
|
331
|
+
* Use uniform **unit slots** (`[i, i+1]`) for the bins instead of their numeric
|
|
332
|
+
* `[start, end]` edges — an **ordinal** band axis (heart-rate zones, Coggan
|
|
333
|
+
* power zones) where every band reads the same width regardless of its numeric
|
|
334
|
+
* span. The caller labels the slots via `<YAxis ticks>` at `i + 0.5`. Omitted /
|
|
335
|
+
* `false` ⇒ real numeric edges (a true value axis — power W, risk %).
|
|
336
|
+
*/
|
|
337
|
+
readonly ordinal?: boolean;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Build a {@link StackedBarSeries} from **`byColumn` bin records** — the array of
|
|
341
|
+
* `{ start, end, …aggregates }` a value-band aggregation returns
|
|
342
|
+
* (`series.byColumn('power', { width: 20 }, { seconds: { from: 'dt', using: 'sum' } })`).
|
|
343
|
+
* `columns` names the aggregate field(s) to draw as segments (`['seconds']` for a
|
|
344
|
+
* plain distribution; several for a stacked value-band histogram).
|
|
345
|
+
*
|
|
346
|
+
* By default each bin keeps its real numeric `[start, end]` edges — a true value
|
|
347
|
+
* axis (power W, risk %). Pass `{ ordinal: true }` for uniform unit slots
|
|
348
|
+
* (`[i, i+1]`) when the bins are **categories** whose numeric width shouldn't
|
|
349
|
+
* distort the layout (heart-rate zones); label them with `<YAxis ticks>`.
|
|
350
|
+
*
|
|
351
|
+
* A missing / non-finite aggregate reads as a gap (`NaN`).
|
|
352
|
+
*/
|
|
353
|
+
export declare function stacksFromBins(bins: readonly BinRecord[], columns: readonly string[], options?: StacksFromBinsOptions): StackedBarSeries;
|
|
354
|
+
/**
|
|
355
|
+
* One category's `{ label, value }` for a categorical bar chart — the row-read /
|
|
356
|
+
* transpose view's `(columnName, cell)` pair (categorical-axis RFC, Phase 1). An
|
|
357
|
+
* ordered list of these is the explicit categorical data source; Phase 2's
|
|
358
|
+
* transpose reader produces the same list from a wide series' row.
|
|
359
|
+
*
|
|
360
|
+
* **Labels are the stable identity** — the axis maps each `label` to a slot and
|
|
361
|
+
* selection/highlight key on it (so a pick survives a reorder). They must be
|
|
362
|
+
* **unique** within the list: two categories sharing a label collapse to one axis
|
|
363
|
+
* tick and both highlight together on a pick. The transpose reader satisfies this
|
|
364
|
+
* for free (a series' column names are unique); only a hand-built list can break
|
|
365
|
+
* it. `value` may be **negative** — a single-series category bar draws it below
|
|
366
|
+
* the baseline (the P&L / delta case).
|
|
367
|
+
*/
|
|
368
|
+
export interface CategoryDatum {
|
|
369
|
+
readonly label: string;
|
|
370
|
+
readonly value: number;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Build a {@link StackedBarSeries} (single group, `G === 1`) from an ordered list
|
|
374
|
+
* of `{ label, value }` categories — one **unit slot** `[i, i+1]` per category, in
|
|
375
|
+
* order. This is the categorical row-read's geometry: the slots are ordinal
|
|
376
|
+
* indices (the bar's pixel span comes from the container's {@link ScaleBand}), and
|
|
377
|
+
* the `label`s become the axis's ordered category names (`xCategories`). A
|
|
378
|
+
* non-finite value reads as a gap (`NaN`). Reuses the shipped stacked geometry —
|
|
379
|
+
* no new draw path.
|
|
380
|
+
*/
|
|
381
|
+
export declare function categoryStack(records: readonly CategoryDatum[]): StackedBarSeries;
|
|
382
|
+
/** Which row {@link transposeRow} reads across. */
|
|
383
|
+
export type RowAt = 'first' | 'last' | number | {
|
|
384
|
+
readonly time: number;
|
|
385
|
+
};
|
|
386
|
+
/** Options for {@link transposeRow}. */
|
|
387
|
+
export interface TransposeRowOptions {
|
|
388
|
+
/**
|
|
389
|
+
* Which row to read across. **Default `'last'`** — the head / latest row (the
|
|
390
|
+
* live snapshot). `'first'`, an **index** (negative from the end), or
|
|
391
|
+
* `{ time }` for the row nearest a key.
|
|
392
|
+
*/
|
|
393
|
+
readonly at?: RowAt;
|
|
394
|
+
/**
|
|
395
|
+
* The columns to lay on the axis, **in order** — a declared / bounded set (a
|
|
396
|
+
* watchlist, or a top-N computed upstream; the RFC §7 "bound in the data layer"
|
|
397
|
+
* stance). Omit to use **every numeric value column** of the series, in schema
|
|
398
|
+
* order. A named column that's missing / non-numeric in the row reads as a gap.
|
|
399
|
+
*/
|
|
400
|
+
readonly columns?: readonly string[];
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* **The transpose reader** (categorical-axis RFC, Phase 1 PR2): read **one row**
|
|
404
|
+
* of a wide `TimeSeries` **across** — its columns become the categories, that
|
|
405
|
+
* row's cells the values — for `<BarChart categories={…}>`. This is "columns on
|
|
406
|
+
* x": the schema's numeric columns (a `pivotByGroup` output's per-group columns,
|
|
407
|
+
* a vol term structure's per-expiry columns, …) laid out at one instant.
|
|
408
|
+
*
|
|
409
|
+
* The row is picked the ordinary way (`options.at`, default the **head row**):
|
|
410
|
+
* `series.last()` / `.first()` / `.at(index)` / `.nearest(time)`. So "which row"
|
|
411
|
+
* is just row selection — the live snapshot is the head row; a static report
|
|
412
|
+
* pins a row by index or time. (Binding the row to a scrubbing time cursor is a
|
|
413
|
+
* later phase.) Pass `options.columns` to bound / order the category set; omit it
|
|
414
|
+
* to take every numeric value column. An empty series (or a row past the ends)
|
|
415
|
+
* yields `[]`; a missing / non-numeric cell reads as a gap (`NaN`).
|
|
416
|
+
*/
|
|
417
|
+
export declare function transposeRow<S extends SeriesSchema>(series: TimeSeries<S>, options?: TransposeRowOptions): CategoryDatum[];
|
|
245
418
|
//# sourceMappingURL=data.d.ts.map
|