@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.
- package/CHANGELOG.md +3254 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/AreaChart.d.ts +85 -0
- package/dist/AreaChart.js +119 -0
- package/dist/BandChart.d.ts +55 -0
- package/dist/BandChart.js +93 -0
- package/dist/BarChart.d.ts +72 -0
- package/dist/BarChart.js +137 -0
- package/dist/BoxPlot.d.ts +77 -0
- package/dist/BoxPlot.js +137 -0
- package/dist/Canvas.d.ts +37 -0
- package/dist/Canvas.js +39 -0
- package/dist/ChartContainer.d.ts +106 -0
- package/dist/ChartContainer.js +306 -0
- package/dist/ChartRow.d.ts +29 -0
- package/dist/ChartRow.js +215 -0
- package/dist/Layers.d.ts +22 -0
- package/dist/Layers.js +399 -0
- package/dist/LineChart.d.ts +60 -0
- package/dist/LineChart.js +105 -0
- package/dist/ScatterChart.d.ts +84 -0
- package/dist/ScatterChart.js +139 -0
- package/dist/TimeAxis.d.ts +9 -0
- package/dist/TimeAxis.js +12 -0
- package/dist/XAxis.d.ts +39 -0
- package/dist/XAxis.js +84 -0
- package/dist/YAxis.d.ts +42 -0
- package/dist/YAxis.js +86 -0
- package/dist/annotations.d.ts +110 -0
- package/dist/annotations.js +459 -0
- package/dist/area.d.ts +54 -0
- package/dist/area.js +186 -0
- package/dist/band.d.ts +31 -0
- package/dist/band.js +57 -0
- package/dist/bars.d.ts +96 -0
- package/dist/bars.js +171 -0
- package/dist/box.d.ts +59 -0
- package/dist/box.js +140 -0
- package/dist/chip.d.ts +23 -0
- package/dist/chip.js +43 -0
- package/dist/cjs-fallback.cjs +16 -0
- package/dist/context.d.ts +362 -0
- package/dist/context.js +5 -0
- package/dist/curve.d.ts +22 -0
- package/dist/curve.js +13 -0
- package/dist/data.d.ts +154 -0
- package/dist/data.js +197 -0
- package/dist/domain.d.ts +19 -0
- package/dist/domain.js +61 -0
- package/dist/encoding.d.ts +89 -0
- package/dist/encoding.js +144 -0
- package/dist/format.d.ts +53 -0
- package/dist/format.js +47 -0
- package/dist/gaps.d.ts +146 -0
- package/dist/gaps.js +209 -0
- package/dist/grid.d.ts +11 -0
- package/dist/grid.js +29 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +34 -0
- package/dist/line.d.ts +46 -0
- package/dist/line.js +88 -0
- package/dist/range.d.ts +15 -0
- package/dist/range.js +27 -0
- package/dist/scatter.d.ts +70 -0
- package/dist/scatter.js +213 -0
- package/dist/select.d.ts +13 -0
- package/dist/select.js +23 -0
- package/dist/slots.d.ts +48 -0
- package/dist/slots.js +64 -0
- package/dist/theme.d.ts +224 -0
- package/dist/theme.js +232 -0
- package/dist/tracker.d.ts +30 -0
- package/dist/tracker.js +47 -0
- package/dist/use-slot-key.d.ts +21 -0
- package/dist/use-slot-key.js +25 -0
- package/dist/viewport.d.ts +20 -0
- package/dist/viewport.js +30 -0
- 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
|