@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.
- package/API.md +576 -0
- package/CHANGELOG.md +1213 -1
- package/dist/AreaChart.d.ts +12 -1
- package/dist/AreaChart.js +131 -13
- package/dist/BarChart.d.ts +56 -7
- package/dist/BarChart.js +263 -39
- package/dist/BarList.d.ts +85 -5
- package/dist/BarList.js +25 -4
- package/dist/BoxList.d.ts +70 -3
- package/dist/BoxList.js +21 -7
- package/dist/BoxPlot.d.ts +2 -1
- package/dist/BoxPlot.js +101 -9
- package/dist/Candlestick.d.ts +13 -1
- package/dist/Candlestick.js +89 -3
- package/dist/ChartContainer.d.ts +36 -48
- package/dist/ChartContainer.js +465 -59
- package/dist/ChartRow.d.ts +9 -2
- package/dist/ChartRow.js +176 -14
- package/dist/HeatMap.d.ts +176 -0
- package/dist/HeatMap.js +344 -0
- package/dist/Layers.d.ts +5 -1
- package/dist/Layers.js +1014 -253
- package/dist/Legend.js +8 -4
- package/dist/LineChart.d.ts +18 -1
- package/dist/LineChart.js +165 -4
- package/dist/ListTable.d.ts +30 -3
- package/dist/ListTable.js +381 -23
- package/dist/ScatterChart.d.ts +3 -2
- package/dist/ScatterChart.js +68 -4
- package/dist/XAxis.js +40 -22
- package/dist/YAxis.d.ts +58 -2
- package/dist/YAxis.js +3 -1
- package/dist/area.d.ts +34 -1
- package/dist/area.js +88 -1
- package/dist/bars.d.ts +67 -6
- package/dist/bars.js +250 -35
- package/dist/box.d.ts +2 -2
- package/dist/box.js +158 -40
- package/dist/brush.d.ts +142 -0
- package/dist/brush.js +179 -0
- package/dist/child-index.d.ts +27 -0
- package/dist/child-index.js +57 -0
- package/dist/context.d.ts +870 -39
- package/dist/cursors.d.ts +161 -0
- package/dist/cursors.js +503 -0
- package/dist/data.d.ts +38 -0
- package/dist/data.js +43 -0
- package/dist/decimate.d.ts +78 -1
- package/dist/decimate.js +157 -0
- package/dist/format.d.ts +15 -0
- package/dist/format.js +16 -1
- package/dist/heat.d.ts +163 -0
- package/dist/heat.js +659 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +27 -0
- package/dist/line.d.ts +137 -0
- package/dist/line.js +328 -0
- package/dist/ohlc.d.ts +16 -1
- package/dist/ohlc.js +93 -4
- package/dist/range.d.ts +14 -1
- package/dist/range.js +24 -3
- package/dist/scatter.d.ts +17 -9
- package/dist/scatter.js +221 -33
- package/dist/select.d.ts +13 -5
- package/dist/select.js +14 -6
- package/dist/selection-fixtures.d.ts +174 -0
- package/dist/selection-fixtures.js +569 -0
- package/dist/selection-stories.d.ts +73 -0
- package/dist/selection-stories.js +301 -0
- package/dist/selectors.d.ts +316 -0
- package/dist/selectors.js +391 -0
- package/dist/span.d.ts +122 -0
- package/dist/span.js +203 -0
- package/dist/sweep.d.ts +154 -0
- package/dist/sweep.js +282 -0
- package/dist/theme.d.ts +510 -5
- package/dist/theme.js +217 -41
- package/dist/tracker.d.ts +6 -0
- package/dist/tracker.js +6 -0
- package/dist/tradingAxis.fixture.d.ts +78 -0
- package/dist/tradingAxis.fixture.js +215 -0
- package/dist/useChartLegend.js +18 -3
- package/dist/yticks.d.ts +3 -0
- package/dist/yticks.js +104 -0
- package/package.json +6 -5
package/dist/bars.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { barSpanPx } from './range.js';
|
|
2
2
|
import { visibleSpanRange } from './culling.js';
|
|
3
3
|
import { decimateBars } from './decimate.js';
|
|
4
|
+
import { NO_SPANS, spanMatchesAny } from './span.js';
|
|
4
5
|
/**
|
|
5
6
|
* The `[min, max]` vertical extent the bars occupy — the finite values of `cs.y`
|
|
6
7
|
* **widened to include `0`**, since a bar spans from its value to the baseline
|
|
@@ -72,11 +73,11 @@ export function resolveBarBaseline(yScale) {
|
|
|
72
73
|
* hit region are deliberately *not* the same geometry: the `gapPx` inset
|
|
73
74
|
* separates columns visually without carving a dead channel out of the target.
|
|
74
75
|
*/
|
|
75
|
-
export function barRect(cs, i, xScale, yScale, baseline, gapPx, minWidthPx) {
|
|
76
|
+
export function barRect(cs, i, xScale, yScale, baseline, gapPx, minWidthPx, maxWidthPx) {
|
|
76
77
|
const v = cs.y[i];
|
|
77
78
|
if (!Number.isFinite(v))
|
|
78
79
|
return null;
|
|
79
|
-
const [x0, x1] = barSpanPx(cs.begin[i], cs.end[i], xScale, gapPx, minWidthPx);
|
|
80
|
+
const [x0, x1] = barSpanPx(cs.begin[i], cs.end[i], xScale, gapPx, minWidthPx, maxWidthPx);
|
|
80
81
|
const yValue = yScale(v);
|
|
81
82
|
const yBase = yScale(baseline);
|
|
82
83
|
return [x0, x1, Math.min(yValue, yBase), Math.max(yValue, yBase)];
|
|
@@ -225,6 +226,78 @@ function barMatches(m, seriesId, stable, begin) {
|
|
|
225
226
|
? m.mark === stable
|
|
226
227
|
: m.key === begin;
|
|
227
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Does **any** member of the selection set identify this bar? The set form of
|
|
231
|
+
* {@link barMatches} ([PND-MULTISEL]).
|
|
232
|
+
*
|
|
233
|
+
* Linear over the set on purpose: a selection is a handful of marks a person
|
|
234
|
+
* clicked, not a data structure — building a Set per draw would cost more than
|
|
235
|
+
* it saves at these sizes, and the common cases are 0 or 1 members, which this
|
|
236
|
+
* short-circuits on.
|
|
237
|
+
*
|
|
238
|
+
* **The `<MultiSelector>` sweep broke that assumption for large sets** (RFC
|
|
239
|
+
* §8): a live preview can put every visible bar in `hovered`, and a
|
|
240
|
+
* demoted-on-edit selection can hold as many mark entries — V visible bars ×
|
|
241
|
+
* C entries is quadratic-shaped (measured: 2.6k×2.6k ≈ 4.5 ms/frame, 10k×10k
|
|
242
|
+
* ≈ 64 ms). Past {@link MARK_INDEX_THRESHOLD} the draw builds
|
|
243
|
+
* {@link buildBarMarkIndex} once — the *same* match, O(1) per bar — and small
|
|
244
|
+
* sets keep this scan (cheaper than a Set at click sizes).
|
|
245
|
+
*/
|
|
246
|
+
function barMatchesAny(sel, seriesId, stable, begin) {
|
|
247
|
+
for (let i = 0; i < sel.length; i += 1) {
|
|
248
|
+
if (barMatches(sel[i], seriesId, stable, begin))
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
/** Past this many entries, a per-draw set index beats the linear scan. Small
|
|
254
|
+
* enough that a sweep preview always indexes; big enough that a clicked
|
|
255
|
+
* handful never pays a Set build. */
|
|
256
|
+
const MARK_INDEX_THRESHOLD = 16;
|
|
257
|
+
/**
|
|
258
|
+
* Stroke a selected mark's outline **inside** its ink rect. Canvas strokes
|
|
259
|
+
* centre on the path, so a plain `strokeRect(x0, yTop, …)` paints
|
|
260
|
+
* `lineWidth / 2` *outside* the rect — which, with the default
|
|
261
|
+
* `outlineWidth: 1.5` against the default `gap: 1`, is enough to bridge the
|
|
262
|
+
* whole inter-bar gap from both sides: a swept run of selected bars fused
|
|
263
|
+
* into one unreadable block (you could not count them). Insetting the path by
|
|
264
|
+
* half the line width keeps every stroked pixel within the bar's own ink, so
|
|
265
|
+
* adjacent selected marks stay separated by exactly the gap the resting bars
|
|
266
|
+
* show. A mark too thin to contain its outline (either dimension
|
|
267
|
+
* `<= lineWidth`) skips the stroke — the highlight fill is already the
|
|
268
|
+
* signal, and an outline wider than the bar would only smear into the
|
|
269
|
+
* neighbours this exists to keep distinct.
|
|
270
|
+
*/
|
|
271
|
+
export function strokeSelectedOutline(ctx, x0, x1, yTop, yBottom, lineWidth) {
|
|
272
|
+
const w = x1 - x0;
|
|
273
|
+
const h = yBottom - yTop;
|
|
274
|
+
if (w <= lineWidth || h <= lineWidth)
|
|
275
|
+
return;
|
|
276
|
+
const inset = lineWidth / 2;
|
|
277
|
+
ctx.lineWidth = lineWidth;
|
|
278
|
+
ctx.strokeRect(x0 + inset, yTop + inset, w - lineWidth, h - lineWidth);
|
|
279
|
+
}
|
|
280
|
+
function buildBarMarkIndex(sel, seriesId) {
|
|
281
|
+
const marks = new Set();
|
|
282
|
+
const keysMarkless = new Set();
|
|
283
|
+
const keysAll = new Set();
|
|
284
|
+
for (let i = 0; i < sel.length; i += 1) {
|
|
285
|
+
const m = sel[i];
|
|
286
|
+
if (m.id !== seriesId)
|
|
287
|
+
continue;
|
|
288
|
+
keysAll.add(m.key);
|
|
289
|
+
if (m.mark !== undefined)
|
|
290
|
+
marks.add(m.mark);
|
|
291
|
+
else
|
|
292
|
+
keysMarkless.add(m.key);
|
|
293
|
+
}
|
|
294
|
+
return { marks, keysMarkless, keysAll };
|
|
295
|
+
}
|
|
296
|
+
function indexMatches(ix, stable, begin) {
|
|
297
|
+
return stable !== undefined
|
|
298
|
+
? ix.marks.has(stable) || ix.keysMarkless.has(begin)
|
|
299
|
+
: ix.keysAll.has(begin);
|
|
300
|
+
}
|
|
228
301
|
/**
|
|
229
302
|
* Fill one rectangle per bar in `cs`, each spanning its key's `[begin, end]`
|
|
230
303
|
* (inset by `gapPx`) from the resolved `baseline` to the value.
|
|
@@ -277,7 +350,16 @@ function barMatches(m, seriesId, stable, begin) {
|
|
|
277
350
|
* repaint them one flat colour; per-bar-coloured layers draw every visible bar.
|
|
278
351
|
* Returns {@link LayerDrawStats} for `onDrawStats`.
|
|
279
352
|
*/
|
|
280
|
-
export function drawBars(ctx, cs, xScale, yScale, style, baseline, gapPx, seriesId, selection, hovered, decimate = true, binFills, banding
|
|
353
|
+
export function drawBars(ctx, cs, xScale, yScale, style, baseline, gapPx, seriesId, selection, hovered, decimate = true, binFills, banding,
|
|
354
|
+
// Span descriptors covering this layer (interaction RFC A5.2), already
|
|
355
|
+
// narrowed to its `id` (and constant-label `rows` resolved) by the component
|
|
356
|
+
// — see `spansForLayer`. A bar is selected when a mark entry names it OR a
|
|
357
|
+
// span covers it: an O(1)-per-bar half-open test of `begin` against the
|
|
358
|
+
// span's `x` (and the bar's value against `y` when present), so a span over
|
|
359
|
+
// ten thousand bars costs each one interval test, not a scan. Like the mark
|
|
360
|
+
// highlight, spans are ignored on the decimated envelope path (aggregate
|
|
361
|
+
// columns have no per-bar identity).
|
|
362
|
+
spans = NO_SPANS) {
|
|
281
363
|
ctx.save();
|
|
282
364
|
ctx.globalAlpha = style.opacity;
|
|
283
365
|
const sourceCount = cs.length; // pre-cull, pre-decimation (for draw stats)
|
|
@@ -286,6 +368,12 @@ export function drawBars(ctx, cs, xScale, yScale, style, baseline, gapPx, series
|
|
|
286
368
|
// the `begin[i]` selection/hover match stays correct; full range when `xScale`
|
|
287
369
|
// has no domain (a test stub). A selected/hovered bar off-screen isn't drawn
|
|
288
370
|
// (its highlight would be off-screen anyway).
|
|
371
|
+
// Dim only when a selection actually exists AND the theme opted in: with an
|
|
372
|
+
// empty set there is nothing to recede from, and with no `dimmed` value the
|
|
373
|
+
// library never invents one (RFC A2.3). A span is a selection too — a swept
|
|
374
|
+
// range recedes the bars outside it exactly as a clicked set does.
|
|
375
|
+
const hasSpans = spans.length > 0;
|
|
376
|
+
const dimming = style.dimmed !== undefined && (selection.length > 0 || hasSpans);
|
|
289
377
|
const [vStart, vEnd] = visibleSpanRange(cs.begin, cs.end, cs.length, xScale);
|
|
290
378
|
// Decimate the visible bars to per-column envelope rects once dense (see the
|
|
291
379
|
// header). `null` below the visible-density threshold ⇒ the full per-bar loop.
|
|
@@ -328,12 +416,21 @@ export function drawBars(ctx, cs, xScale, yScale, style, baseline, gapPx, series
|
|
|
328
416
|
// may already have: an interactive layer echoes the hovered bar's mark on
|
|
329
417
|
// every pointer move. See BarSeries.marks — this hoist keeps the draw path
|
|
330
418
|
// clean, it doesn't make the channel free.)
|
|
331
|
-
const marks = selection
|
|
419
|
+
const marks = selection.some((m) => m.mark !== undefined) ||
|
|
420
|
+
hovered.some((m) => m.mark !== undefined)
|
|
332
421
|
? cs.marks
|
|
333
422
|
: undefined;
|
|
423
|
+
// Large sets (a sweep preview in `hovered`, a demoted sweep in `selection`)
|
|
424
|
+
// switch to the O(1)-per-bar set index; a clicked handful keeps the scan.
|
|
425
|
+
const selIndex = selection.length > MARK_INDEX_THRESHOLD
|
|
426
|
+
? buildBarMarkIndex(selection, seriesId)
|
|
427
|
+
: null;
|
|
428
|
+
const hovIndex = hovered.length > MARK_INDEX_THRESHOLD
|
|
429
|
+
? buildBarMarkIndex(hovered, seriesId)
|
|
430
|
+
: null;
|
|
334
431
|
let drawn = 0;
|
|
335
432
|
for (let i = vStart; i < vEnd; i += 1) {
|
|
336
|
-
const rect = barRect(cs, i, xScale, yScale, baseline, gapPx, style.minWidth);
|
|
433
|
+
const rect = barRect(cs, i, xScale, yScale, baseline, gapPx, style.minWidth, style.maxWidth);
|
|
337
434
|
if (rect === null)
|
|
338
435
|
continue;
|
|
339
436
|
const [x0, x1, yTop, yBottom] = rect;
|
|
@@ -345,22 +442,28 @@ export function drawBars(ctx, cs, xScale, yScale, style, baseline, gapPx, series
|
|
|
345
442
|
// sets one and `highlight` otherwise, always without the outline — so hover
|
|
346
443
|
// reads as a lighter "this bar is live" and select as the committed pick.
|
|
347
444
|
const stable = marks?.[i];
|
|
348
|
-
const selected =
|
|
349
|
-
|
|
445
|
+
const selected = (selIndex !== null
|
|
446
|
+
? indexMatches(selIndex, stable, cs.begin[i])
|
|
447
|
+
: barMatchesAny(selection, seriesId, stable, cs.begin[i])) ||
|
|
448
|
+
(hasSpans && spanMatchesAny(spans, cs.begin[i], cs.y[i]));
|
|
449
|
+
const isHovered = hovIndex !== null
|
|
450
|
+
? indexMatches(hovIndex, stable, cs.begin[i])
|
|
451
|
+
: barMatchesAny(hovered, seriesId, stable, cs.begin[i]);
|
|
350
452
|
if (fills !== undefined) {
|
|
351
453
|
// Per-bar fills: the bar keeps its own colour under hover / selection —
|
|
352
454
|
// highlight pops the alpha to 1 and outlines the selection in the bar's
|
|
353
455
|
// own fill (the drawStacks binFills convention; see the header).
|
|
354
|
-
const fill =
|
|
456
|
+
const fill = dimming && !selected && !isHovered
|
|
457
|
+
? style.dimmed
|
|
458
|
+
: (fills[i] ?? style.fill);
|
|
355
459
|
ctx.globalAlpha =
|
|
356
460
|
selected || isHovered ? (style.emphasisOpacity ?? 1) : style.opacity;
|
|
357
461
|
ctx.fillStyle = fill;
|
|
358
462
|
ctx.fillRect(x0, yTop, x1 - x0, yBottom - yTop);
|
|
359
463
|
drawn += 1;
|
|
360
464
|
if (selected) {
|
|
361
|
-
ctx.lineWidth = style.outlineWidth;
|
|
362
465
|
ctx.strokeStyle = style.selectedOutline ?? fill;
|
|
363
|
-
ctx
|
|
466
|
+
strokeSelectedOutline(ctx, x0, x1, yTop, yBottom, style.outlineWidth);
|
|
364
467
|
}
|
|
365
468
|
continue;
|
|
366
469
|
}
|
|
@@ -373,6 +476,15 @@ export function drawBars(ctx, cs, xScale, yScale, style, baseline, gapPx, series
|
|
|
373
476
|
ctx.globalAlpha =
|
|
374
477
|
selected || isHovered ? (style.emphasisOpacity ?? 1) : style.opacity;
|
|
375
478
|
const v = cs.y[i];
|
|
479
|
+
if (dimming && !selected && !isHovered) {
|
|
480
|
+
// A dimmed banded bar draws flat: the ladder encodes *where the value
|
|
481
|
+
// sits*, which is exactly what a receded bar should stop competing
|
|
482
|
+
// over. Painting a dim gradient just lowers its voice.
|
|
483
|
+
ctx.fillStyle = style.dimmed;
|
|
484
|
+
ctx.fillRect(x0, yTop, x1 - x0, yBottom - yTop);
|
|
485
|
+
drawn += 1;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
376
488
|
let topFill = ladder.colors[0];
|
|
377
489
|
for (let bk = 0; bk < ladder.colors.length; bk += 1) {
|
|
378
490
|
if (!bandSpanInto(baseline, v, ladder.thresholds, bk))
|
|
@@ -390,9 +502,8 @@ export function drawBars(ctx, cs, xScale, yScale, style, baseline, gapPx, series
|
|
|
390
502
|
// Outline the whole bar (not the last band) in the colour of the band
|
|
391
503
|
// the value reached — the one colour that means something for a bar
|
|
392
504
|
// painted in several.
|
|
393
|
-
ctx.lineWidth = style.outlineWidth;
|
|
394
505
|
ctx.strokeStyle = style.selectedOutline ?? topFill;
|
|
395
|
-
ctx
|
|
506
|
+
strokeSelectedOutline(ctx, x0, x1, yTop, yBottom, style.outlineWidth);
|
|
396
507
|
}
|
|
397
508
|
continue;
|
|
398
509
|
}
|
|
@@ -407,11 +518,17 @@ export function drawBars(ctx, cs, xScale, yScale, style, baseline, gapPx, series
|
|
|
407
518
|
// selected. Selection outranks hover on a bar that is both (as the outline
|
|
408
519
|
// already did). With no `hover` colour this is the shipped two-step —
|
|
409
520
|
// `highlight` for either state (see BarStyle.hover).
|
|
521
|
+
// Precedence, shared with `drawStacks`: selected > hovered > dimmed > rest.
|
|
522
|
+
// Hover outranks dim deliberately — dim says "this is not in your
|
|
523
|
+
// selection", hover says "your pointer is here now", and suppressing live
|
|
524
|
+
// pointer feedback to keep a bar receded makes the chart feel broken.
|
|
410
525
|
ctx.fillStyle = selected
|
|
411
526
|
? style.highlight
|
|
412
527
|
: isHovered
|
|
413
528
|
? (style.hover ?? style.highlight)
|
|
414
|
-
:
|
|
529
|
+
: dimming
|
|
530
|
+
? style.dimmed
|
|
531
|
+
: style.fill;
|
|
415
532
|
ctx.fillRect(x0, yTop, x1 - x0, yBottom - yTop);
|
|
416
533
|
drawn += 1;
|
|
417
534
|
if (selected) {
|
|
@@ -421,9 +538,8 @@ export function drawBars(ctx, cs, xScale, yScale, style, baseline, gapPx, series
|
|
|
421
538
|
// fill, so only the half-stroke falling outside the rect reads. A theme
|
|
422
539
|
// that needs the two states clearly apart sets `BarStyle.hover` (#577);
|
|
423
540
|
// the outline is the shape cue, not the whole signal.
|
|
424
|
-
ctx.lineWidth = style.outlineWidth;
|
|
425
541
|
ctx.strokeStyle = style.selectedOutline ?? style.highlight;
|
|
426
|
-
ctx
|
|
542
|
+
strokeSelectedOutline(ctx, x0, x1, yTop, yBottom, style.outlineWidth);
|
|
427
543
|
}
|
|
428
544
|
}
|
|
429
545
|
ctx.restore();
|
|
@@ -542,6 +658,29 @@ export function barAt(cs, px, py, xScale, yScale, baseline, minWidthPx) {
|
|
|
542
658
|
}
|
|
543
659
|
return null;
|
|
544
660
|
}
|
|
661
|
+
function buildStackMarkIndex(sel, seriesId) {
|
|
662
|
+
const marks = new Set();
|
|
663
|
+
const labelsByKey = new Map();
|
|
664
|
+
for (let i = 0; i < sel.length; i += 1) {
|
|
665
|
+
const m = sel[i];
|
|
666
|
+
if (m.id !== seriesId)
|
|
667
|
+
continue;
|
|
668
|
+
if (m.mark !== undefined)
|
|
669
|
+
marks.add(m.mark);
|
|
670
|
+
let labels = labelsByKey.get(m.key);
|
|
671
|
+
if (labels === undefined) {
|
|
672
|
+
labels = new Set();
|
|
673
|
+
labelsByKey.set(m.key, labels);
|
|
674
|
+
}
|
|
675
|
+
labels.add(m.label);
|
|
676
|
+
}
|
|
677
|
+
return { marks, labelsByKey };
|
|
678
|
+
}
|
|
679
|
+
function stackIndexMatches(ix, stableMark, begin, group) {
|
|
680
|
+
return stableMark !== undefined
|
|
681
|
+
? ix.marks.has(stableMark)
|
|
682
|
+
: (ix.labelsByKey.get(begin)?.has(group) ?? false);
|
|
683
|
+
}
|
|
545
684
|
/**
|
|
546
685
|
* The `[min, max]` extent of the **value (stacked) axis**. For a true multi-group
|
|
547
686
|
* stack it is `[minNegTotal, maxPosTotal]` — each bin's positive segments summed
|
|
@@ -640,7 +779,7 @@ export function stackBase(orientation, xScale, yScale) {
|
|
|
640
779
|
* is unfloored. Shared by {@link drawStacks} and {@link stackAt} so the drawn rect
|
|
641
780
|
* and the hit rect are identical.
|
|
642
781
|
*/
|
|
643
|
-
export function segmentRect(ss, b, g, orientation, xScale, yScale, cumBefore, gapPx, minSpanPx) {
|
|
782
|
+
export function segmentRect(ss, b, g, orientation, xScale, yScale, cumBefore, gapPx, minSpanPx, maxSpanPx) {
|
|
644
783
|
const G = ss.groups.length;
|
|
645
784
|
const v = ss.values[b * G + g];
|
|
646
785
|
// Skip non-finite (a gap) or zero (a zero-extent rect that can't draw or be
|
|
@@ -662,12 +801,12 @@ export function segmentRect(ss, b, g, orientation, xScale, yScale, cumBefore, ga
|
|
|
662
801
|
if (!Number.isFinite(v) || v === 0)
|
|
663
802
|
return null;
|
|
664
803
|
if (orientation === 'vertical') {
|
|
665
|
-
const [x0, x1] = barSpanPx(ss.begin[b], ss.end[b], xScale, gapPx, minSpanPx);
|
|
804
|
+
const [x0, x1] = barSpanPx(ss.begin[b], ss.end[b], xScale, gapPx, minSpanPx, maxSpanPx);
|
|
666
805
|
const yA = yScale(cumBefore);
|
|
667
806
|
const yB = yScale(cumBefore + v);
|
|
668
807
|
return [x0, x1, Math.min(yA, yB), Math.max(yA, yB)];
|
|
669
808
|
}
|
|
670
|
-
const [y0, y1] = barSpanPx(ss.begin[b], ss.end[b], yScale, gapPx, minSpanPx);
|
|
809
|
+
const [y0, y1] = barSpanPx(ss.begin[b], ss.end[b], yScale, gapPx, minSpanPx, maxSpanPx);
|
|
671
810
|
const xA = xScale(cumBefore);
|
|
672
811
|
const xB = xScale(cumBefore + v);
|
|
673
812
|
return [Math.min(xA, xB), Math.max(xA, xB), y0, y1];
|
|
@@ -686,7 +825,15 @@ export function segmentRect(ss, b, g, orientation, xScale, yScale, cumBefore, ga
|
|
|
686
825
|
*
|
|
687
826
|
* O(N·G) over bins × groups, one fill (+ optional stroke) per drawn segment.
|
|
688
827
|
*/
|
|
689
|
-
export function drawStacks(ctx, ss, orientation, xScale, yScale, style, gapPx, minSpanPx, seriesId, selection, hover, banding
|
|
828
|
+
export function drawStacks(ctx, ss, orientation, xScale, yScale, style, gapPx, minSpanPx, seriesId, selection, hover, banding,
|
|
829
|
+
// Span descriptors covering this layer (interaction RFC A5.2), already
|
|
830
|
+
// narrowed to its `id` by the component (`spansForLayer`). A segment is
|
|
831
|
+
// selected when a mark entry names it OR a span covers it — the O(1) test of
|
|
832
|
+
// the bin `begin` against the half-open `x` interval, the segment's own
|
|
833
|
+
// value against `y` when present, and its group against `rows` when present
|
|
834
|
+
// (the group is this layer's label channel, so `rows` stays testable per
|
|
835
|
+
// segment where `drawBars`' constant label lets the component resolve it).
|
|
836
|
+
spans = NO_SPANS) {
|
|
690
837
|
const G = ss.groups.length;
|
|
691
838
|
const base = stackBase(orientation, xScale, yScale);
|
|
692
839
|
// Threshold banding applies to a **plain** bar only. `G === 1` is exactly the
|
|
@@ -695,6 +842,19 @@ export function drawStacks(ctx, ss, orientation, xScale, yScale, style, gapPx, m
|
|
|
695
842
|
// each segment is already a slice of a total — so the ladder is dropped here
|
|
696
843
|
// and warned about at the prop boundary rather than half-applied.
|
|
697
844
|
const ladder = G === 1 && style.binFills === undefined ? banding : undefined;
|
|
845
|
+
// See `drawBars` — dim only with a real selection (marks or spans) and an
|
|
846
|
+
// opted-in theme.
|
|
847
|
+
const hasSpans = spans.length > 0;
|
|
848
|
+
const dimming = (style.dimmed !== undefined || style.dimmedFills !== undefined) &&
|
|
849
|
+
(selection.length > 0 || hasSpans);
|
|
850
|
+
// The large-set switch (see barMatchesAny): a sweep preview / demoted sweep
|
|
851
|
+
// indexes once, a clicked handful keeps the linear scan.
|
|
852
|
+
const selIndex = selection.length > MARK_INDEX_THRESHOLD
|
|
853
|
+
? buildStackMarkIndex(selection, seriesId)
|
|
854
|
+
: null;
|
|
855
|
+
const hovIndex = hover.length > MARK_INDEX_THRESHOLD
|
|
856
|
+
? buildStackMarkIndex(hover, seriesId)
|
|
857
|
+
: null;
|
|
698
858
|
ctx.save();
|
|
699
859
|
ctx.globalAlpha = style.opacity;
|
|
700
860
|
for (let b = 0; b < ss.length; b += 1) {
|
|
@@ -705,7 +865,7 @@ export function drawStacks(ctx, ss, orientation, xScale, yScale, style, gapPx, m
|
|
|
705
865
|
let cumNeg = base;
|
|
706
866
|
for (let g = 0; g < G; g += 1) {
|
|
707
867
|
const v = ss.values[b * G + g];
|
|
708
|
-
const rect = segmentRect(ss, b, g, orientation, xScale, yScale, v < 0 ? cumNeg : cumPos, gapPx, minSpanPx);
|
|
868
|
+
const rect = segmentRect(ss, b, g, orientation, xScale, yScale, v < 0 ? cumNeg : cumPos, gapPx, minSpanPx, style.maxWidth);
|
|
709
869
|
if (Number.isFinite(v)) {
|
|
710
870
|
if (v > 0)
|
|
711
871
|
cumPos += v;
|
|
@@ -724,8 +884,42 @@ export function drawStacks(ctx, ss, orientation, xScale, yScale, style, gapPx, m
|
|
|
724
884
|
(stableMark !== undefined
|
|
725
885
|
? m.mark === stableMark
|
|
726
886
|
: m.key === ss.begin[b] && m.label === ss.groups[g]);
|
|
727
|
-
|
|
728
|
-
|
|
887
|
+
// Indexed rather than `.some(cb)` — this is the inner loop of the draw,
|
|
888
|
+
// and a closure per segment per frame is avoidable garbage.
|
|
889
|
+
let selected = false;
|
|
890
|
+
if (selIndex !== null) {
|
|
891
|
+
selected = stackIndexMatches(selIndex, stableMark, ss.begin[b], ss.groups[g]);
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
for (let si = 0; si < selection.length; si += 1) {
|
|
895
|
+
if (matches(selection[si])) {
|
|
896
|
+
selected = true;
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
// A span covers this segment when the bin's `begin` is inside its
|
|
902
|
+
// half-open `x` (and its `y` / `rows` channels pass — see the param doc).
|
|
903
|
+
// The label channel is what the hit reports: the stable per-bin `mark`
|
|
904
|
+
// (a categorical bar's category name) when the series carries marks,
|
|
905
|
+
// else the group — `hitTest` builds `label: stableMark ?? name`, and the
|
|
906
|
+
// span test must read the same field or `selectionContains` and the
|
|
907
|
+
// canvas disagree on every categorical bar.
|
|
908
|
+
if (!selected && hasSpans) {
|
|
909
|
+
selected = spanMatchesAny(spans, ss.begin[b], v, stableMark ?? ss.groups[g]);
|
|
910
|
+
}
|
|
911
|
+
let isHovered = false;
|
|
912
|
+
if (hovIndex !== null) {
|
|
913
|
+
isHovered = stackIndexMatches(hovIndex, stableMark, ss.begin[b], ss.groups[g]);
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
for (let hi = 0; hi < hover.length; hi += 1) {
|
|
917
|
+
if (matches(hover[hi])) {
|
|
918
|
+
isHovered = true;
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
729
923
|
// A hovered / selected segment pops its alpha; a resting one draws at the
|
|
730
924
|
// shared one. `emphasisOpacity` makes the *difference* themeable, where
|
|
731
925
|
// before only the resting floor was.
|
|
@@ -738,6 +932,11 @@ export function drawStacks(ctx, ss, orientation, xScale, yScale, style, gapPx, m
|
|
|
738
932
|
// the bar's length at the ladder boundaries, transposing on
|
|
739
933
|
// orientation — vertical bars band along y, horizontal along x, while
|
|
740
934
|
// the bin span (the other axis) is shared by every band.
|
|
935
|
+
if (dimming && !selected && !isHovered) {
|
|
936
|
+
ctx.fillStyle = style.dimmedFills?.[g] ?? style.dimmed ?? fill;
|
|
937
|
+
ctx.fillRect(x0, yTop, x1 - x0, yBottom - yTop);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
741
940
|
let topFill = ladder.colors[0];
|
|
742
941
|
for (let bk = 0; bk < ladder.colors.length; bk += 1) {
|
|
743
942
|
if (!bandSpanInto(base, v, ladder.thresholds, bk))
|
|
@@ -758,11 +957,10 @@ export function drawStacks(ctx, ss, orientation, xScale, yScale, style, gapPx, m
|
|
|
758
957
|
topFill = ladder.colors[bk];
|
|
759
958
|
}
|
|
760
959
|
if (selected) {
|
|
761
|
-
ctx.lineWidth = style.outlineWidth;
|
|
762
960
|
// A themed outline if the theme sets one, else the band the value
|
|
763
961
|
// reached — the one colour that means anything on a banded bar.
|
|
764
962
|
ctx.strokeStyle = style.selectedOutline ?? topFill;
|
|
765
|
-
ctx
|
|
963
|
+
strokeSelectedOutline(ctx, x0, x1, yTop, yBottom, style.outlineWidth);
|
|
766
964
|
}
|
|
767
965
|
continue;
|
|
768
966
|
}
|
|
@@ -771,19 +969,32 @@ export function drawStacks(ctx, ss, orientation, xScale, yScale, style, gapPx, m
|
|
|
771
969
|
// selected segment takes `highlight` and a hovered one `hover`, exactly
|
|
772
970
|
// as the single-series path does. With `binFills` the bar keeps its own
|
|
773
971
|
// colour (the design exclusion) and the alpha pop above is the signal.
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
972
|
+
// Same precedence as `drawBars`: selected > hovered > dimmed > rest.
|
|
973
|
+
const emphasised = dimming && !selected && !isHovered
|
|
974
|
+
? (style.dimmedFills?.[g] ?? style.dimmed ?? fill)
|
|
975
|
+
: style.binFills !== undefined
|
|
976
|
+
? fill
|
|
977
|
+
: selected
|
|
978
|
+
? // A group ramp is meaning-carrying colour, so a selected
|
|
979
|
+
// segment keeps it: the outline and the receded neighbours are
|
|
980
|
+
// the cue. Same exclusion `binFills` gets, one line up.
|
|
981
|
+
style.groupColored
|
|
982
|
+
? fill
|
|
983
|
+
: (style.highlight ?? fill)
|
|
984
|
+
: isHovered
|
|
985
|
+
? // Per-group where the ramp supplies one, so hovering
|
|
986
|
+
// brightens the segment instead of recolouring it out of
|
|
987
|
+
// its group.
|
|
988
|
+
(style.hoverFills?.[g] ??
|
|
989
|
+
style.hover ??
|
|
990
|
+
style.highlight ??
|
|
991
|
+
fill)
|
|
992
|
+
: fill;
|
|
781
993
|
ctx.fillStyle = emphasised;
|
|
782
994
|
ctx.fillRect(x0, yTop, x1 - x0, yBottom - yTop);
|
|
783
995
|
if (selected) {
|
|
784
|
-
ctx.lineWidth = style.outlineWidth;
|
|
785
996
|
ctx.strokeStyle = style.selectedOutline ?? emphasised;
|
|
786
|
-
ctx
|
|
997
|
+
strokeSelectedOutline(ctx, x0, x1, yTop, yBottom, style.outlineWidth);
|
|
787
998
|
}
|
|
788
999
|
}
|
|
789
1000
|
}
|
|
@@ -800,7 +1011,11 @@ export function drawStacks(ctx, ss, orientation, xScale, yScale, style, gapPx, m
|
|
|
800
1011
|
* O(N·G) over bins × groups (no spatial index — histogram bin/group counts are
|
|
801
1012
|
* small; click / hover are cheap events).
|
|
802
1013
|
*/
|
|
803
|
-
export function stackAt(ss, px, py, orientation, xScale, yScale, gapPx, minSpanPx
|
|
1014
|
+
export function stackAt(ss, px, py, orientation, xScale, yScale, gapPx, minSpanPx,
|
|
1015
|
+
/** Must match the draw's cap ([PND-BARWIDTH]) — this function's whole
|
|
1016
|
+
* contract is that its rect is the drawn rect, so a cap applied to one and
|
|
1017
|
+
* not the other silently drifts the hit target off the ink. */
|
|
1018
|
+
maxSpanPx) {
|
|
804
1019
|
const G = ss.groups.length;
|
|
805
1020
|
const base = stackBase(orientation, xScale, yScale);
|
|
806
1021
|
for (let b = 0; b < ss.length; b += 1) {
|
|
@@ -810,7 +1025,7 @@ export function stackAt(ss, px, py, orientation, xScale, yScale, gapPx, minSpanP
|
|
|
810
1025
|
let cumNeg = base;
|
|
811
1026
|
for (let g = 0; g < G; g += 1) {
|
|
812
1027
|
const v = ss.values[b * G + g];
|
|
813
|
-
const rect = segmentRect(ss, b, g, orientation, xScale, yScale, v < 0 ? cumNeg : cumPos, gapPx, minSpanPx);
|
|
1028
|
+
const rect = segmentRect(ss, b, g, orientation, xScale, yScale, v < 0 ? cumNeg : cumPos, gapPx, minSpanPx, maxSpanPx);
|
|
814
1029
|
if (Number.isFinite(v)) {
|
|
815
1030
|
if (v > 0)
|
|
816
1031
|
cumPos += v;
|
package/dist/box.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BoxSeries } from './data.js';
|
|
2
2
|
import type { Scale } from './line.js';
|
|
3
3
|
import type { BoxStyle } from './theme.js';
|
|
4
|
-
import type { LayerDrawStats } from './context.js';
|
|
4
|
+
import type { LayerDrawStats, SpanSelection } from './context.js';
|
|
5
5
|
import { type DecimateOption } from './decimate.js';
|
|
6
6
|
/**
|
|
7
7
|
* The `[min, max]` vertical extent of the **drawn** boxes — the lowest `lower`
|
|
@@ -79,7 +79,7 @@ export type BoxShape = 'whisker' | 'solid' | 'none';
|
|
|
79
79
|
* O(N) over the keys, a fixed number of path ops each — no per-key allocation
|
|
80
80
|
* beyond the `barSpanPx` tuple.
|
|
81
81
|
*/
|
|
82
|
-
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,
|
|
82
|
+
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, selectedKeys?: readonly number[], hoveredKeys?: readonly number[], decimate?: DecimateOption, spans?: readonly SpanSelection[]): LayerDrawStats;
|
|
83
83
|
/**
|
|
84
84
|
* This key is drawable — the quantiles it actually carries are all finite at `i`.
|
|
85
85
|
* `lower`/`upper` (the whisker reach) are always required; `q1`/`q3` only when the
|