insomni-plot 0.1.0-alpha.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/LICENSE.md +674 -0
- package/README.md +81 -0
- package/dist/core.d.mts +340 -0
- package/dist/core.mjs +1047 -0
- package/dist/index.d.mts +3426 -0
- package/dist/index.mjs +12762 -0
- package/dist/interactions-DEFL_F4E.mjs +5395 -0
- package/dist/range-presets-CzECsu3V.d.mts +1523 -0
- package/package.json +34 -0
- package/src/annotations.d.ts +121 -0
- package/src/annotations.ts +438 -0
- package/src/axis.d.ts +184 -0
- package/src/axis.test.ts +131 -0
- package/src/axis.ts +765 -0
- package/src/colorbar.d.ts +69 -0
- package/src/colorbar.ts +294 -0
- package/src/colors.d.ts +57 -0
- package/src/colors.test.ts +28 -0
- package/src/colors.ts +486 -0
- package/src/core.ts +299 -0
- package/src/format.d.ts +54 -0
- package/src/format.ts +138 -0
- package/src/grammar/accessibility.d.ts +147 -0
- package/src/grammar/accessibility.test.ts +199 -0
- package/src/grammar/accessibility.ts +443 -0
- package/src/grammar/aes.d.ts +35 -0
- package/src/grammar/aes.test.ts +75 -0
- package/src/grammar/aes.ts +120 -0
- package/src/grammar/annotations.d.ts +86 -0
- package/src/grammar/annotations.test.ts +68 -0
- package/src/grammar/annotations.ts +336 -0
- package/src/grammar/attach-brush.d.ts +44 -0
- package/src/grammar/attach-brush.test.ts +214 -0
- package/src/grammar/attach-brush.ts +111 -0
- package/src/grammar/attach-presets.d.ts +33 -0
- package/src/grammar/attach-presets.test.ts +106 -0
- package/src/grammar/attach-presets.ts +215 -0
- package/src/grammar/chart.d.ts +952 -0
- package/src/grammar/chart.test.ts +118 -0
- package/src/grammar/chart.ts +1172 -0
- package/src/grammar/color-utils.d.ts +29 -0
- package/src/grammar/color-utils.test.ts +53 -0
- package/src/grammar/color-utils.ts +66 -0
- package/src/grammar/constants.d.ts +45 -0
- package/src/grammar/constants.ts +61 -0
- package/src/grammar/coord.d.ts +183 -0
- package/src/grammar/coord.test.ts +355 -0
- package/src/grammar/coord.ts +619 -0
- package/src/grammar/data/pivot.d.ts +57 -0
- package/src/grammar/data/pivot.ts +107 -0
- package/src/grammar/emphasis-driver.d.ts +69 -0
- package/src/grammar/emphasis-driver.test.ts +199 -0
- package/src/grammar/emphasis-driver.ts +205 -0
- package/src/grammar/equality.d.ts +3 -0
- package/src/grammar/equality.ts +40 -0
- package/src/grammar/facet.d.ts +63 -0
- package/src/grammar/facet.test.ts +60 -0
- package/src/grammar/facet.ts +175 -0
- package/src/grammar/geoms/_categorical.d.ts +94 -0
- package/src/grammar/geoms/_categorical.ts +0 -0
- package/src/grammar/geoms/_distribution.d.ts +52 -0
- package/src/grammar/geoms/_distribution.ts +125 -0
- package/src/grammar/geoms/_mark.d.ts +69 -0
- package/src/grammar/geoms/_mark.ts +136 -0
- package/src/grammar/geoms/_shape.d.ts +41 -0
- package/src/grammar/geoms/_shape.ts +74 -0
- package/src/grammar/geoms/aggregate.d.ts +95 -0
- package/src/grammar/geoms/aggregate.test.ts +554 -0
- package/src/grammar/geoms/aggregate.ts +840 -0
- package/src/grammar/geoms/area.d.ts +32 -0
- package/src/grammar/geoms/area.test.ts +165 -0
- package/src/grammar/geoms/area.ts +578 -0
- package/src/grammar/geoms/band.d.ts +27 -0
- package/src/grammar/geoms/band.test.ts +57 -0
- package/src/grammar/geoms/band.ts +126 -0
- package/src/grammar/geoms/bar.d.ts +56 -0
- package/src/grammar/geoms/bar.test.ts +367 -0
- package/src/grammar/geoms/bar.ts +1054 -0
- package/src/grammar/geoms/boxplot.d.ts +129 -0
- package/src/grammar/geoms/boxplot.test.ts +299 -0
- package/src/grammar/geoms/boxplot.ts +834 -0
- package/src/grammar/geoms/connected-scatter.d.ts +27 -0
- package/src/grammar/geoms/connected-scatter.test.ts +157 -0
- package/src/grammar/geoms/connected-scatter.ts +63 -0
- package/src/grammar/geoms/emphasis.d.ts +76 -0
- package/src/grammar/geoms/emphasis.test.ts +135 -0
- package/src/grammar/geoms/emphasis.ts +162 -0
- package/src/grammar/geoms/histogram.d.ts +75 -0
- package/src/grammar/geoms/histogram.test.ts +262 -0
- package/src/grammar/geoms/histogram.ts +740 -0
- package/src/grammar/geoms/index.d.ts +20 -0
- package/src/grammar/geoms/index.ts +77 -0
- package/src/grammar/geoms/interval.d.ts +31 -0
- package/src/grammar/geoms/interval.test.ts +154 -0
- package/src/grammar/geoms/interval.ts +342 -0
- package/src/grammar/geoms/line.d.ts +38 -0
- package/src/grammar/geoms/line.test.ts +247 -0
- package/src/grammar/geoms/line.ts +659 -0
- package/src/grammar/geoms/point.d.ts +57 -0
- package/src/grammar/geoms/point.test.ts +163 -0
- package/src/grammar/geoms/point.ts +545 -0
- package/src/grammar/geoms/polar.test.ts +216 -0
- package/src/grammar/geoms/ribbon.d.ts +21 -0
- package/src/grammar/geoms/ribbon.test.ts +170 -0
- package/src/grammar/geoms/ribbon.ts +87 -0
- package/src/grammar/geoms/ridgeline.d.ts +89 -0
- package/src/grammar/geoms/ridgeline.test.ts +247 -0
- package/src/grammar/geoms/ridgeline.ts +1164 -0
- package/src/grammar/geoms/rolling.d.ts +43 -0
- package/src/grammar/geoms/rolling.test.ts +217 -0
- package/src/grammar/geoms/rolling.ts +387 -0
- package/src/grammar/geoms/rug.d.ts +28 -0
- package/src/grammar/geoms/rug.test.ts +126 -0
- package/src/grammar/geoms/rug.ts +214 -0
- package/src/grammar/geoms/rule.d.ts +23 -0
- package/src/grammar/geoms/rule.test.ts +69 -0
- package/src/grammar/geoms/rule.ts +212 -0
- package/src/grammar/geoms/smooth.d.ts +54 -0
- package/src/grammar/geoms/smooth.test.ts +78 -0
- package/src/grammar/geoms/smooth.ts +337 -0
- package/src/grammar/geoms/text.d.ts +29 -0
- package/src/grammar/geoms/text.test.ts +64 -0
- package/src/grammar/geoms/text.ts +234 -0
- package/src/grammar/geoms/tile.d.ts +61 -0
- package/src/grammar/geoms/tile.test.ts +157 -0
- package/src/grammar/geoms/tile.ts +621 -0
- package/src/grammar/geoms/types.d.ts +319 -0
- package/src/grammar/geoms/types.ts +362 -0
- package/src/grammar/geoms/violin.d.ts +85 -0
- package/src/grammar/geoms/violin.test.ts +187 -0
- package/src/grammar/geoms/violin.ts +672 -0
- package/src/grammar/index.d.ts +22 -0
- package/src/grammar/index.ts +269 -0
- package/src/grammar/interactions/_disposable.d.ts +5 -0
- package/src/grammar/interactions/_disposable.ts +23 -0
- package/src/grammar/interactions/_z.d.ts +4 -0
- package/src/grammar/interactions/_z.ts +16 -0
- package/src/grammar/interactions/brush-selection.test.ts +262 -0
- package/src/grammar/interactions/brush.d.ts +63 -0
- package/src/grammar/interactions/brush.test.ts +483 -0
- package/src/grammar/interactions/brush.ts +452 -0
- package/src/grammar/interactions/crosshair.d.ts +19 -0
- package/src/grammar/interactions/crosshair.test.ts +127 -0
- package/src/grammar/interactions/crosshair.ts +76 -0
- package/src/grammar/interactions/hit-layer.d.ts +64 -0
- package/src/grammar/interactions/hit-layer.ts +246 -0
- package/src/grammar/interactions/legend.d.ts +19 -0
- package/src/grammar/interactions/legend.ts +101 -0
- package/src/grammar/interactions/menu.d.ts +93 -0
- package/src/grammar/interactions/menu.test.ts +373 -0
- package/src/grammar/interactions/menu.ts +342 -0
- package/src/grammar/interactions/selection.d.ts +25 -0
- package/src/grammar/interactions/selection.test.ts +289 -0
- package/src/grammar/interactions/selection.ts +142 -0
- package/src/grammar/interactions/series-readout.d.ts +91 -0
- package/src/grammar/interactions/series-readout.test.ts +668 -0
- package/src/grammar/interactions/series-readout.ts +422 -0
- package/src/grammar/interactions/series-snap.d.ts +70 -0
- package/src/grammar/interactions/series-snap.test.ts +214 -0
- package/src/grammar/interactions/series-snap.ts +218 -0
- package/src/grammar/interactions/tooltip-axis.test.ts +176 -0
- package/src/grammar/interactions/tooltip-touch.browser.test.ts +49 -0
- package/src/grammar/interactions/tooltip-touch.test.ts +161 -0
- package/src/grammar/interactions/tooltip.d.ts +140 -0
- package/src/grammar/interactions/tooltip.test.ts +406 -0
- package/src/grammar/interactions/tooltip.ts +622 -0
- package/src/grammar/interactions/transitions.d.ts +34 -0
- package/src/grammar/interactions/transitions.test.ts +172 -0
- package/src/grammar/interactions/transitions.ts +160 -0
- package/src/grammar/layout.d.ts +68 -0
- package/src/grammar/layout.ts +186 -0
- package/src/grammar/legend-merge.test.ts +332 -0
- package/src/grammar/mount.d.ts +78 -0
- package/src/grammar/mount.test.ts +479 -0
- package/src/grammar/mount.ts +2112 -0
- package/src/grammar/palettes.d.ts +54 -0
- package/src/grammar/palettes.test.ts +80 -0
- package/src/grammar/palettes.ts +167 -0
- package/src/grammar/pan-zoom.test.ts +398 -0
- package/src/grammar/phylo.d.ts +65 -0
- package/src/grammar/phylo.test.ts +59 -0
- package/src/grammar/phylo.ts +112 -0
- package/src/grammar/pipeline.auto-ticks.test.ts +40 -0
- package/src/grammar/pipeline.d.ts +158 -0
- package/src/grammar/pipeline.test.ts +463 -0
- package/src/grammar/pipeline.ts +1233 -0
- package/src/grammar/profiling.d.ts +8 -0
- package/src/grammar/profiling.ts +24 -0
- package/src/grammar/scales.d.ts +188 -0
- package/src/grammar/scales.test.ts +181 -0
- package/src/grammar/scales.ts +800 -0
- package/src/grammar/svg.d.ts +3 -0
- package/src/grammar/svg.ts +39 -0
- package/src/grammar/theme.d.ts +261 -0
- package/src/grammar/theme.test.ts +105 -0
- package/src/grammar/theme.ts +490 -0
- package/src/heatmap/cpu.ts +109 -0
- package/src/heatmap/gpu.ts +565 -0
- package/src/heatmap/types.ts +177 -0
- package/src/heatmap.browser.test.ts +308 -0
- package/src/heatmap.test.ts +320 -0
- package/src/heatmap.ts +123 -0
- package/src/index.d.ts +1 -0
- package/src/index.ts +8 -0
- package/src/interactions.d.ts +48 -0
- package/src/interactions.test.ts +226 -0
- package/src/interactions.ts +394 -0
- package/src/layout/box.d.ts +48 -0
- package/src/layout/box.test.ts +107 -0
- package/src/layout/box.ts +143 -0
- package/src/legend.d.ts +115 -0
- package/src/legend.ts +422 -0
- package/src/marks/curve.d.ts +43 -0
- package/src/marks/curve.ts +244 -0
- package/src/marks/stack.d.ts +53 -0
- package/src/marks/stack.ts +184 -0
- package/src/marks.d.ts +273 -0
- package/src/marks.test.ts +541 -0
- package/src/marks.ts +1292 -0
- package/src/navigator.test.ts +174 -0
- package/src/navigator.ts +393 -0
- package/src/range-presets.d.ts +113 -0
- package/src/range-presets.test.ts +345 -0
- package/src/range-presets.ts +349 -0
- package/src/scales.d.ts +98 -0
- package/src/scales.test.ts +103 -0
- package/src/scales.ts +695 -0
- package/src/stats/index.d.ts +200 -0
- package/src/stats/index.test.ts +349 -0
- package/src/stats/index.ts +740 -0
- package/src/stats/regression.d.ts +38 -0
- package/src/stats/regression.test.ts +56 -0
- package/src/stats/regression.ts +396 -0
- package/src/stats/rolling-window.d.ts +55 -0
- package/src/stats/rolling-window.test.ts +237 -0
- package/src/stats/rolling-window.ts +256 -0
- package/src/test-setup.ts +19 -0
- package/src/viewport/axis-state.d.ts +72 -0
- package/src/viewport/axis-state.ts +476 -0
- package/src/viewport.d.ts +170 -0
- package/src/viewport.test.ts +363 -0
- package/src/viewport.ts +510 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Theme
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Visual style for a chart. Themes are values; live-toggle works because the
|
|
5
|
+
// chart owns a `Signal<Theme>` (see chart.ts) and re-renders on `.set(...)`.
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
easeInOutCubic,
|
|
9
|
+
easeOutCubic,
|
|
10
|
+
easeOutQuad,
|
|
11
|
+
linear as linearEasing,
|
|
12
|
+
rgba,
|
|
13
|
+
type BlendSpace,
|
|
14
|
+
type Color,
|
|
15
|
+
type Easing,
|
|
16
|
+
} from "insomni";
|
|
17
|
+
import { category10, viridis, type CategoricalPalette, type ContinuousPalette } from "../colors.ts";
|
|
18
|
+
import { DEFAULT_ACCESSIBILITY, type Accessibility, type TextEffects } from "./accessibility.ts";
|
|
19
|
+
|
|
20
|
+
export type { Accessibility, AccessibilityMode, TextEffects } from "./accessibility.ts";
|
|
21
|
+
|
|
22
|
+
export interface ThemeText {
|
|
23
|
+
color: Color;
|
|
24
|
+
fontFamily: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ThemeTitle {
|
|
28
|
+
fontSize: number;
|
|
29
|
+
fontWeight: string;
|
|
30
|
+
color: Color;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ThemeSubtitle {
|
|
34
|
+
fontSize: number;
|
|
35
|
+
color: Color;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ThemeAxis {
|
|
39
|
+
color: Color;
|
|
40
|
+
gridColor: Color;
|
|
41
|
+
labelFontSize: number;
|
|
42
|
+
labelColor: Color;
|
|
43
|
+
titleFontSize: number;
|
|
44
|
+
titleColor: Color;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ThemeLegend {
|
|
48
|
+
fontSize: number;
|
|
49
|
+
labelColor: Color;
|
|
50
|
+
swatchGap: number;
|
|
51
|
+
entryGap: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ThemeMarks {
|
|
55
|
+
pointRadius: number;
|
|
56
|
+
pointStroke: Color | undefined;
|
|
57
|
+
pointStrokeWidth: number;
|
|
58
|
+
/**
|
|
59
|
+
* Default stroke width for line-based marks (`line`, `smooth`, `statRolling`,
|
|
60
|
+
* `area` outline, `aggregate` line geom). Geom-level `strokeWidth` options
|
|
61
|
+
* override this token per layer.
|
|
62
|
+
*/
|
|
63
|
+
strokeWidth: number;
|
|
64
|
+
barCornerRadius: number;
|
|
65
|
+
fillAlpha: number;
|
|
66
|
+
/** Font size for value/total labels rendered alongside marks (bar, text). */
|
|
67
|
+
labelFontSize: number;
|
|
68
|
+
/** Default stroke width for `rule()` lines. */
|
|
69
|
+
ruleStrokeWidth: number;
|
|
70
|
+
/** Default label inset for `rule()` annotations. */
|
|
71
|
+
ruleLabelInset: number;
|
|
72
|
+
/** Default font size for inline annotations (rule, band labels). */
|
|
73
|
+
annotationFontSize: number;
|
|
74
|
+
/** Default fill alpha for `ribbon()`. */
|
|
75
|
+
ribbonFillAlpha: number;
|
|
76
|
+
/** Default fill alpha for `band()` highlight regions. */
|
|
77
|
+
bandFillAlpha: number;
|
|
78
|
+
/** Default size-channel pixel range. */
|
|
79
|
+
sizeRange: readonly [number, number];
|
|
80
|
+
/** Default alpha-channel range. */
|
|
81
|
+
alphaRange: readonly [number, number];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Motion tokens
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Durations are milliseconds. Geoms and interaction primitives read these
|
|
88
|
+
// names rather than hard-coded numbers so a single theme switch retunes the
|
|
89
|
+
// whole chart's motion. `enabled: false` is the master kill — interactions
|
|
90
|
+
// should snap rather than animate. Use `applyReducedMotion(theme)` to derive
|
|
91
|
+
// a theme honoring `prefers-reduced-motion`.
|
|
92
|
+
|
|
93
|
+
export type ThemeDurationKey = "fast" | "base" | "slow";
|
|
94
|
+
export type ThemeEasingKey = "standard" | "emphasized" | "decelerate" | "linear";
|
|
95
|
+
|
|
96
|
+
export interface ThemeDurations {
|
|
97
|
+
fast: number;
|
|
98
|
+
base: number;
|
|
99
|
+
slow: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ThemeEasings {
|
|
103
|
+
standard: Easing;
|
|
104
|
+
emphasized: Easing;
|
|
105
|
+
decelerate: Easing;
|
|
106
|
+
linear: Easing;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ThemeMotionChannel {
|
|
110
|
+
duration: ThemeDurationKey;
|
|
111
|
+
easing: ThemeEasingKey;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface ThemeMotionTooltip {
|
|
115
|
+
showDelay: number;
|
|
116
|
+
hideDelay: number;
|
|
117
|
+
fadeMs: number;
|
|
118
|
+
/**
|
|
119
|
+
* Hover-intent settle (ms). Rapid enter/leave/switch events are coalesced —
|
|
120
|
+
* the tooltip only commits a show/hide after the cursor holds a target this
|
|
121
|
+
* long. Prevents an overlay re-render per cell while sweeping across a dense
|
|
122
|
+
* mark grid or through gaps. 0 disables (commit immediately).
|
|
123
|
+
*/
|
|
124
|
+
settleDelay: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface ThemeMotion {
|
|
128
|
+
/** Master switch. False → snap, no scheduled motion. */
|
|
129
|
+
enabled: boolean;
|
|
130
|
+
duration: ThemeDurations;
|
|
131
|
+
easing: ThemeEasings;
|
|
132
|
+
/** Data update / scale change transitions. */
|
|
133
|
+
data: ThemeMotionChannel;
|
|
134
|
+
/** Axis tick / domain transitions. */
|
|
135
|
+
axis: ThemeMotionChannel;
|
|
136
|
+
tooltip: ThemeMotionTooltip;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const defaultMotion: ThemeMotion = {
|
|
140
|
+
enabled: true,
|
|
141
|
+
duration: { fast: 120, base: 240, slow: 480 },
|
|
142
|
+
easing: {
|
|
143
|
+
standard: easeInOutCubic,
|
|
144
|
+
emphasized: easeOutCubic,
|
|
145
|
+
decelerate: easeOutQuad,
|
|
146
|
+
linear: linearEasing,
|
|
147
|
+
},
|
|
148
|
+
data: { duration: "base", easing: "emphasized" },
|
|
149
|
+
axis: { duration: "base", easing: "standard" },
|
|
150
|
+
tooltip: { showDelay: 0, hideDelay: 50, fadeMs: 80, settleDelay: 30 },
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Interaction visuals — hover / selection emphasis tokens
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Geoms read these to render their hover halo and (optionally) dim the
|
|
157
|
+
// non-hovered rows. Two complementary techniques compose: a halo that draws
|
|
158
|
+
// the active row in a stronger stroke on top, and a dim factor applied to the
|
|
159
|
+
// fill alpha of the others. Both are off by default for sparse marks (point,
|
|
160
|
+
// line) and on for shape-based marks (bar, histogram, ridgeline, tile,
|
|
161
|
+
// boxplot, violin) where overlap makes a point-only halo hard to read.
|
|
162
|
+
//
|
|
163
|
+
// `dim` is a multiplier applied to the resolved fill alpha of non-hovered
|
|
164
|
+
// rows (1 = no dim, 0 = invisible). `haloStrokeWidth` is in CSS px.
|
|
165
|
+
|
|
166
|
+
export interface ThemeInteractionEmphasis {
|
|
167
|
+
/** Master enable. `false` skips both halo and dim. */
|
|
168
|
+
enabled: boolean;
|
|
169
|
+
/** Multiplier on non-active rows' fill alpha. `1` = no dim. */
|
|
170
|
+
dim: number;
|
|
171
|
+
/** Halo stroke width in CSS px. `0` skips the halo. */
|
|
172
|
+
haloStrokeWidth: number;
|
|
173
|
+
/**
|
|
174
|
+
* Halo ring color. When omitted, geoms fall back to a high-contrast
|
|
175
|
+
* foreground (`theme.text.color`) so the focus ring reads against the
|
|
176
|
+
* marks rather than blending in with the datum's own color. Set a `Color`
|
|
177
|
+
* to pin the ring (e.g. a brand accent).
|
|
178
|
+
*/
|
|
179
|
+
haloColor?: Color;
|
|
180
|
+
/**
|
|
181
|
+
* Duration (ms) of the GPU dim animation driven through the core's emphasis
|
|
182
|
+
* uniform (P5-T3). On a hover hit-change over a dim-participating geom the
|
|
183
|
+
* mount ramps the emphasis `t` 0→1 (ease-out cubic) over this window; on
|
|
184
|
+
* hover exit it ramps t→0. Only consumed by `hover` (selection dim stays a
|
|
185
|
+
* compile-time treatment). `<= 0` snaps. Default ~120ms.
|
|
186
|
+
*/
|
|
187
|
+
durationMs?: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Semantic row-tint tokens for tooltip rows that use the
|
|
192
|
+
* `accent: "positive" | "negative" | "neutral"` shorthand. `neutral` is
|
|
193
|
+
* intentionally omitted — it means "no override, use the default row text
|
|
194
|
+
* color from the underlying tooltip style." Consumers can also pass a raw
|
|
195
|
+
* `Color` per row to bypass these tokens entirely.
|
|
196
|
+
*/
|
|
197
|
+
export interface ThemeTooltipAccents {
|
|
198
|
+
positive: Color;
|
|
199
|
+
negative: Color;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface ThemeInteractions {
|
|
203
|
+
/** Visual treatment applied while a row is hovered. */
|
|
204
|
+
hover: ThemeInteractionEmphasis;
|
|
205
|
+
/** Visual treatment applied while rows are selected. */
|
|
206
|
+
selection: ThemeInteractionEmphasis;
|
|
207
|
+
/**
|
|
208
|
+
* Grace window (ms) applied when hover transitions to "nothing" before
|
|
209
|
+
* propagating the null state to dim/halo and crosshair. If a new hit lands
|
|
210
|
+
* within this window, the swap goes directly from prev → next without
|
|
211
|
+
* passing through null — eliminates the un-dim → re-dim flash when the
|
|
212
|
+
* cursor crosses between adjacent or near-adjacent geoms. `0` disables.
|
|
213
|
+
*/
|
|
214
|
+
hoverSwapGraceMs: number;
|
|
215
|
+
/** Semantic row colors for the tooltip `accent` shorthand. */
|
|
216
|
+
tooltipAccents: ThemeTooltipAccents;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export const defaultInteractions: ThemeInteractions = {
|
|
220
|
+
hover: { enabled: true, dim: 0.45, haloStrokeWidth: 2, durationMs: 120 },
|
|
221
|
+
selection: { enabled: true, dim: 0.3, haloStrokeWidth: 2 },
|
|
222
|
+
hoverSwapGraceMs: 50,
|
|
223
|
+
tooltipAccents: {
|
|
224
|
+
positive: rgba(0.45, 0.83, 0.58, 1),
|
|
225
|
+
negative: rgba(0.96, 0.55, 0.55, 1),
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export interface ThemePalettes {
|
|
230
|
+
categorical: CategoricalPalette;
|
|
231
|
+
continuous: ContinuousPalette;
|
|
232
|
+
diverging: ContinuousPalette;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Semantic accent palette consumed by chart-level marks that carry meaning
|
|
237
|
+
* (reference regions, threshold rules, callouts, tooltip rows). Geoms that
|
|
238
|
+
* accept an accent key (`fill: "positive"`, `stroke: "warn"`) resolve through
|
|
239
|
+
* this block; literal `Color` values still bypass it.
|
|
240
|
+
*
|
|
241
|
+
* Naming exception to the "name geoms by what they are, not what they do"
|
|
242
|
+
* principle: these tokens exist explicitly to carry semantics. The names
|
|
243
|
+
* follow conventional status vocabulary so any reader can predict the role.
|
|
244
|
+
*
|
|
245
|
+
* `neutral` is intentionally omitted — it means "no override, use the
|
|
246
|
+
* surrounding default" (mark fill, axis label color, …) so consumers can fall
|
|
247
|
+
* back without spelling out a token.
|
|
248
|
+
*
|
|
249
|
+
* Distinct from {@link ThemeTooltipAccents} (under `interactions.tooltipAccents`)
|
|
250
|
+
* which is sized for tooltip-text contrast and may differ in luminance.
|
|
251
|
+
*/
|
|
252
|
+
export interface ThemeAccents {
|
|
253
|
+
positive: Color;
|
|
254
|
+
negative: Color;
|
|
255
|
+
warn: Color;
|
|
256
|
+
info: Color;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export type ThemeAccentKey = keyof ThemeAccents;
|
|
260
|
+
|
|
261
|
+
export interface Theme {
|
|
262
|
+
background: Color;
|
|
263
|
+
text: ThemeText;
|
|
264
|
+
title: ThemeTitle;
|
|
265
|
+
subtitle: ThemeSubtitle;
|
|
266
|
+
axis: ThemeAxis;
|
|
267
|
+
legend: ThemeLegend;
|
|
268
|
+
marks: ThemeMarks;
|
|
269
|
+
palettes: ThemePalettes;
|
|
270
|
+
/** Semantic accent palette for chart-level marks (reference regions, threshold rules, callouts). */
|
|
271
|
+
accents: ThemeAccents;
|
|
272
|
+
/**
|
|
273
|
+
* Text-readability policy. Applied at every draw site where the chart
|
|
274
|
+
* knows the local background color (titles/axis/legend versus the panel
|
|
275
|
+
* background; tile labels versus their cell color).
|
|
276
|
+
*/
|
|
277
|
+
accessibility: Accessibility;
|
|
278
|
+
/**
|
|
279
|
+
* Default text effects (outline / drop shadow). API stub today — the SDF
|
|
280
|
+
* text path doesn't render these yet. See
|
|
281
|
+
* dev_docs/2026-04-30-text-accessibility.md.
|
|
282
|
+
*/
|
|
283
|
+
textEffects?: TextEffects;
|
|
284
|
+
/**
|
|
285
|
+
* Color space used to interpolate continuous palettes (`viridis`, etc.)
|
|
286
|
+
* when sampled by color scales and color bars. `oklch` is the default —
|
|
287
|
+
* perceptually uniform with hue preserved along the gradient. Override
|
|
288
|
+
* per chart (`theme({ paletteBlendSpace: "srgb" })`) or per call (the
|
|
289
|
+
* `blendSpace` option on color-scale and `colorBar` specs).
|
|
290
|
+
*/
|
|
291
|
+
paletteBlendSpace: BlendSpace;
|
|
292
|
+
/**
|
|
293
|
+
* Motion tokens — durations, easings, and per-channel defaults consumed by
|
|
294
|
+
* interactions (hover, tooltip), data transitions, and axis updates. See
|
|
295
|
+
* {@link defaultMotion} and {@link applyReducedMotion}.
|
|
296
|
+
*/
|
|
297
|
+
motion: ThemeMotion;
|
|
298
|
+
/**
|
|
299
|
+
* Hover / selection emphasis tokens. Geoms read these to render the active
|
|
300
|
+
* row's halo and optionally dim the rest. See {@link defaultInteractions}.
|
|
301
|
+
*/
|
|
302
|
+
interactions: ThemeInteractions;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const FONT_STACK = "system-ui, -apple-system, Inter, sans-serif";
|
|
306
|
+
|
|
307
|
+
export const themeDefault: Theme = {
|
|
308
|
+
background: rgba(0.02, 0.04, 0.07, 1),
|
|
309
|
+
text: {
|
|
310
|
+
color: rgba(0.85, 0.9, 0.98, 1),
|
|
311
|
+
fontFamily: FONT_STACK,
|
|
312
|
+
},
|
|
313
|
+
title: {
|
|
314
|
+
fontSize: 14,
|
|
315
|
+
fontWeight: "600",
|
|
316
|
+
color: rgba(0.92, 0.95, 1, 1),
|
|
317
|
+
},
|
|
318
|
+
subtitle: {
|
|
319
|
+
fontSize: 14,
|
|
320
|
+
color: rgba(0.7, 0.78, 0.9, 0.9),
|
|
321
|
+
},
|
|
322
|
+
axis: {
|
|
323
|
+
color: rgba(0.6, 0.68, 0.78, 0.85),
|
|
324
|
+
gridColor: rgba(0.38, 0.5, 0.62, 0.18),
|
|
325
|
+
labelFontSize: 14,
|
|
326
|
+
labelColor: rgba(0.6, 0.68, 0.78, 0.85),
|
|
327
|
+
titleFontSize: 14,
|
|
328
|
+
titleColor: rgba(0.85, 0.9, 0.98, 1),
|
|
329
|
+
},
|
|
330
|
+
legend: {
|
|
331
|
+
fontSize: 14,
|
|
332
|
+
labelColor: rgba(0.85, 0.9, 0.98, 1),
|
|
333
|
+
swatchGap: 6,
|
|
334
|
+
entryGap: 14,
|
|
335
|
+
},
|
|
336
|
+
marks: {
|
|
337
|
+
pointRadius: 3,
|
|
338
|
+
pointStroke: rgba(1, 1, 1, 0.18),
|
|
339
|
+
pointStrokeWidth: 0.75,
|
|
340
|
+
strokeWidth: 1.5,
|
|
341
|
+
barCornerRadius: 1,
|
|
342
|
+
fillAlpha: 0.85,
|
|
343
|
+
labelFontSize: 14,
|
|
344
|
+
ruleStrokeWidth: 1,
|
|
345
|
+
ruleLabelInset: 4,
|
|
346
|
+
annotationFontSize: 14,
|
|
347
|
+
ribbonFillAlpha: 0.18,
|
|
348
|
+
bandFillAlpha: 0.08,
|
|
349
|
+
sizeRange: [2, 14],
|
|
350
|
+
alphaRange: [0.2, 1],
|
|
351
|
+
},
|
|
352
|
+
palettes: {
|
|
353
|
+
categorical: category10,
|
|
354
|
+
continuous: viridis,
|
|
355
|
+
diverging: viridis,
|
|
356
|
+
},
|
|
357
|
+
accents: {
|
|
358
|
+
positive: rgba(0.45, 0.83, 0.58, 1),
|
|
359
|
+
negative: rgba(0.96, 0.55, 0.55, 1),
|
|
360
|
+
warn: rgba(0.98, 0.78, 0.36, 1),
|
|
361
|
+
info: rgba(0.5, 0.72, 0.96, 1),
|
|
362
|
+
},
|
|
363
|
+
accessibility: DEFAULT_ACCESSIBILITY,
|
|
364
|
+
paletteBlendSpace: "oklch",
|
|
365
|
+
motion: defaultMotion,
|
|
366
|
+
interactions: defaultInteractions,
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Light/minimal theme — white plot background, soft gray gridlines, dark
|
|
371
|
+
* labels. Pairs well with the `category10` palette and is a good default for
|
|
372
|
+
* scatter / smooth charts that should read like a ggplot2 / R reference.
|
|
373
|
+
*/
|
|
374
|
+
export const themeMinimalGrid: Theme = {
|
|
375
|
+
...themeDefault,
|
|
376
|
+
background: rgba(1, 1, 1, 1),
|
|
377
|
+
text: { color: rgba(0.2, 0.22, 0.27, 1), fontFamily: FONT_STACK },
|
|
378
|
+
title: { fontSize: 14, fontWeight: "600", color: rgba(0.15, 0.17, 0.22, 1) },
|
|
379
|
+
subtitle: { fontSize: 14, color: rgba(0.4, 0.43, 0.5, 1) },
|
|
380
|
+
axis: {
|
|
381
|
+
color: rgba(0.6, 0.62, 0.68, 1),
|
|
382
|
+
gridColor: rgba(0.85, 0.87, 0.9, 1),
|
|
383
|
+
labelFontSize: 14,
|
|
384
|
+
labelColor: rgba(0.35, 0.38, 0.45, 1),
|
|
385
|
+
titleFontSize: 14,
|
|
386
|
+
titleColor: rgba(0.2, 0.22, 0.27, 1),
|
|
387
|
+
},
|
|
388
|
+
legend: {
|
|
389
|
+
fontSize: 14,
|
|
390
|
+
labelColor: rgba(0.2, 0.22, 0.27, 1),
|
|
391
|
+
swatchGap: 6,
|
|
392
|
+
entryGap: 14,
|
|
393
|
+
},
|
|
394
|
+
marks: {
|
|
395
|
+
...themeDefault.marks,
|
|
396
|
+
pointStroke: undefined,
|
|
397
|
+
pointStrokeWidth: 0,
|
|
398
|
+
fillAlpha: 1,
|
|
399
|
+
},
|
|
400
|
+
accents: {
|
|
401
|
+
positive: rgba(0.16, 0.62, 0.36, 1),
|
|
402
|
+
negative: rgba(0.78, 0.22, 0.22, 1),
|
|
403
|
+
warn: rgba(0.85, 0.55, 0.1, 1),
|
|
404
|
+
info: rgba(0.18, 0.45, 0.78, 1),
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
export type DeepPartial<T> = {
|
|
409
|
+
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
413
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function deepMerge<T>(base: T, override: DeepPartial<T>): T {
|
|
417
|
+
if (!isPlainObject(base) || !isPlainObject(override)) {
|
|
418
|
+
return (override as T) ?? base;
|
|
419
|
+
}
|
|
420
|
+
const out = { ...base } as Record<string, unknown>;
|
|
421
|
+
for (const key of Object.keys(override)) {
|
|
422
|
+
const a = (base as Record<string, unknown>)[key];
|
|
423
|
+
const b = (override as Record<string, unknown>)[key];
|
|
424
|
+
if (b === undefined) continue;
|
|
425
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
426
|
+
out[key] = deepMerge(a, b as DeepPartial<unknown>);
|
|
427
|
+
} else {
|
|
428
|
+
out[key] = b;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return out as T;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Compose a theme by deep-merging overrides into a base theme. Accepts a
|
|
436
|
+
* shallow override on any nested group (e.g. `{ marks: { strokeWidth: 2 } }`).
|
|
437
|
+
*/
|
|
438
|
+
export function theme(overrides: DeepPartial<Theme>, base: Theme = themeDefault): Theme {
|
|
439
|
+
return deepMerge(base, overrides);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
// Motion helpers
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
export interface ResolvedMotion {
|
|
447
|
+
/** Duration in milliseconds. 0 when motion is disabled. */
|
|
448
|
+
durationMs: number;
|
|
449
|
+
easing: Easing;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Resolve a motion channel into concrete numbers using the theme's duration
|
|
454
|
+
* and easing tables. When `motion.enabled` is false, duration collapses to 0
|
|
455
|
+
* so callers can branch on `durationMs <= 0` to snap.
|
|
456
|
+
*/
|
|
457
|
+
export function resolveMotion(motion: ThemeMotion, channel: ThemeMotionChannel): ResolvedMotion {
|
|
458
|
+
return {
|
|
459
|
+
durationMs: motion.enabled ? motion.duration[channel.duration] : 0,
|
|
460
|
+
easing: motion.easing[channel.easing],
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* True when the runtime reports `prefers-reduced-motion: reduce`. SSR-safe:
|
|
466
|
+
* returns false when `window`/`matchMedia` are unavailable.
|
|
467
|
+
*/
|
|
468
|
+
export function prefersReducedMotion(): boolean {
|
|
469
|
+
if (typeof globalThis === "undefined") return false;
|
|
470
|
+
const w = (globalThis as { matchMedia?: (q: string) => { matches: boolean } }).matchMedia;
|
|
471
|
+
if (typeof w !== "function") return false;
|
|
472
|
+
return w("(prefers-reduced-motion: reduce)").matches;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Return a theme with motion disabled and tooltip delays/fade zeroed. Apply
|
|
477
|
+
* to honor the user's `prefers-reduced-motion` setting. Keeps easings intact
|
|
478
|
+
* so callers that ignore `enabled` and read durations directly still work.
|
|
479
|
+
*/
|
|
480
|
+
export function applyReducedMotion(t: Theme): Theme {
|
|
481
|
+
return {
|
|
482
|
+
...t,
|
|
483
|
+
motion: {
|
|
484
|
+
...t.motion,
|
|
485
|
+
enabled: false,
|
|
486
|
+
duration: { fast: 0, base: 0, slow: 0 },
|
|
487
|
+
tooltip: { showDelay: 0, hideDelay: 0, fadeMs: 0, settleDelay: 0 },
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// CPU heatmap renderer — used by the SVGRenderer path.
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { createLayer, type Layer } from "insomni";
|
|
6
|
+
import { normalizeValue, type ResolvedSpec } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* CPU heatmap output: v3 `Layer`s of vector rects, consumed by the SVG export
|
|
10
|
+
* path and device-less consumers.
|
|
11
|
+
*
|
|
12
|
+
* The v1-only rasterized `ImageLayer` fast-path (dense grids / `svgExport:
|
|
13
|
+
* "image"`) was dropped in the v3 migration: v3's SVG renderer walks
|
|
14
|
+
* `Layer.pack` only and cannot emit `<image>`/sprite drawables, so that path
|
|
15
|
+
* silently rendered nothing post-migration. Dense grids now always emit vector
|
|
16
|
+
* rects (correct, if heavier). Re-introducing image export is future work on
|
|
17
|
+
* the v3 SVG backend (sprite → `<image>`).
|
|
18
|
+
*/
|
|
19
|
+
export type HeatmapCpuDrawable = Layer;
|
|
20
|
+
|
|
21
|
+
export class HeatmapCpuRenderer<T> {
|
|
22
|
+
private readonly spec: ResolvedSpec<T>;
|
|
23
|
+
private data: readonly T[];
|
|
24
|
+
private bins: Float64Array;
|
|
25
|
+
private max = 0;
|
|
26
|
+
private dirty = true;
|
|
27
|
+
|
|
28
|
+
constructor(spec: ResolvedSpec<T>, data: readonly T[]) {
|
|
29
|
+
this.spec = spec;
|
|
30
|
+
this.data = data;
|
|
31
|
+
this.bins = new Float64Array(spec.nx * spec.ny);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setData(data: readonly T[]): void {
|
|
35
|
+
this.data = data;
|
|
36
|
+
this.dirty = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
markDirty(): void {
|
|
40
|
+
this.dirty = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Largest bin weight after the most recent rebin. Useful for legend domains. */
|
|
44
|
+
getMax(): number {
|
|
45
|
+
if (this.dirty) this.rebin();
|
|
46
|
+
return this.max;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
build(): readonly HeatmapCpuDrawable[] {
|
|
50
|
+
if (this.dirty) this.rebin();
|
|
51
|
+
const { nx, ny, frame, colorMap, normalize } = this.spec;
|
|
52
|
+
const max = this.max;
|
|
53
|
+
|
|
54
|
+
const layer = createLayer({ space: "ui" });
|
|
55
|
+
if (max <= 0) return [layer];
|
|
56
|
+
|
|
57
|
+
const cellW = frame.width / nx;
|
|
58
|
+
const cellH = frame.height / ny;
|
|
59
|
+
for (let iy = 0; iy < ny; iy++) {
|
|
60
|
+
for (let ix = 0; ix < nx; ix++) {
|
|
61
|
+
const v = this.bins[iy * nx + ix]!;
|
|
62
|
+
if (v <= 0) continue;
|
|
63
|
+
layer.pushRect({
|
|
64
|
+
x: frame.x + ix * cellW,
|
|
65
|
+
y: frame.y + iy * cellH,
|
|
66
|
+
width: cellW,
|
|
67
|
+
height: cellH,
|
|
68
|
+
fill: colorMap(normalizeValue(v, max, normalize)),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return [layer];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private rebin(): void {
|
|
76
|
+
const spec = this.spec;
|
|
77
|
+
const { nx, ny, x0, x1, y0, y1 } = spec;
|
|
78
|
+
const dx = x1 - x0;
|
|
79
|
+
const dy = y1 - y0;
|
|
80
|
+
const bins = this.bins;
|
|
81
|
+
bins.fill(0);
|
|
82
|
+
|
|
83
|
+
if (dx === 0 || dy === 0) {
|
|
84
|
+
this.max = 0;
|
|
85
|
+
this.dirty = false;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let max = 0;
|
|
90
|
+
for (const datum of this.data) {
|
|
91
|
+
const xv = spec.x(datum);
|
|
92
|
+
const yv = spec.y(datum);
|
|
93
|
+
const tx = (xv - x0) / dx;
|
|
94
|
+
const ty = (yv - y0) / dy;
|
|
95
|
+
if (tx < 0 || tx >= 1 || ty < 0 || ty >= 1) continue;
|
|
96
|
+
const ix = Math.min(nx - 1, Math.floor(tx * nx));
|
|
97
|
+
// Flip iy so row 0 is the *top* of the frame visually — matches axis
|
|
98
|
+
// convention where higher data-y is drawn higher on screen.
|
|
99
|
+
const iy = ny - 1 - Math.min(ny - 1, Math.floor(ty * ny));
|
|
100
|
+
const idx = iy * nx + ix;
|
|
101
|
+
const w = spec.weight(datum);
|
|
102
|
+
const next = bins[idx]! + w;
|
|
103
|
+
bins[idx] = next;
|
|
104
|
+
if (next > max) max = next;
|
|
105
|
+
}
|
|
106
|
+
this.max = max;
|
|
107
|
+
this.dirty = false;
|
|
108
|
+
}
|
|
109
|
+
}
|