@pond-ts/charts 0.57.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +1070 -1
  2. package/dist/AreaChart.d.ts +12 -1
  3. package/dist/AreaChart.js +131 -13
  4. package/dist/BarChart.js +184 -30
  5. package/dist/BarList.d.ts +85 -5
  6. package/dist/BarList.js +25 -4
  7. package/dist/BoxList.d.ts +70 -3
  8. package/dist/BoxList.js +21 -7
  9. package/dist/BoxPlot.d.ts +2 -1
  10. package/dist/BoxPlot.js +101 -9
  11. package/dist/Candlestick.d.ts +13 -1
  12. package/dist/Candlestick.js +89 -3
  13. package/dist/ChartContainer.d.ts +36 -48
  14. package/dist/ChartContainer.js +465 -59
  15. package/dist/ChartRow.d.ts +9 -2
  16. package/dist/ChartRow.js +86 -12
  17. package/dist/HeatMap.d.ts +176 -0
  18. package/dist/HeatMap.js +344 -0
  19. package/dist/Layers.d.ts +5 -1
  20. package/dist/Layers.js +1014 -253
  21. package/dist/Legend.js +8 -4
  22. package/dist/LineChart.d.ts +18 -1
  23. package/dist/LineChart.js +165 -4
  24. package/dist/ListTable.d.ts +30 -3
  25. package/dist/ListTable.js +381 -23
  26. package/dist/ScatterChart.d.ts +3 -2
  27. package/dist/ScatterChart.js +68 -4
  28. package/dist/XAxis.js +40 -22
  29. package/dist/area.d.ts +34 -1
  30. package/dist/area.js +88 -1
  31. package/dist/bars.d.ts +57 -3
  32. package/dist/bars.js +237 -26
  33. package/dist/box.d.ts +2 -2
  34. package/dist/box.js +158 -40
  35. package/dist/brush.d.ts +142 -0
  36. package/dist/brush.js +179 -0
  37. package/dist/child-index.d.ts +27 -0
  38. package/dist/child-index.js +57 -0
  39. package/dist/context.d.ts +859 -33
  40. package/dist/cursors.d.ts +161 -0
  41. package/dist/cursors.js +503 -0
  42. package/dist/decimate.d.ts +78 -1
  43. package/dist/decimate.js +157 -0
  44. package/dist/heat.d.ts +163 -0
  45. package/dist/heat.js +659 -0
  46. package/dist/index.d.ts +11 -2
  47. package/dist/index.js +22 -0
  48. package/dist/line.d.ts +137 -0
  49. package/dist/line.js +328 -0
  50. package/dist/ohlc.d.ts +16 -1
  51. package/dist/ohlc.js +93 -4
  52. package/dist/scatter.d.ts +17 -9
  53. package/dist/scatter.js +221 -33
  54. package/dist/select.d.ts +13 -5
  55. package/dist/select.js +14 -6
  56. package/dist/selection-fixtures.d.ts +174 -0
  57. package/dist/selection-fixtures.js +569 -0
  58. package/dist/selection-stories.d.ts +73 -0
  59. package/dist/selection-stories.js +301 -0
  60. package/dist/selectors.d.ts +316 -0
  61. package/dist/selectors.js +391 -0
  62. package/dist/span.d.ts +122 -0
  63. package/dist/span.js +203 -0
  64. package/dist/sweep.d.ts +154 -0
  65. package/dist/sweep.js +282 -0
  66. package/dist/theme.d.ts +456 -5
  67. package/dist/theme.js +217 -41
  68. package/dist/tracker.d.ts +6 -0
  69. package/dist/tracker.js +6 -0
  70. package/dist/tradingAxis.fixture.d.ts +78 -0
  71. package/dist/tradingAxis.fixture.js +215 -0
  72. package/dist/useChartLegend.js +18 -3
  73. package/package.json +3 -3
@@ -45,7 +45,7 @@
45
45
  * `xScale` and breaks its subpath on `NaN`) — decimation is a pre-pass that
46
46
  * shrinks the point count, not a second renderer.
47
47
  */
48
- import type { ChartSeries, BandSeries, OhlcSeries, BoxSeries, BarSeries } from './data.js';
48
+ import type { ChartSeries, BandSeries, OhlcSeries, BoxSeries, BarSeries, StackedBarSeries } from './data.js';
49
49
  import type { Scale } from './line.js';
