@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/theme.d.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual styling for a chart, threaded through {@link ChartContainer} via
|
|
3
|
+
* context. Canvas has no CSS cascade into drawn pixels, so this typed object is
|
|
4
|
+
* the single styling channel for the drawn layers; DOM chrome (axis labels)
|
|
5
|
+
* derives from it too.
|
|
6
|
+
*
|
|
7
|
+
* The styling pipeline is **time series → columns → semantic identifier →
|
|
8
|
+
* style**: a draw layer tags its column with a *semantic identifier* (what the
|
|
9
|
+
* data _is_ — e.g. `heartrate`, `power`, or a generic `primary`), and the theme
|
|
10
|
+
* is the map from identifier → {@link LineStyle}. The visual discipline ("a
|
|
11
|
+
* handful of roles, not a hue per channel") lives in the theme, not the type: a
|
|
12
|
+
* good theme maps many identifiers onto few shared styles (estela maps
|
|
13
|
+
* power / speed / cadence → one foam style). Tokens grow as components land
|
|
14
|
+
* (axis tokens with `YAxis`, band tokens with `BandChart`).
|
|
15
|
+
*/
|
|
16
|
+
export interface ChartTheme {
|
|
17
|
+
/** Painted behind the layers; omit for a transparent background. */
|
|
18
|
+
readonly background?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Map from a line's semantic identifier to its style. `default` is the
|
|
21
|
+
* fallback for an identifier the theme doesn't name, so a chart always
|
|
22
|
+
* renders; a line resolves `line[semantic] ?? line.default`.
|
|
23
|
+
*/
|
|
24
|
+
readonly line: {
|
|
25
|
+
readonly default: LineStyle;
|
|
26
|
+
readonly [semantic: string]: LineStyle;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Map from a band's semantic identifier to its fill style — the variance
|
|
30
|
+
* underlay ({@link BandChart}). `default` is the fallback; a two-tone spread
|
|
31
|
+
* is two bands composed in the z-stack (e.g. `outer` p5/p95 + `inner`
|
|
32
|
+
* p25/p75), each resolving `band[semantic] ?? band.default`.
|
|
33
|
+
*/
|
|
34
|
+
readonly band: {
|
|
35
|
+
readonly default: BandStyle;
|
|
36
|
+
readonly [semantic: string]: BandStyle;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Map from an area's semantic identifier to its outline-plus-graded-fill style
|
|
40
|
+
* (`AreaChart`) — outline colour/width and the gradient (opaque at the line,
|
|
41
|
+
* fading to transparent at the baseline). `default` is the fallback; the
|
|
42
|
+
* esnet two-colour traffic look is two areas composed in the z-stack (e.g.
|
|
43
|
+
* `in` above the axis + `out` below), each resolving `area[semantic] ??
|
|
44
|
+
* area.default`.
|
|
45
|
+
*/
|
|
46
|
+
readonly area: {
|
|
47
|
+
readonly default: AreaStyle;
|
|
48
|
+
readonly [semantic: string]: AreaStyle;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Map from a scatter's semantic identifier to its point style — the single
|
|
52
|
+
* styling channel for the **base mark** ({@link ScatterChart}): fill colour,
|
|
53
|
+
* base radius, outline, and the optional per-point label colour. `default` is
|
|
54
|
+
* the fallback; a scatter resolves `scatter[semantic] ?? scatter.default`.
|
|
55
|
+
*
|
|
56
|
+
* Scatter additionally supports **data-driven** radius + colour (a column run
|
|
57
|
+
* through a scale — see `src/encoding.ts`); that is the deliberate, signed-off
|
|
58
|
+
* exception to one-channel styling. The data-driven encoding *overrides* this
|
|
59
|
+
* style's `radius` / `color` per point; this remains the fallback for
|
|
60
|
+
* unencoded points and the source of the outline + label styling.
|
|
61
|
+
*/
|
|
62
|
+
readonly scatter: {
|
|
63
|
+
readonly default: ScatterStyle;
|
|
64
|
+
readonly [semantic: string]: ScatterStyle;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Map from a box's semantic identifier to its style — a discrete
|
|
68
|
+
* box-and-whisker per key ({@link BoxPlot}), the bar-chart analog of the
|
|
69
|
+
* band. `default` is the fallback; a chart tags each box series with a role
|
|
70
|
+
* (`<BoxPlot as="latency" />`) resolving `box[semantic] ?? box.default`.
|
|
71
|
+
*/
|
|
72
|
+
readonly box: {
|
|
73
|
+
readonly default: BoxStyle;
|
|
74
|
+
readonly [semantic: string]: BoxStyle;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Map from a bar's semantic identifier to its style — the fill, the
|
|
78
|
+
* selected-bar highlight, and the slot gap / minimum width ({@link BarChart}).
|
|
79
|
+
* `default` is the fallback; a bar resolves `bar[semantic] ?? bar.default`.
|
|
80
|
+
*/
|
|
81
|
+
readonly bar: {
|
|
82
|
+
readonly default: BarStyle;
|
|
83
|
+
readonly [semantic: string]: BarStyle;
|
|
84
|
+
};
|
|
85
|
+
/** Axis chrome: tick-label colour, gridline stroke + dash pattern. */
|
|
86
|
+
readonly axis: {
|
|
87
|
+
readonly label: string;
|
|
88
|
+
readonly grid: string;
|
|
89
|
+
/** Gridline dash pattern (px on/off pairs); `[]` for solid. */
|
|
90
|
+
readonly gridDash: readonly number[];
|
|
91
|
+
};
|
|
92
|
+
/** Label / tick typography. One source for axes + chrome. */
|
|
93
|
+
readonly font: {
|
|
94
|
+
readonly family: string;
|
|
95
|
+
readonly size: number;
|
|
96
|
+
};
|
|
97
|
+
/** Crosshair / tracker stroke colour. Falls back to {@link axis.label} if unset. */
|
|
98
|
+
readonly cursor?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Readout chip background (the `flag` / `inline` tracker modes). The value text
|
|
101
|
+
* is the series colour; this is the panel behind it. Falls back to the plot
|
|
102
|
+
* background if unset.
|
|
103
|
+
*/
|
|
104
|
+
readonly chip?: {
|
|
105
|
+
readonly background: string;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Styling for the **inferred dashed gap connectors** — the `dashed` and `step`
|
|
109
|
+
* gap modes ({@link GapMode}). Drawn fainter than the solid line (via
|
|
110
|
+
* `connectorOpacity`, 0–1, applied over the layer's colour) so an *inferred*
|
|
111
|
+
* bridge across a gap reads as secondary to measured data. Per-theme, so a
|
|
112
|
+
* dark ground can tune the faintness independently. Falls back to `0.5` if
|
|
113
|
+
* unset. (The `fade` mode has its own gradient and isn't governed by this.)
|
|
114
|
+
*/
|
|
115
|
+
readonly gap?: {
|
|
116
|
+
readonly connectorOpacity: number;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** A resolved line style: stroke colour + width (px). */
|
|
120
|
+
export interface LineStyle {
|
|
121
|
+
readonly color: string;
|
|
122
|
+
readonly width: number;
|
|
123
|
+
}
|
|
124
|
+
/** A resolved band style: fill colour + opacity (0–1) for the variance envelope. */
|
|
125
|
+
export interface BandStyle {
|
|
126
|
+
readonly fill: string;
|
|
127
|
+
readonly opacity: number;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A resolved scatter point style — the single styling channel for the base
|
|
131
|
+
* mark. `color` fills each point and `radius` (px) sizes it (both the fallback
|
|
132
|
+
* when a data-driven encoding leaves a point unencoded). `outline`/`outlineWidth`
|
|
133
|
+
* stroke a ring around each point for legibility on a busy plot; the *selected*
|
|
134
|
+
* point is restroked with `selectedOutline` at `selectedWidth` to lift it.
|
|
135
|
+
* `label` colours the optional per-point text (the font comes from `theme.font`).
|
|
136
|
+
*/
|
|
137
|
+
export interface ScatterStyle {
|
|
138
|
+
readonly color: string;
|
|
139
|
+
/** Base point radius in px (data-driven radius overrides this per point). */
|
|
140
|
+
readonly radius: number;
|
|
141
|
+
/** Per-point outline stroke colour. */
|
|
142
|
+
readonly outline: string;
|
|
143
|
+
/** Per-point outline width in px. */
|
|
144
|
+
readonly outlineWidth: number;
|
|
145
|
+
/** Outline colour for the selected point (the highlight ring). */
|
|
146
|
+
readonly selectedOutline: string;
|
|
147
|
+
/** Outline width for the selected point (px) — wider than `outlineWidth`. */
|
|
148
|
+
readonly selectedWidth: number;
|
|
149
|
+
/** Colour of the optional per-point text label. */
|
|
150
|
+
readonly label: string;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* A resolved box-and-whisker style ({@link BoxPlot}). The q1→q3 box is a filled
|
|
154
|
+
* rect (`fill` at `fillOpacity`) outlined by `stroke`/`strokeWidth`; the
|
|
155
|
+
* `median` line and the `whisker` (the lower/upper stems + caps) get their own
|
|
156
|
+
* colour/width so the median reads against the fill. Whiskers and the box
|
|
157
|
+
* outline are drawn at full alpha (only the box fill is graded by `fillOpacity`).
|
|
158
|
+
*/
|
|
159
|
+
export interface BoxStyle {
|
|
160
|
+
readonly fill: string;
|
|
161
|
+
readonly fillOpacity: number;
|
|
162
|
+
readonly stroke: string;
|
|
163
|
+
readonly strokeWidth: number;
|
|
164
|
+
readonly median: string;
|
|
165
|
+
readonly medianWidth: number;
|
|
166
|
+
readonly whisker: string;
|
|
167
|
+
readonly whiskerWidth: number;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* A resolved area style: an outline stroke plus a graded fill. `color`/`width`
|
|
171
|
+
* stroke the value line on top; `fill` is the gradient base colour, opaque
|
|
172
|
+
* (scaled by `fillOpacity`, 0–1) at the line and grading to transparent at the
|
|
173
|
+
* baseline. `fill` must be a CSS hex (`#rgb` / `#rrggbb`) so the transparent
|
|
174
|
+
* stop can be derived; other formats fall back to a flat fill.
|
|
175
|
+
*/
|
|
176
|
+
export interface AreaStyle {
|
|
177
|
+
readonly color: string;
|
|
178
|
+
readonly width: number;
|
|
179
|
+
readonly fill: string;
|
|
180
|
+
readonly fillOpacity: number;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* A resolved bar style: the flat `fill` (scaled by `opacity`, 0–1) plus the
|
|
184
|
+
* `highlight` colour the selected bar takes (also its outline), and the bar
|
|
185
|
+
* geometry — `gap` (px between adjacent bars, the default for `<BarChart gap>`)
|
|
186
|
+
* and `minWidth` (the px floor so a too-thin bucket stays visible) handed to
|
|
187
|
+
* `barSpanPx`, with `outlineWidth` for the selected-bar stroke.
|
|
188
|
+
*/
|
|
189
|
+
export interface BarStyle {
|
|
190
|
+
readonly fill: string;
|
|
191
|
+
readonly opacity: number;
|
|
192
|
+
readonly highlight: string;
|
|
193
|
+
readonly gap: number;
|
|
194
|
+
readonly minWidth: number;
|
|
195
|
+
readonly outlineWidth: number;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The neutral default theme. `default` / `primary` match the M1 `LineChart`
|
|
199
|
+
* colour (`#2563eb`) so adopting the theme channel doesn't shift existing
|
|
200
|
+
* renders. `primary` / `secondary` / `context` are a built-in generic role
|
|
201
|
+
* vocabulary; an unrecognised (e.g. domain-specific) identifier falls back to
|
|
202
|
+
* `default`.
|
|
203
|
+
*/
|
|
204
|
+
export declare const defaultTheme: ChartTheme;
|
|
205
|
+
/**
|
|
206
|
+
* The estela theme — estela's real `@estela/ui` palette as *one theme*, on its
|
|
207
|
+
* dark ground. A chart tags a column with a role (`<LineChart as="foam" />`) and
|
|
208
|
+
* the colour lives here, not at the call site. The proving consumer for "target
|
|
209
|
+
* other uses too": the same engine, restyled by swapping this for
|
|
210
|
+
* {@link defaultTheme}.
|
|
211
|
+
*
|
|
212
|
+
* Line roles map to estela's palette tokens:
|
|
213
|
+
* - `default` → `--es-estela` `#15B3A6` (primary / action — the brand teal)
|
|
214
|
+
* - `foam` → `--es-foam` `#F1FBF9` (the shared "motion" trace estela uses for
|
|
215
|
+
* its primary channels — power / speed / cadence all render foam)
|
|
216
|
+
* - `hr` → `--es-filament` `#E0B36A` (the rare warm accent — heart rate)
|
|
217
|
+
*
|
|
218
|
+
* Chrome: `--es-bg` ground, `--es-ink` gridlines, `--es-slate` labels, and the
|
|
219
|
+
* `--es-font-data` (JetBrains Mono) face for crisp numeric ticks (falls back to
|
|
220
|
+
* `ui-monospace` where the webfont isn't loaded). Band fills: `outer`
|
|
221
|
+
* (`--es-reef`) + `inner` (`--es-shallows`) for the two-tone variance spread.
|
|
222
|
+
*/
|
|
223
|
+
export declare const estelaTheme: ChartTheme;
|
|
224
|
+
//# sourceMappingURL=theme.d.ts.map
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The neutral default theme. `default` / `primary` match the M1 `LineChart`
|
|
3
|
+
* colour (`#2563eb`) so adopting the theme channel doesn't shift existing
|
|
4
|
+
* renders. `primary` / `secondary` / `context` are a built-in generic role
|
|
5
|
+
* vocabulary; an unrecognised (e.g. domain-specific) identifier falls back to
|
|
6
|
+
* `default`.
|
|
7
|
+
*/
|
|
8
|
+
export const defaultTheme = {
|
|
9
|
+
line: {
|
|
10
|
+
default: { color: '#2563eb', width: 1.5 },
|
|
11
|
+
primary: { color: '#2563eb', width: 1.5 },
|
|
12
|
+
secondary: { color: '#e8836b', width: 1.5 },
|
|
13
|
+
context: { color: '#5eb5a6', width: 1.5 },
|
|
14
|
+
},
|
|
15
|
+
band: {
|
|
16
|
+
default: { fill: '#2563eb', opacity: 0.15 },
|
|
17
|
+
outer: { fill: '#2563eb', opacity: 0.1 },
|
|
18
|
+
inner: { fill: '#2563eb', opacity: 0.2 },
|
|
19
|
+
},
|
|
20
|
+
area: {
|
|
21
|
+
// Outline at the line colour; graded fill from it. `in`/`out` are the
|
|
22
|
+
// above/below-axis roles (esnet traffic), composed as two layers.
|
|
23
|
+
default: {
|
|
24
|
+
color: '#2563eb',
|
|
25
|
+
width: 1.5,
|
|
26
|
+
fill: '#2563eb',
|
|
27
|
+
fillOpacity: 0.3,
|
|
28
|
+
},
|
|
29
|
+
in: { color: '#2563eb', width: 1.5, fill: '#2563eb', fillOpacity: 0.3 },
|
|
30
|
+
out: { color: '#e8836b', width: 1.5, fill: '#e8836b', fillOpacity: 0.3 },
|
|
31
|
+
},
|
|
32
|
+
scatter: {
|
|
33
|
+
// Brand blue fill with a white ring (legible on a busy plot); the selected
|
|
34
|
+
// point gets a darker, wider ring. `primary`/`secondary` mirror the line
|
|
35
|
+
// roles so a scatter overlaid on a line can share its identity.
|
|
36
|
+
default: {
|
|
37
|
+
color: '#2563eb',
|
|
38
|
+
radius: 4,
|
|
39
|
+
outline: '#ffffff',
|
|
40
|
+
outlineWidth: 1,
|
|
41
|
+
selectedOutline: '#1e293b',
|
|
42
|
+
selectedWidth: 2,
|
|
43
|
+
label: '#334155',
|
|
44
|
+
},
|
|
45
|
+
primary: {
|
|
46
|
+
color: '#2563eb',
|
|
47
|
+
radius: 4,
|
|
48
|
+
outline: '#ffffff',
|
|
49
|
+
outlineWidth: 1,
|
|
50
|
+
selectedOutline: '#1e293b',
|
|
51
|
+
selectedWidth: 2,
|
|
52
|
+
label: '#334155',
|
|
53
|
+
},
|
|
54
|
+
secondary: {
|
|
55
|
+
color: '#e8836b',
|
|
56
|
+
radius: 4,
|
|
57
|
+
outline: '#ffffff',
|
|
58
|
+
outlineWidth: 1,
|
|
59
|
+
selectedOutline: '#1e293b',
|
|
60
|
+
selectedWidth: 2,
|
|
61
|
+
label: '#334155',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
box: {
|
|
65
|
+
// The blue brand box: a translucent fill outlined in the line colour, a
|
|
66
|
+
// bolder median, and matching whiskers.
|
|
67
|
+
default: {
|
|
68
|
+
fill: '#2563eb',
|
|
69
|
+
fillOpacity: 0.3,
|
|
70
|
+
stroke: '#2563eb',
|
|
71
|
+
strokeWidth: 1.5,
|
|
72
|
+
median: '#1e3a8a',
|
|
73
|
+
medianWidth: 2,
|
|
74
|
+
whisker: '#aabee9',
|
|
75
|
+
whiskerWidth: 1,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
bar: {
|
|
79
|
+
// Flat blue fill; the selected bar brightens + outlines. `secondary` reuses
|
|
80
|
+
// the line's warm accent for a second series.
|
|
81
|
+
default: {
|
|
82
|
+
fill: '#2563eb',
|
|
83
|
+
opacity: 0.85,
|
|
84
|
+
highlight: '#1d4ed8',
|
|
85
|
+
gap: 1,
|
|
86
|
+
minWidth: 1,
|
|
87
|
+
outlineWidth: 1.5,
|
|
88
|
+
},
|
|
89
|
+
secondary: {
|
|
90
|
+
fill: '#e8836b',
|
|
91
|
+
opacity: 0.85,
|
|
92
|
+
highlight: '#d65f43',
|
|
93
|
+
gap: 1,
|
|
94
|
+
minWidth: 1,
|
|
95
|
+
outlineWidth: 1.5,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
axis: {
|
|
99
|
+
label: '#64748b',
|
|
100
|
+
grid: '#e2e8f0',
|
|
101
|
+
gridDash: [2, 2],
|
|
102
|
+
},
|
|
103
|
+
font: {
|
|
104
|
+
family: 'system-ui, -apple-system, sans-serif',
|
|
105
|
+
size: 11,
|
|
106
|
+
},
|
|
107
|
+
cursor: '#64748b',
|
|
108
|
+
chip: { background: '#ffffff' },
|
|
109
|
+
gap: { connectorOpacity: 0.5 },
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* The estela theme — estela's real `@estela/ui` palette as *one theme*, on its
|
|
113
|
+
* dark ground. A chart tags a column with a role (`<LineChart as="foam" />`) and
|
|
114
|
+
* the colour lives here, not at the call site. The proving consumer for "target
|
|
115
|
+
* other uses too": the same engine, restyled by swapping this for
|
|
116
|
+
* {@link defaultTheme}.
|
|
117
|
+
*
|
|
118
|
+
* Line roles map to estela's palette tokens:
|
|
119
|
+
* - `default` → `--es-estela` `#15B3A6` (primary / action — the brand teal)
|
|
120
|
+
* - `foam` → `--es-foam` `#F1FBF9` (the shared "motion" trace estela uses for
|
|
121
|
+
* its primary channels — power / speed / cadence all render foam)
|
|
122
|
+
* - `hr` → `--es-filament` `#E0B36A` (the rare warm accent — heart rate)
|
|
123
|
+
*
|
|
124
|
+
* Chrome: `--es-bg` ground, `--es-ink` gridlines, `--es-slate` labels, and the
|
|
125
|
+
* `--es-font-data` (JetBrains Mono) face for crisp numeric ticks (falls back to
|
|
126
|
+
* `ui-monospace` where the webfont isn't loaded). Band fills: `outer`
|
|
127
|
+
* (`--es-reef`) + `inner` (`--es-shallows`) for the two-tone variance spread.
|
|
128
|
+
*/
|
|
129
|
+
export const estelaTheme = {
|
|
130
|
+
background: '#06191D', // --es-bg
|
|
131
|
+
line: {
|
|
132
|
+
default: { color: '#15B3A6', width: 1.5 }, // --es-estela (primary / action)
|
|
133
|
+
foam: { color: '#F1FBF9', width: 2 }, // --es-foam (motion — shared primary trace)
|
|
134
|
+
hr: { color: '#E0B36A', width: 1.5 }, // --es-filament (rare warm accent)
|
|
135
|
+
},
|
|
136
|
+
band: {
|
|
137
|
+
default: { fill: '#45CDBE', opacity: 0.18 }, // --es-shallows
|
|
138
|
+
outer: { fill: '#7FE2D2', opacity: 0.12 }, // --es-reef (wide p5/p95 spread)
|
|
139
|
+
inner: { fill: '#45CDBE', opacity: 0.22 }, // --es-shallows (tight p25/p75)
|
|
140
|
+
},
|
|
141
|
+
area: {
|
|
142
|
+
// Elevation: the brand teal outline over a graded teal shade. `in`/`out`
|
|
143
|
+
// are the above/below-axis traffic roles — teal `in`, warm filament `out`.
|
|
144
|
+
default: {
|
|
145
|
+
color: '#15B3A6',
|
|
146
|
+
width: 1.5,
|
|
147
|
+
fill: '#15B3A6',
|
|
148
|
+
fillOpacity: 0.35,
|
|
149
|
+
}, // --es-estela
|
|
150
|
+
in: { color: '#15B3A6', width: 1.5, fill: '#15B3A6', fillOpacity: 0.35 }, // --es-estela
|
|
151
|
+
out: { color: '#E0B36A', width: 1.5, fill: '#E0B36A', fillOpacity: 0.35 }, // --es-filament
|
|
152
|
+
},
|
|
153
|
+
scatter: {
|
|
154
|
+
// Brand-teal points ringed in the dark ground so they read as discrete
|
|
155
|
+
// marks; the selected point gets the bright reef ring (the tracker colour).
|
|
156
|
+
// `foam`/`hr` mirror the line roles for a scatter overlaid on those traces.
|
|
157
|
+
default: {
|
|
158
|
+
color: '#15B3A6', // --es-estela
|
|
159
|
+
radius: 4,
|
|
160
|
+
outline: '#06191D', // --es-bg (ring punches the point off the ground)
|
|
161
|
+
outlineWidth: 1,
|
|
162
|
+
selectedOutline: '#7FE2D2', // --es-reef (matches the tracker highlight)
|
|
163
|
+
selectedWidth: 2,
|
|
164
|
+
label: '#DBEAE8', // --es-mist (legible label on the dark ground)
|
|
165
|
+
},
|
|
166
|
+
foam: {
|
|
167
|
+
color: '#F1FBF9', // --es-foam (motion — shared primary trace)
|
|
168
|
+
radius: 4,
|
|
169
|
+
outline: '#06191D',
|
|
170
|
+
outlineWidth: 1,
|
|
171
|
+
selectedOutline: '#7FE2D2',
|
|
172
|
+
selectedWidth: 2,
|
|
173
|
+
label: '#DBEAE8', // --es-mist
|
|
174
|
+
},
|
|
175
|
+
hr: {
|
|
176
|
+
color: '#E0B36A', // --es-filament (rare warm accent — heart rate)
|
|
177
|
+
radius: 4,
|
|
178
|
+
outline: '#06191D',
|
|
179
|
+
outlineWidth: 1,
|
|
180
|
+
selectedOutline: '#7FE2D2',
|
|
181
|
+
selectedWidth: 2,
|
|
182
|
+
label: '#DBEAE8', // --es-mist
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
box: {
|
|
186
|
+
// A teal box on the dark ground: `--es-shallows` fill, `--es-estela` outline
|
|
187
|
+
// + whiskers, and a bright `--es-foam` median so it reads against the fill.
|
|
188
|
+
default: {
|
|
189
|
+
fill: '#45CDBE', // --es-shallows
|
|
190
|
+
fillOpacity: 0.28,
|
|
191
|
+
stroke: '#15B3A6', // --es-estela
|
|
192
|
+
strokeWidth: 1.5,
|
|
193
|
+
median: '#F1FBF9', // --es-foam
|
|
194
|
+
medianWidth: 2,
|
|
195
|
+
whisker: '#a4e4d9', // --es-reef
|
|
196
|
+
whiskerWidth: 1.5,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
bar: {
|
|
200
|
+
// Brand-teal fill on the dark ground; the selected bar lifts to the bright
|
|
201
|
+
// reef + an outline. `secondary` is the warm filament accent.
|
|
202
|
+
default: {
|
|
203
|
+
fill: '#15B3A6', // --es-estela
|
|
204
|
+
opacity: 0.85,
|
|
205
|
+
highlight: '#7FE2D2', // --es-reef (bright on the dark ground)
|
|
206
|
+
gap: 1,
|
|
207
|
+
minWidth: 1,
|
|
208
|
+
outlineWidth: 1.5,
|
|
209
|
+
},
|
|
210
|
+
secondary: {
|
|
211
|
+
fill: '#E0B36A', // --es-filament
|
|
212
|
+
opacity: 0.85,
|
|
213
|
+
highlight: '#F1D9A8',
|
|
214
|
+
gap: 1,
|
|
215
|
+
minWidth: 1,
|
|
216
|
+
outlineWidth: 1.5,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
axis: {
|
|
220
|
+
label: '#4E6B6B', // --es-slate
|
|
221
|
+
grid: '#06343A', // --es-ink
|
|
222
|
+
gridDash: [2, 3],
|
|
223
|
+
},
|
|
224
|
+
font: {
|
|
225
|
+
family: '"JetBrains Mono", ui-monospace, monospace', // --es-font-data
|
|
226
|
+
size: 11,
|
|
227
|
+
},
|
|
228
|
+
cursor: '#7FE2D2', // --es-reef (bright tracker on the dark ground)
|
|
229
|
+
chip: { background: '#0B4E58' }, // --es-deep (panel behind readout text)
|
|
230
|
+
gap: { connectorOpacity: 0.5 },
|
|
231
|
+
};
|
|
232
|
+
//# sourceMappingURL=theme.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor geometry helpers — pure functions deciding *what* the cursor draws
|
|
3
|
+
* ({@link cursorParts}) and *where* it sits ({@link resolveCursorX}). The marks
|
|
4
|
+
* themselves render as an SVG overlay in `Layers` (no cursor canvas); these
|
|
5
|
+
* helpers stay pure, so they're unit-tested directly.
|
|
6
|
+
*/
|
|
7
|
+
import type { CursorMode } from './context.js';
|
|
8
|
+
/** Default cursor mode — the synced vertical line (cursor enabled on the
|
|
9
|
+
* container by default; pair with an off-chart readout via `onTrackerChanged`). */
|
|
10
|
+
export declare const DEFAULT_CURSOR_MODE: CursorMode;
|
|
11
|
+
/**
|
|
12
|
+
* Decompose a {@link CursorMode} into what it draws: the shared vertical line,
|
|
13
|
+
* the per-series dots, and which value chip (if any). The modes are exclusive
|
|
14
|
+
* presets — `line` is line-only, `point` / `inline` / `flag` are dot-based with
|
|
15
|
+
* no line, `none` draws nothing. `flag` raises a staff from each point to a
|
|
16
|
+
* value flag stacked near the top of the row (drawn in `Layers`).
|
|
17
|
+
*/
|
|
18
|
+
export declare function cursorParts(mode: CursorMode): {
|
|
19
|
+
readonly line: boolean;
|
|
20
|
+
readonly dots: boolean;
|
|
21
|
+
readonly chip: 'none' | 'inline' | 'flag';
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The crosshair's plot-pixel x from the tracker inputs. A controlled
|
|
25
|
+
* `trackerPosition` (epoch ms) maps through `xScale`, so a pinned time rides with
|
|
26
|
+
* the data; `null` hides it; `undefined` (uncontrolled) uses the stored hover
|
|
27
|
+
* pixel, so a still cursor stays put while a live window slides under it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveCursorX(trackerPosition: number | null | undefined, hoverX: number | null, xScale: (time: number) => number): number | null;
|
|
30
|
+
//# sourceMappingURL=tracker.d.ts.map
|
package/dist/tracker.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor geometry helpers — pure functions deciding *what* the cursor draws
|
|
3
|
+
* ({@link cursorParts}) and *where* it sits ({@link resolveCursorX}). The marks
|
|
4
|
+
* themselves render as an SVG overlay in `Layers` (no cursor canvas); these
|
|
5
|
+
* helpers stay pure, so they're unit-tested directly.
|
|
6
|
+
*/
|
|
7
|
+
/** Default cursor mode — the synced vertical line (cursor enabled on the
|
|
8
|
+
* container by default; pair with an off-chart readout via `onTrackerChanged`). */
|
|
9
|
+
export const DEFAULT_CURSOR_MODE = 'line';
|
|
10
|
+
/**
|
|
11
|
+
* Decompose a {@link CursorMode} into what it draws: the shared vertical line,
|
|
12
|
+
* the per-series dots, and which value chip (if any). The modes are exclusive
|
|
13
|
+
* presets — `line` is line-only, `point` / `inline` / `flag` are dot-based with
|
|
14
|
+
* no line, `none` draws nothing. `flag` raises a staff from each point to a
|
|
15
|
+
* value flag stacked near the top of the row (drawn in `Layers`).
|
|
16
|
+
*/
|
|
17
|
+
export function cursorParts(mode) {
|
|
18
|
+
switch (mode) {
|
|
19
|
+
case 'line':
|
|
20
|
+
return { line: true, dots: false, chip: 'none' };
|
|
21
|
+
case 'point':
|
|
22
|
+
return { line: false, dots: true, chip: 'none' };
|
|
23
|
+
case 'inline':
|
|
24
|
+
return { line: false, dots: true, chip: 'inline' };
|
|
25
|
+
case 'flag':
|
|
26
|
+
return { line: false, dots: true, chip: 'flag' };
|
|
27
|
+
case 'none':
|
|
28
|
+
return { line: false, dots: false, chip: 'none' };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The crosshair's plot-pixel x from the tracker inputs. A controlled
|
|
33
|
+
* `trackerPosition` (epoch ms) maps through `xScale`, so a pinned time rides with
|
|
34
|
+
* the data; `null` hides it; `undefined` (uncontrolled) uses the stored hover
|
|
35
|
+
* pixel, so a still cursor stays put while a live window slides under it.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveCursorX(trackerPosition, hoverX, xScale) {
|
|
38
|
+
if (trackerPosition === undefined)
|
|
39
|
+
return hoverX;
|
|
40
|
+
if (trackerPosition === null)
|
|
41
|
+
return null;
|
|
42
|
+
return xScale(trackerPosition);
|
|
43
|
+
}
|
|
44
|
+
// The cursor's line / dots / flag-staffs render as an SVG overlay in `Layers`
|
|
45
|
+
// (DOM, crisp, positioned in plot space) — there is no cursor canvas, so the
|
|
46
|
+
// former `drawCrosshair` / `drawTrackerDot` canvas primitives are gone.
|
|
47
|
+
//# sourceMappingURL=tracker.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stable, unique key for **this component instance**, used to claim a slot in
|
|
3
|
+
* a parent collection (the axis / layer registries in `ChartRow`).
|
|
4
|
+
*
|
|
5
|
+
* This is *instance identity*, deliberately **not** `useId` and **not** a
|
|
6
|
+
* data-derived value:
|
|
7
|
+
* - `useId` is for SSR-stable accessibility attributes; the React docs
|
|
8
|
+
* explicitly discourage it as a collection key.
|
|
9
|
+
* - A React reconciliation `key` should come from your data — but that rule is
|
|
10
|
+
* about rendering *lists of data*. A registry of mounted *component instances*
|
|
11
|
+
* is a different thing: two `<LineChart>`s with identical props are still two
|
|
12
|
+
* distinct layers, and an axis's data `id` can legitimately repeat. The
|
|
13
|
+
* correct identity for "which mounted instance is this" is the instance
|
|
14
|
+
* itself, which a ref token captures (and which lets the registry update a
|
|
15
|
+
* slot in place rather than reorder — see `ChartRow`).
|
|
16
|
+
*
|
|
17
|
+
* The token is a `Symbol` created once per instance (lazily, no per-render
|
|
18
|
+
* allocation) and never escapes into rendered output.
|
|
19
|
+
*/
|
|
20
|
+
export declare function useSlotKey(): symbol;
|
|
21
|
+
//# sourceMappingURL=use-slot-key.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* A stable, unique key for **this component instance**, used to claim a slot in
|
|
4
|
+
* a parent collection (the axis / layer registries in `ChartRow`).
|
|
5
|
+
*
|
|
6
|
+
* This is *instance identity*, deliberately **not** `useId` and **not** a
|
|
7
|
+
* data-derived value:
|
|
8
|
+
* - `useId` is for SSR-stable accessibility attributes; the React docs
|
|
9
|
+
* explicitly discourage it as a collection key.
|
|
10
|
+
* - A React reconciliation `key` should come from your data — but that rule is
|
|
11
|
+
* about rendering *lists of data*. A registry of mounted *component instances*
|
|
12
|
+
* is a different thing: two `<LineChart>`s with identical props are still two
|
|
13
|
+
* distinct layers, and an axis's data `id` can legitimately repeat. The
|
|
14
|
+
* correct identity for "which mounted instance is this" is the instance
|
|
15
|
+
* itself, which a ref token captures (and which lets the registry update a
|
|
16
|
+
* slot in place rather than reorder — see `ChartRow`).
|
|
17
|
+
*
|
|
18
|
+
* The token is a `Symbol` created once per instance (lazily, no per-render
|
|
19
|
+
* allocation) and never escapes into rendered output.
|
|
20
|
+
*/
|
|
21
|
+
export function useSlotKey() {
|
|
22
|
+
const ref = useRef(null);
|
|
23
|
+
return (ref.current ??= Symbol('chart-slot'));
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=use-slot-key.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure time-range viewport math for pan/zoom (M4.2). The container holds the view
|
|
3
|
+
* range; these compute the next range from a gesture (the event surface in
|
|
4
|
+
* `Layers` supplies the pixel→time deltas). Kept pure + free of React/canvas so
|
|
5
|
+
* the geometry is unit-tested directly, like {@link maxSlotWidths} / the tracker.
|
|
6
|
+
*/
|
|
7
|
+
export type TimeRange = readonly [number, number];
|
|
8
|
+
/**
|
|
9
|
+
* Shift a range by `dt` ms (drag-pan). The caller signs `dt` from the gesture —
|
|
10
|
+
* dragging the plot right reveals earlier data, i.e. a negative `dt`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function panRange(range: TimeRange, dt: number): [number, number];
|
|
13
|
+
/**
|
|
14
|
+
* Zoom `range` around `pivot` (ms) by `factor` — `< 1` zooms in, `> 1` out, with
|
|
15
|
+
* the pivot held fixed (the time under the cursor stays put). Clamped so the
|
|
16
|
+
* duration never drops below `minDuration` (the zoom-in floor); at the floor the
|
|
17
|
+
* pivot keeps its fractional position in the window.
|
|
18
|
+
*/
|
|
19
|
+
export declare function zoomRange(range: TimeRange, pivot: number, factor: number, minDuration?: number): [number, number];
|
|
20
|
+
//# sourceMappingURL=viewport.d.ts.map
|
package/dist/viewport.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure time-range viewport math for pan/zoom (M4.2). The container holds the view
|
|
3
|
+
* range; these compute the next range from a gesture (the event surface in
|
|
4
|
+
* `Layers` supplies the pixel→time deltas). Kept pure + free of React/canvas so
|
|
5
|
+
* the geometry is unit-tested directly, like {@link maxSlotWidths} / the tracker.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Shift a range by `dt` ms (drag-pan). The caller signs `dt` from the gesture —
|
|
9
|
+
* dragging the plot right reveals earlier data, i.e. a negative `dt`.
|
|
10
|
+
*/
|
|
11
|
+
export function panRange(range, dt) {
|
|
12
|
+
return [range[0] + dt, range[1] + dt];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Zoom `range` around `pivot` (ms) by `factor` — `< 1` zooms in, `> 1` out, with
|
|
16
|
+
* the pivot held fixed (the time under the cursor stays put). Clamped so the
|
|
17
|
+
* duration never drops below `minDuration` (the zoom-in floor); at the floor the
|
|
18
|
+
* pivot keeps its fractional position in the window.
|
|
19
|
+
*/
|
|
20
|
+
export function zoomRange(range, pivot, factor, minDuration = 1) {
|
|
21
|
+
const lo = pivot - (pivot - range[0]) * factor;
|
|
22
|
+
const hi = pivot + (range[1] - pivot) * factor;
|
|
23
|
+
if (hi - lo >= minDuration)
|
|
24
|
+
return [lo, hi];
|
|
25
|
+
// Floor reached: hold the pivot's fractional position, set span = minDuration.
|
|
26
|
+
const span = range[1] - range[0];
|
|
27
|
+
const frac = span > 0 ? (pivot - range[0]) / span : 0.5;
|
|
28
|
+
return [pivot - minDuration * frac, pivot + minDuration * (1 - frac)];
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=viewport.js.map
|