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.
Files changed (242) hide show
  1. package/LICENSE.md +674 -0
  2. package/README.md +81 -0
  3. package/dist/core.d.mts +340 -0
  4. package/dist/core.mjs +1047 -0
  5. package/dist/index.d.mts +3426 -0
  6. package/dist/index.mjs +12762 -0
  7. package/dist/interactions-DEFL_F4E.mjs +5395 -0
  8. package/dist/range-presets-CzECsu3V.d.mts +1523 -0
  9. package/package.json +34 -0
  10. package/src/annotations.d.ts +121 -0
  11. package/src/annotations.ts +438 -0
  12. package/src/axis.d.ts +184 -0
  13. package/src/axis.test.ts +131 -0
  14. package/src/axis.ts +765 -0
  15. package/src/colorbar.d.ts +69 -0
  16. package/src/colorbar.ts +294 -0
  17. package/src/colors.d.ts +57 -0
  18. package/src/colors.test.ts +28 -0
  19. package/src/colors.ts +486 -0
  20. package/src/core.ts +299 -0
  21. package/src/format.d.ts +54 -0
  22. package/src/format.ts +138 -0
  23. package/src/grammar/accessibility.d.ts +147 -0
  24. package/src/grammar/accessibility.test.ts +199 -0
  25. package/src/grammar/accessibility.ts +443 -0
  26. package/src/grammar/aes.d.ts +35 -0
  27. package/src/grammar/aes.test.ts +75 -0
  28. package/src/grammar/aes.ts +120 -0
  29. package/src/grammar/annotations.d.ts +86 -0
  30. package/src/grammar/annotations.test.ts +68 -0
  31. package/src/grammar/annotations.ts +336 -0
  32. package/src/grammar/attach-brush.d.ts +44 -0
  33. package/src/grammar/attach-brush.test.ts +214 -0
  34. package/src/grammar/attach-brush.ts +111 -0
  35. package/src/grammar/attach-presets.d.ts +33 -0
  36. package/src/grammar/attach-presets.test.ts +106 -0
  37. package/src/grammar/attach-presets.ts +215 -0
  38. package/src/grammar/chart.d.ts +952 -0
  39. package/src/grammar/chart.test.ts +118 -0
  40. package/src/grammar/chart.ts +1172 -0
  41. package/src/grammar/color-utils.d.ts +29 -0
  42. package/src/grammar/color-utils.test.ts +53 -0
  43. package/src/grammar/color-utils.ts +66 -0
  44. package/src/grammar/constants.d.ts +45 -0
  45. package/src/grammar/constants.ts +61 -0
  46. package/src/grammar/coord.d.ts +183 -0
  47. package/src/grammar/coord.test.ts +355 -0
  48. package/src/grammar/coord.ts +619 -0
  49. package/src/grammar/data/pivot.d.ts +57 -0
  50. package/src/grammar/data/pivot.ts +107 -0
  51. package/src/grammar/emphasis-driver.d.ts +69 -0
  52. package/src/grammar/emphasis-driver.test.ts +199 -0
  53. package/src/grammar/emphasis-driver.ts +205 -0
  54. package/src/grammar/equality.d.ts +3 -0
  55. package/src/grammar/equality.ts +40 -0
  56. package/src/grammar/facet.d.ts +63 -0
  57. package/src/grammar/facet.test.ts +60 -0
  58. package/src/grammar/facet.ts +175 -0
  59. package/src/grammar/geoms/_categorical.d.ts +94 -0
  60. package/src/grammar/geoms/_categorical.ts +0 -0
  61. package/src/grammar/geoms/_distribution.d.ts +52 -0
  62. package/src/grammar/geoms/_distribution.ts +125 -0
  63. package/src/grammar/geoms/_mark.d.ts +69 -0
  64. package/src/grammar/geoms/_mark.ts +136 -0
  65. package/src/grammar/geoms/_shape.d.ts +41 -0
  66. package/src/grammar/geoms/_shape.ts +74 -0
  67. package/src/grammar/geoms/aggregate.d.ts +95 -0
  68. package/src/grammar/geoms/aggregate.test.ts +554 -0
  69. package/src/grammar/geoms/aggregate.ts +840 -0
  70. package/src/grammar/geoms/area.d.ts +32 -0
  71. package/src/grammar/geoms/area.test.ts +165 -0
  72. package/src/grammar/geoms/area.ts +578 -0
  73. package/src/grammar/geoms/band.d.ts +27 -0
  74. package/src/grammar/geoms/band.test.ts +57 -0
  75. package/src/grammar/geoms/band.ts +126 -0
  76. package/src/grammar/geoms/bar.d.ts +56 -0
  77. package/src/grammar/geoms/bar.test.ts +367 -0
  78. package/src/grammar/geoms/bar.ts +1054 -0
  79. package/src/grammar/geoms/boxplot.d.ts +129 -0
  80. package/src/grammar/geoms/boxplot.test.ts +299 -0
  81. package/src/grammar/geoms/boxplot.ts +834 -0
  82. package/src/grammar/geoms/connected-scatter.d.ts +27 -0
  83. package/src/grammar/geoms/connected-scatter.test.ts +157 -0
  84. package/src/grammar/geoms/connected-scatter.ts +63 -0
  85. package/src/grammar/geoms/emphasis.d.ts +76 -0
  86. package/src/grammar/geoms/emphasis.test.ts +135 -0
  87. package/src/grammar/geoms/emphasis.ts +162 -0
  88. package/src/grammar/geoms/histogram.d.ts +75 -0
  89. package/src/grammar/geoms/histogram.test.ts +262 -0
  90. package/src/grammar/geoms/histogram.ts +740 -0
  91. package/src/grammar/geoms/index.d.ts +20 -0
  92. package/src/grammar/geoms/index.ts +77 -0
  93. package/src/grammar/geoms/interval.d.ts +31 -0
  94. package/src/grammar/geoms/interval.test.ts +154 -0
  95. package/src/grammar/geoms/interval.ts +342 -0
  96. package/src/grammar/geoms/line.d.ts +38 -0
  97. package/src/grammar/geoms/line.test.ts +247 -0
  98. package/src/grammar/geoms/line.ts +659 -0
  99. package/src/grammar/geoms/point.d.ts +57 -0
  100. package/src/grammar/geoms/point.test.ts +163 -0
  101. package/src/grammar/geoms/point.ts +545 -0
  102. package/src/grammar/geoms/polar.test.ts +216 -0
  103. package/src/grammar/geoms/ribbon.d.ts +21 -0
  104. package/src/grammar/geoms/ribbon.test.ts +170 -0
  105. package/src/grammar/geoms/ribbon.ts +87 -0
  106. package/src/grammar/geoms/ridgeline.d.ts +89 -0
  107. package/src/grammar/geoms/ridgeline.test.ts +247 -0
  108. package/src/grammar/geoms/ridgeline.ts +1164 -0
  109. package/src/grammar/geoms/rolling.d.ts +43 -0
  110. package/src/grammar/geoms/rolling.test.ts +217 -0
  111. package/src/grammar/geoms/rolling.ts +387 -0
  112. package/src/grammar/geoms/rug.d.ts +28 -0
  113. package/src/grammar/geoms/rug.test.ts +126 -0
  114. package/src/grammar/geoms/rug.ts +214 -0
  115. package/src/grammar/geoms/rule.d.ts +23 -0
  116. package/src/grammar/geoms/rule.test.ts +69 -0
  117. package/src/grammar/geoms/rule.ts +212 -0
  118. package/src/grammar/geoms/smooth.d.ts +54 -0
  119. package/src/grammar/geoms/smooth.test.ts +78 -0
  120. package/src/grammar/geoms/smooth.ts +337 -0
  121. package/src/grammar/geoms/text.d.ts +29 -0
  122. package/src/grammar/geoms/text.test.ts +64 -0
  123. package/src/grammar/geoms/text.ts +234 -0
  124. package/src/grammar/geoms/tile.d.ts +61 -0
  125. package/src/grammar/geoms/tile.test.ts +157 -0
  126. package/src/grammar/geoms/tile.ts +621 -0
  127. package/src/grammar/geoms/types.d.ts +319 -0
  128. package/src/grammar/geoms/types.ts +362 -0
  129. package/src/grammar/geoms/violin.d.ts +85 -0
  130. package/src/grammar/geoms/violin.test.ts +187 -0
  131. package/src/grammar/geoms/violin.ts +672 -0
  132. package/src/grammar/index.d.ts +22 -0
  133. package/src/grammar/index.ts +269 -0
  134. package/src/grammar/interactions/_disposable.d.ts +5 -0
  135. package/src/grammar/interactions/_disposable.ts +23 -0
  136. package/src/grammar/interactions/_z.d.ts +4 -0
  137. package/src/grammar/interactions/_z.ts +16 -0
  138. package/src/grammar/interactions/brush-selection.test.ts +262 -0
  139. package/src/grammar/interactions/brush.d.ts +63 -0
  140. package/src/grammar/interactions/brush.test.ts +483 -0
  141. package/src/grammar/interactions/brush.ts +452 -0
  142. package/src/grammar/interactions/crosshair.d.ts +19 -0
  143. package/src/grammar/interactions/crosshair.test.ts +127 -0
  144. package/src/grammar/interactions/crosshair.ts +76 -0
  145. package/src/grammar/interactions/hit-layer.d.ts +64 -0
  146. package/src/grammar/interactions/hit-layer.ts +246 -0
  147. package/src/grammar/interactions/legend.d.ts +19 -0
  148. package/src/grammar/interactions/legend.ts +101 -0
  149. package/src/grammar/interactions/menu.d.ts +93 -0
  150. package/src/grammar/interactions/menu.test.ts +373 -0
  151. package/src/grammar/interactions/menu.ts +342 -0
  152. package/src/grammar/interactions/selection.d.ts +25 -0
  153. package/src/grammar/interactions/selection.test.ts +289 -0
  154. package/src/grammar/interactions/selection.ts +142 -0
  155. package/src/grammar/interactions/series-readout.d.ts +91 -0
  156. package/src/grammar/interactions/series-readout.test.ts +668 -0
  157. package/src/grammar/interactions/series-readout.ts +422 -0
  158. package/src/grammar/interactions/series-snap.d.ts +70 -0
  159. package/src/grammar/interactions/series-snap.test.ts +214 -0
  160. package/src/grammar/interactions/series-snap.ts +218 -0
  161. package/src/grammar/interactions/tooltip-axis.test.ts +176 -0
  162. package/src/grammar/interactions/tooltip-touch.browser.test.ts +49 -0
  163. package/src/grammar/interactions/tooltip-touch.test.ts +161 -0
  164. package/src/grammar/interactions/tooltip.d.ts +140 -0
  165. package/src/grammar/interactions/tooltip.test.ts +406 -0
  166. package/src/grammar/interactions/tooltip.ts +622 -0
  167. package/src/grammar/interactions/transitions.d.ts +34 -0
  168. package/src/grammar/interactions/transitions.test.ts +172 -0
  169. package/src/grammar/interactions/transitions.ts +160 -0
  170. package/src/grammar/layout.d.ts +68 -0
  171. package/src/grammar/layout.ts +186 -0
  172. package/src/grammar/legend-merge.test.ts +332 -0
  173. package/src/grammar/mount.d.ts +78 -0
  174. package/src/grammar/mount.test.ts +479 -0
  175. package/src/grammar/mount.ts +2112 -0
  176. package/src/grammar/palettes.d.ts +54 -0
  177. package/src/grammar/palettes.test.ts +80 -0
  178. package/src/grammar/palettes.ts +167 -0
  179. package/src/grammar/pan-zoom.test.ts +398 -0
  180. package/src/grammar/phylo.d.ts +65 -0
  181. package/src/grammar/phylo.test.ts +59 -0
  182. package/src/grammar/phylo.ts +112 -0
  183. package/src/grammar/pipeline.auto-ticks.test.ts +40 -0
  184. package/src/grammar/pipeline.d.ts +158 -0
  185. package/src/grammar/pipeline.test.ts +463 -0
  186. package/src/grammar/pipeline.ts +1233 -0
  187. package/src/grammar/profiling.d.ts +8 -0
  188. package/src/grammar/profiling.ts +24 -0
  189. package/src/grammar/scales.d.ts +188 -0
  190. package/src/grammar/scales.test.ts +181 -0
  191. package/src/grammar/scales.ts +800 -0
  192. package/src/grammar/svg.d.ts +3 -0
  193. package/src/grammar/svg.ts +39 -0
  194. package/src/grammar/theme.d.ts +261 -0
  195. package/src/grammar/theme.test.ts +105 -0
  196. package/src/grammar/theme.ts +490 -0
  197. package/src/heatmap/cpu.ts +109 -0
  198. package/src/heatmap/gpu.ts +565 -0
  199. package/src/heatmap/types.ts +177 -0
  200. package/src/heatmap.browser.test.ts +308 -0
  201. package/src/heatmap.test.ts +320 -0
  202. package/src/heatmap.ts +123 -0
  203. package/src/index.d.ts +1 -0
  204. package/src/index.ts +8 -0
  205. package/src/interactions.d.ts +48 -0
  206. package/src/interactions.test.ts +226 -0
  207. package/src/interactions.ts +394 -0
  208. package/src/layout/box.d.ts +48 -0
  209. package/src/layout/box.test.ts +107 -0
  210. package/src/layout/box.ts +143 -0
  211. package/src/legend.d.ts +115 -0
  212. package/src/legend.ts +422 -0
  213. package/src/marks/curve.d.ts +43 -0
  214. package/src/marks/curve.ts +244 -0
  215. package/src/marks/stack.d.ts +53 -0
  216. package/src/marks/stack.ts +184 -0
  217. package/src/marks.d.ts +273 -0
  218. package/src/marks.test.ts +541 -0
  219. package/src/marks.ts +1292 -0
  220. package/src/navigator.test.ts +174 -0
  221. package/src/navigator.ts +393 -0
  222. package/src/range-presets.d.ts +113 -0
  223. package/src/range-presets.test.ts +345 -0
  224. package/src/range-presets.ts +349 -0
  225. package/src/scales.d.ts +98 -0
  226. package/src/scales.test.ts +103 -0
  227. package/src/scales.ts +695 -0
  228. package/src/stats/index.d.ts +200 -0
  229. package/src/stats/index.test.ts +349 -0
  230. package/src/stats/index.ts +740 -0
  231. package/src/stats/regression.d.ts +38 -0
  232. package/src/stats/regression.test.ts +56 -0
  233. package/src/stats/regression.ts +396 -0
  234. package/src/stats/rolling-window.d.ts +55 -0
  235. package/src/stats/rolling-window.test.ts +237 -0
  236. package/src/stats/rolling-window.ts +256 -0
  237. package/src/test-setup.ts +19 -0
  238. package/src/viewport/axis-state.d.ts +72 -0
  239. package/src/viewport/axis-state.ts +476 -0
  240. package/src/viewport.d.ts +170 -0
  241. package/src/viewport.test.ts +363 -0
  242. package/src/viewport.ts +510 -0
