@pond-ts/charts 0.31.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 (79) hide show
  1. package/CHANGELOG.md +3254 -0
  2. package/LICENSE +21 -0
  3. package/README.md +229 -0
  4. package/dist/AreaChart.d.ts +85 -0
  5. package/dist/AreaChart.js +119 -0
  6. package/dist/BandChart.d.ts +55 -0
  7. package/dist/BandChart.js +93 -0
  8. package/dist/BarChart.d.ts +72 -0
  9. package/dist/BarChart.js +137 -0
  10. package/dist/BoxPlot.d.ts +77 -0
  11. package/dist/BoxPlot.js +137 -0
  12. package/dist/Canvas.d.ts +37 -0
  13. package/dist/Canvas.js +39 -0
  14. package/dist/ChartContainer.d.ts +106 -0
  15. package/dist/ChartContainer.js +306 -0
  16. package/dist/ChartRow.d.ts +29 -0
  17. package/dist/ChartRow.js +215 -0
  18. package/dist/Layers.d.ts +22 -0
  19. package/dist/Layers.js +399 -0
  20. package/dist/LineChart.d.ts +60 -0
  21. package/dist/LineChart.js +105 -0
  22. package/dist/ScatterChart.d.ts +84 -0
  23. package/dist/ScatterChart.js +139 -0
  24. package/dist/TimeAxis.d.ts +9 -0
  25. package/dist/TimeAxis.js +12 -0
  26. package/dist/XAxis.d.ts +39 -0
  27. package/dist/XAxis.js +84 -0
  28. package/dist/YAxis.d.ts +42 -0
  29. package/dist/YAxis.js +86 -0
  30. package/dist/annotations.d.ts +110 -0
  31. package/dist/annotations.js +459 -0
  32. package/dist/area.d.ts +54 -0
  33. package/dist/area.js +186 -0
  34. package/dist/band.d.ts +31 -0
  35. package/dist/band.js +57 -0
  36. package/dist/bars.d.ts +96 -0
  37. package/dist/bars.js +171 -0
  38. package/dist/box.d.ts +59 -0
  39. package/dist/box.js +140 -0
  40. package/dist/chip.d.ts +23 -0
  41. package/dist/chip.js +43 -0
  42. package/dist/cjs-fallback.cjs +16 -0
  43. package/dist/context.d.ts +362 -0
  44. package/dist/context.js +5 -0
  45. package/dist/curve.d.ts +22 -0
  46. package/dist/curve.js +13 -0
  47. package/dist/data.d.ts +154 -0
  48. package/dist/data.js +197 -0
  49. package/dist/domain.d.ts +19 -0
  50. package/dist/domain.js +61 -0
  51. package/dist/encoding.d.ts +89 -0
  52. package/dist/encoding.js +144 -0
  53. package/dist/format.d.ts +53 -0
  54. package/dist/format.js +47 -0
  55. package/dist/gaps.d.ts +146 -0
  56. package/dist/gaps.js +209 -0
  57. package/dist/grid.d.ts +11 -0
  58. package/dist/grid.js +29 -0
  59. package/dist/index.d.ts +53 -0
  60. package/dist/index.js +34 -0
  61. package/dist/line.d.ts +46 -0
  62. package/dist/line.js +88 -0
  63. package/dist/range.d.ts +15 -0
  64. package/dist/range.js +27 -0
  65. package/dist/scatter.d.ts +70 -0
  66. package/dist/scatter.js +213 -0
  67. package/dist/select.d.ts +13 -0
  68. package/dist/select.js +23 -0
  69. package/dist/slots.d.ts +48 -0
  70. package/dist/slots.js +64 -0
  71. package/dist/theme.d.ts +224 -0
  72. package/dist/theme.js +232 -0
  73. package/dist/tracker.d.ts +30 -0
  74. package/dist/tracker.js +47 -0
  75. package/dist/use-slot-key.d.ts +21 -0
  76. package/dist/use-slot-key.js +25 -0
  77. package/dist/viewport.d.ts +20 -0
  78. package/dist/viewport.js +30 -0
  79. package/package.json +67 -0
