@pond-ts/charts 0.42.0 → 0.44.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 +164 -1
- package/dist/BarChart.d.ts +24 -2
- package/dist/BarChart.js +106 -23
- package/dist/BoxPlot.d.ts +80 -28
- package/dist/BoxPlot.js +67 -40
- package/dist/CategoryAxis.d.ts +16 -0
- package/dist/CategoryAxis.js +19 -0
- package/dist/ChartContainer.d.ts +56 -1
- package/dist/ChartContainer.js +101 -3
- package/dist/Layers.js +77 -5
- package/dist/ScatterChart.d.ts +33 -6
- package/dist/ScatterChart.js +36 -16
- 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 +23 -6
- package/dist/bars.js +47 -15
- package/dist/box.d.ts +26 -9
- package/dist/box.js +86 -42
- package/dist/context.d.ts +88 -13
- package/dist/data.d.ts +145 -30
- package/dist/data.js +146 -21
- package/dist/index.d.ts +5 -2
- package/dist/index.js +8 -1
- package/dist/scatter.d.ts +2 -2
- package/dist/scatter.js +8 -5
- package/dist/tracker.d.ts +37 -0
- package/dist/tracker.js +77 -6
- package/package.json +3 -3
package/dist/bars.js
CHANGED
|
@@ -172,9 +172,12 @@ export function barAt(cs, px, py, xScale, yScale, baseline, gapPx, minWidthPx) {
|
|
|
172
172
|
return null;
|
|
173
173
|
}
|
|
174
174
|
/**
|
|
175
|
-
* The `[min, max]` extent of the **value (stacked) axis
|
|
176
|
-
* where `maxTotal` is the tallest bin's summed finite
|
|
177
|
-
*
|
|
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
|
|
178
181
|
* {@link barExtent}). An empty / all-gap series returns `[0, 1]` so the axis still
|
|
179
182
|
* has a usable domain. Feeds the y auto-fit for a vertical histogram, the x
|
|
180
183
|
* auto-fit for a horizontal one.
|
|
@@ -182,17 +185,32 @@ export function barAt(cs, px, py, xScale, yScale, baseline, gapPx, minWidthPx) {
|
|
|
182
185
|
export function stackValueExtent(ss) {
|
|
183
186
|
const G = ss.groups.length;
|
|
184
187
|
let max = 0;
|
|
188
|
+
let min = 0;
|
|
185
189
|
for (let b = 0; b < ss.length; b += 1) {
|
|
186
190
|
let cum = 0;
|
|
187
191
|
for (let g = 0; g < G; g += 1) {
|
|
188
192
|
const v = ss.values[b * G + g];
|
|
189
|
-
if (Number.isFinite(v)
|
|
190
|
-
|
|
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
|
+
}
|
|
191
205
|
}
|
|
192
206
|
if (cum > max)
|
|
193
207
|
max = cum;
|
|
194
208
|
}
|
|
195
|
-
|
|
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];
|
|
196
214
|
}
|
|
197
215
|
/**
|
|
198
216
|
* The `[min, max]` extent of the **bin axis** — the first bin's `begin` to the
|
|
@@ -224,9 +242,14 @@ export function stackBinExtent(ss) {
|
|
|
224
242
|
export function segmentRect(ss, b, g, orientation, xScale, yScale, cumBefore, gapPx, minSpanPx) {
|
|
225
243
|
const G = ss.groups.length;
|
|
226
244
|
const v = ss.values[b * G + g];
|
|
227
|
-
// Skip non-finite
|
|
228
|
-
//
|
|
229
|
-
|
|
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))
|
|
230
253
|
return null;
|
|
231
254
|
if (orientation === 'vertical') {
|
|
232
255
|
const [x0, x1] = barSpanPx(ss.begin[b], ss.end[b], xScale, gapPx, minSpanPx);
|
|
@@ -242,8 +265,10 @@ export function segmentRect(ss, b, g, orientation, xScale, yScale, cumBefore, ga
|
|
|
242
265
|
/**
|
|
243
266
|
* Fill every segment of every bin in `ss`, stacking each bin's groups from the
|
|
244
267
|
* value baseline outward (bottom → top vertical, left → right horizontal). A gap
|
|
245
|
-
* (non-finite
|
|
246
|
-
* total, so the segments above it close the space
|
|
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
|
|
247
272
|
* `selection` (same series `id`, bin `key` **and** group `label`) draws in its
|
|
248
273
|
* group's `highlight` **and** outlined; one matching `hover` draws in `highlight`
|
|
249
274
|
* without the outline; all others use the flat `fill`. `globalAlpha` carries the
|
|
@@ -265,20 +290,27 @@ export function drawStacks(ctx, ss, orientation, xScale, yScale, style, gapPx, m
|
|
|
265
290
|
if (rect === null)
|
|
266
291
|
continue;
|
|
267
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];
|
|
268
297
|
const matches = (m) => m !== null &&
|
|
269
298
|
m.id === seriesId &&
|
|
270
|
-
|
|
271
|
-
|
|
299
|
+
(stableMark !== undefined
|
|
300
|
+
? m.mark === stableMark
|
|
301
|
+
: m.key === ss.begin[b] && m.label === ss.groups[g]);
|
|
272
302
|
const selected = matches(selection);
|
|
273
303
|
const isHovered = matches(hover);
|
|
274
304
|
// A hovered / selected segment pops to full opacity in its own colour; a
|
|
275
305
|
// resting one draws at the shared alpha.
|
|
276
306
|
ctx.globalAlpha = selected || isHovered ? 1 : style.opacity;
|
|
277
|
-
|
|
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;
|
|
278
310
|
ctx.fillRect(x0, yTop, x1 - x0, yBottom - yTop);
|
|
279
311
|
if (selected) {
|
|
280
312
|
ctx.lineWidth = style.outlineWidth;
|
|
281
|
-
ctx.strokeStyle =
|
|
313
|
+
ctx.strokeStyle = fill;
|
|
282
314
|
ctx.strokeRect(x0, yTop, x1 - x0, yBottom - yTop);
|
|
283
315
|
}
|
|
284
316
|
}
|
package/dist/box.d.ts
CHANGED
|
@@ -3,9 +3,10 @@ import type { Scale } from './line.js';
|
|
|
3
3
|
import type { BoxStyle } from './theme.js';
|
|
4
4
|
/**
|
|
5
5
|
* The `[min, max]` vertical extent of the **drawn** boxes — the lowest `lower`
|
|
6
|
-
* whisker and highest `upper` whisker over keys
|
|
7
|
-
*
|
|
8
|
-
* matching what {@link drawBox}
|
|
6
|
+
* whisker and highest `upper` whisker over the keys {@link isFiniteBox} draws
|
|
7
|
+
* (a full box needs all five quantiles; a range-only box just `lower`/`upper`) —
|
|
8
|
+
* or `null` if none are. Gap keys are excluded, matching what {@link drawBox}
|
|
9
|
+
* draws, so they don't drag the y-domain.
|
|
9
10
|
*
|
|
10
11
|
* Only `lower`/`upper` bound the extent: they are the outermost reach of a key
|
|
11
12
|
* (the whisker ends), so `q1`/`median`/`q3` lie within `[lower, upper]` for any
|
|
@@ -44,16 +45,32 @@ export type BoxShape = 'whisker' | 'solid' | 'none';
|
|
|
44
45
|
* reads darker on a light ground, brighter on a dark one), no stems/outline.
|
|
45
46
|
* - **`none`** — the `q1→q3` box fill + outline only, no spread marks.
|
|
46
47
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
48
|
+
* **Range-only** (`box.hasBox === false` — no `q1`/`q3`): there's no body, so
|
|
49
|
+
* `whisker` draws **one** full `lower→upper` stem with caps, `solid` draws just
|
|
50
|
+
* the outer bar, and `none` draws **nothing** (no body + no spread ⇒ empty — pick
|
|
51
|
+
* `whisker`/`solid` for a range-only box). `showMedian` is a no-op when the box
|
|
52
|
+
* carries no `median` (`hasMedian === false`).
|
|
49
53
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
54
|
+
* Then, if `showMedian` (and a median is present), the median line on top. Fills
|
|
55
|
+
* are bracketed by `save`/`restore` so their `globalAlpha` doesn't leak.
|
|
56
|
+
* `offsetPx` shifts every mark in pixel space (for pairing same-key marks);
|
|
57
|
+
* `capWidthPx` sets a fixed whisker-cap width (else half the box width — a small
|
|
58
|
+
* fixed cap keeps paired offset marks' T-bars from overlapping), clamped to the
|
|
59
|
+
* box width.
|
|
60
|
+
*
|
|
61
|
+
* **Gap-aware**: a key whose present quantiles aren't all finite is skipped
|
|
62
|
+
* entirely (no partial box) — the same contract as a band gap.
|
|
52
63
|
*
|
|
53
64
|
* O(N) over the keys, a fixed number of path ops each — no per-key allocation
|
|
54
65
|
* beyond the `barSpanPx` tuple.
|
|
55
66
|
*/
|
|
56
|
-
export declare function drawBox(ctx: CanvasRenderingContext2D, box: BoxSeries, xScale: Scale, yScale: Scale, style: BoxStyle, gapPx?: number, minWidthPx?: number, shape?: BoxShape, showMedian?: boolean): void;
|
|
57
|
-
/**
|
|
67
|
+
export declare function drawBox(ctx: CanvasRenderingContext2D, box: BoxSeries, xScale: Scale, yScale: Scale, style: BoxStyle, gapPx?: number, minWidthPx?: number, shape?: BoxShape, showMedian?: boolean, offsetPx?: number, capWidthPx?: number): void;
|
|
68
|
+
/**
|
|
69
|
+
* This key is drawable — the quantiles it actually carries are all finite at `i`.
|
|
70
|
+
* `lower`/`upper` (the whisker reach) are always required; `q1`/`q3` only when the
|
|
71
|
+
* box has a body (`hasBox !== false`), `median` only when it has a centre line
|
|
72
|
+
* (`hasMedian !== false`). So a **range-only** box (bid→ask, no body/median) draws
|
|
73
|
+
* wherever `lower`/`upper` are finite, and a full box still needs all five.
|
|
74
|
+
*/
|
|
58
75
|
export declare function isFiniteBox(box: BoxSeries, i: number): boolean;
|
|
59
76
|
//# sourceMappingURL=box.d.ts.map
|
package/dist/box.js
CHANGED
|
@@ -3,9 +3,10 @@ import { barSpanPx } from './range.js';
|
|
|
3
3
|
const WHISKER_CAP_FRACTION = 0.5;
|
|
4
4
|
/**
|
|
5
5
|
* The `[min, max]` vertical extent of the **drawn** boxes — the lowest `lower`
|
|
6
|
-
* whisker and highest `upper` whisker over keys
|
|
7
|
-
*
|
|
8
|
-
* matching what {@link drawBox}
|
|
6
|
+
* whisker and highest `upper` whisker over the keys {@link isFiniteBox} draws
|
|
7
|
+
* (a full box needs all five quantiles; a range-only box just `lower`/`upper`) —
|
|
8
|
+
* or `null` if none are. Gap keys are excluded, matching what {@link drawBox}
|
|
9
|
+
* draws, so they don't drag the y-domain.
|
|
9
10
|
*
|
|
10
11
|
* Only `lower`/`upper` bound the extent: they are the outermost reach of a key
|
|
11
12
|
* (the whisker ends), so `q1`/`median`/`q3` lie within `[lower, upper]` for any
|
|
@@ -56,70 +57,101 @@ export function boxIndexAtTime(box, time) {
|
|
|
56
57
|
* reads darker on a light ground, brighter on a dark one), no stems/outline.
|
|
57
58
|
* - **`none`** — the `q1→q3` box fill + outline only, no spread marks.
|
|
58
59
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
60
|
+
* **Range-only** (`box.hasBox === false` — no `q1`/`q3`): there's no body, so
|
|
61
|
+
* `whisker` draws **one** full `lower→upper` stem with caps, `solid` draws just
|
|
62
|
+
* the outer bar, and `none` draws **nothing** (no body + no spread ⇒ empty — pick
|
|
63
|
+
* `whisker`/`solid` for a range-only box). `showMedian` is a no-op when the box
|
|
64
|
+
* carries no `median` (`hasMedian === false`).
|
|
61
65
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
66
|
+
* Then, if `showMedian` (and a median is present), the median line on top. Fills
|
|
67
|
+
* are bracketed by `save`/`restore` so their `globalAlpha` doesn't leak.
|
|
68
|
+
* `offsetPx` shifts every mark in pixel space (for pairing same-key marks);
|
|
69
|
+
* `capWidthPx` sets a fixed whisker-cap width (else half the box width — a small
|
|
70
|
+
* fixed cap keeps paired offset marks' T-bars from overlapping), clamped to the
|
|
71
|
+
* box width.
|
|
72
|
+
*
|
|
73
|
+
* **Gap-aware**: a key whose present quantiles aren't all finite is skipped
|
|
74
|
+
* entirely (no partial box) — the same contract as a band gap.
|
|
64
75
|
*
|
|
65
76
|
* O(N) over the keys, a fixed number of path ops each — no per-key allocation
|
|
66
77
|
* beyond the `barSpanPx` tuple.
|
|
67
78
|
*/
|
|
68
|
-
export function drawBox(ctx, box, xScale, yScale, style, gapPx = 0, minWidthPx = 1, shape = 'whisker', showMedian = true) {
|
|
79
|
+
export function drawBox(ctx, box, xScale, yScale, style, gapPx = 0, minWidthPx = 1, shape = 'whisker', showMedian = true, offsetPx = 0, capWidthPx) {
|
|
80
|
+
// A range-only box (bid→ask segment) has no body / median; the whisker (or the
|
|
81
|
+
// solid bar) runs the full lower→upper. Flags default true (a full box).
|
|
82
|
+
const hasBox = box.hasBox !== false;
|
|
83
|
+
const drawMedian = showMedian && box.hasMedian !== false;
|
|
69
84
|
for (let i = 0; i < box.length; i += 1) {
|
|
70
85
|
if (!isFiniteBox(box, i))
|
|
71
86
|
continue;
|
|
72
|
-
const [
|
|
87
|
+
const [span0, span1] = barSpanPx(box.x[i], box.xEnd[i], xScale, gapPx, minWidthPx);
|
|
88
|
+
// `offsetPx` nudges the whole mark in pixel space (zoom-stable) — for pairing
|
|
89
|
+
// same-key marks (call/put at one strike) side by side without overlap.
|
|
90
|
+
const x0 = span0 + offsetPx;
|
|
91
|
+
const x1 = span1 + offsetPx;
|
|
73
92
|
const mid = (x0 + x1) / 2;
|
|
74
93
|
const yLower = yScale(box.lower[i]);
|
|
75
|
-
const yQ1 = yScale(box.q1[i]);
|
|
76
|
-
const yMedian = yScale(box.median[i]);
|
|
77
|
-
const yQ3 = yScale(box.q3[i]);
|
|
78
94
|
const yUpper = yScale(box.upper[i]);
|
|
95
|
+
// q1/q3 are NaN on a range-only box — read them only when there's a body.
|
|
96
|
+
const yQ1 = hasBox ? yScale(box.q1[i]) : 0;
|
|
97
|
+
const yQ3 = hasBox ? yScale(box.q3[i]) : 0;
|
|
79
98
|
if (shape === 'solid') {
|
|
80
|
-
// Candlestick: a light outer bar over the full lower→upper spread, then
|
|
81
|
-
// more-prominent inner q1→q3 box on top (same fill
|
|
82
|
-
//
|
|
83
|
-
// no outline.
|
|
99
|
+
// Candlestick: a light outer bar over the full lower→upper spread, then —
|
|
100
|
+
// when there's a body — a more-prominent inner q1→q3 box on top (same fill
|
|
101
|
+
// at rising opacity). No stems, no outline.
|
|
84
102
|
ctx.save();
|
|
85
103
|
ctx.fillStyle = style.fill;
|
|
86
104
|
ctx.globalAlpha = style.fillOpacity;
|
|
87
105
|
ctx.fillRect(x0, yUpper, x1 - x0, yLower - yUpper);
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
if (hasBox) {
|
|
107
|
+
ctx.globalAlpha = Math.min(1, style.fillOpacity * 2);
|
|
108
|
+
ctx.fillRect(x0, yQ3, x1 - x0, yQ1 - yQ3);
|
|
109
|
+
}
|
|
90
110
|
ctx.restore();
|
|
91
111
|
}
|
|
92
112
|
else {
|
|
93
|
-
// `whisker` / `none`: the graded q1→q3 box fill + outline.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
// `whisker` / `none`: the graded q1→q3 box fill + outline (body only).
|
|
114
|
+
if (hasBox) {
|
|
115
|
+
ctx.save();
|
|
116
|
+
ctx.fillStyle = style.fill;
|
|
117
|
+
ctx.globalAlpha = style.fillOpacity;
|
|
118
|
+
ctx.fillRect(x0, yQ3, x1 - x0, yQ1 - yQ3);
|
|
119
|
+
ctx.restore();
|
|
120
|
+
ctx.strokeStyle = style.stroke;
|
|
121
|
+
ctx.lineWidth = style.strokeWidth;
|
|
122
|
+
ctx.strokeRect(x0, yQ3, x1 - x0, yQ1 - yQ3);
|
|
123
|
+
}
|
|
102
124
|
if (shape === 'whisker') {
|
|
103
|
-
// Whiskers
|
|
104
|
-
|
|
125
|
+
// Whiskers with end-caps. With a body: two stems (q3→upper, q1→lower).
|
|
126
|
+
// Range-only (no body): one stem spanning the full lower→upper.
|
|
127
|
+
// Cap half-width: an explicit `capWidthPx` (a fixed pixel cap — for
|
|
128
|
+
// pairing offset marks without their T-bars overlapping) else a fraction
|
|
129
|
+
// of the box width (responsive default). Never wider than the box.
|
|
130
|
+
const capHalf = capWidthPx !== undefined
|
|
131
|
+
? Math.min(capWidthPx, x1 - x0) / 2
|
|
132
|
+
: ((x1 - x0) * WHISKER_CAP_FRACTION) / 2;
|
|
105
133
|
ctx.strokeStyle = style.whisker;
|
|
106
134
|
ctx.lineWidth = style.whiskerWidth;
|
|
107
135
|
ctx.beginPath();
|
|
108
|
-
// Upper
|
|
109
|
-
ctx.moveTo(mid, yQ3);
|
|
136
|
+
// Upper stem: from the box top (q3) or, range-only, from lower.
|
|
137
|
+
ctx.moveTo(mid, hasBox ? yQ3 : yLower);
|
|
110
138
|
ctx.lineTo(mid, yUpper);
|
|
111
139
|
ctx.moveTo(mid - capHalf, yUpper);
|
|
112
140
|
ctx.lineTo(mid + capHalf, yUpper);
|
|
113
|
-
// Lower
|
|
114
|
-
|
|
115
|
-
|
|
141
|
+
// Lower cap (and, with a body, the lower stem q1→lower).
|
|
142
|
+
if (hasBox) {
|
|
143
|
+
ctx.moveTo(mid, yQ1);
|
|
144
|
+
ctx.lineTo(mid, yLower);
|
|
145
|
+
}
|
|
116
146
|
ctx.moveTo(mid - capHalf, yLower);
|
|
117
147
|
ctx.lineTo(mid + capHalf, yLower);
|
|
118
148
|
ctx.stroke();
|
|
119
149
|
}
|
|
120
150
|
}
|
|
121
|
-
// The median line across the box, on top —
|
|
122
|
-
|
|
151
|
+
// The median line across the box, on top — drawn only when the box carries a
|
|
152
|
+
// median column and `showMedian` is on.
|
|
153
|
+
if (drawMedian) {
|
|
154
|
+
const yMedian = yScale(box.median[i]);
|
|
123
155
|
ctx.strokeStyle = style.median;
|
|
124
156
|
ctx.lineWidth = style.medianWidth;
|
|
125
157
|
ctx.beginPath();
|
|
@@ -129,12 +161,24 @@ export function drawBox(ctx, box, xScale, yScale, style, gapPx = 0, minWidthPx =
|
|
|
129
161
|
}
|
|
130
162
|
}
|
|
131
163
|
}
|
|
132
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* This key is drawable — the quantiles it actually carries are all finite at `i`.
|
|
166
|
+
* `lower`/`upper` (the whisker reach) are always required; `q1`/`q3` only when the
|
|
167
|
+
* box has a body (`hasBox !== false`), `median` only when it has a centre line
|
|
168
|
+
* (`hasMedian !== false`). So a **range-only** box (bid→ask, no body/median) draws
|
|
169
|
+
* wherever `lower`/`upper` are finite, and a full box still needs all five.
|
|
170
|
+
*/
|
|
133
171
|
export function isFiniteBox(box, i) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
Number.isFinite(box.
|
|
172
|
+
if (!Number.isFinite(box.lower[i]) || !Number.isFinite(box.upper[i])) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
if (box.hasBox !== false &&
|
|
176
|
+
(!Number.isFinite(box.q1[i]) || !Number.isFinite(box.q3[i]))) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if (box.hasMedian !== false && !Number.isFinite(box.median[i])) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
139
183
|
}
|
|
140
184
|
//# sourceMappingURL=box.js.map
|
package/dist/context.d.ts
CHANGED
|
@@ -1,7 +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 } from 'pond-ts';
|
|
4
5
|
import type { TradingTimeScale, DiscontinuityProvider } from './tradingTimeScale.js';
|
|
6
|
+
import type { ScaleBand } from './bandScale.js';
|
|
5
7
|
/**
|
|
6
8
|
* The frame a {@link ChartContainer} provides to its rows and the time axis.
|
|
7
9
|
* The container owns the **shared x geometry**: each side is split into *slots*
|
|
@@ -64,6 +66,44 @@ export interface ContainerFrame {
|
|
|
64
66
|
* (`yScale.invert`). The x always snaps to the data grid either way.
|
|
65
67
|
*/
|
|
66
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** in axis units (epoch ms on a time axis,
|
|
79
|
+
* the axis value on a value axis), or `null` when not dragging. A drag on a
|
|
80
|
+
* region cursor (only when {@link onRegionSelect} is set) records the press
|
|
81
|
+
* position here; the band then spans from the anchor's bucket to the pointer's
|
|
82
|
+
* bucket (extending bucket by bucket), or freeform when there are no buckets.
|
|
83
|
+
* Cleared on release.
|
|
84
|
+
*/
|
|
85
|
+
readonly regionAnchor: number | null;
|
|
86
|
+
/** Set / clear the region-drag anchor (see {@link regionAnchor}). */
|
|
87
|
+
setRegionAnchor(value: number | null): void;
|
|
88
|
+
/**
|
|
89
|
+
* One-shot callback fired when a `region`-cursor **drag** is released, with the
|
|
90
|
+
* selected `[lo, hi]` span in **axis units** — epoch ms on a time axis, the axis
|
|
91
|
+
* value on a value axis (snapped to the `cursorSequence` buckets when present,
|
|
92
|
+
* else the raw drag span). The neutral numeric pair mirrors the container's
|
|
93
|
+
* polymorphic `range` input (which never takes the axis *kind* from its value);
|
|
94
|
+
* a time-axis consumer who wants a `TimeRange` constructs one from the pair.
|
|
95
|
+
* Providing it is what makes the region cursor **draggable**; the cursor does
|
|
96
|
+
* not keep the range (it reverts to the single-bucket highlight). Typical use:
|
|
97
|
+
* zoom the view, or map the span onto a subscription's range params.
|
|
98
|
+
*/
|
|
99
|
+
readonly onRegionSelect: ((range: readonly [number, number]) => void) | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Require a modifier key held to start a region-drag — set to `'shift'` to make
|
|
102
|
+
* plain drag **pan** and **shift**-drag select, when `panZoom` is on. Only
|
|
103
|
+
* enforced while pan is enabled (with no pan there's no gesture to share, so the
|
|
104
|
+
* modifier is optional). `undefined` ⇒ a region-drag preempts pan.
|
|
105
|
+
*/
|
|
106
|
+
readonly regionSelectModifier: 'shift' | undefined;
|
|
67
107
|
/**
|
|
68
108
|
* The selected mark, or `null`. Shared across rows (single selection). A layer
|
|
69
109
|
* highlights the mark matching the selection's series **`id`** and the clicked
|
|
@@ -139,7 +179,7 @@ export interface ContainerFrame {
|
|
|
139
179
|
* `invert` / `ticks` / `tickFormat` surface, but the mapping runs through
|
|
140
180
|
* trading time so closed-market gaps collapse (see {@link discontinuities}).
|
|
141
181
|
*/
|
|
142
|
-
readonly xScale: ScaleTime<number, number> | ScaleLinear<number, number> | TradingTimeScale;
|
|
182
|
+
readonly xScale: ScaleTime<number, number> | ScaleLinear<number, number> | TradingTimeScale | ScaleBand;
|
|
143
183
|
/**
|
|
144
184
|
* The discontinuity provider backing a **trading-time** x axis, if one was
|
|
145
185
|
* supplied to the container — closed-market time (weekends, holidays,
|
|
@@ -149,12 +189,13 @@ export interface ContainerFrame {
|
|
|
149
189
|
*/
|
|
150
190
|
readonly discontinuities?: DiscontinuityProvider | undefined;
|
|
151
191
|
/**
|
|
152
|
-
* The resolved kind of the shared x scale — `'time'` (a `scaleTime`)
|
|
153
|
-
* `'value'` (a `scaleLinear`),
|
|
154
|
-
*
|
|
155
|
-
*
|
|
192
|
+
* The resolved kind of the shared x scale — `'time'` (a `scaleTime`),
|
|
193
|
+
* `'value'` (a `scaleLinear`), or `'category'` (a {@link ScaleBand}: an ordinal
|
|
194
|
+
* column-domain axis, one slot per category). Inferred from the layers' data.
|
|
195
|
+
* `<XAxis>` reads it to pick its default tick formatter (time / number / the
|
|
196
|
+
* category label), and the cursor readout to format the x position.
|
|
156
197
|
*/
|
|
157
|
-
readonly xKind: 'time' | 'value';
|
|
198
|
+
readonly xKind: 'time' | 'value' | 'category';
|
|
158
199
|
/** Pan/zoom enabled — the plot drag-pans and wheel-zooms the shared time range. */
|
|
159
200
|
readonly panZoom: boolean;
|
|
160
201
|
/** Minimum visible duration (ms) — the zoom-in floor. */
|
|
@@ -314,17 +355,37 @@ export interface RowLayer {
|
|
|
314
355
|
yExtent(): [number, number] | null;
|
|
315
356
|
/**
|
|
316
357
|
* The **kind of x axis** this layer's data lives on — `'time'` for a
|
|
317
|
-
* `TimeSeries`, `'value'` for a `ValueSeries
|
|
318
|
-
*
|
|
319
|
-
*
|
|
358
|
+
* `TimeSeries`, `'value'` for a `ValueSeries`, `'category'` for a categorical
|
|
359
|
+
* (ordinal column-domain) layer. The container infers the one shared x scale
|
|
360
|
+
* from its layers (all must agree — a mix is an error), so the axis kind never
|
|
361
|
+
* needs declaring. See {@link ContainerFrame.xScale}.
|
|
320
362
|
*/
|
|
321
|
-
readonly xKind: 'time' | 'value';
|
|
363
|
+
readonly xKind: 'time' | 'value' | 'category';
|
|
322
364
|
/**
|
|
323
365
|
* This layer's `[min, max]` along the **x** axis (the key / value-axis extent),
|
|
324
366
|
* or `null` if empty. The container unions these to auto-fit the shared x
|
|
325
|
-
* domain when no explicit `range` is given.
|
|
367
|
+
* domain when no explicit `range` is given. For a `'category'` layer this is
|
|
368
|
+
* the slot extent `[0, n]` (n = category count).
|
|
326
369
|
*/
|
|
327
370
|
xExtent(): readonly [number, number] | null;
|
|
371
|
+
/**
|
|
372
|
+
* A `'category'` layer's ordered category names (the ordinal axis domain the
|
|
373
|
+
* container builds a {@link ScaleBand} + label formatter from). `undefined` /
|
|
374
|
+
* absent for a `'time'` or `'value'` layer. Category layers in one container
|
|
375
|
+
* must agree on this list (a mix is an error), the same way {@link xKind} must.
|
|
376
|
+
*/
|
|
377
|
+
xCategories?(): readonly string[] | null;
|
|
378
|
+
/**
|
|
379
|
+
* A bar/histogram layer's bar `[begin, end)` spans, as pond `Interval`s — the
|
|
380
|
+
* **region cursor's snap buckets**. When present (and no `cursorSequence` is
|
|
381
|
+
* set), a region drag snaps bar by bar and a hover highlights the bar under the
|
|
382
|
+
* pointer, so a histogram gets bin-aligned selection for free. Only a
|
|
383
|
+
* **vertical** bar layer on a **continuous** (time / value) x axis publishes
|
|
384
|
+
* them — a horizontal chart puts the value on x (snapping counts is meaningless)
|
|
385
|
+
* and a **category** (ordinal-slot) axis is excluded from the region cursor.
|
|
386
|
+
* `null` / absent otherwise.
|
|
387
|
+
*/
|
|
388
|
+
binIntervals?(): readonly Interval[] | null;
|
|
328
389
|
/**
|
|
329
390
|
* The layer's value(s) at `time` — the nearest sample — for the scrub tracker:
|
|
330
391
|
* one for a line, two (lower/upper) for a band, empty at a gap. Each carries
|
|
@@ -394,8 +455,12 @@ export interface CursorFlag {
|
|
|
394
455
|
*/
|
|
395
456
|
export interface TrackerSource {
|
|
396
457
|
sampleAt(time: number): readonly TrackerSample[];
|
|
397
|
-
readonly xKind: 'time' | 'value';
|
|
458
|
+
readonly xKind: 'time' | 'value' | 'category';
|
|
398
459
|
xExtent(): readonly [number, number] | null;
|
|
460
|
+
/** A `'category'` source's ordered category names (see {@link RowLayer.xCategories}). */
|
|
461
|
+
xCategories?(): readonly string[] | null;
|
|
462
|
+
/** A bar/histogram source's bar `[begin, end)` spans (see {@link RowLayer.binIntervals}). */
|
|
463
|
+
binIntervals?(): readonly Interval[] | null;
|
|
399
464
|
}
|
|
400
465
|
/**
|
|
401
466
|
* One selection — what {@link RowLayer.hitTest} returns and `onSelect` reports.
|
|
@@ -423,6 +488,16 @@ export interface SelectInfo {
|
|
|
423
488
|
readonly color: string;
|
|
424
489
|
/** Display label (`as` ?? column ?? id) — labels the selection in a readout. */
|
|
425
490
|
readonly label: string;
|
|
491
|
+
/**
|
|
492
|
+
* An optional **stable per-mark identity within the layer** — a *category's
|
|
493
|
+
* column name* on the categorical axis, where every bar shares the layer's
|
|
494
|
+
* `id` but each column needs its own stable handle. When present, the
|
|
495
|
+
* highlight match + controlled `selected` echo key on `(id, mark)` instead of
|
|
496
|
+
* the sample `key`, so a pinned selection survives a column reorder / data
|
|
497
|
+
* update (the slot index is not stable; the column name is). `undefined` for
|
|
498
|
+
* marks whose sample `key` is already their identity (a time / value bar).
|
|
499
|
+
*/
|
|
500
|
+
readonly mark?: string;
|
|
426
501
|
}
|
|
427
502
|
/** The hover snapshot handed to `onTrackerChanged` — the cursor time + every
|
|
428
503
|
* series' value there, so a consumer can render the readout outside the chart. */
|
|
@@ -446,7 +521,7 @@ export interface TrackerInfo {
|
|
|
446
521
|
* time pinned to the x-axis. The ChartIQ / trading-terminal readout. Values
|
|
447
522
|
* snap to the series (the axis pills read like ticks), not the raw mouse Y.
|
|
448
523
|
*/
|
|
449
|
-
export type CursorMode = 'none' | 'line' | 'point' | 'inline' | 'flag' | 'crosshair';
|
|
524
|
+
export type CursorMode = 'none' | 'line' | 'point' | 'inline' | 'flag' | 'crosshair' | 'region';
|
|
450
525
|
/** A registered layer plus the axis id it draws against. */
|
|
451
526
|
export interface LayerEntry {
|
|
452
527
|
readonly layer: RowLayer;
|