@@ -0,0 +1,3 @@
1
+ import { type GlyphAtlas } from "insomni";
2
+ import { type ChartConfig } from "./pipeline.ts";
3
+ export declare function renderSVG<T>(config: ChartConfig<T>, width: number | undefined, height: number | undefined, externalAtlas?: GlyphAtlas): SVGSVGElement;
@@ -0,0 +1,39 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Static SVG export — runs the pipeline once into an off-DOM SVG renderer.
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import { createLayer, createSVGRenderer, type GlyphAtlas } from "insomni";
6
+ import { sharedCpuGlyphAtlas } from "insomni/text-ttf";
7
+ import { DEFAULT_CHART_HEIGHT, DEFAULT_CHART_WIDTH } from "./constants.ts";
8
+ import { currentData, runPipeline, type ChartConfig } from "./pipeline.ts";
9
+
10
+ export function renderSVG<T>(
11
+ config: ChartConfig<T>,
12
+ width: number | undefined,
13
+ height: number | undefined,
14
+ externalAtlas?: GlyphAtlas,
15
+ ): SVGSVGElement {
16
+ const w = width ?? config.width ?? DEFAULT_CHART_WIDTH;
17
+ const h = height ?? config.height ?? DEFAULT_CHART_HEIGHT;
18
+ const renderer = createSVGRenderer({ width: w, height: h, dpr: 1 });
19
+ renderer.setBackground(config.background ?? config.theme.background);
20
+
21
+ // SVG path: resolve the glyph atlas for text rendering (axis labels,
22
+ // legend, title, value-label annotations). `externalAtlas` and
23
+ // `config.externalAtlas` take precedence; when neither is supplied,
24
+ // `sharedCpuGlyphAtlas()` provides a zero-GPU default backed by the bundled
25
+ // Lato subset — so `chart.toSVG()` emits `<text>` elements with no extra
26
+ // setup from the caller.
27
+ const atlas = externalAtlas ?? config.externalAtlas ?? sharedCpuGlyphAtlas();
28
+
29
+ const axisLayer = createLayer({ space: "ui", atlas, retainAllPushRecords: true });
30
+ const marksLayer = createLayer({ space: "ui", atlas, retainAllPushRecords: true });
31
+ const hudLayer = createLayer({ space: "ui", atlas, retainAllPushRecords: true });
32
+
33
+ const snapshot: ChartConfig<T> = { ...config, width: w, height: h };
34
+ runPipeline(snapshot, currentData(config.data), axisLayer, marksLayer, hudLayer, atlas);
35
+ // v3 SVG renders shapes in pixel space; `ui`-space layers are scaled by dpr
36
+ // (1 here). `.element()` is a METHOD on the v3 backend (was a getter in v1).
37
+ renderer.render([axisLayer, marksLayer, hudLayer]);
38
+ return renderer.element();
39
+ }
@@ -0,0 +1,261 @@
1
+ import { type BlendSpace, type Color, type Easing } from "insomni";
2
+ import { type CategoricalPalette, type ContinuousPalette } from "../colors.ts";
3
+ import { type Accessibility, type TextEffects } from "./accessibility.ts";
4
+ export type { Accessibility, AccessibilityMode, TextEffects } from "./accessibility.ts";
5
+ export interface ThemeText {
6
+ color: Color;
7
+ fontFamily: string;
8
+ }
9
+ export interface ThemeTitle {
10
+ fontSize: number;
11
+ fontWeight: string;
12
+ color: Color;
13
+ }
14
+ export interface ThemeSubtitle {
15
+ fontSize: number;
16
+ color: Color;
17
+ }
18
+ export interface ThemeAxis {
19
+ color: Color;
20
+ gridColor: Color;
21
+ labelFontSize: number;
22
+ labelColor: Color;
23
+ titleFontSize: number;
24
+ titleColor: Color;
25
+ }
26
+ export interface ThemeLegend {
27
+ fontSize: number;
28
+ labelColor: Color;
29
+ swatchGap: number;
30
+ entryGap: number;
31
+ }
32
+ export interface ThemeMarks {
33
+ pointRadius: number;
34
+ pointStroke: Color | undefined;
35
+ pointStrokeWidth: number;
36
+ /**
37
+ * Default stroke width for line-based marks (`line`, `smooth`, `statRolling`,
38
+ * `area` outline, `aggregate` line geom). Geom-level `strokeWidth` options
39
+ * override this token per layer.
40
+ */
41
+ strokeWidth: number;
42
+ barCornerRadius: number;
43
+ fillAlpha: number;
44
+ /** Font size for value/total labels rendered alongside marks (bar, text). */
45
+ labelFontSize: number;
46
+ /** Default stroke width for `rule()` lines. */
47
+ ruleStrokeWidth: number;
48
+ /** Default label inset for `rule()` annotations. */
49
+ ruleLabelInset: number;
50
+ /** Default font size for inline annotations (rule, band labels). */
51
+ annotationFontSize: number;
52
+ /** Default fill alpha for `ribbon()`. */
53
+ ribbonFillAlpha: number;
54
+ /** Default fill alpha for `band()` highlight regions. */
55
+ bandFillAlpha: number;
56
+ /** Default size-channel pixel range. */
57
+ sizeRange: readonly [number, number];
58
+ /** Default alpha-channel range. */
59
+ alphaRange: readonly [number, number];
60
+ }
61
+ export type ThemeDurationKey = "fast" | "base" | "slow";
62
+ export type ThemeEasingKey = "standard" | "emphasized" | "decelerate" | "linear";
63
+ export interface ThemeDurations {
64
+ fast: number;
65
+ base: number;
66
+ slow: number;
67
+ }
68
+ export interface ThemeEasings {
69
+ standard: Easing;
70
+ emphasized: Easing;
71
+ decelerate: Easing;
72
+ linear: Easing;
73
+ }
74
+ export interface ThemeMotionChannel {
75
+ duration: ThemeDurationKey;
76
+ easing: ThemeEasingKey;
77
+ }
78
+ export interface ThemeMotionTooltip {
79
+ showDelay: number;
80
+ hideDelay: number;
81
+ fadeMs: number;
82
+ /**
83
+ * Hover-intent settle (ms). Rapid enter/leave/switch events are coalesced —
84
+ * the tooltip only commits a show/hide after the cursor holds a target this
85
+ * long. Prevents an overlay re-render per cell while sweeping across a dense
86
+ * mark grid or through gaps. 0 disables (commit immediately).
87
+ */
88
+ settleDelay: number;
89
+ }
90
+ export interface ThemeMotion {
91
+ /** Master switch. False → snap, no scheduled motion. */
92
+ enabled: boolean;
93
+ duration: ThemeDurations;
94
+ easing: ThemeEasings;
95
+ /** Data update / scale change transitions. */
96
+ data: ThemeMotionChannel;
97
+ /** Axis tick / domain transitions. */
98
+ axis: ThemeMotionChannel;
99
+ tooltip: ThemeMotionTooltip;
100
+ }
101
+ export declare const defaultMotion: ThemeMotion;
102
+ export interface ThemeInteractionEmphasis {
103
+ /** Master enable. `false` skips both halo and dim. */
104
+ enabled: boolean;
105
+ /** Multiplier on non-active rows' fill alpha. `1` = no dim. */
106
+ dim: number;
107
+ /** Halo stroke width in CSS px. `0` skips the halo. */
108
+ haloStrokeWidth: number;
109
+ /**
110
+ * Halo ring color. When omitted, geoms fall back to a high-contrast
111
+ * foreground (`theme.text.color`) so the focus ring reads against the
112
+ * marks rather than blending in with the datum's own color. Set a `Color`
113
+ * to pin the ring (e.g. a brand accent).
114
+ */
115
+ haloColor?: Color;
116
+ /**
117
+ * Duration (ms) of the GPU dim animation driven through the core's emphasis
118
+ * uniform (P5-T3). On a hover hit-change over a dim-participating geom the
119
+ * mount ramps the emphasis `t` 0→1 (ease-out cubic) over this window; on
120
+ * hover exit it ramps t→0. Only consumed by `hover` (selection dim stays a
121
+ * compile-time treatment). `<= 0` snaps. Default ~120ms.
122
+ */
123
+ durationMs?: number;
124
+ }
125
+ /**
126
+ * Semantic row-tint tokens for tooltip rows that use the
127
+ * `accent: "positive" | "negative" | "neutral"` shorthand. `neutral` is
128
+ * intentionally omitted — it means "no override, use the default row text
129
+ * color from the underlying tooltip style." Consumers can also pass a raw
130
+ * `Color` per row to bypass these tokens entirely.
131
+ */
132
+ export interface ThemeTooltipAccents {
133
+ positive: Color;
134
+ negative: Color;
135
+ }
136
+ export interface ThemeInteractions {
137
+ /** Visual treatment applied while a row is hovered. */
138
+ hover: ThemeInteractionEmphasis;
139
+ /** Visual treatment applied while rows are selected. */
140
+ selection: ThemeInteractionEmphasis;
141
+ /**
142
+ * Grace window (ms) applied when hover transitions to "nothing" before
143
+ * propagating the null state to dim/halo and crosshair. If a new hit lands
144
+ * within this window, the swap goes directly from prev → next without
145
+ * passing through null — eliminates the un-dim → re-dim flash when the
146
+ * cursor crosses between adjacent or near-adjacent geoms. `0` disables.
147
+ */
148
+ hoverSwapGraceMs: number;
149
+ /** Semantic row colors for the tooltip `accent` shorthand. */
150
+ tooltipAccents: ThemeTooltipAccents;
151
+ }
152
+ export declare const defaultInteractions: ThemeInteractions;
153
+ export interface ThemePalettes {
154
+ categorical: CategoricalPalette;
155
+ continuous: ContinuousPalette;
156
+ diverging: ContinuousPalette;
157
+ }
158
+ /**
159
+ * Semantic accent palette consumed by chart-level marks that carry meaning
160
+ * (reference regions, threshold rules, callouts, tooltip rows). Geoms that
161
+ * accept an accent key (`fill: "positive"`, `stroke: "warn"`) resolve through
162
+ * this block; literal `Color` values still bypass it.
163
+ *
164
+ * Naming exception to the "name geoms by what they are, not what they do"
165
+ * principle: these tokens exist explicitly to carry semantics. The names
166
+ * follow conventional status vocabulary so any reader can predict the role.
167
+ *
168
+ * `neutral` is intentionally omitted — it means "no override, use the
169
+ * surrounding default" (mark fill, axis label color, …) so consumers can fall
170
+ * back without spelling out a token.
171
+ *
172
+ * Distinct from {@link ThemeTooltipAccents} (under `interactions.tooltipAccents`)
173
+ * which is sized for tooltip-text contrast and may differ in luminance.
174
+ */
175
+ export interface ThemeAccents {
176
+ positive: Color;
177
+ negative: Color;
178
+ warn: Color;
179
+ info: Color;
180
+ }
181
+ export type ThemeAccentKey = keyof ThemeAccents;
182
+ export interface Theme {
183
+ background: Color;
184
+ text: ThemeText;
185
+ title: ThemeTitle;
186
+ subtitle: ThemeSubtitle;
187
+ axis: ThemeAxis;
188
+ legend: ThemeLegend;
189
+ marks: ThemeMarks;
190
+ palettes: ThemePalettes;
191
+ /** Semantic accent palette for chart-level marks (reference regions, threshold rules, callouts). */
192
+ accents: ThemeAccents;
193
+ /**
194
+ * Text-readability policy. Applied at every draw site where the chart
195
+ * knows the local background color (titles/axis/legend versus the panel
196
+ * background; tile labels versus their cell color).
197
+ */
198
+ accessibility: Accessibility;
199
+ /**
200
+ * Default text effects (outline / drop shadow). API stub today — the SDF
201
+ * text path doesn't render these yet. See
202
+ * dev_docs/2026-04-30-text-accessibility.md.
203
+ */
204
+ textEffects?: TextEffects;
205
+ /**
206
+ * Color space used to interpolate continuous palettes (`viridis`, etc.)
207
+ * when sampled by color scales and color bars. `oklch` is the default —
208
+ * perceptually uniform with hue preserved along the gradient. Override
209
+ * per chart (`theme({ paletteBlendSpace: "srgb" })`) or per call (the
210
+ * `blendSpace` option on color-scale and `colorBar` specs).
211
+ */
212
+ paletteBlendSpace: BlendSpace;
213
+ /**
214
+ * Motion tokens — durations, easings, and per-channel defaults consumed by
215
+ * interactions (hover, tooltip), data transitions, and axis updates. See
216
+ * {@link defaultMotion} and {@link applyReducedMotion}.
217
+ */
218
+ motion: ThemeMotion;
219
+ /**
220
+ * Hover / selection emphasis tokens. Geoms read these to render the active
221
+ * row's halo and optionally dim the rest. See {@link defaultInteractions}.
222
+ */
223
+ interactions: ThemeInteractions;
224
+ }
225
+ export declare const themeDefault: Theme;
226
+ /**
227
+ * Light/minimal theme — white plot background, soft gray gridlines, dark
228
+ * labels. Pairs well with the `category10` palette and is a good default for
229
+ * scatter / smooth charts that should read like a ggplot2 / R reference.
230
+ */
231
+ export declare const themeMinimalGrid: Theme;
232
+ export type DeepPartial<T> = {
233
+ [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
234
+ };
235
+ /**
236
+ * Compose a theme by deep-merging overrides into a base theme. Accepts a
237
+ * shallow override on any nested group (e.g. `{ marks: { strokeWidth: 2 } }`).
238
+ */
239
+ export declare function theme(overrides: DeepPartial<Theme>, base?: Theme): Theme;
240
+ export interface ResolvedMotion {
241
+ /** Duration in milliseconds. 0 when motion is disabled. */
242
+ durationMs: number;
243
+ easing: Easing;
244
+ }
245
+ /**
246
+ * Resolve a motion channel into concrete numbers using the theme's duration
247
+ * and easing tables. When `motion.enabled` is false, duration collapses to 0
248
+ * so callers can branch on `durationMs <= 0` to snap.
249
+ */
250
+ export declare function resolveMotion(motion: ThemeMotion, channel: ThemeMotionChannel): ResolvedMotion;
251
+ /**
252
+ * True when the runtime reports `prefers-reduced-motion: reduce`. SSR-safe:
253
+ * returns false when `window`/`matchMedia` are unavailable.
254
+ */
255
+ export declare function prefersReducedMotion(): boolean;
256
+ /**
257
+ * Return a theme with motion disabled and tooltip delays/fade zeroed. Apply
258
+ * to honor the user's `prefers-reduced-motion` setting. Keeps easings intact
259
+ * so callers that ignore `enabled` and read durations directly still work.
260
+ */
261
+ export declare function applyReducedMotion(t: Theme): Theme;
@@ -0,0 +1,105 @@
1
+ import { describe, expect, test } from "vite-plus/test";
2
+
3
+ import {
4
+ applyReducedMotion,
5
+ defaultInteractions,
6
+ defaultMotion,
7
+ prefersReducedMotion,
8
+ resolveMotion,
9
+ theme,
10
+ themeDefault,
11
+ themeMinimalGrid,
12
+ } from "./theme.ts";
13
+
14
+ describe("theme — interactions.hover.durationMs (P5-T3)", () => {
15
+ test("default hover emphasis carries a ~120ms GPU-dim duration", () => {
16
+ expect(defaultInteractions.hover.durationMs).toBe(120);
17
+ expect(themeDefault.interactions.hover.durationMs).toBe(120);
18
+ });
19
+
20
+ test("selection emphasis has no durationMs (selection dim stays compile-time)", () => {
21
+ expect(defaultInteractions.selection.durationMs).toBeUndefined();
22
+ });
23
+ });
24
+
25
+ describe("theme — motion tokens", () => {
26
+ test("themeDefault carries defaultMotion", () => {
27
+ expect(themeDefault.motion).toBe(defaultMotion);
28
+ expect(themeDefault.motion.enabled).toBe(true);
29
+ expect(themeDefault.motion.duration.fast).toBe(120);
30
+ expect(themeDefault.motion.duration.base).toBe(240);
31
+ expect(themeDefault.motion.duration.slow).toBe(480);
32
+ });
33
+
34
+ test("themeMinimalGrid inherits motion via spread", () => {
35
+ expect(themeMinimalGrid.motion).toBe(defaultMotion);
36
+ });
37
+
38
+ test("resolveMotion looks up named duration + easing when enabled", () => {
39
+ const r = resolveMotion(defaultMotion, { duration: "fast", easing: "standard" });
40
+ expect(r.durationMs).toBe(120);
41
+ expect(r.easing).toBe(defaultMotion.easing.standard);
42
+ });
43
+
44
+ test("resolveMotion collapses duration to 0 when disabled", () => {
45
+ const m = { ...defaultMotion, enabled: false };
46
+ const r = resolveMotion(m, m.data);
47
+ expect(r.durationMs).toBe(0);
48
+ expect(r.easing).toBe(defaultMotion.easing.emphasized);
49
+ });
50
+
51
+ test("applyReducedMotion zeros durations + tooltip delays, keeps easings", () => {
52
+ const reduced = applyReducedMotion(themeDefault);
53
+ expect(reduced.motion.enabled).toBe(false);
54
+ expect(reduced.motion.duration).toEqual({ fast: 0, base: 0, slow: 0 });
55
+ expect(reduced.motion.tooltip).toEqual({
56
+ showDelay: 0,
57
+ hideDelay: 0,
58
+ fadeMs: 0,
59
+ settleDelay: 0,
60
+ });
61
+ expect(reduced.motion.easing.standard).toBe(defaultMotion.easing.standard);
62
+ // original is untouched
63
+ expect(themeDefault.motion.duration.fast).toBe(120);
64
+ });
65
+
66
+ test("theme() override merges motion field", () => {
67
+ const t = theme({ motion: { duration: { fast: 50, base: 100, slow: 200 } } });
68
+ expect(t.motion.duration.fast).toBe(50);
69
+ expect(t.motion.enabled).toBe(true);
70
+ // other channels untouched
71
+ expect(t.motion.data.duration).toBe("base");
72
+ });
73
+
74
+ test("prefersReducedMotion returns false when matchMedia missing", () => {
75
+ // jsdom typically lacks matchMedia by default; if present, this just
76
+ // exercises the call path without asserting the boolean.
77
+ const result = prefersReducedMotion();
78
+ expect(typeof result).toBe("boolean");
79
+ });
80
+ });
81
+
82
+ describe("theme — semantic accents", () => {
83
+ test("themeDefault carries the four semantic accent tokens", () => {
84
+ expect(themeDefault.accents).toBeDefined();
85
+ expect(themeDefault.accents.positive).toBeDefined();
86
+ expect(themeDefault.accents.negative).toBeDefined();
87
+ expect(themeDefault.accents.warn).toBeDefined();
88
+ expect(themeDefault.accents.info).toBeDefined();
89
+ });
90
+
91
+ test("themeMinimalGrid overrides accents for light-background contrast", () => {
92
+ // Light theme picks higher-saturation values than the dark theme so
93
+ // shaded regions still read on white. Sanity-check that the override
94
+ // happened — the green channel of `positive` differs between themes.
95
+ expect(themeMinimalGrid.accents.positive).not.toBe(themeDefault.accents.positive);
96
+ });
97
+
98
+ test("theme() override merges accents field", () => {
99
+ const customRed = { r: 1, g: 0, b: 0, a: 1 };
100
+ const t = theme({ accents: { negative: customRed } });
101
+ expect(t.accents.negative).toEqual(customRed);
102
+ // other accents preserved
103
+ expect(t.accents.positive).toBe(themeDefault.accents.positive);
104
+ });
105
+ });