package/dist/area.js ADDED
@@ -0,0 +1,186 @@
1
+ import { area as d3area, curveLinear } from 'd3-shape';
2
+ import { bridgeGaps, collectGapEdges, drawGapBridges, drawGapFades, drawGapSteps, withAlpha, DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
3
+ /**
4
+ * The `[min, max]` vertical extent an area occupies — the finite values of
5
+ * `cs.y` widened to include `baseline`, since the fill spans from each value to
6
+ * the baseline (so the baseline must be in-domain or the fill clips). `null` if
7
+ * no value is finite. When `baseline` is `undefined` the area rests on the
8
+ * axis's own lower bound (resolved later), so only the values constrain the
9
+ * domain — matching {@link yExtent}.
10
+ *
11
+ * NaN values (the gap signal) are ignored, so a coast doesn't drag the domain.
12
+ */
13
+ export function areaExtent(cs, baseline) {
14
+ let min = Infinity;
15
+ let max = -Infinity;
16
+ for (let i = 0; i < cs.length; i += 1) {
17
+ const v = cs.y[i];
18
+ if (Number.isFinite(v)) {
19
+ if (v < min)
20
+ min = v;
21
+ if (v > max)
22
+ max = v;
23
+ }
24
+ }
25
+ if (min === Infinity)
26
+ return null;
27
+ // The fill reaches the baseline, so it must be inside the domain (an
28
+ // above/below-axis area with baseline 0 has to show the zero line).
29
+ if (baseline !== undefined) {
30
+ if (baseline < min)
31
+ min = baseline;
32
+ if (baseline > max)
33
+ max = baseline;
34
+ }
35
+ return [min, max];
36
+ }
37
+ /**
38
+ * Fill the area between `cs`'s value line and a horizontal `baseline`, with a
39
+ * vertical gradient (most opaque at the line, fading to transparent at the
40
+ * baseline) and an outline stroke on top.
41
+ *
42
+ * Two forms, selected by `baseline`:
43
+ *
44
+ * - **Elevation** (`baseline` = the axis lower bound, supplied as the resolved
45
+ * `baselineValue`): the line sits above the baseline, the shade grades down
46
+ * from it — the estela elevation look.
47
+ * - **Above/below axis** (`baseline` = `0`): positive values fill up, negative
48
+ * fill down (d3's `area` handles the zero crossing in one path). The gradient
49
+ * is anchored at the baseline pixel so each side grades *away* from the axis —
50
+ * opaque at the line, transparent at the axis — in both directions. Compose
51
+ * two layers (e.g. an "in" column and an "out" column) for the esnet
52
+ * two-colour traffic look; each layer's colour is its own `as` token (the
53
+ * single styling channel).
54
+ *
55
+ * **Gap handling is driven by `gaps`** (a {@link GapMode}, default `'empty'`).
56
+ * In every mode **the fill obeys the mode's break/bridge decision**: `'none'`
57
+ * fills straight across the gap (interior gaps interpolated via
58
+ * {@link bridgeGaps}, so the value edge bridges and the fill spans it); every
59
+ * other mode breaks the fill (`.defined(Number.isFinite)` — a coast is a hole in
60
+ * the shade, never a slab to the baseline, `docs/rfcs/charts.md` trap #2). For
61
+ * `'dashed'` / `'step'` / `'fade'` the **outline** (the value line on top)
62
+ * additionally gets an inferred bridge across each interior gap — a dashed line,
63
+ * a flat dashed line at the average of the edge values, or estela's
64
+ * fade-to-baseline — while the *fill* stays broken. `dashed` / `step` are drawn
65
+ * faint (`gapConnectorOpacity`). So the shade is always honest about absence;
66
+ * only the line offers the inferred connector.
67
+ *
68
+ * `cs.y` (a `Float64Array`) is the datum iterable; accessors read by index, so
69
+ * there's no per-point object allocation. The gradient + `globalAlpha` are
70
+ * bracketed by `save`/`restore` so they don't leak into later layers. Gap edges
71
+ * are collected by one O(N) walk ({@link collectGapEdges}).
72
+ */
73
+ export function drawArea(ctx, cs, xScale, yScale, style, baselineValue, curve = curveLinear, gaps = DEFAULT_GAP_MODE, gapConnectorOpacity = DEFAULT_GAP_CONNECTOR_OPACITY) {
74
+ const baselinePx = yScale(baselineValue);
75
+ // `none` interpolates interior gaps so the fill + outline bridge them; every
76
+ // other mode keeps NaN so d3 breaks both (the inferred line bridge, if any, is
77
+ // a separate overlay pass below).
78
+ const ys = gaps === 'none' ? bridgeGaps(cs.y, cs.length) : cs.y;
79
+ const gen = d3area()
80
+ .defined((v) => Number.isFinite(v))
81
+ .x((_, i) => xScale(cs.x[i]))
82
+ .y0(() => baselinePx)
83
+ .y1((v) => yScale(v))
84
+ .curve(curve)
85
+ .context(ctx);
86
+ ctx.save();
87
+ // The fill: a vertical gradient anchored at the baseline pixel, opaque at the
88
+ // line and transparent at the baseline (see buildGradient — handles both the
89
+ // one-sided elevation form and the two-sided above/below form). The gradient
90
+ // spans the drawn region; `ys` (gap-bridged for `none`) is what's drawn.
91
+ ctx.fillStyle = buildGradient(ctx, ys, cs.length, yScale, baselinePx, style);
92
+ ctx.globalAlpha = style.fillOpacity;
93
+ ctx.beginPath();
94
+ gen(ys);
95
+ ctx.fill();
96
+ ctx.restore();
97
+ // The outline on top: the area's top edge as a line (`lineY1` inherits the
98
+ // area's `defined`, `curve`, and `context`, so it breaks at the same gaps),
99
+ // at full opacity over the graded fill.
100
+ const outline = gen.lineY1();
101
+ ctx.save();
102
+ ctx.beginPath();
103
+ outline(ys);
104
+ ctx.strokeStyle = style.color;
105
+ ctx.lineWidth = style.width;
106
+ ctx.stroke();
107
+ ctx.restore();
108
+ // Inferred bridges for the line edge (fill stays broken). `dashed` / `step`
109
+ // are faint dashed connectors (gapConnectorOpacity); only `fade` drops to the
110
+ // area's own baseline pixel (the fill floor).
111
+ if (gaps === 'dashed' || gaps === 'step' || gaps === 'fade') {
112
+ const edges = collectGapEdges(cs.length, cs.x, (i) => cs.y[i], xScale, (i) => yScale(cs.y[i]));
113
+ if (gaps === 'dashed') {
114
+ drawGapBridges(ctx, edges, style.color, style.width, gapConnectorOpacity);
115
+ }
116
+ else if (gaps === 'step') {
117
+ drawGapSteps(ctx, edges, style.color, style.width, gapConnectorOpacity);
118
+ }
119
+ else {
120
+ drawGapFades(ctx, edges, baselinePx, style.color, style.width);
121
+ }
122
+ }
123
+ }
124
+ /**
125
+ * A vertical `CanvasGradient` for the fill, spanning the drawn region's pixel
126
+ * extent (the finite values plus the baseline) and anchored so the shade is
127
+ * most opaque at the line and fully transparent at the baseline.
128
+ *
129
+ * - **One-sided** (all values on one side of the baseline — the elevation form,
130
+ * and any single-signed traffic channel): a plain two-stop grade, opaque at
131
+ * the line edge → transparent at the baseline edge.
132
+ * - **Two-sided** (values straddle the baseline — a signed above/below series):
133
+ * a three-stop grade, opaque at the top, transparent at the baseline pixel,
134
+ * opaque again at the bottom — so each side fades toward the axis.
135
+ *
136
+ * The gradient colour is `style.fill` at full alpha (the layer's `globalAlpha`
137
+ * carries `fillOpacity`); the transparent stop is the same colour at alpha 0.
138
+ * Falls back to a solid `style.fill` when the region is degenerate (a single
139
+ * finite point, or values exactly on the baseline) — a zero-height gradient
140
+ * would paint nothing.
141
+ */
142
+ function buildGradient(ctx, ys, length, yScale, baselinePx, style) {
143
+ let topPx = Infinity; // smallest pixel y (highest on screen)
144
+ let bottomPx = -Infinity; // largest pixel y (lowest on screen)
145
+ for (let i = 0; i < length; i += 1) {
146
+ const v = ys[i];
147
+ if (!Number.isFinite(v))
148
+ continue;
149
+ const py = yScale(v);
150
+ if (py < topPx)
151
+ topPx = py;
152
+ if (py > bottomPx)
153
+ bottomPx = py;
154
+ }
155
+ if (topPx === Infinity)
156
+ return style.fill; // no finite values (caller no-ops)
157
+ // The drawn region runs from the topmost of {values, baseline} to the
158
+ // bottommost — the fill reaches the baseline, so include it.
159
+ const regionTop = Math.min(topPx, baselinePx);
160
+ const regionBottom = Math.max(bottomPx, baselinePx);
161
+ if (regionBottom - regionTop < 1e-6)
162
+ return style.fill; // degenerate height
163
+ const opaque = style.fill;
164
+ const transparent = withAlpha(style.fill, 0);
165
+ const grad = ctx.createLinearGradient(0, regionTop, 0, regionBottom);
166
+ // Baseline position within the region, as a 0..1 offset.
167
+ const baseOffset = (baselinePx - regionTop) / (regionBottom - regionTop);
168
+ if (baseOffset <= 1e-6 || baseOffset >= 1 - 1e-6) {
169
+ // One-sided: the baseline is at an edge of the region (elevation form, or a
170
+ // single-signed traffic channel), so a plain two-stop grade runs opaque at
171
+ // the line edge → transparent at the baseline edge. Both edges map to the
172
+ // same stop shape (0 opaque, 1 transparent) — the opaque end is always the
173
+ // line because the region's other extreme is the baseline.
174
+ grad.addColorStop(0, baseOffset <= 1e-6 ? transparent : opaque);
175
+ grad.addColorStop(1, baseOffset <= 1e-6 ? opaque : transparent);
176
+ }
177
+ else {
178
+ // Two-sided: values straddle the baseline — opaque at both extremes,
179
+ // transparent at the baseline pixel, so each side fades toward the axis.
180
+ grad.addColorStop(0, opaque);
181
+ grad.addColorStop(baseOffset, transparent);
182
+ grad.addColorStop(1, opaque);
183
+ }
184
+ return grad;
185
+ }
186
+ //# sourceMappingURL=area.js.map
package/dist/band.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { type CurveFactory } from 'd3-shape';
2
+ import type { BandSeries } from './data.js';
3
+ import type { Scale } from './line.js';
4
+ import type { BandStyle } from './theme.js';
5
+ /**
6
+ * The `[min, max]` vertical extent of the **drawn** band — the lowest `lower`
7
+ * and highest `upper` over samples where both edges are finite — or `null` if
8
+ * none are. Gap samples (either edge `NaN`) are excluded, matching what
9
+ * {@link drawBand} fills, so they don't drag the y-domain.
10
+ */
11
+ export declare function bandExtent(band: BandSeries): [number, number] | null;
12
+ /**
13
+ * Fill the variance envelope between `band.lower` and `band.upper`, connecting
14
+ * edges with `curve` (d3-shape; default linear).
15
+ *
16
+ * Built on d3-shape's `area()` (`y0`=lower, `y1`=upper). A **gap** — a sample
17
+ * with either edge non-finite — **breaks the fill**: a sample counts only where
18
+ * *both* edges are finite (`.defined`), so a gap ends the current subpath and
19
+ * the next finite run starts a fresh one, leaving an honest hole in the envelope
20
+ * (`docs/rfcs/charts.md` trap #2).
21
+ *
22
+ * Unlike {@link LineChart} / {@link AreaChart}, a band has **no `gaps` mode** — a
23
+ * filled envelope's break wants its own treatment (sharp edge vs. blurred),
24
+ * still to be designed; for now a band always breaks honestly at a gap.
25
+ *
26
+ * `band.lower` (a `Float64Array`) is the datum iterable; every accessor reads by
27
+ * index, so there's no per-point object allocation. `globalAlpha` carries the
28
+ * opacity and is restored so it doesn't leak into later layers.
29
+ */
30
+ export declare function drawBand(ctx: CanvasRenderingContext2D, band: BandSeries, xScale: Scale, yScale: Scale, style: BandStyle, curve?: CurveFactory): void;
31
+ //# sourceMappingURL=band.d.ts.map
package/dist/band.js ADDED
@@ -0,0 +1,57 @@
1
+ import { area as d3area, curveLinear } from 'd3-shape';
2
+ /**
3
+ * The `[min, max]` vertical extent of the **drawn** band — the lowest `lower`
4
+ * and highest `upper` over samples where both edges are finite — or `null` if
5
+ * none are. Gap samples (either edge `NaN`) are excluded, matching what
6
+ * {@link drawBand} fills, so they don't drag the y-domain.
7
+ */
8
+ export function bandExtent(band) {
9
+ let min = Infinity;
10
+ let max = -Infinity;
11
+ for (let i = 0; i < band.length; i += 1) {
12
+ const lo = band.lower[i];
13
+ const hi = band.upper[i];
14
+ if (Number.isFinite(lo) && Number.isFinite(hi)) {
15
+ if (lo < min)
16
+ min = lo;
17
+ if (hi > max)
18
+ max = hi;
19
+ }
20
+ }
21
+ return min === Infinity ? null : [min, max];
22
+ }
23
+ /**
24
+ * Fill the variance envelope between `band.lower` and `band.upper`, connecting
25
+ * edges with `curve` (d3-shape; default linear).
26
+ *
27
+ * Built on d3-shape's `area()` (`y0`=lower, `y1`=upper). A **gap** — a sample
28
+ * with either edge non-finite — **breaks the fill**: a sample counts only where
29
+ * *both* edges are finite (`.defined`), so a gap ends the current subpath and
30
+ * the next finite run starts a fresh one, leaving an honest hole in the envelope
31
+ * (`docs/rfcs/charts.md` trap #2).
32
+ *
33
+ * Unlike {@link LineChart} / {@link AreaChart}, a band has **no `gaps` mode** — a
34
+ * filled envelope's break wants its own treatment (sharp edge vs. blurred),
35
+ * still to be designed; for now a band always breaks honestly at a gap.
36
+ *
37
+ * `band.lower` (a `Float64Array`) is the datum iterable; every accessor reads by
38
+ * index, so there's no per-point object allocation. `globalAlpha` carries the
39
+ * opacity and is restored so it doesn't leak into later layers.
40
+ */
41
+ export function drawBand(ctx, band, xScale, yScale, style, curve = curveLinear) {
42
+ const gen = d3area()
43
+ .defined((_, i) => Number.isFinite(band.lower[i]) && Number.isFinite(band.upper[i]))
44
+ .x((_, i) => xScale(band.x[i]))
45
+ .y0((_, i) => yScale(band.lower[i]))
46
+ .y1((_, i) => yScale(band.upper[i]))
47
+ .curve(curve)
48
+ .context(ctx);
49
+ ctx.save();
50
+ ctx.fillStyle = style.fill;
51
+ ctx.globalAlpha = style.opacity;
52
+ ctx.beginPath();
53
+ gen(band.lower);
54
+ ctx.fill();
55
+ ctx.restore();
56
+ }
57
+ //# sourceMappingURL=band.js.map
package/dist/bars.d.ts ADDED
@@ -0,0 +1,96 @@
1
+ import type { BarSeries } from './data.js';
2
+ import type { Scale } from './line.js';
3
+ import type { BarStyle } from './theme.js';
4
+ /**
5
+ * The `[min, max]` vertical extent the bars occupy — the finite values of `cs.y`
6
+ * **widened to include `0`**, since a bar spans from its value to the baseline
7
+ * and the baseline must be in-domain or the bar clips. `null` if no value is
8
+ * finite.
9
+ *
10
+ * Including `0` is the bar analog of {@link areaExtent} pulling a fixed baseline
11
+ * into the domain: an all-positive series auto-fits to `[0, max]` so the bars
12
+ * rest on a visible floor (the zero line), and a series that straddles zero
13
+ * shows the zero line both above and below it. An explicit `<YAxis min>` still
14
+ * wins — `resolveBarBaseline` rests the bars on that floor instead. NaN values
15
+ * (the gap signal) are ignored, so a sparse bucket doesn't drag the domain.
16
+ */
17
+ export declare function barExtent(cs: BarSeries): [number, number] | null;
18
+ /**
19
+ * The value a bar rests on, in **data** units — the baseline edge opposite its
20
+ * value. Resolved late from the axis's own domain (so it tracks the auto-fit):
21
+ *
22
+ * - When the domain spans `0` (floor ≤ 0 ≤ top — the common all-positive
23
+ * auto-fit case, since {@link barExtent} pulls `0` in): the bars rest on the
24
+ * **zero line**.
25
+ * - When the domain sits entirely above `0` (an explicit `<YAxis min={…}>` above
26
+ * zero): the bars rest on the **axis floor**, so a thin bar still reads from
27
+ * the bottom of the plot rather than hanging off a zero line below it.
28
+ * - When the domain sits entirely below `0`: the bars hang from the **axis top**
29
+ * (`0` clamped down into the domain) — the symmetric case.
30
+ *
31
+ * I.e. `0` clamped into `[floor, top]`. The domain bounds come from the plain
32
+ * `(value) => pixel` scale the row hands `draw`/`hitTest`; the runtime object is
33
+ * a d3 `ScaleLinear` carrying `.domain()`, read through a localized shape rather
34
+ * than widening the contract to d3-scale (same approach as `AreaChart`).
35
+ */
36
+ export declare function resolveBarBaseline(yScale: Scale): number;
37
+ /**
38
+ * The pixel rect of bar `i` — `[x0, x1, yTop, yBottom]`, with `x0 <= x1` and
39
+ * `yTop <= yBottom` — or `null` for a gap (non-finite value). The x-span comes
40
+ * from {@link barSpanPx} (the key's `[begin, end]`, inset by `gapPx`, floored at
41
+ * `minWidthPx`); the y-span runs between the value and the `baseline` pixel,
42
+ * normalized so a value above *or* below the baseline both yield an ascending
43
+ * rect. Shared by {@link drawBars} and {@link barAt} so the drawn rect and the
44
+ * hit rect are the same geometry.
45
+ */
46
+ export declare function barRect(cs: BarSeries, i: number, xScale: Scale, yScale: Scale, baseline: number, gapPx: number, minWidthPx: number): [x0: number, x1: number, yTop: number, yBottom: number] | null;
47
+ /**
48
+ * Fill one rectangle per bar in `cs`, each spanning its key's `[begin, end]`
49
+ * (inset by `gapPx`) from the resolved `baseline` to the value.
50
+ *
51
+ * A gap (non-finite value) is skipped — no bar, no zero-height sliver. A bar
52
+ * matching the current `selection` (same `begin` **and** the layer's own `label`)
53
+ * draws in the style's `highlight` colour **and outlined**, so a click reads back
54
+ * on the canvas; a bar matching `hovered` draws in `highlight` **without** the
55
+ * outline (a lighter "this bar is live" on pointer-over); all others use the flat
56
+ * `fill`. `globalAlpha` carries the fill opacity and is restored so it doesn't
57
+ * leak into later layers.
58
+ *
59
+ * O(N) over the events, one fill (+ optional stroke) per bar, no per-bar
60
+ * allocation beyond the rect tuple.
61
+ */
62
+ export declare function drawBars(ctx: CanvasRenderingContext2D, cs: BarSeries, xScale: Scale, yScale: Scale, style: BarStyle, baseline: number, gapPx: number, label: string, selection: {
63
+ key: number;
64
+ label: string;
65
+ } | null, hovered: {
66
+ key: number;
67
+ label: string;
68
+ } | null): void;
69
+ /**
70
+ * The index of the bar whose key span `[begin, end]` contains `time` — the bar
71
+ * **under the cursor** — or `-1` if `time` falls in no bar's span. This is the
72
+ * cursor analog of {@link barAt}'s rect-containment: unlike nearest-by-`begin`,
73
+ * it doesn't flip to the next bar once the cursor passes a wide bar's midpoint
74
+ * (the readout-on-the-wrong-bar bug). At a shared edge (`end[i] === begin[i+1]`,
75
+ * contiguous bars) the left bar wins (first match). A gap bar (non-finite value)
76
+ * still owns its span here; the caller drops it on the finiteness check, so
77
+ * hovering a gap reads no value — as the line/area tracker breaks at a gap.
78
+ *
79
+ * O(N) over the bars (view-scale counts; the cursor moves often but the scan is
80
+ * cheap and allocation-free).
81
+ */
82
+ export declare function barIndexAtTime(cs: BarSeries, time: number): number;
83
+ /**
84
+ * Hit-test plot-pixel `(px, py)` against `cs`'s bars — the **first** bar whose
85
+ * rect contains the point, or `null`. The geometry is {@link barRect}, so the
86
+ * hit rect is exactly the drawn rect (same `baseline`/`gapPx`/`minWidth`). The
87
+ * returned tuple is `[index, begin, value]` for the chart to assemble a
88
+ * `SelectInfo` (it owns the colour + label); keeping this layer free of the
89
+ * theme keeps it unit-testable without a `ChartTheme`.
90
+ *
91
+ * O(N) over the events (no spatial index — bar counts are view-scale, hundreds
92
+ * not millions; click is a rare event). Bars don't overlap in x for a sorted
93
+ * series, so "first match" is unambiguous in practice.
94
+ */
95
+ export declare function barAt(cs: BarSeries, px: number, py: number, xScale: Scale, yScale: Scale, baseline: number, gapPx: number, minWidthPx: number): [index: number, begin: number, value: number] | null;
96
+ //# sourceMappingURL=bars.d.ts.map
package/dist/bars.js ADDED
@@ -0,0 +1,171 @@
1
+ import { barSpanPx } from './range.js';
2
+ /**
3
+ * The `[min, max]` vertical extent the bars occupy — the finite values of `cs.y`
4
+ * **widened to include `0`**, since a bar spans from its value to the baseline
5
+ * and the baseline must be in-domain or the bar clips. `null` if no value is
6
+ * finite.
7
+ *
8
+ * Including `0` is the bar analog of {@link areaExtent} pulling a fixed baseline
9
+ * into the domain: an all-positive series auto-fits to `[0, max]` so the bars
10
+ * rest on a visible floor (the zero line), and a series that straddles zero
11
+ * shows the zero line both above and below it. An explicit `<YAxis min>` still
12
+ * wins — `resolveBarBaseline` rests the bars on that floor instead. NaN values
13
+ * (the gap signal) are ignored, so a sparse bucket doesn't drag the domain.
14
+ */
15
+ export function barExtent(cs) {
16
+ let min = Infinity;
17
+ let max = -Infinity;
18
+ for (let i = 0; i < cs.length; i += 1) {
19
+ const v = cs.y[i];
20
+ if (Number.isFinite(v)) {
21
+ if (v < min)
22
+ min = v;
23
+ if (v > max)
24
+ max = v;
25
+ }
26
+ }
27
+ if (min === Infinity)
28
+ return null;
29
+ // The bar reaches the baseline (0), so it must be inside the domain.
30
+ if (0 < min)
31
+ min = 0;
32
+ if (0 > max)
33
+ max = 0;
34
+ return [min, max];
35
+ }
36
+ /**
37
+ * The value a bar rests on, in **data** units — the baseline edge opposite its
38
+ * value. Resolved late from the axis's own domain (so it tracks the auto-fit):
39
+ *
40
+ * - When the domain spans `0` (floor ≤ 0 ≤ top — the common all-positive
41
+ * auto-fit case, since {@link barExtent} pulls `0` in): the bars rest on the
42
+ * **zero line**.
43
+ * - When the domain sits entirely above `0` (an explicit `<YAxis min={…}>` above
44
+ * zero): the bars rest on the **axis floor**, so a thin bar still reads from
45
+ * the bottom of the plot rather than hanging off a zero line below it.
46
+ * - When the domain sits entirely below `0`: the bars hang from the **axis top**
47
+ * (`0` clamped down into the domain) — the symmetric case.
48
+ *
49
+ * I.e. `0` clamped into `[floor, top]`. The domain bounds come from the plain
50
+ * `(value) => pixel` scale the row hands `draw`/`hitTest`; the runtime object is
51
+ * a d3 `ScaleLinear` carrying `.domain()`, read through a localized shape rather
52
+ * than widening the contract to d3-scale (same approach as `AreaChart`).
53
+ */
54
+ export function resolveBarBaseline(yScale) {
55
+ const d = yScale.domain?.();
56
+ if (!d || d.length === 0)
57
+ return 0;
58
+ const floor = Math.min(d[0], d[d.length - 1]);
59
+ const top = Math.max(d[0], d[d.length - 1]);
60
+ return Math.min(Math.max(0, floor), top);
61
+ }
62
+ /**
63
+ * The pixel rect of bar `i` — `[x0, x1, yTop, yBottom]`, with `x0 <= x1` and
64
+ * `yTop <= yBottom` — or `null` for a gap (non-finite value). The x-span comes
65
+ * from {@link barSpanPx} (the key's `[begin, end]`, inset by `gapPx`, floored at
66
+ * `minWidthPx`); the y-span runs between the value and the `baseline` pixel,
67
+ * normalized so a value above *or* below the baseline both yield an ascending
68
+ * rect. Shared by {@link drawBars} and {@link barAt} so the drawn rect and the
69
+ * hit rect are the same geometry.
70
+ */
71
+ export function barRect(cs, i, xScale, yScale, baseline, gapPx, minWidthPx) {
72
+ const v = cs.y[i];
73
+ if (!Number.isFinite(v))
74
+ return null;
75
+ const [x0, x1] = barSpanPx(cs.begin[i], cs.end[i], xScale, gapPx, minWidthPx);
76
+ const yValue = yScale(v);
77
+ const yBase = yScale(baseline);
78
+ return [x0, x1, Math.min(yValue, yBase), Math.max(yValue, yBase)];
79
+ }
80
+ /**
81
+ * Fill one rectangle per bar in `cs`, each spanning its key's `[begin, end]`
82
+ * (inset by `gapPx`) from the resolved `baseline` to the value.
83
+ *
84
+ * A gap (non-finite value) is skipped — no bar, no zero-height sliver. A bar
85
+ * matching the current `selection` (same `begin` **and** the layer's own `label`)
86
+ * draws in the style's `highlight` colour **and outlined**, so a click reads back
87
+ * on the canvas; a bar matching `hovered` draws in `highlight` **without** the
88
+ * outline (a lighter "this bar is live" on pointer-over); all others use the flat
89
+ * `fill`. `globalAlpha` carries the fill opacity and is restored so it doesn't
90
+ * leak into later layers.
91
+ *
92
+ * O(N) over the events, one fill (+ optional stroke) per bar, no per-bar
93
+ * allocation beyond the rect tuple.
94
+ */
95
+ export function drawBars(ctx, cs, xScale, yScale, style, baseline, gapPx, label, selection, hovered) {
96
+ ctx.save();
97
+ ctx.globalAlpha = style.opacity;
98
+ for (let i = 0; i < cs.length; i += 1) {
99
+ const rect = barRect(cs, i, xScale, yScale, baseline, gapPx, style.minWidth);
100
+ if (rect === null)
101
+ continue;
102
+ const [x0, x1, yTop, yBottom] = rect;
103
+ // Match by key (begin) **and** label, so two series sharing a timestamp don't
104
+ // both light up. Both the committed selection and the transient hover use the
105
+ // `highlight` fill; only the selection adds the outline, so hover reads as a
106
+ // lighter "this bar is live" and select as the committed pick.
107
+ const selected = selection !== null &&
108
+ selection.key === cs.begin[i] &&
109
+ selection.label === label;
110
+ const isHovered = hovered !== null &&
111
+ hovered.key === cs.begin[i] &&
112
+ hovered.label === label;
113
+ ctx.fillStyle = selected || isHovered ? style.highlight : style.fill;
114
+ ctx.fillRect(x0, yTop, x1 - x0, yBottom - yTop);
115
+ if (selected) {
116
+ // The selected bar gets an outline so it reads at full strength over the
117
+ // (alpha'd) fills. Stroke at full opacity (reset within the save bracket).
118
+ ctx.globalAlpha = 1;
119
+ ctx.lineWidth = style.outlineWidth;
120
+ ctx.strokeStyle = style.highlight;
121
+ ctx.strokeRect(x0, yTop, x1 - x0, yBottom - yTop);
122
+ ctx.globalAlpha = style.opacity;
123
+ }
124
+ }
125
+ ctx.restore();
126
+ }
127
+ /**
128
+ * The index of the bar whose key span `[begin, end]` contains `time` — the bar
129
+ * **under the cursor** — or `-1` if `time` falls in no bar's span. This is the
130
+ * cursor analog of {@link barAt}'s rect-containment: unlike nearest-by-`begin`,
131
+ * it doesn't flip to the next bar once the cursor passes a wide bar's midpoint
132
+ * (the readout-on-the-wrong-bar bug). At a shared edge (`end[i] === begin[i+1]`,
133
+ * contiguous bars) the left bar wins (first match). A gap bar (non-finite value)
134
+ * still owns its span here; the caller drops it on the finiteness check, so
135
+ * hovering a gap reads no value — as the line/area tracker breaks at a gap.
136
+ *
137
+ * O(N) over the bars (view-scale counts; the cursor moves often but the scan is
138
+ * cheap and allocation-free).
139
+ */
140
+ export function barIndexAtTime(cs, time) {
141
+ for (let i = 0; i < cs.length; i += 1) {
142
+ if (time >= cs.begin[i] && time <= cs.end[i])
143
+ return i;
144
+ }
145
+ return -1;
146
+ }
147
+ /**
148
+ * Hit-test plot-pixel `(px, py)` against `cs`'s bars — the **first** bar whose
149
+ * rect contains the point, or `null`. The geometry is {@link barRect}, so the
150
+ * hit rect is exactly the drawn rect (same `baseline`/`gapPx`/`minWidth`). The
151
+ * returned tuple is `[index, begin, value]` for the chart to assemble a
152
+ * `SelectInfo` (it owns the colour + label); keeping this layer free of the
153
+ * theme keeps it unit-testable without a `ChartTheme`.
154
+ *
155
+ * O(N) over the events (no spatial index — bar counts are view-scale, hundreds
156
+ * not millions; click is a rare event). Bars don't overlap in x for a sorted
157
+ * series, so "first match" is unambiguous in practice.
158
+ */
159
+ export function barAt(cs, px, py, xScale, yScale, baseline, gapPx, minWidthPx) {
160
+ for (let i = 0; i < cs.length; i += 1) {
161
+ const rect = barRect(cs, i, xScale, yScale, baseline, gapPx, minWidthPx);
162
+ if (rect === null)
163
+ continue;
164
+ const [x0, x1, yTop, yBottom] = rect;
165
+ if (px >= x0 && px <= x1 && py >= yTop && py <= yBottom) {
166
+ return [i, cs.begin[i], cs.y[i]];
167
+ }
168
+ }
169
+ return null;
170
+ }
171
+ //# sourceMappingURL=bars.js.map
package/dist/box.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ import type { BoxSeries } from './data.js';
2
+ import type { Scale } from './line.js';
3
+ import type { BoxStyle } from './theme.js';
4
+ /**
5
+ * The `[min, max]` vertical extent of the **drawn** boxes — the lowest `lower`
6
+ * whisker and highest `upper` whisker over keys where **all five** quantiles are
7
+ * finite — or `null` if none are. Gap keys (any quantile `NaN`) are excluded,
8
+ * matching what {@link drawBox} draws, so they don't drag the y-domain.
9
+ *
10
+ * Only `lower`/`upper` bound the extent: they are the outermost reach of a key
11
+ * (the whisker ends), so `q1`/`median`/`q3` lie within `[lower, upper]` for any
12
+ * well-formed quantile set and never widen it. (A malformed set where, say,
13
+ * `q3 > upper` would clip — that's an upstream data error, not the chart's to
14
+ * paper over; document, don't defend.)
15
+ */
16
+ export declare function boxExtent(box: BoxSeries): [number, number] | null;
17
+ /**
18
+ * The index of the box whose interval `[x, xEnd]` contains `time` — the box
19
+ * **under the cursor** — or `-1` if `time` is in no box. The box analog of
20
+ * `barIndexAtTime`: containment, not nearest-by-`begin` (which flips to the next
21
+ * box past a wide box's midpoint). Boxes are sorted by `x`; at a shared edge the
22
+ * left box wins. A gap box (some quantile non-finite) still owns its span here;
23
+ * the caller drops it on the finiteness check. O(N) over the boxes (view-scale).
24
+ */
25
+ export declare function boxIndexAtTime(box: BoxSeries, time: number): number;
26
+ /**
27
+ * How a box renders its spread (pjm17971): **`whisker`** (today's thin stems +
28
+ * end-caps), **`solid`** (the candlestick look — a light outer bar over the full
29
+ * `lower→upper` range with a more-prominent inner `q1→q3` box, no stems), or
30
+ * **`none`** (the `q1→q3` box only, no spread marks). The median line is drawn
31
+ * separately and is always optional (`showMedian`).
32
+ */
33
+ export type BoxShape = 'whisker' | 'solid' | 'none';
34
+ /**
35
+ * Draw a discrete box per key of `box`, mapping data→pixels through
36
+ * `xScale`/`yScale`. The bar-chart analog of {@link drawBand}: each key gets its
37
+ * own mark over its interval x-span (`barSpanPx`, inset by `gapPx` so adjacent
38
+ * boxes breathe), in the chosen {@link BoxShape}:
39
+ *
40
+ * - **`whisker`** (default) — the graded `q1→q3` box fill + outline, two whisker
41
+ * stems with end-caps out to `lower`/`upper`.
42
+ * - **`solid`** — a light outer bar over `lower→upper` (the spread) with a
43
+ * more-prominent inner `q1→q3` box (the same fill at rising opacity — so it
44
+ * reads darker on a light ground, brighter on a dark one), no stems/outline.
45
+ * - **`none`** — the `q1→q3` box fill + outline only, no spread marks.
46
+ *
47
+ * Then, if `showMedian`, the median line across the box on top. Fills are
48
+ * bracketed by `save`/`restore` so their `globalAlpha` doesn't leak.
49
+ *
50
+ * **Gap-aware**: a key with any quantile non-finite is skipped entirely (no
51
+ * partial box) — the same contract as a band gap.
52
+ *
53
+ * O(N) over the keys, a fixed number of path ops each — no per-key allocation
54
+ * beyond the `barSpanPx` tuple.
55
+ */
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
+ /** All five quantiles finite at `i` — i.e. this key is drawn. */
58
+ export declare function isFiniteBox(box: BoxSeries, i: number): boolean;
59
+ //# sourceMappingURL=box.d.ts.map