@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
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Axis value formatting — the single formatter shared by an axis's tick labels
3
+ * and the cursor readout, so a value reads the same in both places (the readout
4
+ * "matches the axes"). d3-style: a format **specifier string** (e.g. `'.0%'`,
5
+ * `',.2f'`) is applied through the scale's own `tickFormat` (the same path d3
6
+ * uses for the ticks), a **function** is used verbatim, and `undefined` falls
7
+ * back to the scale's default `tickFormat`.
8
+ */
9
+ /**
10
+ * How to format an axis's values — a d3 [format specifier]
11
+ * (https://github.com/d3/d3-format#locale_format) string, or a custom
12
+ * `(value) => string` function. Omit for the scale's d3 default.
13
+ */
14
+ export type AxisFormat = string | ((value: number) => string);
15
+ /** The slice of a d3 scale {@link resolveAxisFormat} needs — `tickFormat` with an
16
+ * optional specifier. A d3 `ScaleLinear` / `ScaleTime` satisfies it. */
17
+ interface Tickable {
18
+ tickFormat(count: number, specifier?: string): (value: number) => string;
19
+ }
20
+ /**
21
+ * Resolve an {@link AxisFormat} (or `undefined`) to a `(value) => string`
22
+ * formatter, given the `scale` it formats against and the axis `count` (so the
23
+ * default formatter is calibrated to the tick density, exactly as the axis is):
24
+ *
25
+ * - a **function** → used as-is (the scale is ignored);
26
+ * - a **specifier string** → `scale.tickFormat(count, specifier)` — d3 applies
27
+ * the specifier, so the readout matches ticks formatted the same way;
28
+ * - **`undefined`** → `scale.tickFormat(count)` — the scale's default.
29
+ */
30
+ export declare function resolveAxisFormat(scale: Tickable, count: number, format: AxisFormat | undefined): (value: number) => string;
31
+ /** The slice of a d3 **time** scale {@link resolveTimeFormat} needs. A d3
32
+ * `ScaleTime` satisfies it; its formatter takes a `Date`. */
33
+ interface TimeTickable {
34
+ tickFormat(count?: number, specifier?: string): (date: Date) => string;
35
+ }
36
+ /**
37
+ * The time analog of {@link resolveAxisFormat} — resolve an {@link AxisFormat}
38
+ * (here the string is a d3 [time specifier](https://github.com/d3/d3-time-format#locale_format),
39
+ * e.g. `'%H:%M'`) to an `(epochMs) => string` formatter against a d3 `scaleTime`,
40
+ * so the cursor-time readout matches the time-axis ticks:
41
+ *
42
+ * - a **function** → used as-is (called with epoch ms);
43
+ * - a **specifier string** → `scale.tickFormat(count, specifier)` (one format for
44
+ * every value), wrapped to take epoch ms;
45
+ * - **`undefined`** → `scale.tickFormat()` — d3's **multi-scale** time format (the
46
+ * time axis's default; no `count` so it matches `<TimeAxis>` exactly).
47
+ *
48
+ * The cursor time is epoch ms, so the resolved formatter wraps the d3 `Date`
49
+ * formatter in `new Date(ms)`.
50
+ */
51
+ export declare function resolveTimeFormat(scale: TimeTickable, count: number, format: AxisFormat | undefined): (epochMs: number) => string;
52
+ export {};
53
+ //# sourceMappingURL=format.d.ts.map
package/dist/format.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Axis value formatting — the single formatter shared by an axis's tick labels
3
+ * and the cursor readout, so a value reads the same in both places (the readout
4
+ * "matches the axes"). d3-style: a format **specifier string** (e.g. `'.0%'`,
5
+ * `',.2f'`) is applied through the scale's own `tickFormat` (the same path d3
6
+ * uses for the ticks), a **function** is used verbatim, and `undefined` falls
7
+ * back to the scale's default `tickFormat`.
8
+ */
9
+ /**
10
+ * Resolve an {@link AxisFormat} (or `undefined`) to a `(value) => string`
11
+ * formatter, given the `scale` it formats against and the axis `count` (so the
12
+ * default formatter is calibrated to the tick density, exactly as the axis is):
13
+ *
14
+ * - a **function** → used as-is (the scale is ignored);
15
+ * - a **specifier string** → `scale.tickFormat(count, specifier)` — d3 applies
16
+ * the specifier, so the readout matches ticks formatted the same way;
17
+ * - **`undefined`** → `scale.tickFormat(count)` — the scale's default.
18
+ */
19
+ export function resolveAxisFormat(scale, count, format) {
20
+ if (typeof format === 'function')
21
+ return format;
22
+ return format !== undefined
23
+ ? scale.tickFormat(count, format)
24
+ : scale.tickFormat(count);
25
+ }
26
+ /**
27
+ * The time analog of {@link resolveAxisFormat} — resolve an {@link AxisFormat}
28
+ * (here the string is a d3 [time specifier](https://github.com/d3/d3-time-format#locale_format),
29
+ * e.g. `'%H:%M'`) to an `(epochMs) => string` formatter against a d3 `scaleTime`,
30
+ * so the cursor-time readout matches the time-axis ticks:
31
+ *
32
+ * - a **function** → used as-is (called with epoch ms);
33
+ * - a **specifier string** → `scale.tickFormat(count, specifier)` (one format for
34
+ * every value), wrapped to take epoch ms;
35
+ * - **`undefined`** → `scale.tickFormat()` — d3's **multi-scale** time format (the
36
+ * time axis's default; no `count` so it matches `<TimeAxis>` exactly).
37
+ *
38
+ * The cursor time is epoch ms, so the resolved formatter wraps the d3 `Date`
39
+ * formatter in `new Date(ms)`.
40
+ */
41
+ export function resolveTimeFormat(scale, count, format) {
42
+ if (typeof format === 'function')
43
+ return format;
44
+ const tf = format !== undefined ? scale.tickFormat(count, format) : scale.tickFormat();
45
+ return (ms) => tf(new Date(ms));
46
+ }
47
+ //# sourceMappingURL=format.js.map
package/dist/gaps.d.ts ADDED
@@ -0,0 +1,146 @@
1
+ import type { Scale } from './line.js';
2
+ /**
3
+ * How a gap-aware draw layer ({@link LineChart} / {@link AreaChart}) renders a
4
+ * **gap** — a run of non-finite (`NaN`) samples, the signal a coast / dropout /
5
+ * missing bucket leaves in the columnar data (`Number.isFinite`, never
6
+ * `!= null`; see `docs/rfcs/charts.md` trap #2). One concept shared across a
7
+ * line and its area fill, so both speak the same vocabulary.
8
+ *
9
+ * **Bands deliberately have no gap mode.** A filled envelope's break wants its
10
+ * own treatment (sharp edge vs. blurred), still to be designed; for now a band
11
+ * always breaks honestly at a gap.
12
+ *
13
+ * - **`none`** — bridge straight across the gap: an interior gap is linearly
14
+ * interpolated ({@link bridgeGaps}) so the line connects the bordering points
15
+ * and the fill / band spans the gap, as if the data were continuous. A leading
16
+ * / trailing gap (no finite sample on one side) stays a break — there's nothing
17
+ * to bridge from. This is the only mode that is *not* gap-honest — use it when
18
+ * the gap is an artefact to ignore (evenly-sampled data with the odd dropped
19
+ * read), not a real absence.
20
+ * - **`empty`** *(default)* — break: end the subpath at the gap, start a fresh
21
+ * one after it (d3-shape's `.defined`). Today's behavior; the fill / band
22
+ * leaves a hole. The honest default — a gap reads as a gap.
23
+ * - **`dashed`** — the solid segments break as in `empty`, **plus** a dashed
24
+ * line bridges each gap **straight** (last-good point → next-good point). The
25
+ * fill stays broken. Reads as "we know the value resumed here, but didn't
26
+ * measure between" without pretending the line was continuous.
27
+ * - **`step`** — the solid segments break as in `empty`, **plus** a single
28
+ * **flat dashed line at the average** of the two edge values bridges each gap
29
+ * (a horizontal `- - -`, no vertical step — see {@link drawGapSteps}). The
30
+ * fill stays broken. A neutral "the value sat around here" estimate — flatter
31
+ * and less committal than `dashed`'s straight interpolation between the edges.
32
+ * - **`fade`** — estela's coast look: at each gap edge the line drops to the
33
+ * baseline on a **vertical fade to transparent** (opaque at the line,
34
+ * transparent at the baseline), and fades back in on the far side. The fill
35
+ * stays broken. Replicates estela's `es-drop` gradient
36
+ * (`packages/ui/src/DataChart.tsx`) in the canvas renderer.
37
+ *
38
+ * `dashed` and `step` are the **inferred dashed connectors** — both drawn fainter
39
+ * than the solid line (theme `gap.connectorOpacity`, {@link DEFAULT_GAP_CONNECTOR_OPACITY})
40
+ * so an inferred bridge reads as secondary to measured data. Only `fade` drops to
41
+ * a "baseline": for a line the axis floor (the y-scale's domain lower bound,
42
+ * resolved at draw time), for an {@link AreaChart} its own fill baseline.
43
+ */
44
+ export type GapMode = 'none' | 'empty' | 'dashed' | 'step' | 'fade';
45
+ /** The default gap mode — break at the gap, leave a hole (today's behavior). */
46
+ export declare const DEFAULT_GAP_MODE: GapMode;
47
+ /**
48
+ * Default opacity for the inferred dashed gap connectors (`dashed` / `step`) when
49
+ * a theme sets no `gap.connectorOpacity` — fainter than the solid line so the
50
+ * inferred bridge reads as secondary to measured data.
51
+ */
52
+ export declare const DEFAULT_GAP_CONNECTOR_OPACITY = 0.5;
53
+ /**
54
+ * One gap in a columnar value series: the index of the last finite sample
55
+ * **before** the gap and the first finite sample **after** it, with their pixel
56
+ * coordinates already resolved. Only **interior** gaps (a finite sample on each
57
+ * side) become edges — a leading / trailing gap has nothing to bridge, so it's
58
+ * skipped (the solid path simply starts / ends at the first / last finite run,
59
+ * matching the d3 `.defined` break).
60
+ */
61
+ export interface GapEdge {
62
+ /** Index of the last finite sample before the gap. */
63
+ readonly fromIndex: number;
64
+ /** Index of the first finite sample after the gap. */
65
+ readonly toIndex: number;
66
+ /** Pixel x of the last-good sample (`fromIndex`). */
67
+ readonly fromX: number;
68
+ /** Pixel y of the last-good sample (`fromIndex`). */
69
+ readonly fromY: number;
70
+ /** Pixel x of the next-good sample (`toIndex`). */
71
+ readonly toX: number;
72
+ /** Pixel y of the next-good sample (`toIndex`). */
73
+ readonly toY: number;
74
+ }
75
+ /**
76
+ * Return a copy of `values` with **interior** gaps (NaN runs that have a finite
77
+ * sample on each side) linearly interpolated across, for the `none` mode — so d3
78
+ * sees a continuous finite series and both the line and (for an area) the fill
79
+ * bridge the gap with real path ops. Leading / trailing NaNs (no finite anchor
80
+ * on one side) are left NaN: there's nothing to interpolate from, so they stay a
81
+ * break (a `lineTo`/`moveTo` with a NaN coord is dropped by the canvas spec, so
82
+ * leaving them NaN is the honest no-op rather than fabricating an edge value).
83
+ *
84
+ * O(N): one forward pass tracking the last finite value + index, filling the
85
+ * pending run when the next finite sample closes it. Allocates one `Float64Array`
86
+ * (only taken on the `none` path).
87
+ */
88
+ export declare function bridgeGaps(values: Float64Array, length: number): Float64Array;
89
+ /**
90
+ * Walk a columnar series once (O(N)) and collect the **interior** gaps — runs of
91
+ * non-finite `value(i)` that have a finite sample on *both* sides — as
92
+ * {@link GapEdge}s with pixel coordinates resolved through `xScale`/`lineY`.
93
+ *
94
+ * `value(i)` reads the gap-deciding value at index `i` (for a line that's the
95
+ * `y` value; for a band, "finite" means *both* edges finite — pass a function
96
+ * that returns `NaN` unless both are). `lineY(i)` reads the pixel y the bridge
97
+ * should start / end at (the value line for a line / area; the upper edge for a
98
+ * band). Leading and trailing gaps are skipped — a bridge needs a point on each
99
+ * side.
100
+ */
101
+ export declare function collectGapEdges(length: number, x: Float64Array, value: (i: number) => number, xScale: Scale, lineY: (i: number) => number): GapEdge[];
102
+ /**
103
+ * Stroke a **dashed straight bridge** across each gap (`from` → `to`), for the
104
+ * `dashed` mode. The solid segments are drawn separately (the `empty` pass); this
105
+ * adds only the bridges, dashed (and faint, via `opacity`) so they read as
106
+ * inferred, not measured. Bracketed by `save`/`restore` so the dash pattern,
107
+ * alpha, and stroke don't leak into later layers.
108
+ */
109
+ export declare function drawGapBridges(ctx: CanvasRenderingContext2D, edges: readonly GapEdge[], color: string, width: number, opacity?: number): void;
110
+ /**
111
+ * Stroke a **flat dashed line at the average** of the two edge values across each
112
+ * gap, for the `step` mode: one horizontal segment at the midpoint of `fromY` and
113
+ * `toY`, spanning the gap (`- - -`) — no vertical step. A neutral "the value sat
114
+ * around here" estimate, flatter and less committal than `dashed`'s straight
115
+ * interpolation between the edges. Dashed (and faint, via `opacity`) so it reads
116
+ * as inferred. Bracketed by `save`/`restore`.
117
+ */
118
+ export declare function drawGapSteps(ctx: CanvasRenderingContext2D, edges: readonly GapEdge[], color: string, width: number, opacity?: number): void;
119
+ /**
120
+ * Draw the **fade-to-baseline** at each gap edge for the `fade` mode — estela's
121
+ * coast look (`es-drop`). At the last-good point a vertical segment drops to
122
+ * `baselinePx`, stroked with a vertical gradient opaque (`color`) at the line and
123
+ * transparent at the baseline; the same fade rises at the next-good point on the
124
+ * far side. So the line dissolves into the floor approaching the gap and re-forms
125
+ * after it, rather than ending in a hard stub.
126
+ *
127
+ * estela strokes this as one SVG `<path>` with a single `objectBoundingBox`
128
+ * gradient spanning the path's box; canvas gradients are in user space, so we
129
+ * build one short vertical gradient per drop (anchored at that drop's line→base
130
+ * span) and stroke it. Faithful to the *visual* (a per-edge vertical fade from
131
+ * the line colour to nothing); the implementation differs only in that canvas
132
+ * needs a gradient per drop instead of one shared bounding-box gradient.
133
+ *
134
+ * {@link withAlpha} derives the transparent stop from `color` (a CSS hex); a
135
+ * non-hex colour falls back to `transparent`. Bracketed by `save`/`restore`.
136
+ */
137
+ export declare function drawGapFades(ctx: CanvasRenderingContext2D, edges: readonly GapEdge[], baselinePx: number, color: string, width: number): void;
138
+ /**
139
+ * Re-express a CSS hex colour (`#rgb` / `#rrggbb`) as `rgba(...)` with the given
140
+ * alpha — for the transparent stop of a fade gradient. A non-hex string (named
141
+ * colour, already-`rgba`) can't be parsed, so at alpha 0 it falls back to the
142
+ * CSS keyword `transparent` (still see-through); at any other alpha it's returned
143
+ * unchanged. Shared by the `fade` gap mode and {@link AreaChart}'s graded fill.
144
+ */
145
+ export declare function withAlpha(color: string, alpha: number): string;
146
+ //# sourceMappingURL=gaps.d.ts.map
package/dist/gaps.js ADDED
@@ -0,0 +1,209 @@
1
+ /** The default gap mode — break at the gap, leave a hole (today's behavior). */
2
+ export const DEFAULT_GAP_MODE = 'empty';
3
+ /**
4
+ * Default opacity for the inferred dashed gap connectors (`dashed` / `step`) when
5
+ * a theme sets no `gap.connectorOpacity` — fainter than the solid line so the
6
+ * inferred bridge reads as secondary to measured data.
7
+ */
8
+ export const DEFAULT_GAP_CONNECTOR_OPACITY = 0.5;
9
+ /**
10
+ * Return a copy of `values` with **interior** gaps (NaN runs that have a finite
11
+ * sample on each side) linearly interpolated across, for the `none` mode — so d3
12
+ * sees a continuous finite series and both the line and (for an area) the fill
13
+ * bridge the gap with real path ops. Leading / trailing NaNs (no finite anchor
14
+ * on one side) are left NaN: there's nothing to interpolate from, so they stay a
15
+ * break (a `lineTo`/`moveTo` with a NaN coord is dropped by the canvas spec, so
16
+ * leaving them NaN is the honest no-op rather than fabricating an edge value).
17
+ *
18
+ * O(N): one forward pass tracking the last finite value + index, filling the
19
+ * pending run when the next finite sample closes it. Allocates one `Float64Array`
20
+ * (only taken on the `none` path).
21
+ */
22
+ export function bridgeGaps(values, length) {
23
+ const out = values.slice(0, length);
24
+ let lastIdx = -1; // index of the last finite value seen
25
+ for (let i = 0; i < length; i += 1) {
26
+ if (Number.isFinite(out[i])) {
27
+ if (lastIdx >= 0 && i - lastIdx > 1) {
28
+ // Fill the (lastIdx, i) interior run by linear interpolation.
29
+ const a = out[lastIdx];
30
+ const b = out[i];
31
+ const span = i - lastIdx;
32
+ for (let j = lastIdx + 1; j < i; j += 1) {
33
+ out[j] = a + ((b - a) * (j - lastIdx)) / span;
34
+ }
35
+ }
36
+ lastIdx = i;
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+ /**
42
+ * Walk a columnar series once (O(N)) and collect the **interior** gaps — runs of
43
+ * non-finite `value(i)` that have a finite sample on *both* sides — as
44
+ * {@link GapEdge}s with pixel coordinates resolved through `xScale`/`lineY`.
45
+ *
46
+ * `value(i)` reads the gap-deciding value at index `i` (for a line that's the
47
+ * `y` value; for a band, "finite" means *both* edges finite — pass a function
48
+ * that returns `NaN` unless both are). `lineY(i)` reads the pixel y the bridge
49
+ * should start / end at (the value line for a line / area; the upper edge for a
50
+ * band). Leading and trailing gaps are skipped — a bridge needs a point on each
51
+ * side.
52
+ */
53
+ export function collectGapEdges(length, x, value, xScale, lineY) {
54
+ const edges = [];
55
+ let prevGood = -1; // last finite index seen
56
+ let inGap = false; // inside a NaN run that already has a left border
57
+ for (let i = 0; i < length; i += 1) {
58
+ if (Number.isFinite(value(i))) {
59
+ if (inGap && prevGood >= 0) {
60
+ // Close an interior gap: prevGood → i.
61
+ edges.push({
62
+ fromIndex: prevGood,
63
+ toIndex: i,
64
+ fromX: xScale(x[prevGood]),
65
+ fromY: lineY(prevGood),
66
+ toX: xScale(x[i]),
67
+ toY: lineY(i),
68
+ });
69
+ }
70
+ prevGood = i;
71
+ inGap = false;
72
+ }
73
+ else if (prevGood >= 0) {
74
+ // A gap with a left border — a candidate interior gap (closed when the
75
+ // next finite sample arrives; a trailing run never closes, so is skipped).
76
+ inGap = true;
77
+ }
78
+ }
79
+ return edges;
80
+ }
81
+ /**
82
+ * Stroke a **dashed straight bridge** across each gap (`from` → `to`), for the
83
+ * `dashed` mode. The solid segments are drawn separately (the `empty` pass); this
84
+ * adds only the bridges, dashed (and faint, via `opacity`) so they read as
85
+ * inferred, not measured. Bracketed by `save`/`restore` so the dash pattern,
86
+ * alpha, and stroke don't leak into later layers.
87
+ */
88
+ export function drawGapBridges(ctx, edges, color, width, opacity = 1) {
89
+ if (edges.length === 0)
90
+ return;
91
+ ctx.save();
92
+ ctx.strokeStyle = color;
93
+ ctx.lineWidth = width;
94
+ ctx.globalAlpha = opacity;
95
+ ctx.setLineDash(GAP_DASH);
96
+ ctx.beginPath();
97
+ for (const e of edges) {
98
+ ctx.moveTo(e.fromX, e.fromY);
99
+ ctx.lineTo(e.toX, e.toY);
100
+ }
101
+ ctx.stroke();
102
+ ctx.restore();
103
+ }
104
+ /**
105
+ * Stroke a **flat dashed line at the average** of the two edge values across each
106
+ * gap, for the `step` mode: one horizontal segment at the midpoint of `fromY` and
107
+ * `toY`, spanning the gap (`- - -`) — no vertical step. A neutral "the value sat
108
+ * around here" estimate, flatter and less committal than `dashed`'s straight
109
+ * interpolation between the edges. Dashed (and faint, via `opacity`) so it reads
110
+ * as inferred. Bracketed by `save`/`restore`.
111
+ */
112
+ export function drawGapSteps(ctx, edges, color, width, opacity = 1) {
113
+ if (edges.length === 0)
114
+ return;
115
+ ctx.save();
116
+ ctx.strokeStyle = color;
117
+ ctx.lineWidth = width;
118
+ ctx.globalAlpha = opacity;
119
+ ctx.setLineDash(GAP_DASH);
120
+ ctx.beginPath();
121
+ for (const e of edges) {
122
+ // The average of the two edge values; with a linear y-scale the pixel
123
+ // midpoint equals yScale of the value average, so no value round-trip.
124
+ const midY = (e.fromY + e.toY) / 2;
125
+ ctx.moveTo(e.fromX, midY);
126
+ ctx.lineTo(e.toX, midY);
127
+ }
128
+ ctx.stroke();
129
+ ctx.restore();
130
+ }
131
+ /**
132
+ * Draw the **fade-to-baseline** at each gap edge for the `fade` mode — estela's
133
+ * coast look (`es-drop`). At the last-good point a vertical segment drops to
134
+ * `baselinePx`, stroked with a vertical gradient opaque (`color`) at the line and
135
+ * transparent at the baseline; the same fade rises at the next-good point on the
136
+ * far side. So the line dissolves into the floor approaching the gap and re-forms
137
+ * after it, rather than ending in a hard stub.
138
+ *
139
+ * estela strokes this as one SVG `<path>` with a single `objectBoundingBox`
140
+ * gradient spanning the path's box; canvas gradients are in user space, so we
141
+ * build one short vertical gradient per drop (anchored at that drop's line→base
142
+ * span) and stroke it. Faithful to the *visual* (a per-edge vertical fade from
143
+ * the line colour to nothing); the implementation differs only in that canvas
144
+ * needs a gradient per drop instead of one shared bounding-box gradient.
145
+ *
146
+ * {@link withAlpha} derives the transparent stop from `color` (a CSS hex); a
147
+ * non-hex colour falls back to `transparent`. Bracketed by `save`/`restore`.
148
+ */
149
+ export function drawGapFades(ctx, edges, baselinePx, color, width) {
150
+ if (edges.length === 0)
151
+ return;
152
+ const transparent = withAlpha(color, 0);
153
+ ctx.save();
154
+ ctx.lineWidth = width;
155
+ // One vertical drop per edge endpoint: the last-good point and the next-good
156
+ // point each fade from the line down to the baseline.
157
+ for (const e of edges) {
158
+ for (const [px, py] of [
159
+ [e.fromX, e.fromY],
160
+ [e.toX, e.toY],
161
+ ]) {
162
+ // Degenerate (the line already sits on the baseline) → nothing to fade.
163
+ if (Math.abs(py - baselinePx) < 1e-6)
164
+ continue;
165
+ const grad = ctx.createLinearGradient(0, py, 0, baselinePx);
166
+ grad.addColorStop(0, color); // opaque at the line
167
+ grad.addColorStop(1, transparent); // transparent at the baseline
168
+ ctx.strokeStyle = grad;
169
+ ctx.beginPath();
170
+ ctx.moveTo(px, py);
171
+ ctx.lineTo(px, baselinePx);
172
+ ctx.stroke();
173
+ }
174
+ }
175
+ ctx.restore();
176
+ }
177
+ /**
178
+ * Dash pattern for the inferred-bridge modes (`dashed` / `step`): 4 on, 4 off.
179
+ * Mutable (not `readonly`) because `ctx.setLineDash` takes a `number[]`.
180
+ */
181
+ const GAP_DASH = [4, 4];
182
+ /**
183
+ * Re-express a CSS hex colour (`#rgb` / `#rrggbb`) as `rgba(...)` with the given
184
+ * alpha — for the transparent stop of a fade gradient. A non-hex string (named
185
+ * colour, already-`rgba`) can't be parsed, so at alpha 0 it falls back to the
186
+ * CSS keyword `transparent` (still see-through); at any other alpha it's returned
187
+ * unchanged. Shared by the `fade` gap mode and {@link AreaChart}'s graded fill.
188
+ */
189
+ export function withAlpha(color, alpha) {
190
+ const hex = color.trim();
191
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex);
192
+ if (m === null)
193
+ return alpha === 0 ? 'transparent' : color;
194
+ let r;
195
+ let g;
196
+ let b;
197
+ if (m[1].length === 3) {
198
+ r = parseInt(m[1][0] + m[1][0], 16);
199
+ g = parseInt(m[1][1] + m[1][1], 16);
200
+ b = parseInt(m[1][2] + m[1][2], 16);
201
+ }
202
+ else {
203
+ r = parseInt(m[1].slice(0, 2), 16);
204
+ g = parseInt(m[1].slice(2, 4), 16);
205
+ b = parseInt(m[1].slice(4, 6), 16);
206
+ }
207
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
208
+ }
209
+ //# sourceMappingURL=gaps.js.map
package/dist/grid.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Stroke the plot's gridlines: a vertical line at each `xTicks` pixel and a
3
+ * horizontal line at each `yTicks` pixel, faint and dashed. Drawn behind the
4
+ * data layers from the same tick positions the axes label, so grid and labels
5
+ * line up. `+0.5` aligns each 1px stroke to the device grid for a crisp line.
6
+ *
7
+ * `save`/`restore` brackets the dash + stroke state so it doesn't leak into the
8
+ * data layers that draw next.
9
+ */
10
+ export declare function drawGrid(ctx: CanvasRenderingContext2D, xTicks: readonly number[], yTicks: readonly number[], width: number, height: number, color: string, dash: readonly number[]): void;
11
+ //# sourceMappingURL=grid.d.ts.map
package/dist/grid.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Stroke the plot's gridlines: a vertical line at each `xTicks` pixel and a
3
+ * horizontal line at each `yTicks` pixel, faint and dashed. Drawn behind the
4
+ * data layers from the same tick positions the axes label, so grid and labels
5
+ * line up. `+0.5` aligns each 1px stroke to the device grid for a crisp line.
6
+ *
7
+ * `save`/`restore` brackets the dash + stroke state so it doesn't leak into the
8
+ * data layers that draw next.
9
+ */
10
+ export function drawGrid(ctx, xTicks, yTicks, width, height, color, dash) {
11
+ ctx.save();
12
+ ctx.strokeStyle = color;
13
+ ctx.lineWidth = 1;
14
+ ctx.setLineDash([...dash]);
15
+ ctx.beginPath();
16
+ for (const x of xTicks) {
17
+ const px = Math.round(x) + 0.5;
18
+ ctx.moveTo(px, 0);
19
+ ctx.lineTo(px, height);
20
+ }
21
+ for (const y of yTicks) {
22
+ const py = Math.round(y) + 0.5;
23
+ ctx.moveTo(0, py);
24
+ ctx.lineTo(width, py);
25
+ }
26
+ ctx.stroke();
27
+ ctx.restore();
28
+ }
29
+ //# sourceMappingURL=grid.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * `@pond-ts/charts` — the visualization end of pond.
3
+ *
4
+ * Canvas-rendered, streaming-first time-series charts with a
5
+ * react-timeseries-charts-style declarative layout. The architecture (hard
6
+ * layers: adapter → typed-array store → decimator → chunked Path2D cache →
7
+ * canvas renderer → React shell) is documented in the charts RFC at
8
+ * `docs/rfcs/charts.md`; the milestone plan lives in `PLAN.md`.
9
+ *
10
+ * **M1 — rendering spine.** The layout shell + the first draw layer:
11
+ * `<ChartContainer>` (time axis) → `<ChartRow>` (y-axis + canvas) →
12
+ * `<LineChart>` (a gap-aware line), fed from a pond `TimeSeries` via
13
+ * {@link fromTimeSeries}. Axes, themes, the variance band, and interactions
14
+ * land in M2–M4. {@link Canvas} is the low-level DPR-aware primitive the rows
15
+ * sit on.
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+ export { Canvas } from './Canvas.js';
20
+ export type { CanvasProps, CanvasDraw } from './Canvas.js';
21
+ export { ChartContainer } from './ChartContainer.js';
22
+ export type { ChartContainerProps } from './ChartContainer.js';
23
+ export { ChartRow } from './ChartRow.js';
24
+ export type { ChartRowProps } from './ChartRow.js';
25
+ export { Layers } from './Layers.js';
26
+ export type { LayersProps } from './Layers.js';
27
+ export { YAxis } from './YAxis.js';
28
+ export type { YAxisProps } from './YAxis.js';
29
+ export { XAxis } from './XAxis.js';
30
+ export type { XAxisProps } from './XAxis.js';
31
+ export { TimeAxis } from './TimeAxis.js';
32
+ export type { AxisFormat } from './format.js';
33
+ export { LineChart } from './LineChart.js';
34
+ export type { LineChartProps } from './LineChart.js';
35
+ export { BandChart } from './BandChart.js';
36
+ export type { BandChartProps } from './BandChart.js';
37
+ export { AreaChart } from './AreaChart.js';
38
+ export type { AreaChartProps } from './AreaChart.js';
39
+ export { ScatterChart } from './ScatterChart.js';
40
+ export type { ScatterChartProps } from './ScatterChart.js';
41
+ export { BoxPlot } from './BoxPlot.js';
42
+ export type { BoxPlotProps } from './BoxPlot.js';
43
+ export { BarChart } from './BarChart.js';
44
+ export type { BarChartProps } from './BarChart.js';
45
+ export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, } from './data.js';
46
+ export type { ChartSeries, BandSeries, BoxSeries, BoxColumns, BarSeries, } from './data.js';
47
+ export type { RadiusEncoding, ColorEncoding } from './encoding.js';
48
+ export type { Curve } from './curve.js';
49
+ export type { GapMode } from './gaps.js';
50
+ export { defaultTheme, estelaTheme } from './theme.js';
51
+ export type { ChartTheme, LineStyle, BandStyle, AreaStyle, ScatterStyle, BoxStyle, BarStyle, } from './theme.js';
52
+ export type { CursorMode, TrackerInfo, TrackerSample, SelectInfo, } from './context.js';
53
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `@pond-ts/charts` — the visualization end of pond.
3
+ *
4
+ * Canvas-rendered, streaming-first time-series charts with a
5
+ * react-timeseries-charts-style declarative layout. The architecture (hard
6
+ * layers: adapter → typed-array store → decimator → chunked Path2D cache →
7
+ * canvas renderer → React shell) is documented in the charts RFC at
8
+ * `docs/rfcs/charts.md`; the milestone plan lives in `PLAN.md`.
9
+ *
10
+ * **M1 — rendering spine.** The layout shell + the first draw layer:
11
+ * `<ChartContainer>` (time axis) → `<ChartRow>` (y-axis + canvas) →
12
+ * `<LineChart>` (a gap-aware line), fed from a pond `TimeSeries` via
13
+ * {@link fromTimeSeries}. Axes, themes, the variance band, and interactions
14
+ * land in M2–M4. {@link Canvas} is the low-level DPR-aware primitive the rows
15
+ * sit on.
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+ export { Canvas } from './Canvas.js';
20
+ export { ChartContainer } from './ChartContainer.js';
21
+ export { ChartRow } from './ChartRow.js';
22
+ export { Layers } from './Layers.js';
23
+ export { YAxis } from './YAxis.js';
24
+ export { XAxis } from './XAxis.js';
25
+ export { TimeAxis } from './TimeAxis.js';
26
+ export { LineChart } from './LineChart.js';
27
+ export { BandChart } from './BandChart.js';
28
+ export { AreaChart } from './AreaChart.js';
29
+ export { ScatterChart } from './ScatterChart.js';
30
+ export { BoxPlot } from './BoxPlot.js';
31
+ export { BarChart } from './BarChart.js';
32
+ export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, } from './data.js';
33
+ export { defaultTheme, estelaTheme } from './theme.js';
34
+ //# sourceMappingURL=index.js.map
package/dist/line.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { type CurveFactory } from 'd3-shape';
2
+ import type { ChartSeries } from './data.js';
3
+ import type { LineStyle } from './theme.js';
4
+ import { type GapMode } from './gaps.js';
5
+ /** Maps a data value to a pixel coordinate (a d3 scale is assignable to this). */
6
+ export type Scale = (value: number) => number;
7
+ /**
8
+ * The y-scale's domain lower bound (the axis floor) in pixels — where the
9
+ * `step` / `fade` gap bridges drop to. The runtime `yScale` is a d3
10
+ * `ScaleLinear` (it carries `.domain()`); read the bound through a localized,
11
+ * documented shape rather than widening the draw contract to d3-scale. Falls
12
+ * back to `0` if the scale exposes no domain.
13
+ */
14
+ export declare function baselinePxFromScale(yScale: Scale): number;
15
+ /**
16
+ * The `[min, max]` of the **finite** values in `cs.y`, or `null` if none are
17
+ * finite. NaN (the gap signal) is ignored, so a coast doesn't drag the domain.
18
+ */
19
+ export declare function yExtent(cs: ChartSeries): [number, number] | null;
20
+ /**
21
+ * Stroke a line for `cs`, mapping data→pixels through `xScale`/`yScale` and
22
+ * connecting points with `curve` (d3-shape; default linear).
23
+ *
24
+ * Built on d3-shape's `line()`. **Gap handling is driven by `gaps`** (a
25
+ * {@link GapMode}, default `'empty'`):
26
+ *
27
+ * - `'empty'` (default) — `.defined(Number.isFinite)`: a non-finite value ends
28
+ * the current subpath and the next finite point starts a fresh one (`moveTo`,
29
+ * not `lineTo`), so a coast reads as a break, not a `lineTo(NaN, …)` bridge
30
+ * (`docs/rfcs/charts.md` trap #2).
31
+ * - `'none'` — interior gaps are linearly interpolated ({@link bridgeGaps}) so
32
+ * the line bridges straight across (real `lineTo`s, robust to leading /
33
+ * trailing gaps, which stay a break). The one non-honest mode.
34
+ * - `'dashed'` / `'step'` / `'fade'` — the **solid** segments break exactly as
35
+ * in `'empty'`, then a second pass draws the inferred bridge across each
36
+ * interior gap: a dashed straight line, a flat dashed line at the average of
37
+ * the edge values, or estela's fade-to-baseline (the axis floor). `dashed` /
38
+ * `step` are drawn faint (`gapConnectorOpacity`); the gap edges are collected
39
+ * by one O(N) walk ({@link collectGapEdges}).
40
+ *
41
+ * The generator writes path ops to `ctx`; we bracket with `beginPath`/`stroke`.
42
+ * `cs.y` (a `Float64Array`) is the datum iterable — `y` reads the value, `x`
43
+ * reads `cs.x[i]` by index, so there's no per-point object allocation.
44
+ */
45
+ export declare function drawLine(ctx: CanvasRenderingContext2D, cs: ChartSeries, xScale: Scale, yScale: Scale, style: LineStyle, curve?: CurveFactory, gaps?: GapMode, gapConnectorOpacity?: number): void;
46
+ //# sourceMappingURL=line.d.ts.map