50
50
  /**
51
51
  * A line layer's M4-decimation control (`<LineChart decimate>`). **Default
@@ -305,6 +305,83 @@ export interface BarColumnEnvelope {
305
305
  * `resolveBarBaseline`), so the union is honest about the zero line.
306
306
  */
307
307
  export declare function decimateBars(cs: BarSeries, xScale: Scale, ctx: CanvasRenderingContext2D, baseline: number, k?: number, visibleCount?: number): BarColumnEnvelope | null;
308
+ /**
309
+ * Decimate a heat-map grid to **one cell per pixel column per row**, each the
310
+ * **mean** of the source cells that fall in it ([PND-HEATMAP]).
311
+ *
312
+ * **Why the mean, and why a heat map can decimate where a coloured bar cannot.**
313
+ * {@link decimateBars} deliberately gives up when `binFills` is set: its
314
+ * reduction is a *geometric* union (`[min, max]` per column), and one rect
315
+ * spanning many differently-coloured bars has no honest colour. A heat map is
316
+ * not in that bind, because its reduction is not geometric. Cells do not form a
317
+ * silhouette — they **composite**: each one covers its own patch of the column,
318
+ * so what the eye receives from a column holding N cells is their area-weighted
319
+ * average. Taking the mean of the *values* and letting the existing ramp colour
320
+ * it is therefore not a statistical choice imposed on the reader; it is what the
321
+ * full-resolution draw already resolves to at this size. Banding the mean also
322
+ * keeps the result **inside the ramp**, so it stays a colour the legend defines
323
+ * (averaging the colours instead would invent off-ramp shades, and would need
324
+ * linear-light care to avoid the usual downsampling darkening).
325
+ *
326
+ * What this **replaces** is worse than it: undecimated, sub-pixel cells are
327
+ * widened to `minWidth` about their midpoints, so they overlap and later draws
328
+ * overpaint earlier ones. The column already showed one cell out of N — chosen
329
+ * by loop order. The mean is both cheaper and more honest than that.
330
+ *
331
+ * **What it loses:** a lone extreme cell among N averages away, exactly as it
332
+ * would in any image downsample. A reader hunting rare spikes should bin the
333
+ * *series* coarsely with `aggregate` and a reducer that says so (`max`), which
334
+ * is the tool that makes the claim explicit and is unaffected by this.
335
+ *
336
+ * Gates on the **visible** cell columns (`visibleCount`), like the bar path.
337
+ * Returns `null` when decimation doesn't apply — below the density threshold, a
338
+ * domainless or non-invertible scale, or no canvas width — and the caller then
339
+ * draws every visible cell. The result carries **no `marks`**: a pixel column
340
+ * aggregates many source bins and so has no stable per-bin identity, which is
341
+ * also why the caller suppresses the live-cell outline while decimated and
342
+ * keeps hit-testing against the source grid ({@link decimateBars} does the same).
343
+ */
344
+ export declare function decimateHeat(ss: StackedBarSeries, binScale: Scale, ctx: CanvasRenderingContext2D, k?: number, vStart?: number, vEnd?: number,
345
+ /**
346
+ * The bin axis' extent, when it is **not** the x axis. A horizontal heat map
347
+ * puts its bins on y, where the shared helpers do not apply: `scaleRangeWidth`
348
+ * reads `range()[last]`, which is `0` for the usual inverted y range, and
349
+ * `deviceBucketCount` reads the canvas' *width*. Omitted ⇒ the x-axis
350
+ * defaults, which is every other caller.
351
+ */
352
+ axis?: {
353
+ deviceCount: number;
354
+ spanCss: number;
355
+ }): StackedBarSeries | null;
356
+ /**
357
+ * The **y half** of heat-map decimation: collapse runs of `stride` rows into one,
358
+ * each the mean of the rows it covers ([PND-HEATMAP]).
359
+ *
360
+ * The x half ({@link decimateHeat}) is not enough on its own, and a gene
361
+ * expression matrix is the case that proves it: 10,000 genes x 8 samples is
362
+ * **16.7 rows per pixel row** and only 8 bins, so the column decimator declines
363
+ * and every one of the 80,000 cells is drawn to show ~4,800 distinguishable
364
+ * ones. Whichever axis is oversampled, the argument is the same — rows sharing a
365
+ * pixel row composite, so what the eye receives is their mean.
366
+ *
367
+ * **A fixed integer stride, not a pixel-edge walk.** Source rows are *unit
368
+ * slots* (`[g, g+1]`, which is what lets `binCategories` label them), so they
369
+ * are already uniform in row-index space; a run of `stride` of them is exactly
370
+ * `[r·stride, (r+1)·stride]`. That keeps the y coordinate space **unchanged** —
371
+ * which is load-bearing, because `<YAxis>` scales over `[0, G]` and explicit
372
+ * `{ at, label }` ticks are in those units. Rewriting the row bands the way the
373
+ * x half rewrites bin spans would silently slide every axis label.
374
+ *
375
+ * Returns `null` below the gate (`stride < k` — fewer than `k` rows per device
376
+ * row, where drawing every row is honest and the reduction would not pay), so
377
+ * the caller draws the source rows. The final run is short when `rows` is not a
378
+ * multiple of `stride`; it averages what is there.
379
+ */
380
+ export declare function decimateHeatRows(values: Float64Array, bins: number, rows: number, deviceRows: number, k?: number): {
381
+ values: Float64Array;
382
+ rows: number;
383
+ stride: number;
384
+ } | null;
308
385
  /**
309
386
  * Decimate a **uniform** scatter to one representative mark per occupied
310
387
  * **pixel cell** ([PND-MARKDEC] scatter half) — the marks analog of
package/dist/decimate.js CHANGED
@@ -607,6 +607,163 @@ export function decimateBars(cs, xScale, ctx, baseline, k = 2, visibleCount = cs
607
607
  }
608
608
  return { begin, end, lo, hi, length: W };
609
609
  }
610
+ /**
611
+ * Decimate a heat-map grid to **one cell per pixel column per row**, each the
612
+ * **mean** of the source cells that fall in it ([PND-HEATMAP]).
613
+ *
614
+ * **Why the mean, and why a heat map can decimate where a coloured bar cannot.**
615
+ * {@link decimateBars} deliberately gives up when `binFills` is set: its
616
+ * reduction is a *geometric* union (`[min, max]` per column), and one rect
617
+ * spanning many differently-coloured bars has no honest colour. A heat map is
618
+ * not in that bind, because its reduction is not geometric. Cells do not form a
619
+ * silhouette — they **composite**: each one covers its own patch of the column,
620
+ * so what the eye receives from a column holding N cells is their area-weighted
621
+ * average. Taking the mean of the *values* and letting the existing ramp colour
622
+ * it is therefore not a statistical choice imposed on the reader; it is what the
623
+ * full-resolution draw already resolves to at this size. Banding the mean also
624
+ * keeps the result **inside the ramp**, so it stays a colour the legend defines
625
+ * (averaging the colours instead would invent off-ramp shades, and would need
626
+ * linear-light care to avoid the usual downsampling darkening).
627
+ *
628
+ * What this **replaces** is worse than it: undecimated, sub-pixel cells are
629
+ * widened to `minWidth` about their midpoints, so they overlap and later draws
630
+ * overpaint earlier ones. The column already showed one cell out of N — chosen
631
+ * by loop order. The mean is both cheaper and more honest than that.
632
+ *
633
+ * **What it loses:** a lone extreme cell among N averages away, exactly as it
634
+ * would in any image downsample. A reader hunting rare spikes should bin the
635
+ * *series* coarsely with `aggregate` and a reducer that says so (`max`), which
636
+ * is the tool that makes the claim explicit and is unaffected by this.
637
+ *
638
+ * Gates on the **visible** cell columns (`visibleCount`), like the bar path.
639
+ * Returns `null` when decimation doesn't apply — below the density threshold, a
640
+ * domainless or non-invertible scale, or no canvas width — and the caller then
641
+ * draws every visible cell. The result carries **no `marks`**: a pixel column
642
+ * aggregates many source bins and so has no stable per-bin identity, which is
643
+ * also why the caller suppresses the live-cell outline while decimated and
644
+ * keeps hit-testing against the source grid ({@link decimateBars} does the same).
645
+ */
646
+ export function decimateHeat(ss, binScale, ctx, k = 2, vStart = 0, vEnd = ss.length,
647
+ /**
648
+ * The bin axis' extent, when it is **not** the x axis. A horizontal heat map
649
+ * puts its bins on y, where the shared helpers do not apply: `scaleRangeWidth`
650
+ * reads `range()[last]`, which is `0` for the usual inverted y range, and
651
+ * `deviceBucketCount` reads the canvas' *width*. Omitted ⇒ the x-axis
652
+ * defaults, which is every other caller.
653
+ */
654
+ axis) {
655
+ const visibleCount = vEnd - vStart;
656
+ if (axis === undefined
657
+ ? !shouldDecimateCount(visibleCount, ctx, k)
658
+ : visibleCount < k * axis.deviceCount)
659
+ return null;
660
+ const dom = scaleDomain(binScale);
661
+ if (dom === null || dom[1] <= dom[0])
662
+ return null;
663
+ const invert = scaleInvert(binScale);
664
+ const plotWidthCss = axis?.spanCss ?? scaleRangeWidth(binScale);
665
+ if (invert === null || plotWidthCss === null || plotWidthCss <= 0)
666
+ return null;
667
+ const W = axis?.deviceCount ?? deviceBucketCount(ctx);
668
+ if (W <= 0)
669
+ return null;
670
+ const raw = pixelEdges(invert, plotWidthCss, W);
671
+ // `pixelEdges` walks pixels ascending, so on an **inverted** axis — the usual
672
+ // y range `[h, 0]`, which a horizontal heat map's bin axis uses — the key-space
673
+ // edges come back descending. The sweep below and the emitted `begin`/`end`
674
+ // both want ascending key order, and the draw maps back through the scale
675
+ // anyway, so normalize here rather than special-casing two directions.
676
+ const edges = raw[0] <= raw[W] ? raw : raw.slice().reverse();
677
+ const G = ss.groups.length;
678
+ const sum = new Float64Array(W * G);
679
+ const count = new Float64Array(W * G);
680
+ // `ss.begin` is ascending and so are `edges`, so the column advances
681
+ // monotonically — one O(V + W) sweep rather than a search per bin.
682
+ let col = 0;
683
+ for (let b = vStart; b < vEnd; b += 1) {
684
+ const key = ss.begin[b];
685
+ while (col < W - 1 && key >= edges[col + 1])
686
+ col += 1;
687
+ const src = b * G;
688
+ const dst = col * G;
689
+ for (let g = 0; g < G; g += 1) {
690
+ const v = ss.values[src + g];
691
+ // A hole contributes nothing rather than dragging the mean to zero; a
692
+ // column of nothing but holes stays a hole.
693
+ if (!Number.isFinite(v))
694
+ continue;
695
+ sum[dst + g] += v;
696
+ count[dst + g] += 1;
697
+ }
698
+ }
699
+ const begin = new Float64Array(W);
700
+ const end = new Float64Array(W);
701
+ const values = new Float64Array(W * G);
702
+ for (let c = 0; c < W; c += 1) {
703
+ begin[c] = edges[c];
704
+ end[c] = edges[c + 1];
705
+ const dst = c * G;
706
+ for (let g = 0; g < G; g += 1) {
707
+ const n = count[dst + g];
708
+ values[dst + g] = n > 0 ? sum[dst + g] / n : NaN;
709
+ }
710
+ }
711
+ return { begin, end, values, groups: ss.groups, length: W };
712
+ }
713
+ /**
714
+ * The **y half** of heat-map decimation: collapse runs of `stride` rows into one,
715
+ * each the mean of the rows it covers ([PND-HEATMAP]).
716
+ *
717
+ * The x half ({@link decimateHeat}) is not enough on its own, and a gene
718
+ * expression matrix is the case that proves it: 10,000 genes x 8 samples is
719
+ * **16.7 rows per pixel row** and only 8 bins, so the column decimator declines
720
+ * and every one of the 80,000 cells is drawn to show ~4,800 distinguishable
721
+ * ones. Whichever axis is oversampled, the argument is the same — rows sharing a
722
+ * pixel row composite, so what the eye receives is their mean.
723
+ *
724
+ * **A fixed integer stride, not a pixel-edge walk.** Source rows are *unit
725
+ * slots* (`[g, g+1]`, which is what lets `binCategories` label them), so they
726
+ * are already uniform in row-index space; a run of `stride` of them is exactly
727
+ * `[r·stride, (r+1)·stride]`. That keeps the y coordinate space **unchanged** —
728
+ * which is load-bearing, because `<YAxis>` scales over `[0, G]` and explicit
729
+ * `{ at, label }` ticks are in those units. Rewriting the row bands the way the
730
+ * x half rewrites bin spans would silently slide every axis label.
731
+ *
732
+ * Returns `null` below the gate (`stride < k` — fewer than `k` rows per device
733
+ * row, where drawing every row is honest and the reduction would not pay), so
734
+ * the caller draws the source rows. The final run is short when `rows` is not a
735
+ * multiple of `stride`; it averages what is there.
736
+ */
737
+ export function decimateHeatRows(values, bins, rows, deviceRows, k = 2) {
738
+ if (!(deviceRows > 0) || !(rows > 0))
739
+ return null;
740
+ const stride = Math.ceil(rows / deviceRows);
741
+ if (stride < k)
742
+ return null;
743
+ const out = Math.ceil(rows / stride);
744
+ const reduced = new Float64Array(bins * out);
745
+ for (let b = 0; b < bins; b += 1) {
746
+ const src = b * rows;
747
+ const dst = b * out;
748
+ for (let r = 0; r < out; r += 1) {
749
+ const g0 = r * stride;
750
+ const g1 = Math.min(g0 + stride, rows);
751
+ let sum = 0;
752
+ let n = 0;
753
+ for (let g = g0; g < g1; g += 1) {
754
+ const v = values[src + g];
755
+ // A hole contributes nothing rather than pulling the mean toward zero;
756
+ // a run of nothing but holes stays a hole.
757
+ if (!Number.isFinite(v))
758
+ continue;
759
+ sum += v;
760
+ n += 1;
761
+ }
762
+ reduced[dst + r] = n > 0 ? sum / n : NaN;
763
+ }
764
+ }
765
+ return { values: reduced, rows: out, stride };
766
+ }
610
767
  /**
611
768
  * Decimate a **uniform** scatter to one representative mark per occupied
612
769
  * **pixel cell** ([PND-MARKDEC] scatter half) — the marks analog of
package/dist/heat.d.ts ADDED
@@ -0,0 +1,163 @@
1
+ import type { StackedBarSeries } from './data.js';
2
+ import type { Scale } from './line.js';
3
+ import type { Orientation, StackMark } from './bars.js';
4
+ import type { SpanSelection } from './context.js';
5
+ import type { HeatStates } from './theme.js';
6
+ import { type DecimateOption } from './decimate.js';
7
+ /**
8
+ * Heat-map geometry: a grid of cells, each filled by the colour its **value**
9
+ * maps to. Bins run along x, the series' **columns** run down y, and colour
10
+ * carries the aggregate.
11
+ *
12
+ * **Why this reuses {@link StackedBarSeries}.** That type is already exactly a
13
+ * heat map's data: `[begin, end]` spans per bin, a named second dimension in
14
+ * `groups`, and a row-major `length × groups.length` grid of `values`. So a heat
15
+ * map needs **no reader of its own** — `stacksFromColumns(series, columns)`
16
+ * produces all four shapes pond can express today:
17
+ *
18
+ * | source | columns | x axis |
19
+ * | --- | --- | --- |
20
+ * | `TimeSeries` | one | time intervals — a stripe |
21
+ * | `TimeSeries` | many | time intervals — a grid |
22
+ * | `ValueSeries` | one | value intervals — a bin stripe |
23
+ * | `ValueSeries` | many | value intervals — a grid |
24
+ *
25
+ * The stripe is just `groups.length === 1`, so there is one draw path, not two.
26
+ *
27
+ * **What that buys on x.** Because the spans are the ordinary bin spans, the
28
+ * whole of pond's binning machinery applies unchanged — `aggregate` over a
29
+ * trading calendar with sessions, `Sequence.calendar` day/week/month buckets,
30
+ * `byColumn` value bands. The heat map inherits all of it by not having an
31
+ * opinion.
32
+ *
33
+ * **What it costs on y.** The y dimension **must be columns**. A month-of-year
34
+ * grid means building a column per month; a per-city grid means a column per
35
+ * city (`pivotByGroup`'s long→wide output, or `partitionBy` reshaped). That is
36
+ * a real constraint, and a deliberate one: it keeps the second dimension in the
37
+ * data model, where pond's own reshaping operators can produce it, instead of
38
+ * inventing a chart-level pivot.
39
+ */
40
+ /** Cell styling. Colour is data and comes from the caller's ramp, so this is
41
+ * only the geometry and the live-cell treatment. */
42
+ /**
43
+ * How value maps onto the ramp's bands.
44
+ *
45
+ * `'linear'` splits the domain into equal-width bands. `'log'` splits it into
46
+ * equal-*ratio* bands, which is what a quantity spanning orders of magnitude
47
+ * needs: US measles incidence runs from ~2,900 per 100k before the vaccine to
48
+ * under 1 after it, and linear banding over eight colours puts everything below
49
+ * ~360 in one band — the whole post-1965 record, which is the half the chart
50
+ * exists to show.
51
+ */
52
+ export type HeatScale = 'linear' | 'log';
53
+ /** How a cell with no value is drawn. */
54
+ export type HeatNoData = 'blank' | 'hatch';
55
+ export interface HeatStyle {
56
+ /** Alpha for a resting cell. A live cell pops to 1, as bars do. */
57
+ readonly opacity: number;
58
+ /** Outline colour for the selected cell. */
59
+ readonly highlight: string;
60
+ /** Selected-cell stroke width in px. */
61
+ readonly outlineWidth: number;
62
+ /** Px inset around each cell, in both axes. `0` tiles them flush. */
63
+ readonly gap: number;
64
+ /** Px floor on a cell's width, so a thin bin stays visible. */
65
+ readonly minWidth: number;
66
+ /** Stroke for the `'hatch'` no-data fill — the theme's grid colour, so it
67
+ * reads as chart furniture rather than as a value. */
68
+ readonly gridColor: string;
69
+ /**
70
+ * The **interaction states** ({@link HeatStates}), from `theme.heat`. Unset
71
+ * ⇒ the pre-states treatment exactly: a live cell gets one outline of its
72
+ * own in {@link highlight}, `outlineWidth` for hover and twice that for
73
+ * selection, and nothing recedes.
74
+ */
75
+ readonly states?: HeatStates;
76
+ }
77
+ /**
78
+ * Map a value onto a **banded** ramp: `colors` split `[lo, hi]` into equal
79
+ * steps and a value takes the colour of the band it falls in.
80
+ *
81
+ * Banded rather than interpolated on purpose. It is what the climate-stripes
82
+ * card does today (its `anomalyStep` buckets into the ramp's length, which this
83
+ * replaces), it is the conventional reading for stripes and calendar heat maps,
84
+ * and a banded scale is honest about resolution in a way a smooth gradient is
85
+ * not — you can count the steps and read a cell against a legend. With nine or
86
+ * more stops it is visually indistinguishable from a gradient anyway.
87
+ *
88
+ * A non-finite value, or an empty ramp, yields `undefined` — the caller decides
89
+ * whether that is a skipped cell or a fallback fill.
90
+ */
91
+ export declare function bandedColor(value: number, colors: readonly string[], lo: number, hi: number, scale?: HeatScale): string | undefined;
92
+ /**
93
+ * The `[min, max]` of the finite values across **every** cell — the colour
94
+ * domain when the caller does not pin one. `null` when nothing is finite.
95
+ *
96
+ * Deliberately **not** widened to include `0`, unlike `barExtent`: a bar's
97
+ * height is measured from a baseline so zero must be in the domain, but a
98
+ * cell's colour is measured against the data's own range. Widening would waste
99
+ * half the ramp on an all-positive grid.
100
+ *
101
+ * Note this spans the **whole grid**, not each row — every row is read against
102
+ * one scale, which is what makes rows comparable to each other.
103
+ */
104
+ export declare function heatValueExtent(ss: StackedBarSeries): [number, number] | null;
105
+ /**
106
+ * The pixel rect of the cell at bin `b`, row `g` — `[x0, x1, yTop, yBottom]`,
107
+ * ascending on both axes — or `null` for a gap (non-finite value), which draws
108
+ * nothing and owns no hit region so a hole in the record reads as a hole.
109
+ *
110
+ * x comes from the bin's own span via {@link barSpanPx}, shared with bars so
111
+ * cells and bars tile identically. y is the row's **unit slot** `[g, g+1]`
112
+ * through the y scale, which is why the layer reports `yExtent` as `[0, G]` and
113
+ * labels rows via `binCategories` at each slot centre.
114
+ *
115
+ * **Row order follows the y scale**, so with the usual inverted pixel range row
116
+ * `0` sits at the *bottom*. That matches the existing band-axis convention
117
+ * (a horizontal histogram's first bin is its lowest), and a caller who wants
118
+ * the first column at the top reverses the column list.
119
+ */
120
+ export declare function cellRect(ss: StackedBarSeries, b: number, g: number, xScale: Scale, yScale: Scale, gapPx: number, minWidthPx: number, orientation?: Orientation): [x0: number, x1: number, yTop: number, yBottom: number] | null;
121
+ /**
122
+ * Fill one rectangle per cell, coloured by `colorAt(b, g)`. A gap is skipped.
123
+ *
124
+ * A live cell keeps its **own** colour. The colour is never swapped for a
125
+ * highlight, because that colour *is* the datum — replacing it would erase the
126
+ * reading the chart exists to give.
127
+ *
128
+ * That rules out the bar layers' usual affordance too. A bar says "live" by
129
+ * popping from `opacity` to 1, which on a heat map is both invisible (a ramp is
130
+ * normally drawn at full opacity already) and, where it isn't, actively
131
+ * misleading — dimming a cell shifts where the reader places it on the colour
132
+ * scale. So a live cell is marked by an **outline** instead: `outlineWidth` for
133
+ * hover, twice that for selection, both in `style.highlight`. The alpha pop is
134
+ * kept as well, so a theme that does draw cells translucent still behaves like
135
+ * its bars.
136
+ *
137
+ * Hover and selection share one colour deliberately — whether they should
138
+ * diverge is the open question in #577, and this layer should not pre-empt it.
139
+ *
140
+ * Both `selection` and `hovered` are **sets**: `ContainerFrame.selected` has
141
+ * been one since [PND-MULTISEL] and `hovered` since RFC A4.3, so **every** cell
142
+ * a member names lights — a pinned group of cells, or a drag-sweep hovering
143
+ * several at once, all read back rather than only the set's first member. A cell
144
+ * in **both** sets reads as selected (selected outranks hovered, the precedence
145
+ * `drawBars` / `drawStacks` / `drawBox` share) and takes one outline, never two.
146
+ *
147
+ * O(visible × G) after viewport culling on the bin axis, plus O(|set|) per
148
+ * visible **bin** (not per cell — see {@link binLabelsInto}) and only when a set
149
+ * names this layer at all, so a resting draw costs exactly what it did.
150
+ */
151
+ export declare function drawHeat(ctx: CanvasRenderingContext2D, ss: StackedBarSeries, xScale: Scale, yScale: Scale, style: HeatStyle, colorOf: (value: number) => string | undefined, seriesId: string | undefined, selection?: readonly StackMark[], hovered?: readonly StackMark[], decimate?: DecimateOption, orientation?: Orientation, noData?: HeatNoData, spans?: readonly SpanSelection[]): void;
152
+ /**
153
+ * Hit-test plot-pixel `(px, py)` against the grid — the first cell whose rect
154
+ * contains the point, or `null`. Returns `[bin, row, begin, rowName, value]`.
155
+ *
156
+ * The **value** is the whole point of the layer. A constant-height bar carries
157
+ * none, which is why the climate-stripes card looks its number up out-of-band;
158
+ * a cell answers directly, and so can the cursor.
159
+ *
160
+ * O(N × G), as `stackAt` is: bin and row counts are view-scale, clicks are rare.
161
+ */
162
+ export declare function heatAt(ss: StackedBarSeries, px: number, py: number, xScale: Scale, yScale: Scale, gapPx: number, minWidthPx: number, orientation?: Orientation): [bin: number, row: number, begin: number, name: string, value: number] | null;
163
+ //# sourceMappingURL=heat.d.ts.map