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,1172 @@
1
+ // ---------------------------------------------------------------------------
2
+ // plot() — grammar-of-graphics chart builder
3
+ // ---------------------------------------------------------------------------
4
+ // `plot({ data, ... })` returns an immutable Chart<T>. Each builder method
5
+ // returns a NEW chart with the addition recorded — no in-place mutation.
6
+ // `.mount(canvas, opts?)` / `.render(layer)` / `.toSVG()` materialize the spec.
7
+
8
+ import type { Color, Frame, FrameTiming, GlyphAtlas, Invalidator, Layer, Padding, Renderer2D } from "insomni";
9
+ import type { AxisTickFormatter, TickSpec } from "../axis.ts";
10
+ import type { AnnotationSpec } from "./annotations.ts";
11
+ import { DEFAULT_PADDING } from "./constants.ts";
12
+ import { coordCartesian, type Coord } from "./coord.ts";
13
+ import type { FacetSpec } from "./facet.ts";
14
+ import type { Geom } from "./geoms/index.ts";
15
+ import type { HoveredHit } from "./geoms/types.ts";
16
+ import { mountChart } from "./mount.ts";
17
+ import { currentData, runPipeline, type ChartConfig } from "./pipeline.ts";
18
+ import type {
19
+ AlphaScaleOptions,
20
+ Channel,
21
+ ColorScaleOptions,
22
+ PositionScaleOptions,
23
+ ShapeScaleOptions,
24
+ SizeScaleOptions,
25
+ } from "./scales.ts";
26
+ import type { Signal } from "insomni/reactivity";
27
+ import { renderSVG } from "./svg.ts";
28
+ import { themeDefault, type Theme } from "./theme.ts";
29
+
30
+ export type Row = Record<string, unknown>;
31
+
32
+ export interface PlotSpec<T> {
33
+ data: readonly T[] | Signal<readonly T[]>;
34
+ width?: number;
35
+ height?: number;
36
+ background?: Color;
37
+ padding?: Padding;
38
+ /**
39
+ * Inner padding inside the plot frame, in pixels. Adds slack between the
40
+ * axis lines and the data — as if the view were zoomed out a bit. Accepts a
41
+ * single number applied to all sides, or per-side overrides. Different from
42
+ * `padding`, which is the *outer* canvas inset before axes/legend/title.
43
+ */
44
+ framePadding?: number | { top?: number; right?: number; bottom?: number; left?: number };
45
+ device?: GPUDevice;
46
+ /** Initial theme; can be replaced via `.theme(...)`. */
47
+ theme?: Theme;
48
+ }
49
+
50
+ /**
51
+ * Per-axis options surfaced through `chart.axes({ ... })`. `title` accepts
52
+ * either a string or `{ text, maxWidth }`; `label.maxWidth` clips long tick
53
+ * labels with an ellipsis.
54
+ */
55
+ // Re-exported from `../axis.ts` so consumers can pull it from either
56
+ // `insomni-plot/core` (axis-level surface) or `insomni-plot` (grammar
57
+ // surface). Defined in axis.ts to keep core.ts cycle-free.
58
+ export type { AxisTickFormatter } from "../axis.ts";
59
+
60
+ export interface AxisSpec {
61
+ title?: string | { text: string; maxWidth?: number };
62
+ format?: AxisTickFormatter;
63
+ /**
64
+ * How many ticks to render:
65
+ * - `number` — target count (default 5).
66
+ * - `"auto"` — pick the count from the axis's pixel span (~80px/tick
67
+ * horizontal, ~50px/tick vertical). Useful for time-series so dense
68
+ * per-day ticks decay to weeks / months / quarters as the view widens.
69
+ * - `{ interval, step? }` — explicit time interval (time scales only).
70
+ * `interval: "quarter"` is sugar for `month, step: 3`.
71
+ */
72
+ ticks?: TickSpec;
73
+ gridLines?: boolean;
74
+ /** Draw the axis line itself (the spine along the plot frame). Default `true`. */
75
+ axisLine?: boolean;
76
+ label?: { maxWidth?: number };
77
+ /**
78
+ * Auto-decimate tick labels until they no longer visually overlap.
79
+ * `"auto"` measures the formatted labels and inflates the effective
80
+ * `labelStep` accordingly. Useful for dense band axes (≥15 categories)
81
+ * and time axes whose density shifts under zoom. Default off — opt in
82
+ * per axis.
83
+ */
84
+ labelCollision?: "auto";
85
+ }
86
+
87
+ export interface AxesSpec {
88
+ x?: AxisSpec;
89
+ y?: AxisSpec;
90
+ }
91
+
92
+ /**
93
+ * Chart title / subtitle. Each accepts a string or `{ text, maxWidth }` —
94
+ * the object form clips with an ellipsis when the rendered text exceeds the
95
+ * given pixel width.
96
+ */
97
+ export interface TitleSpec {
98
+ title?: string | { text: string; maxWidth?: number };
99
+ subtitle?: string | { text: string; maxWidth?: number };
100
+ }
101
+
102
+ /**
103
+ * Float-over-plot position. `x` and `y` are normalized to the plot frame
104
+ * (`0..1`); `justify` controls which corner of the legend's bbox lands on
105
+ * that point. Inspired by ggplot2's `legend.position = c(x, y)`.
106
+ */
107
+ export interface LegendInsidePosition {
108
+ inside: { x: number; y: number };
109
+ /** Default `"start"` — top-left of legend lands on the point. */
110
+ justify?: { x?: "start" | "center" | "end"; y?: "start" | "center" | "end" };
111
+ }
112
+
113
+ export type LegendPosition = "top" | "right" | "bottom" | "left" | LegendInsidePosition;
114
+
115
+ /**
116
+ * Channels eligible for being merged into a single legend entry. When merged,
117
+ * each domain value's swatch shows the resolved value from every listed scale
118
+ * — e.g. `merge: ["color", "shape"]` produces one row per category whose
119
+ * swatch is colored by the color scale AND shaped by the shape scale.
120
+ *
121
+ * `size` participation is supported only when another categorical channel is
122
+ * also in the merge list (the entries are still iterated over the categorical
123
+ * domain; size is sampled from its numeric range at evenly-spaced positions).
124
+ */
125
+ export type LegendMergeChannel = "color" | "shape" | "size" | "borderStyle" | "overlayGlyph";
126
+
127
+ export interface LegendSpec {
128
+ /** Disable the auto-legend. */
129
+ show?: boolean;
130
+ /**
131
+ * Where the legend sits. Outer slots: `"top" | "right" | "bottom" | "left"`.
132
+ * For an over-plot legend, pass `{ inside: { x, y } }` with `x`/`y` in `0..1`
133
+ * (plot-frame coords). Default depends on legend type — `"right"` for
134
+ * continuous color bars, `"top"` for categorical swatches.
135
+ */
136
+ position?: LegendPosition;
137
+ /** Optional title rendered above the legend entries / color bar. */
138
+ title?: string;
139
+ /** Override color-bar long-axis pixel length. Default 160. */
140
+ length?: number;
141
+ /** Override color-bar thickness. Default 12. */
142
+ thickness?: number;
143
+ /** Continuous legend — major tick count target. Default `5`. */
144
+ ticks?: number;
145
+ /** Continuous legend — explicit major tick values (override `ticks`). */
146
+ tickValues?: readonly number[];
147
+ /**
148
+ * Continuous legend — minor (unlabeled) tick marks. Number subdivides each
149
+ * major interval (`2` = one minor halfway); array places ticks explicitly.
150
+ */
151
+ minorTicks?: number | readonly number[];
152
+ /**
153
+ * Continuous legend — render a tick label every `N`th major tick.
154
+ * Default `1`. Combine with `minorTicks` for "lines every 0.5, labels
155
+ * every 1.0" style guides.
156
+ */
157
+ labelStep?: number;
158
+ /** Continuous legend — tick label formatter. */
159
+ format?: (value: number) => string;
160
+ /**
161
+ * Continuous legend — override the color space used to interpolate the
162
+ * palette stops for the bar's gradient. Falls back to the chart's color
163
+ * scale (which itself defaults to `theme.paletteBlendSpace = "oklch"`).
164
+ */
165
+ blendSpace?: import("insomni").BlendSpace;
166
+ /**
167
+ * Merge the listed aesthetic channels into a single combined legend. When
168
+ * provided, every listed channel must encode the same field (the consumer
169
+ * is asserting this) and the merged legend iterates the primary scale's
170
+ * domain (priority: color → shape → borderStyle → overlayGlyph). Each
171
+ * entry's swatch combines the resolved values across every listed scale.
172
+ *
173
+ * Channels that are listed but have no active scale on the chart are
174
+ * silently skipped — they contribute nothing to the swatch. Omitting this
175
+ * option leaves the default behavior unchanged (color-only legend).
176
+ */
177
+ merge?: readonly LegendMergeChannel[];
178
+ }
179
+
180
+ export type ScaleOptionsByChannel<C extends Channel> = C extends "x" | "y"
181
+ ? PositionScaleOptions
182
+ : C extends "color"
183
+ ? ColorScaleOptions<unknown>
184
+ : C extends "size"
185
+ ? SizeScaleOptions
186
+ : C extends "alpha"
187
+ ? AlphaScaleOptions
188
+ : C extends "shape"
189
+ ? ShapeScaleOptions
190
+ : never;
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Mount API — what `chart.mount(canvas, opts?)` accepts and returns.
194
+ // ---------------------------------------------------------------------------
195
+
196
+ export interface TransitionsConfig {
197
+ /** Duration in milliseconds. Default: theme.motion.data duration (240ms). */
198
+ duration?: number;
199
+ /** Stable key for matching rows across data refreshes. Required for enter animations. */
200
+ key?: (datum: unknown, index: number) => string | number;
201
+ }
202
+
203
+ /** Handle exposed on MountedPlot to request manual transitions. */
204
+ export interface TransitionsHandle {
205
+ /** Trigger a transition on the next draw (e.g. after a scale domain change). */
206
+ requestTransition(): void;
207
+ }
208
+
209
+ export interface MountPlotOptions<T> {
210
+ /**
211
+ * GPU device. Required when the mount has to create a `Renderer2D` or
212
+ * `GlyphAtlas` (i.e. when you don't pass them yourself). Either source
213
+ * works — `plot({ device })` or `mount(canvas, { device })`.
214
+ */
215
+ device?: GPUDevice;
216
+
217
+ /**
218
+ * Builder closure called on every `invalidate()`. Use this when chart
219
+ * settings live in module-scope state (controls, signals, etc.) and you
220
+ * want the chart to be re-derived on each draw. The closure should return
221
+ * a fresh `Chart<T>` (typically by calling the same `plot().layer()...`
222
+ * chain that produced this chart).
223
+ *
224
+ * If omitted, the mount renders the chart you mounted on, statically.
225
+ * Either way, `handle.update(newChart | newBuilder)` swaps it.
226
+ */
227
+ build?: () => Chart<T>;
228
+
229
+ // -----------------------------------------------------------------------
230
+ // Bring-your-own (advanced, optional)
231
+ // -----------------------------------------------------------------------
232
+
233
+ /**
234
+ * Use an externally owned renderer. The mount won't call `setBackground`,
235
+ * `resize`, `setDpr`, or `destroy()` on it — those become the caller's
236
+ * responsibility. Useful for sharing a single renderer across multiple
237
+ * charts or compositing a chart over a world-space scene.
238
+ */
239
+ renderer?: Renderer2D;
240
+
241
+ /**
242
+ * External layers (axis under, marks middle, hud over). Mount won't clear
243
+ * them outside the pipeline. The optional `overlay` layer sits on top of the
244
+ * hud and carries the cursor-driven overlays (tooltip / crosshair / brush /
245
+ * menu); omit it and the mount creates its own.
246
+ */
247
+ layers?: { axis: Layer; marks: Layer; hud: Layer; overlay?: Layer };
248
+
249
+ /**
250
+ * Extra layers rendered alongside the chart's own layers. Called each draw
251
+ * to read the current list, so callers can lazy-create or swap layers
252
+ * without re-mounting. Layers returned here are NOT cleared by the chart
253
+ * pipeline — they persist across draws until the caller clears them
254
+ * themselves. Use this for retained, world-anchored content (large label
255
+ * sets that ride the camera, scatter overlays driven by a separate camera
256
+ * viewport, etc.) where per-frame re-emission would dominate CPU cost.
257
+ *
258
+ * Two slots:
259
+ * - `belowMarks`: rendered after axes, before chart marks. Behind data.
260
+ * - `aboveMarks`: rendered after chart marks, before the HUD. Above
261
+ * data but below tooltips / brushes / interactive overlays.
262
+ */
263
+ extraLayers?: () => {
264
+ belowMarks?: readonly Layer[];
265
+ aboveMarks?: readonly Layer[];
266
+ };
267
+
268
+ // -----------------------------------------------------------------------
269
+ // Sizing
270
+ // -----------------------------------------------------------------------
271
+
272
+ /** Fixed width (CSS px). When set with `height`, disables auto-resize by default. */
273
+ width?: number;
274
+ /** Fixed height (CSS px). When set with `width`, disables auto-resize by default. */
275
+ height?: number;
276
+ /** Override DPR. Default: `window.devicePixelRatio`. */
277
+ dpr?: number;
278
+ /** Track canvas size via `ResizeObserver`. Default: `true` unless `width` and `height` are both set. */
279
+ autoResize?: boolean;
280
+
281
+ // -----------------------------------------------------------------------
282
+ // Loop
283
+ // -----------------------------------------------------------------------
284
+
285
+ /** Run an internal `requestAnimationFrame` loop. Default `true`. Set false to call `update()` from your own loop. */
286
+ autoFrame?: boolean;
287
+ /** Pause draws while `document.hidden`. Default `true`. */
288
+ pauseOnHidden?: boolean;
289
+
290
+ // -----------------------------------------------------------------------
291
+ // Visual
292
+ // -----------------------------------------------------------------------
293
+
294
+ /** Background override. Falls back to `chart.background` then `theme.background`. */
295
+ background?: Color;
296
+
297
+ // -----------------------------------------------------------------------
298
+ // Telemetry
299
+ // -----------------------------------------------------------------------
300
+
301
+ /** Per-frame CPU/GPU timing callback. Forwarded to the underlying `Renderer2D`. */
302
+ onFrameTiming?: (t: FrameTiming) => void;
303
+ /** Enable detailed GPU pipeline-level profiling. Forwarded to the underlying `Renderer2D`. */
304
+ enablePipelineProfiling?: boolean;
305
+
306
+ // -----------------------------------------------------------------------
307
+ // Interactions
308
+ // -----------------------------------------------------------------------
309
+
310
+ /**
311
+ * Default interactions wired into the chart. Pass `false` to disable all of
312
+ * them. Pass `true` (or omit) to enable the defaults: a hover tooltip on
313
+ * geoms that support hit-testing (`point` / `bar` / `line` / `area`) plus a
314
+ * snapping crosshair on continuous x-scale charts. Pass an object to enable
315
+ * a subset and tune options.
316
+ */
317
+ interactions?: boolean | InteractionsConfig;
318
+
319
+ /**
320
+ * Animated transitions when data or chart config changes. On by default
321
+ * (inherits `theme.motion.data` duration and easing). Pass `false` to disable.
322
+ */
323
+ transitions?: boolean | TransitionsConfig;
324
+
325
+ // -----------------------------------------------------------------------
326
+ // Pan / zoom
327
+ // -----------------------------------------------------------------------
328
+
329
+ /**
330
+ * Wire up pan/zoom on the chart's x/y scales. Off by default. Pass `true`
331
+ * to enable with default zoom limits, or an object to override.
332
+ *
333
+ * Behavior when enabled:
334
+ * - The mount creates a `DataViewport` seeded from the chart's x/y scale
335
+ * domain options (the values you passed to `.scale("x", { domain })`).
336
+ * - Pointer events on the canvas pan/zoom the viewport.
337
+ * - The viewport's visible domains are written back into the chart's x/y
338
+ * scale options on every draw, so axes/ticks update automatically.
339
+ * - The viewport is exposed on `MountedPlot.viewport` for read-only
340
+ * introspection (e.g. a `build:` closure that re-bins data into the
341
+ * visible domain).
342
+ *
343
+ * Requires both x and y scales to be continuous (linear / log / sqrt /
344
+ * time). Throws if either scale is missing a numeric/date domain.
345
+ */
346
+ panZoom?: boolean | PanZoomConfig;
347
+
348
+ /**
349
+ * Clip marks to the inner plot panel rect. Default `true`. When on, the
350
+ * mount sets `marksLayer.setClipRect(plotFrame)` after each pipeline run
351
+ * so geom rects/lines/etc. don't bleed into axis / legend / title slots.
352
+ */
353
+ clipMarks?: boolean;
354
+
355
+ /**
356
+ * Damage-tracked partial redraw. On by default — set `false` to opt out,
357
+ * which renders byte-identically to the classic full-frame path.
358
+ *
359
+ * When on (and the mount owns its renderer), the renderer is given a
360
+ * persistent backbuffer and the static marks are baked to a texture, so
361
+ * cursor-driven overlays (tooltip / crosshair / brush / menu) repaint only
362
+ * their own damage rect instead of clearing + re-compiling the whole chart
363
+ * on every pointer move. View-changing events (data, pan/zoom, resize,
364
+ * selection, legend toggle, transitions) still run a full frame and re-bake.
365
+ *
366
+ * Silently ignored when an external `renderer` or `layers` is supplied (the
367
+ * mount can't reconfigure pieces it doesn't own); only an *explicit*
368
+ * `partialRedraw: true` in that situation logs a one-time warning.
369
+ */
370
+ partialRedraw?: boolean;
371
+ }
372
+
373
+ /**
374
+ * Per-axis pan / zoom enable. Either axis can be silenced independently.
375
+ * Sugar: `true` ≡ `"xy"`, `false` ≡ `"none"`.
376
+ */
377
+ export type PanZoomAxes = "x" | "y" | "xy" | "none";
378
+
379
+ /**
380
+ * Pan-bound margins, expressed as fractions of the visible span. A bare
381
+ * number applies to both axes. See `DataPanBoundsOptions` for the full
382
+ * `{ overshoot, drift }` shape inherited from the viewport.
383
+ */
384
+ export type ChartPanBounds = ChartPanBoundsOptions | false;
385
+
386
+ export interface ChartPanBoundsOptions {
387
+ /** Pan-past-content allowance when zoomed in (content > viewport). */
388
+ overshoot?: number | { x?: number; y?: number };
389
+ /** Drift allowance when zoomed out (content ≤ viewport). */
390
+ drift?: number | { x?: number; y?: number };
391
+ }
392
+
393
+ export interface PanZoomConfig {
394
+ /**
395
+ * Minimum zoom factor relative to the chart's base domain. `1` means the
396
+ * user cannot zoom *out* past the data extent — a sensible chart default.
397
+ * Smaller values (e.g. `0.1`) allow zooming out into empty space.
398
+ *
399
+ * Default `1` (zoom-out clamped to the data extent).
400
+ */
401
+ minZoom?: number;
402
+ /**
403
+ * Maximum zoom factor relative to the chart's base domain. `100` lets the
404
+ * user zoom in to 1% of the data range — usually a sane floor before axis
405
+ * ticks and labels collapse.
406
+ *
407
+ * Default `100`.
408
+ */
409
+ maxZoom?: number;
410
+ /**
411
+ * Pan-bound the visible domain to the chart's base (data) domain. When
412
+ * zoomed in, panning stops at content edges plus an `overshoot` margin;
413
+ * when zoomed out far enough that content fits in view, the viewport can
414
+ * still drift by `drift × visibleSpan`.
415
+ *
416
+ * Default `{ overshoot: 0.1 }` (the user can rubber-band 10% past the
417
+ * data edges but can't drag the chart fully off-screen). Pass `false`
418
+ * to opt out entirely.
419
+ */
420
+ panBounds?: ChartPanBounds;
421
+ /**
422
+ * Lock pan to a subset of axes. Default `"xy"` (both axes pan).
423
+ */
424
+ pan?: PanZoomAxes;
425
+ /**
426
+ * Lock zoom to a subset of axes. Default `"xy"` (both axes zoom).
427
+ */
428
+ zoom?: PanZoomAxes;
429
+ /**
430
+ * When set, the Y scale's domain is recomputed every frame from whatever
431
+ * data is currently visible along X (financial-chart "auto-Y" behavior).
432
+ * Implies `pan: "x"` and `zoom: "x"` — Y becomes read-only.
433
+ *
434
+ * `true` uses the default 5% padding around the visible Y extent; pass
435
+ * `{ padding }` to override (0..0.5).
436
+ *
437
+ * The Y scale must still declare an explicit `domain` (used as a fallback
438
+ * when no data is visible in the current X window).
439
+ */
440
+ yFit?: boolean | { padding?: number };
441
+ }
442
+
443
+ export interface InteractionsConfig {
444
+ tooltip?: boolean | TooltipConfig;
445
+ /**
446
+ * Hover crosshair (guide line). Default: enabled on charts whose x-scale
447
+ * is continuous (`linear` / `log` / `sqrt` / `time`); auto-disabled on
448
+ * band x-scales. Pass `false` to disable, `true` for defaults, or an
449
+ * object to tune. Snaps to the same hit positions the tooltip uses.
450
+ */
451
+ crosshair?: boolean | CrosshairConfig;
452
+ /**
453
+ * Click-to-select. Off by default — selection has app-level semantics so
454
+ * the chart shouldn't claim it silently. Pass `true` to enable defaults
455
+ * (plain click replaces, shift toggles, meta/ctrl removes, click-empty
456
+ * clears) or an object to tune. The active selection is exposed on
457
+ * `MountedPlot.selected` and surfaces to geoms via `CompileContext.selected`
458
+ * — selected rows get a stroke ring and unselected rows dim while a
459
+ * selection is active.
460
+ */
461
+ selection?: boolean | SelectionConfig;
462
+ /**
463
+ * Drag-to-rect brush. Off by default — like click-selection, brush semantics
464
+ * are app-specific. Pass `true` to enable defaults (xy rect, dismiss on
465
+ * background tap) or an object to tune. The active brushed rows are exposed
466
+ * on `MountedPlot.brushed`. Brush populates a separate signal from
467
+ * click-selection so the two coexist with different semantics (range query
468
+ * vs. discrete pick).
469
+ */
470
+ brush?: boolean | BrushConfig;
471
+ /**
472
+ * Legend interactivity. Default: enabled when `interactions` is on.
473
+ * Enables click-to-toggle visibility of categorical series.
474
+ */
475
+ legend?: boolean | LegendInteractionConfig;
476
+ /**
477
+ * Context-menu trigger. Off by default — requires an `onTrigger` handler so
478
+ * the host app can render its own menu UI. Fires for right-click, touch
479
+ * long-press (≥500ms), and the keyboard ContextMenu / Shift+F10 keys. The
480
+ * library only emits the event; menu rendering / positioning stays the
481
+ * consumer's responsibility.
482
+ */
483
+ contextMenu?: ContextMenuConfig;
484
+ }
485
+
486
+ export interface ContextMenuConfig {
487
+ /**
488
+ * Resolution policy for the trigger point. Default `"nearest-point"`.
489
+ *
490
+ * - `"nearest-point"`: snap to the nearest mark within its `pickRadius`
491
+ * (skips region-based geoms like bars). When no mark is within range, the
492
+ * trigger is **suppressed entirely** — matches the behavior of the hover
493
+ * tooltip / crosshair (no popup unless something would be highlighted).
494
+ * - `"any-mark"`: include region-based geoms (bars, histograms, tiles).
495
+ * Same suppression behavior as `"nearest-point"` when no mark is hit.
496
+ * - `"background"`: never resolve — `hit`/`datum`/`mark` are always null,
497
+ * and the trigger always fires. Useful for "add a point here" menus.
498
+ */
499
+ hitMode?: "nearest-point" | "any-mark" | "background";
500
+ /** Touch long-press hold duration (ms). Default `500`. */
501
+ holdMs?: number;
502
+ /** Max pointer movement during long-press hold (CSS px). Default `5`. */
503
+ slopPx?: number;
504
+ /**
505
+ * Menu items. Static array or a per-trigger resolver. When provided, the
506
+ * library renders a GPU-drawn menu inside the canvas (no DOM, no host
507
+ * popover required) and routes clicks through `onAction`. When omitted the
508
+ * library only emits `onTrigger` and rendering is the consumer's problem.
509
+ */
510
+ items?:
511
+ | readonly ContextMenuItem[]
512
+ | ((info: ContextMenuTriggerPayload) => readonly ContextMenuItem[]);
513
+ /** Called when an item in the rendered menu is clicked. */
514
+ onAction?(itemId: string, info: ContextMenuTriggerPayload): void;
515
+ /** Placement of the rendered menu relative to the trigger point. */
516
+ placement?: import("insomni").MenuPlacement;
517
+ /** Style overrides for the rendered menu. */
518
+ style?: import("insomni").MenuStyle;
519
+ /**
520
+ * Low-level trigger callback. Fires even when `items` is set, so consumers
521
+ * can hook telemetry / aria-live announcements alongside the rendered menu.
522
+ * Required only when `items` is omitted (otherwise the library has no idea
523
+ * what menu to show).
524
+ */
525
+ onTrigger?(info: ContextMenuTriggerPayload): void;
526
+ }
527
+
528
+ export interface ContextMenuItem {
529
+ /** Stable id passed to `onAction`. Ignored for separators. */
530
+ id: string;
531
+ /** Display label. Ignored for separators. */
532
+ label: string;
533
+ /** Renders as a thin separator line; non-interactive. */
534
+ separator?: boolean;
535
+ /** Greyed out; ignored by hover/click. */
536
+ disabled?: boolean;
537
+ /** Tinted with the menu's `dangerColor` (destructive actions). */
538
+ danger?: boolean;
539
+ /** Optional swatch dot at the start of the row. */
540
+ swatch?: Color;
541
+ /** Optional right-aligned shortcut hint (e.g. "⌘D"). */
542
+ kbd?: string;
543
+ }
544
+
545
+ export interface ContextMenuTriggerPayload {
546
+ /**
547
+ * Resolved hit at the trigger point, or `null` for a background trigger /
548
+ * `hitMode: "background"`.
549
+ */
550
+ hit: HoveredHit | null;
551
+ /** Convenience: the data row at `hit.dataIndex`, or `null` for background. */
552
+ datum: unknown | null;
553
+ /** Convenience: the geom kind that produced the hit. */
554
+ mark: HoveredHit["geomKind"] | null;
555
+ /** Element-local CSS px where the menu should anchor. */
556
+ screenX: number;
557
+ screenY: number;
558
+ source: "mouse" | "touch" | "pen" | "keyboard";
559
+ mods: import("insomni").Mods;
560
+ originalEvent: Event | null;
561
+ }
562
+
563
+ export interface LegendInteractionConfig {
564
+ /** Default `0.2` — alpha for hidden series labels/swatches. */
565
+ dimAlpha?: number;
566
+ }
567
+
568
+ /**
569
+ * Semantic accents for `TooltipShorthandRow.accent`. `positive` / `negative`
570
+ * resolve through `theme.interactions.tooltipAccents`. `neutral` is a
571
+ * no-op marker — the row uses the default text color. Passing a raw `Color`
572
+ * bypasses theme tokens entirely.
573
+ */
574
+ export type TooltipAccent = "positive" | "negative" | "neutral" | Color;
575
+
576
+ export interface TooltipShorthandRow {
577
+ label?: string;
578
+ /**
579
+ * Row value. Numbers / Dates / null / undefined are formatted through the
580
+ * library's default formatter; strings are emitted verbatim.
581
+ */
582
+ value: string | number | Date | boolean | null | undefined;
583
+ /** Optional swatch dot to the left of the row. */
584
+ swatch?: Color;
585
+ /**
586
+ * Semantic accent applied to the row's text color (positive/negative tint
587
+ * for diffs, deltas, status changes). `neutral` is a no-op.
588
+ */
589
+ accent?: TooltipAccent;
590
+ }
591
+
592
+ /**
593
+ * Shorthand object form of `TooltipContent`. The grammar normalizes this into
594
+ * the canvas-tooltip's full `TooltipContent` shape, applying default
595
+ * formatting to non-string values and resolving accents through the theme.
596
+ */
597
+ export interface TooltipContentShorthand {
598
+ title?: string;
599
+ rows: readonly TooltipShorthandRow[];
600
+ }
601
+
602
+ export interface TooltipContentInfo<T = unknown> {
603
+ /** The hovered datum (the row from the geom's data array). */
604
+ datum: T;
605
+ /** Index into the data array. */
606
+ dataIndex: number;
607
+ /** Geom kind (e.g. `"point"`, `"bar"`, `"line"`). */
608
+ mark: string;
609
+ /** Series key when the geom emits multiple segments per row (stacks, dodges). */
610
+ seriesKey?: string;
611
+ /**
612
+ * Resolved aesthetic accessors for the geom. Useful for advanced resolvers
613
+ * that need to read a channel's raw value or column name — most consumers
614
+ * can ignore this and just read fields off `datum`.
615
+ */
616
+ channels: Readonly<Record<string, unknown>>;
617
+ }
618
+
619
+ /**
620
+ * Override the auto-built tooltip content. Receives the hovered datum and
621
+ * returns either the canvas-tooltip's full `TooltipContent` (with
622
+ * `swatch`/`color`/`fontSize` per row), the shorthand `{ title, rows }` form
623
+ * with optional `accent` tokens, or a plain string (single-row body), or
624
+ * `null` to fall back to the default channel-driven content.
625
+ */
626
+ export type TooltipContentResolver<T = unknown> = (
627
+ info: TooltipContentInfo<T>,
628
+ ) => TooltipContentResult | null | undefined;
629
+
630
+ export type TooltipContentResult = TooltipContentShorthand | string;
631
+
632
+ /** One auto-built series row in axis-mode tooltips (mirrors SeriesReadoutRow). */
633
+ export interface AxisTooltipRow {
634
+ /** Series label (color-channel value, seriesKey, or layer label). */
635
+ label: string;
636
+ /** Formatted value at the snapped column. */
637
+ value: string;
638
+ /** Raw value at the snapped column (pre-format) for custom transforms. */
639
+ rawValue: unknown;
640
+ /** Series swatch color, when resolvable. */
641
+ color?: Color;
642
+ /** True for the row whose snapped position is closest to the cursor. */
643
+ active: boolean;
644
+ /** seriesKey when the row came from a stacked / dodged segment. */
645
+ seriesKey?: string;
646
+ }
647
+
648
+ /** Info handed to the axis-mode tooltip content hook. */
649
+ export interface AxisTooltipInfo {
650
+ /** Raw snapped column value (the X for `axis:"x"`, the Y for `axis:"y"`). */
651
+ columnValue: unknown;
652
+ /** Formatted column value (`axisTitleFormat` / `defaultFormat`). */
653
+ columnLabel: string;
654
+ /** Which screen axis we snapped on. */
655
+ axis: "x" | "y";
656
+ /** Auto-built series rows (one per series at the snapped column). */
657
+ rows: readonly AxisTooltipRow[];
658
+ /**
659
+ * The snapped datum(s) — one entry per hit-testable layer that produced a
660
+ * group at this column, in layer order. Each carries the source datum, its
661
+ * data index, and the layer's mark label, so a consumer can read any field
662
+ * off a wide row (incl. fields belonging to ribbon/band/text layers that
663
+ * have no hit tests of their own). For wide rows, `data[0].datum` already
664
+ * holds every column.
665
+ */
666
+ data: readonly { datum: unknown; dataIndex: number; mark: string }[];
667
+ }
668
+
669
+ /**
670
+ * Axis-mode tooltip content hook. Return a `{ title?, rows }` shorthand (the
671
+ * same shape the item resolver returns) to REPLACE the auto rows, or
672
+ * `null` / `undefined` to keep them verbatim. `accent` / `swatch` per row
673
+ * resolve through the theme exactly like the item path.
674
+ */
675
+ export type AxisTooltipContentResolver = (
676
+ info: AxisTooltipInfo,
677
+ ) => TooltipContentShorthand | null | undefined;
678
+
679
+ export interface TooltipConfig {
680
+ showDelay?: number;
681
+ hideDelay?: number;
682
+ fadeMs?: number;
683
+ placement?: "top" | "bottom" | "left" | "right" | "auto";
684
+ /**
685
+ * Override the default channel-driven tooltip body. Receives the hovered
686
+ * datum and returns either:
687
+ * - a plain string (single-row body, no label)
688
+ * - a `{ title?, rows: [{label?, value, accent?, swatch?}] }` shorthand —
689
+ * `accent: "positive" | "negative" | "neutral" | Color` tints the row
690
+ * text via `theme.interactions.tooltipAccents`
691
+ * - `null` / `undefined` to fall back to the auto-built content
692
+ *
693
+ * The library renders the canvas tooltip primitive — there's no HTML/CSS
694
+ * surface to style; row colors come from the theme.
695
+ *
696
+ * Ignored in `trigger:"axis"` mode (use `axisContent`).
697
+ */
698
+ content?: TooltipContentResolver;
699
+ /**
700
+ * Tooltip trigger model. Default `"item"` (today's behavior — one box per
701
+ * hovered mark, fully backward compatible). `"axis"` turns the tooltip into
702
+ * a floating, cursor-following unified readout: it snaps to the nearest data
703
+ * column across ALL hit-testable layers and lists one row per series in a
704
+ * single box.
705
+ */
706
+ trigger?: "item" | "axis";
707
+ /**
708
+ * Which screen axis to snap on in `trigger:"axis"` mode. `"x"` (default) =
709
+ * nearest-X (vertical time-series — pick the column under the cursor's x).
710
+ * `"y"` = nearest-Y (horizontal charts — pick the row under the cursor's y).
711
+ * Ignored when `trigger:"item"`.
712
+ */
713
+ axis?: "x" | "y";
714
+ /**
715
+ * Axis-mode content hook. Receives the snapped column plus the auto-built
716
+ * series rows AND the snapped datum(s), so a consumer can append rows that
717
+ * are NOT y-series (a wind-direction arrow, a day/night flag, a UV risk
718
+ * band) or transform the auto rows. Return value REPLACES the auto rows when
719
+ * non-null; return null/undefined to keep the auto-built rows verbatim.
720
+ * Ignored unless `trigger:"axis"`.
721
+ */
722
+ axisContent?: AxisTooltipContentResolver;
723
+ /** Format the snapped column value used as the tooltip title in axis mode. */
724
+ axisTitleFormat?: (value: unknown) => string;
725
+ /** Format each auto-built series row's value in axis mode. */
726
+ axisValueFormat?: (value: unknown) => string;
727
+ }
728
+
729
+ export interface CrosshairConfig {
730
+ /** Which guide line(s) to draw. Default `"x"` (vertical line tracking cursor). */
731
+ axis?: "x" | "y" | "xy";
732
+ /** Stroke color. Default theme.colors.muted at 0.6 alpha. */
733
+ color?: Color;
734
+ /** Stroke width in CSS px. Default 1. */
735
+ width?: number;
736
+ /**
737
+ * Track the raw pointer position even between data points (no snap). Useful
738
+ * for sparse line/area charts where the snapped-to-hit default leaves big
739
+ * gaps. Snapping still wins when the cursor is over a hit; outside hits the
740
+ * crosshair follows the cursor within the plot frame. Default `false`.
741
+ */
742
+ followPointer?: boolean;
743
+ }
744
+
745
+ export interface BrushConfig {
746
+ /** Which axes the brush spans. Default `"xy"`. */
747
+ axis?: "x" | "y" | "xy";
748
+ /** Translucent fill. Default theme axis color × 0.12 alpha. */
749
+ fillColor?: Color;
750
+ /** Outline color. Default theme axis color × 0.6 alpha. */
751
+ strokeColor?: Color;
752
+ /** Outline width in CSS px. Default 1. */
753
+ strokeWidth?: number;
754
+ /** Enable edge/corner resize handles. Default `true`. */
755
+ handles?: boolean;
756
+ /** Enable drag-the-rect-interior translate. Default `true`. */
757
+ translate?: boolean;
758
+ /**
759
+ * Snap brush rect edges to nearest hit-test position. `true` snaps on the
760
+ * brush's `axis`; pass `"x"` / `"y"` / `"xy"` to override. Default `false`.
761
+ */
762
+ snap?: boolean | "x" | "y" | "xy";
763
+ }
764
+
765
+ /**
766
+ * Read-only view of a `Signal<HoveredHit[]>` exposed on `MountedPlot.brushed`.
767
+ * Empty array when nothing is brushed; remains an empty array (never `null`)
768
+ * when brush is disabled. Independent of `selected` — click + brush coexist
769
+ * with different semantics.
770
+ */
771
+ export interface BrushedSignal {
772
+ get(): readonly HoveredHit[];
773
+ peek(): readonly HoveredHit[];
774
+ subscribe(fn: (v: readonly HoveredHit[]) => void): () => void;
775
+ /** Imperatively clear the active brush. */
776
+ clear(): void;
777
+ }
778
+
779
+ export interface SelectionConfig {
780
+ /**
781
+ * Alpha applied to non-selected marks while a selection is active. Default
782
+ * `0.3`. Set `1` to disable dimming (selected rows still get a stroke ring).
783
+ */
784
+ dimAlpha?: number;
785
+ /** Stroke ring color override; defaults to theme.axis.color. */
786
+ ringColor?: Color;
787
+ /** Stroke ring width in CSS px. Default 2. */
788
+ ringWidth?: number;
789
+ }
790
+
791
+ /**
792
+ * Read-only view of a `Signal<HoveredHit[]>` exposed on `MountedPlot`. The
793
+ * array is empty when nothing is selected; `null` is never produced (use
794
+ * `length === 0` to detect the empty case).
795
+ */
796
+ export interface SelectedSignal {
797
+ get(): readonly HoveredHit[];
798
+ peek(): readonly HoveredHit[];
799
+ subscribe(fn: (v: readonly HoveredHit[]) => void): () => void;
800
+ /** Imperatively clear the active selection. */
801
+ clear(): void;
802
+ }
803
+
804
+ /**
805
+ * Read-only view of a `Signal<ReadonlySet<string>>` exposed on `MountedPlot.hidden`.
806
+ * Empty set when all series are visible.
807
+ */
808
+ export interface HiddenSignal {
809
+ get(): ReadonlySet<string>;
810
+ peek(): ReadonlySet<string>;
811
+ subscribe(fn: (v: ReadonlySet<string>) => void): () => void;
812
+ /** Clear the hidden set (show all). */
813
+ clear(): void;
814
+ }
815
+
816
+ /**
817
+ * Read-only view of a `Signal<HoveredHit | null>` exposed on `MountedPlot`.
818
+ * Drops `set`/`update` so callers can subscribe + peek without writing.
819
+ */
820
+ export interface HoveredSignal {
821
+ get(): HoveredHit | null;
822
+ peek(): HoveredHit | null;
823
+ subscribe(fn: (v: HoveredHit | null) => void): () => void;
824
+ }
825
+
826
+ export interface MountedPlot<T> {
827
+ /** Schedule a redraw on the next animation frame. */
828
+ invalidate(): void;
829
+ /**
830
+ * Replace the active chart spec or builder.
831
+ * - Pass a `Chart<T>` to swap to a static spec.
832
+ * - Pass `() => Chart<T>` to swap the rebuild closure.
833
+ * - Pass nothing to force a rebuild via the existing builder.
834
+ *
835
+ * When `autoFrame: false`, also draws synchronously.
836
+ */
837
+ update(source?: Chart<T> | (() => Chart<T>)): void;
838
+ /** Replace the data array (drops any reactive data signal subscription). */
839
+ setData(data: readonly T[]): void;
840
+ /**
841
+ * Manual resize. If `autoResize` is on you usually don't need this.
842
+ * Pass no args to re-read the canvas's current bounding rect.
843
+ */
844
+ resize(w?: number, h?: number): void;
845
+ /** Override the background color. Pass `null` to revert to the chart/theme default. */
846
+ setBackground(color: Color | null): void;
847
+ /** Static SVG export from the current spec + data. */
848
+ toSVG(opts?: { width?: number; height?: number; atlas?: GlyphAtlas }): SVGSVGElement;
849
+ /** Tear down: cancels the rAF loop, ResizeObserver, signal subs; destroys what we created. */
850
+ destroy(): void;
851
+
852
+ // -----------------------------------------------------------------------
853
+ // Escape hatches — read-only refs to internal pieces.
854
+ // -----------------------------------------------------------------------
855
+
856
+ readonly renderer: Renderer2D;
857
+ readonly axisLayer: Layer;
858
+ readonly marksLayer: Layer;
859
+ readonly hudLayer: Layer;
860
+ /** Cursor-overlay layer (tooltip / crosshair / brush / menu), above the hud. */
861
+ readonly overlayLayer: Layer;
862
+ readonly invalidator: Invalidator;
863
+ /**
864
+ * Read-only signal carrying the currently hovered hit (geom kind, dataIndex,
865
+ * data array reference, snapped position) or `null` when nothing is
866
+ * hovered. Updated synchronously from pointer events. Geoms also receive
867
+ * the same value via `CompileContext.hovered` during compile.
868
+ */
869
+ readonly hovered: HoveredSignal;
870
+ /**
871
+ * Read-only signal carrying the currently selected rows (one `HoveredHit`
872
+ * per selected datum). Empty array when nothing is selected; remains an
873
+ * empty array (never `null`) when selection is disabled. Geoms receive
874
+ * the same payload via `CompileContext.selected` during compile.
875
+ */
876
+ readonly selected: SelectedSignal;
877
+ /**
878
+ * Read-only signal carrying the rows currently inside the brush rect.
879
+ * Empty array when nothing is brushed (or when brush is disabled).
880
+ * Independent of `selected`.
881
+ */
882
+ readonly brushed: BrushedSignal;
883
+ /**
884
+ * Read-only signal carrying the set of series keys currently hidden via
885
+ * legend toggle. Empty set when all series are visible; remains an empty
886
+ * set (never `null`) when legend interaction is disabled.
887
+ */
888
+ readonly hidden: HiddenSignal;
889
+
890
+ // -----------------------------------------------------------------------
891
+ // Transitions
892
+ // -----------------------------------------------------------------------
893
+
894
+ /**
895
+ * Handle for programmatic transition control. Always present even when
896
+ * transitions are disabled (calls become no-ops in that case).
897
+ */
898
+ readonly transitions: TransitionsHandle;
899
+
900
+ // -----------------------------------------------------------------------
901
+ // Stats
902
+ // -----------------------------------------------------------------------
903
+
904
+ /** CSS-pixel width currently rendered. */
905
+ readonly width: number;
906
+ /** CSS-pixel height currently rendered. */
907
+ readonly height: number;
908
+ /** Active device-pixel ratio. */
909
+ readonly dpr: number;
910
+ /** Combined shape count across the three internal layers. */
911
+ readonly shapeCount: number;
912
+ /** Combined triangle count across the three internal layers. */
913
+ readonly triangleCount: number;
914
+ /** True when `invalidate()` has been called and a draw hasn't happened yet. */
915
+ readonly needsFrame: boolean;
916
+ /**
917
+ * Inner plot panel rect (absolute element-CSS px) — the area enclosed by
918
+ * the axes, where geom marks draw. Updated each frame from the pipeline.
919
+ * For faceted charts this is the union of all panel frames. Returns a
920
+ * zero-rect before the first draw.
921
+ */
922
+ readonly plotFrame: Frame;
923
+
924
+ /**
925
+ * The chart's pan/zoom viewport, present only when `panZoom` was enabled
926
+ * on mount. Lets the caller subscribe to view changes, read the visible
927
+ * domain (e.g. for re-binning data), or call `.reset()`. The mount keeps
928
+ * its frame in sync with `plotFrame` automatically.
929
+ */
930
+ readonly viewport: import("../viewport.ts").DataViewport<number, number> | null;
931
+
932
+ /**
933
+ * Wire a `createRangePresets` controller against this chart's viewport and
934
+ * optionally render a chip strip into a host element. Throws if `panZoom`
935
+ * was not enabled at mount time (no `viewport` to drive).
936
+ *
937
+ * The headless flow is preserved — omit `opts.ui` and you get the same
938
+ * controller (`setActive` / `getActive` / `subscribe`) without any DOM
939
+ * construction. Strings in `presets` are expanded to `timePreset(key)`.
940
+ */
941
+ attachRangePresets(
942
+ opts: import("./attach-presets.ts").AttachRangePresetsOptions,
943
+ ): import("./attach-presets.ts").AttachedRangePresets;
944
+
945
+ /**
946
+ * Attach a dygraph-style pinned legend — a side panel that updates with each
947
+ * series' value at the cursor's snapped x. Reuses the chart's compiled
948
+ * hit-tests (no separate spatial index) so the snap stays aligned with what
949
+ * the tooltip would have shown.
950
+ *
951
+ * Headless: omit `opts.ui` and observe via `subscribe()` to render your own
952
+ * UI. With `opts.ui` set, a minimal absolute-positioned panel is appended
953
+ * into the mount element and disposed alongside this readout.
954
+ */
955
+ attachSeriesReadout(
956
+ opts: import("./interactions/series-readout.ts").AttachSeriesReadoutOptions,
957
+ ): import("./interactions/series-readout.ts").AttachedSeriesReadout;
958
+
959
+ /**
960
+ * Wire a drag-to-rect brush onto the chart imperatively. Mirrors the
961
+ * declarative `interactions: { brush: ... }` config path — the same
962
+ * `MountedPlot.brushed` signal observes the brushed set — but lets a
963
+ * consumer attach (and later dispose) a brush after mount without
964
+ * rebuilding the chart spec.
965
+ *
966
+ * Throws if a brush is already configured on this mount, either via
967
+ * `interactions.brush` or a prior `attachBrush` call that hasn't been
968
+ * disposed. The two surfaces are mutually exclusive: the brush rect is a
969
+ * singleton.
970
+ */
971
+ attachBrush(
972
+ opts: import("./attach-brush.ts").AttachBrushOptions,
973
+ ): import("./attach-brush.ts").AttachedBrush;
974
+
975
+ /**
976
+ * Convert an event-relative canvas coordinate (e.g. `event.offsetX/Y`, or
977
+ * `clientX - rect.left`) into the chart's data space, routing through the
978
+ * active coord's `unproject` (identity for Cartesian, polar inverse for
979
+ * `coordPolar` / `coordRadial`).
980
+ *
981
+ * Returns `null` when:
982
+ * - the point falls outside the active plot frame, or
983
+ * - `coord.unproject` returns null (e.g. polar point outside the radius
984
+ * band), or
985
+ * - the pipeline has not produced scales yet (no draw has happened), or
986
+ * - either position scale lacks an `invert` (band scales).
987
+ *
988
+ * `dataX` / `dataY` are the result of `xScale.axisScale.invert(plotFrameX)`
989
+ * and `yScale.axisScale.invert(plotFrameY)` respectively. `plotFrameX/Y` are
990
+ * relative to the plot frame's top-left.
991
+ *
992
+ * This is the screen → data primitive consumers use to drive picking on top
993
+ * of their own spatial index. The mount doesn't know what's in the chart —
994
+ * downstream layers (phylo's `hitTestPhylo`, custom geoms, etc.) take it
995
+ * from here.
996
+ */
997
+ pickAt(
998
+ canvasX: number,
999
+ canvasY: number,
1000
+ ): {
1001
+ plotFrameX: number;
1002
+ plotFrameY: number;
1003
+ dataX: number;
1004
+ dataY: number;
1005
+ frame: Frame;
1006
+ } | null;
1007
+ }
1008
+
1009
+ /** @deprecated kept for back-compat; use `MountedPlot<T>`. */
1010
+ export type MountedChart<T = Row> = MountedPlot<T>;
1011
+
1012
+ // ---------------------------------------------------------------------------
1013
+ // Render target — for hand-wired use without mount().
1014
+ // ---------------------------------------------------------------------------
1015
+
1016
+ export interface RenderTarget {
1017
+ axisLayer: Layer;
1018
+ marksLayer: Layer;
1019
+ hudLayer: Layer;
1020
+ width?: number;
1021
+ height?: number;
1022
+ }
1023
+
1024
+ export interface Chart<T> {
1025
+ layer(geom: Geom<T>): Chart<T>;
1026
+ layers(geoms: readonly Geom<T>[]): Chart<T>;
1027
+ /**
1028
+ * Configure a scale. `domain` is always optional — when omitted it is
1029
+ * inferred from the data, and when `panZoom` is enabled on `.mount()` the
1030
+ * pipeline overrides the x/y position-scale domain each frame from the
1031
+ * viewport's visible window. Consumers should NOT re-thread the viewport's
1032
+ * visible domain back through `.scale()` from a `build:` closure; it's
1033
+ * redundant and forces a rebuild on every dirty draw.
1034
+ */
1035
+ scale<C extends Channel>(channel: C, options: ScaleOptionsByChannel<C>): Chart<T>;
1036
+ axes(spec: AxesSpec): Chart<T>;
1037
+ title(spec: TitleSpec | string): Chart<T>;
1038
+ legend(spec: LegendSpec | false): Chart<T>;
1039
+ /** Add a free-form text/box annotation. Multiple calls accumulate. */
1040
+ annotate(spec: AnnotationSpec): Chart<T>;
1041
+ /** Split data into a grid of panels by `spec.by`. Replaces any prior facet. */
1042
+ facet(spec: FacetSpec<T>): Chart<T>;
1043
+ /**
1044
+ * Set the coordinate system. Defaults to `coordCartesian()`. Polar / radial
1045
+ * coords land in Phase 3 of the phylo-on-plot rewrite.
1046
+ */
1047
+ coord(coord: Coord): Chart<T>;
1048
+ theme(theme: Theme): Chart<T>;
1049
+ /**
1050
+ * Bind this chart to a canvas. The mount owns the renderer, atlas, three
1051
+ * layers, rAF loop, ResizeObserver and visibility-pause unless you pass
1052
+ * your own pieces in `opts`. See `MountPlotOptions` for every escape hatch.
1053
+ */
1054
+ mount(canvas: HTMLCanvasElement, opts?: MountPlotOptions<T>): MountedPlot<T>;
1055
+ /**
1056
+ * Compile + emit into externally owned layers. Use when the host already
1057
+ * owns a `Renderer2D` (and wants its own per-frame timing). For simpler
1058
+ * cases, prefer `mount(canvas)`.
1059
+ */
1060
+ render(target: RenderTarget): void;
1061
+ /** Static SVG export. Caller mounts the returned element where they want. */
1062
+ toSVG(options?: { width?: number; height?: number; atlas?: GlyphAtlas }): SVGSVGElement;
1063
+ }
1064
+
1065
+ // ---------------------------------------------------------------------------
1066
+ // Builder
1067
+ // ---------------------------------------------------------------------------
1068
+
1069
+ function resolveFramePadding(fp: PlotSpec<unknown>["framePadding"]): {
1070
+ top: number;
1071
+ right: number;
1072
+ bottom: number;
1073
+ left: number;
1074
+ } {
1075
+ if (fp === undefined) return { top: 0, right: 0, bottom: 0, left: 0 };
1076
+ if (typeof fp === "number") return { top: fp, right: fp, bottom: fp, left: fp };
1077
+ return {
1078
+ top: fp.top ?? 0,
1079
+ right: fp.right ?? 0,
1080
+ bottom: fp.bottom ?? 0,
1081
+ left: fp.left ?? 0,
1082
+ };
1083
+ }
1084
+
1085
+ export function plot<T>(spec: PlotSpec<T>): Chart<T> {
1086
+ const config: ChartConfig<T> = {
1087
+ data: spec.data,
1088
+ width: spec.width,
1089
+ height: spec.height,
1090
+ background: spec.background,
1091
+ padding: spec.padding ?? DEFAULT_PADDING,
1092
+ framePadding: resolveFramePadding(spec.framePadding),
1093
+ device: spec.device,
1094
+ externalAtlas: undefined,
1095
+ theme: spec.theme ?? themeDefault,
1096
+ layers: [],
1097
+ axes: {},
1098
+ titles: {},
1099
+ legend: { show: true },
1100
+ scaleOverrides: {},
1101
+ annotations: [],
1102
+ coord: coordCartesian(),
1103
+ };
1104
+ return makeChart(config);
1105
+ }
1106
+
1107
+ function makeChart<T>(config: ChartConfig<T>): Chart<T> {
1108
+ const chart: Chart<T> = {
1109
+ layer(geom) {
1110
+ return makeChart({ ...config, layers: [...config.layers, geom] });
1111
+ },
1112
+ layers(geoms) {
1113
+ return makeChart({ ...config, layers: [...config.layers, ...geoms] });
1114
+ },
1115
+ scale(channel, options) {
1116
+ return makeChart({
1117
+ ...config,
1118
+ scaleOverrides: { ...config.scaleOverrides, [channel]: options },
1119
+ });
1120
+ },
1121
+ axes(spec) {
1122
+ return makeChart({ ...config, axes: { ...config.axes, ...spec } });
1123
+ },
1124
+ title(spec) {
1125
+ const next = typeof spec === "string" ? { title: spec } : spec;
1126
+ return makeChart({ ...config, titles: { ...config.titles, ...next } });
1127
+ },
1128
+ legend(spec) {
1129
+ const next: LegendSpec = spec === false ? { show: false } : { show: true, ...spec };
1130
+ return makeChart({ ...config, legend: next });
1131
+ },
1132
+ annotate(spec) {
1133
+ return makeChart({ ...config, annotations: [...config.annotations, spec] });
1134
+ },
1135
+ facet(spec) {
1136
+ return makeChart({ ...config, facet: spec });
1137
+ },
1138
+ coord(coord) {
1139
+ return makeChart({ ...config, coord });
1140
+ },
1141
+ theme(theme) {
1142
+ return makeChart({ ...config, theme });
1143
+ },
1144
+ mount(canvas, opts) {
1145
+ return mountChart(config, canvas, opts);
1146
+ },
1147
+ render(target) {
1148
+ const data = currentData(config.data);
1149
+ const snapshot: ChartConfig<T> = {
1150
+ ...config,
1151
+ width: target.width ?? config.width,
1152
+ height: target.height ?? config.height,
1153
+ };
1154
+ runPipeline(
1155
+ snapshot,
1156
+ data,
1157
+ target.axisLayer,
1158
+ target.marksLayer,
1159
+ target.hudLayer,
1160
+ config.externalAtlas,
1161
+ );
1162
+ },
1163
+ toSVG(options = {}) {
1164
+ return renderSVG(config, options.width, options.height, options.atlas);
1165
+ },
1166
+ };
1167
+ // Stash the frozen config so `mount()` can compile against it without
1168
+ // re-deriving from the immutable builder chain. Non-enumerable to keep it
1169
+ // out of devtools / spreads. See mount.ts:configOf().
1170
+ Object.defineProperty(chart, "__config__", { value: config, enumerable: false });
1171
+ return chart;
1172
+ }