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,476 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Per-axis state machine: transform, domain endpoints, range endpoints,
3
+ // pan/zoom math, scale rebuild. The DataViewport composes one of these per
4
+ // axis (x, y) and orchestrates them.
5
+ // ---------------------------------------------------------------------------
6
+
7
+ import {
8
+ bandScale,
9
+ linearScale,
10
+ logScale,
11
+ logTransform,
12
+ logUntransform,
13
+ signedSqrt,
14
+ signedSquare,
15
+ sqrtScale,
16
+ timeScale,
17
+ type AxisScale,
18
+ type BandScale,
19
+ type BandScaleOptions,
20
+ type ContinuousScale,
21
+ type DateDomain,
22
+ type NumericDomain,
23
+ type TimeScale,
24
+ } from "../scales.ts";
25
+
26
+ export interface ContinuousAxisConfig {
27
+ type: "linear" | "log" | "sqrt";
28
+ domain: NumericDomain;
29
+ }
30
+
31
+ export interface TimeAxisConfig {
32
+ type: "time";
33
+ domain: DateDomain;
34
+ }
35
+
36
+ export interface BandAxisConfig<T = unknown> extends BandScaleOptions {
37
+ type: "band";
38
+ domain: readonly T[];
39
+ }
40
+
41
+ export type ViewportAxisConfig<T = unknown> =
42
+ | ContinuousAxisConfig
43
+ | TimeAxisConfig
44
+ | BandAxisConfig<T>;
45
+
46
+ export type VisibleDomainInput = readonly [number, number] | readonly [Date, Date];
47
+
48
+ interface Transform {
49
+ to: (v: number) => number;
50
+ from: (v: number) => number;
51
+ }
52
+
53
+ const LINEAR_T: Transform = { to: (v) => v, from: (v) => v };
54
+ const LOG_T: Transform = { to: logTransform, from: logUntransform };
55
+ const SQRT_T: Transform = { to: signedSqrt, from: signedSquare };
56
+
57
+ export interface ContinuousAxisState {
58
+ kind: "continuous";
59
+ type: "linear" | "log" | "sqrt" | "time";
60
+ transform: Transform;
61
+ t0: number;
62
+ t1: number;
63
+ baseT0: number;
64
+ baseT1: number;
65
+ r0: number;
66
+ r1: number;
67
+ /**
68
+ * Memoized scale instance for the current (t0, t1, r0, r1, type, transform)
69
+ * tuple. Null after construction and after any mutation; populated lazily by
70
+ * `buildScale` / `applyAxis`. Pan/zoom/setVisibleDomain/clampAxisToBase/
71
+ * resetAxis/setAxisRange/copyContinuous all clear it via `invalidateScale`.
72
+ */
73
+ scaleCache: ContinuousScale | TimeScale | null;
74
+ }
75
+
76
+ export interface BandAxisState<T> {
77
+ kind: "band";
78
+ domain: readonly T[];
79
+ options: BandScaleOptions;
80
+ r0: number;
81
+ r1: number;
82
+ /** See `ContinuousAxisState.scaleCache`. */
83
+ scaleCache: BandScale<T> | null;
84
+ }
85
+
86
+ export type AxisState<T> = ContinuousAxisState | BandAxisState<T>;
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Construction
90
+ // ---------------------------------------------------------------------------
91
+
92
+ export function createAxisState<T>(
93
+ config: ViewportAxisConfig<T>,
94
+ r0: number,
95
+ r1: number,
96
+ ): AxisState<T> {
97
+ if (config.type === "band") {
98
+ const options: BandScaleOptions = {};
99
+ if (config.padding !== undefined) options.padding = config.padding;
100
+ if (config.paddingInner !== undefined) options.paddingInner = config.paddingInner;
101
+ if (config.paddingOuter !== undefined) options.paddingOuter = config.paddingOuter;
102
+ if (config.align !== undefined) options.align = config.align;
103
+ return {
104
+ kind: "band",
105
+ domain: [...config.domain],
106
+ options,
107
+ r0,
108
+ r1,
109
+ scaleCache: null,
110
+ };
111
+ }
112
+
113
+ const transform = pickTransform(config.type);
114
+ let d0: number;
115
+ let d1: number;
116
+ if (config.type === "time") {
117
+ d0 = config.domain[0].getTime();
118
+ d1 = config.domain[1].getTime();
119
+ } else {
120
+ d0 = config.domain[0];
121
+ d1 = config.domain[1];
122
+ }
123
+ if (config.type === "log") {
124
+ if (d0 === 0 || d1 === 0) {
125
+ throw new Error("log axis domain values must be non-zero.");
126
+ }
127
+ if (Math.sign(d0) !== Math.sign(d1)) {
128
+ throw new Error("log axis domain values must share the same sign.");
129
+ }
130
+ }
131
+
132
+ const t0 = transform.to(d0);
133
+ const t1 = transform.to(d1);
134
+ return {
135
+ kind: "continuous",
136
+ type: config.type,
137
+ transform,
138
+ t0,
139
+ t1,
140
+ baseT0: t0,
141
+ baseT1: t1,
142
+ r0,
143
+ r1,
144
+ scaleCache: null,
145
+ };
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Cache helpers
150
+ // ---------------------------------------------------------------------------
151
+
152
+ /**
153
+ * Drop the memoized scale. Call from every mutator that touches t0/t1/r0/r1/
154
+ * domain/options so the next `buildScale`/`applyAxis`/`invertAxis` rebuilds.
155
+ * Cheap (single null write); the hot path is the cache hit, not invalidation.
156
+ */
157
+ function invalidateScale(axis: AxisState<unknown>): void {
158
+ axis.scaleCache = null;
159
+ }
160
+
161
+ function pickTransform(type: "linear" | "log" | "sqrt" | "time"): Transform {
162
+ switch (type) {
163
+ case "linear":
164
+ case "time":
165
+ return LINEAR_T;
166
+ case "log":
167
+ return LOG_T;
168
+ case "sqrt":
169
+ return SQRT_T;
170
+ }
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Scale rebuild
175
+ // ---------------------------------------------------------------------------
176
+
177
+ function buildContinuousScale(state: ContinuousAxisState): ContinuousScale | TimeScale {
178
+ const { transform, t0, t1, r0, r1, type } = state;
179
+ let d0 = transform.from(t0);
180
+ let d1 = transform.from(t1);
181
+
182
+ if (type === "log") {
183
+ const eps = 1e-12;
184
+ if (d0 <= 0) d0 = eps;
185
+ if (d1 <= 0) d1 = eps;
186
+ }
187
+
188
+ switch (type) {
189
+ case "linear":
190
+ return linearScale([d0, d1], [r0, r1]);
191
+ case "log":
192
+ return logScale([d0, d1], [r0, r1]);
193
+ case "sqrt":
194
+ return sqrtScale([d0, d1], [r0, r1]);
195
+ case "time":
196
+ return timeScale([new Date(d0), new Date(d1)], [r0, r1]);
197
+ }
198
+ }
199
+
200
+ function buildBandScale<T>(state: BandAxisState<T>): BandScale<T> {
201
+ return bandScale(state.domain, [state.r0, state.r1], state.options);
202
+ }
203
+
204
+ export function buildScale<T>(state: AxisState<T>): AxisScale<T> {
205
+ if (state.kind === "continuous") {
206
+ return getContinuousScale(state) as unknown as AxisScale<T>;
207
+ }
208
+ return getBandScale(state) as unknown as AxisScale<T>;
209
+ }
210
+
211
+ function getContinuousScale(state: ContinuousAxisState): ContinuousScale | TimeScale {
212
+ let cached = state.scaleCache;
213
+ if (cached === null) {
214
+ cached = buildContinuousScale(state);
215
+ state.scaleCache = cached;
216
+ }
217
+ return cached;
218
+ }
219
+
220
+ function getBandScale<T>(state: BandAxisState<T>): BandScale<T> {
221
+ let cached = state.scaleCache;
222
+ if (cached === null) {
223
+ cached = buildBandScale(state);
224
+ state.scaleCache = cached;
225
+ }
226
+ return cached;
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // Pan / zoom
231
+ // ---------------------------------------------------------------------------
232
+
233
+ export function panAxis(axis: AxisState<unknown>, dPx: number): boolean {
234
+ if (axis.kind !== "continuous") return false;
235
+ const span = axis.r1 - axis.r0;
236
+ if (span === 0) return false;
237
+ const delta = (-dPx * (axis.t1 - axis.t0)) / span;
238
+ if (delta === 0) return false;
239
+ axis.t0 += delta;
240
+ axis.t1 += delta;
241
+ invalidateScale(axis);
242
+ return true;
243
+ }
244
+
245
+ export function setAxisVisibleDomain(
246
+ axis: AxisState<unknown>,
247
+ domain: VisibleDomainInput,
248
+ minZoom: number,
249
+ maxZoom: number,
250
+ ): boolean {
251
+ if (axis.kind !== "continuous") return false;
252
+
253
+ const r0 = domain[0];
254
+ const r1 = domain[1];
255
+ const d0 = typeof r0 === "number" ? r0 : r0.getTime();
256
+ const d1 = typeof r1 === "number" ? r1 : r1.getTime();
257
+ if (!Number.isFinite(d0) || !Number.isFinite(d1)) return false;
258
+
259
+ if (axis.type === "log") {
260
+ if (d0 === 0 || d1 === 0) {
261
+ throw new Error("log axis domain values must be non-zero.");
262
+ }
263
+ if (Math.sign(d0) !== Math.sign(d1)) {
264
+ throw new Error("log axis domain values must share the same sign.");
265
+ }
266
+ }
267
+
268
+ const reqT0 = axis.transform.to(d0);
269
+ const reqT1 = axis.transform.to(d1);
270
+ if (!Number.isFinite(reqT0) || !Number.isFinite(reqT1)) return false;
271
+ if (reqT0 === reqT1) return false;
272
+
273
+ const baseSpan = axis.baseT1 - axis.baseT0;
274
+ if (baseSpan === 0) return false;
275
+
276
+ const reqSpan = reqT1 - reqT0;
277
+ const baseAbs = Math.abs(baseSpan);
278
+ const minAbs = baseAbs / maxZoom;
279
+ const maxAbs = baseAbs / minZoom;
280
+ const absReq = Math.abs(reqSpan);
281
+
282
+ let nextT0 = reqT0;
283
+ let nextT1 = reqT1;
284
+ if (absReq < minAbs || absReq > maxAbs) {
285
+ const clampedAbs = absReq < minAbs ? minAbs : maxAbs;
286
+ const sign = Math.sign(reqSpan) || 1;
287
+ const center = (reqT0 + reqT1) / 2;
288
+ const half = (sign * clampedAbs) / 2;
289
+ nextT0 = center - half;
290
+ nextT1 = center + half;
291
+ }
292
+
293
+ if (nextT0 === axis.t0 && nextT1 === axis.t1) return false;
294
+ axis.t0 = nextT0;
295
+ axis.t1 = nextT1;
296
+ invalidateScale(axis);
297
+ return true;
298
+ }
299
+
300
+ export function zoomAxis(
301
+ axis: AxisState<unknown>,
302
+ anchorPx: number,
303
+ factor: number,
304
+ minZoom: number,
305
+ maxZoom: number,
306
+ ): boolean {
307
+ if (axis.kind !== "continuous") return false;
308
+ if (!Number.isFinite(factor) || factor <= 0) return false;
309
+
310
+ const baseSpan = axis.baseT1 - axis.baseT0;
311
+ const curSpan = axis.t1 - axis.t0;
312
+ if (curSpan === 0 || baseSpan === 0) return false;
313
+
314
+ let newSpan = curSpan / factor;
315
+ const signSpan = Math.sign(baseSpan);
316
+ const minAbs = Math.abs(baseSpan) / maxZoom;
317
+ const maxAbs = Math.abs(baseSpan) / minZoom;
318
+ const absNew = Math.abs(newSpan);
319
+ if (absNew < minAbs) newSpan = signSpan * minAbs;
320
+ else if (absNew > maxAbs) newSpan = signSpan * maxAbs;
321
+
322
+ if (newSpan === curSpan) return false;
323
+
324
+ const range = axis.r1 - axis.r0;
325
+ if (range === 0) return false;
326
+ const anchorT = axis.t0 + ((anchorPx - axis.r0) * curSpan) / range;
327
+ const newT0 = anchorT - ((anchorPx - axis.r0) * newSpan) / range;
328
+ axis.t0 = newT0;
329
+ axis.t1 = newT0 + newSpan;
330
+ invalidateScale(axis);
331
+ return true;
332
+ }
333
+
334
+ // ---------------------------------------------------------------------------
335
+ // Pan-bound clamping (per-axis)
336
+ // ---------------------------------------------------------------------------
337
+
338
+ function clampOffset(
339
+ s0: number,
340
+ V: number,
341
+ c0: number,
342
+ L: number,
343
+ overshoot: number,
344
+ drift: number,
345
+ ): number {
346
+ const offset = s0 - c0;
347
+ let lo: number;
348
+ let hi: number;
349
+ if (V < L) {
350
+ const m = Math.abs(overshoot) * V;
351
+ lo = -m;
352
+ hi = L - V + m;
353
+ } else {
354
+ const m = Math.abs(drift) * V;
355
+ lo = L - V - m;
356
+ hi = m;
357
+ }
358
+ if (offset < lo) return lo - offset;
359
+ if (offset > hi) return hi - offset;
360
+ return 0;
361
+ }
362
+
363
+ export function clampAxisToBase(
364
+ axis: AxisState<unknown>,
365
+ overshoot: number,
366
+ drift: number,
367
+ ): boolean {
368
+ if (axis.kind !== "continuous") return false;
369
+ const baseSpan = axis.baseT1 - axis.baseT0;
370
+ const visSpan = axis.t1 - axis.t0;
371
+ if (baseSpan === 0 || visSpan === 0) return false;
372
+ const reversed = baseSpan < 0;
373
+ const c0 = reversed ? axis.baseT1 : axis.baseT0;
374
+ const c1 = reversed ? axis.baseT0 : axis.baseT1;
375
+ const sLow = reversed ? axis.t1 : axis.t0;
376
+ const sHigh = reversed ? axis.t0 : axis.t1;
377
+ const L = c1 - c0;
378
+ const V = sHigh - sLow;
379
+ if (V === 0) return false;
380
+ const delta = clampOffset(sLow, V, c0, L, overshoot, drift);
381
+ if (delta === 0) return false;
382
+ axis.t0 += delta;
383
+ axis.t1 += delta;
384
+ invalidateScale(axis);
385
+ return true;
386
+ }
387
+
388
+ // ---------------------------------------------------------------------------
389
+ // Misc accessors
390
+ // ---------------------------------------------------------------------------
391
+
392
+ export function setAxisRange(axis: AxisState<unknown>, r0: number, r1: number): void {
393
+ if (axis.r0 === r0 && axis.r1 === r1) return;
394
+ axis.r0 = r0;
395
+ axis.r1 = r1;
396
+ invalidateScale(axis);
397
+ }
398
+
399
+ export function resetAxis(axis: AxisState<unknown>): boolean {
400
+ if (axis.kind !== "continuous") return false;
401
+ if (axis.t0 === axis.baseT0 && axis.t1 === axis.baseT1) return false;
402
+ axis.t0 = axis.baseT0;
403
+ axis.t1 = axis.baseT1;
404
+ invalidateScale(axis);
405
+ return true;
406
+ }
407
+
408
+ export function readVisibleDomain(axis: AxisState<unknown>): unknown {
409
+ if (axis.kind === "band") return [...axis.domain];
410
+ const d0 = axis.transform.from(axis.t0);
411
+ const d1 = axis.transform.from(axis.t1);
412
+ if (axis.type === "time") {
413
+ return [new Date(d0), new Date(d1)] as const;
414
+ }
415
+ return [d0, d1] as const;
416
+ }
417
+
418
+ export function invertAxis(axis: AxisState<unknown>, localPx: number): unknown {
419
+ if (axis.kind === "continuous") {
420
+ const range = axis.r1 - axis.r0;
421
+ if (range === 0) return axis.transform.from(axis.t0);
422
+ const t = axis.t0 + ((localPx - axis.r0) * (axis.t1 - axis.t0)) / range;
423
+ const value = axis.transform.from(t);
424
+ return axis.type === "time" ? new Date(value) : value;
425
+ }
426
+ const scale = getBandScale(axis);
427
+ const bw = scale.bandwidth();
428
+ for (const v of axis.domain) {
429
+ const start = scale(v);
430
+ if (localPx >= start && localPx <= start + bw) return v;
431
+ }
432
+ return undefined;
433
+ }
434
+
435
+ export function applyAxis(axis: AxisState<unknown>, value: unknown): number {
436
+ if (axis.kind === "continuous") {
437
+ const scale = getContinuousScale(axis);
438
+ if (axis.type === "time") {
439
+ return (scale as TimeScale)(value as Date);
440
+ }
441
+ return (scale as ContinuousScale)(value as number);
442
+ }
443
+ return getBandScale(axis)(value);
444
+ }
445
+
446
+ // ---------------------------------------------------------------------------
447
+ // Linear-axis fast-path helper
448
+ // ---------------------------------------------------------------------------
449
+
450
+ export interface LinearAxisFn {
451
+ readonly t0: number; // domain start (transformed)
452
+ readonly t1: number; // domain end (transformed)
453
+ readonly r0: number; // range start (px, axis-local)
454
+ readonly r1: number; // range end (px, axis-local)
455
+ }
456
+
457
+ /**
458
+ * If the axis is a linear, continuous, non-time axis, return its current
459
+ * (t0, t1, r0, r1) tuple. Otherwise return null. Used by callers that want
460
+ * to hoist the linear projection out of a hot loop.
461
+ */
462
+ export function asLinearAxisFn(axis: AxisState<unknown>): LinearAxisFn | null {
463
+ if (axis.kind !== "continuous") return null;
464
+ if (axis.type !== "linear") return null;
465
+ const { t0, t1, r0, r1 } = axis;
466
+ return { t0, t1, r0, r1 };
467
+ }
468
+
469
+ export function copyContinuous(source: AxisState<unknown>, target: AxisState<unknown>): void {
470
+ if (source.kind !== "continuous" || target.kind !== "continuous") return;
471
+ if (source.type !== target.type) return;
472
+ if (target.t0 === source.t0 && target.t1 === source.t1) return;
473
+ target.t0 = source.t0;
474
+ target.t1 = source.t1;
475
+ invalidateScale(target);
476
+ }
@@ -0,0 +1,170 @@
1
+ import { type Frame, type FrameRect, type ResolvedCameraState, type Vec2 } from "insomni";
2
+ import type { CameraViewport, ViewportLike } from "insomni/viewport";
3
+ import type { AxisScale, DateDomain, NumericDomain } from "./scales.ts";
4
+ import { type AxisState, type ViewportAxisConfig, type VisibleDomainInput } from "./viewport/axis-state.ts";
5
+ export type { BandAxisConfig, ContinuousAxisConfig, TimeAxisConfig, ViewportAxisConfig, VisibleDomainInput, } from "./viewport/axis-state.ts";
6
+ /**
7
+ * Per-axis margin spec. A bare number applies to both axes. Values are
8
+ * fractions of the *visible* span and apply symmetrically to both edges.
9
+ */
10
+ export type PanBoundsMargin = number | {
11
+ x?: number;
12
+ y?: number;
13
+ };
14
+ export interface PanBoundsMargins {
15
+ /** Pan-past-content allowance when content is larger than the viewport. */
16
+ overshoot?: PanBoundsMargin;
17
+ /** Content drift allowance when content fits inside the viewport. */
18
+ drift?: PanBoundsMargin;
19
+ }
20
+ export interface DataPanBoundsOptions extends PanBoundsMargins {
21
+ }
22
+ export interface DataViewportOptions<X = unknown, Y = unknown> {
23
+ /** Frame in pixel space (relative to parent if `parent` is supplied). */
24
+ frame: Frame;
25
+ /** Optional parent viewport — `frame` is interpreted in the parent's space. */
26
+ parent?: ViewportLike;
27
+ x: ViewportAxisConfig<X>;
28
+ y: ViewportAxisConfig<Y>;
29
+ /** Minimum domain-span multiplier (relative to base). Default: 0.001 */
30
+ minZoom?: number;
31
+ /** Maximum domain-span multiplier (relative to base). Default: 1000 */
32
+ maxZoom?: number;
33
+ /**
34
+ * Pan-bound the visible domain to the axis base domain (the initial domain
35
+ * passed in `x` / `y`). When zoomed in, panning stops at content edges
36
+ * (plus margin); when zoomed out far enough that content fits in view,
37
+ * the viewport cannot pan content off-screen. Zoom itself remains
38
+ * unrestricted past content — this only governs pan position.
39
+ */
40
+ panBounds?: DataPanBoundsOptions;
41
+ }
42
+ export type ZoomFactor = number | {
43
+ x?: number;
44
+ y?: number;
45
+ };
46
+ export interface LinearProjector {
47
+ /** sx = worldX * txSlope + txConst */
48
+ readonly txSlope: number;
49
+ readonly txConst: number;
50
+ /** sy = worldY * tySlope + tyConst */
51
+ readonly tySlope: number;
52
+ readonly tyConst: number;
53
+ }
54
+ export interface DataViewport<X = unknown, Y = unknown> {
55
+ readonly mode: "data";
56
+ /** Frame relative to `parent` (or absolute if no parent). */
57
+ readonly frame: Frame;
58
+ /** Frame in root / canvas space — walks the parent chain on each read. */
59
+ readonly absoluteFrame: Frame;
60
+ /** Parent viewport, if this is a nested viewport. */
61
+ readonly parent: ViewportLike | null;
62
+ /** Mutable rect equal to `absoluteFrame`, refreshed in place on each access. */
63
+ readonly clipRect: FrameRect;
64
+ /** Identity camera (data-mode pan/zoom flows through axis scales). */
65
+ readonly camera: ResolvedCameraState;
66
+ readonly x: AxisScale<X>;
67
+ readonly y: AxisScale<Y>;
68
+ readonly visibleXDomain: NumericDomain | DateDomain | readonly X[];
69
+ readonly visibleYDomain: NumericDomain | DateDomain | readonly Y[];
70
+ /** Pan by pixel deltas in this viewport's frame space. */
71
+ panBy(dxPx: number, dyPx: number): void;
72
+ /** Zoom around a screen-pixel anchor in the viewport's absolute frame space. */
73
+ zoomAt(anchorSx: number, anchorSy: number, factor: ZoomFactor): void;
74
+ /** Reset to initial domain. */
75
+ reset(): void;
76
+ /**
77
+ * Replace the frame (in the same space as the original frame).
78
+ *
79
+ * `reserve` (CSS-px insets) shrinks the axis pixel range without changing
80
+ * the visible frame extent or `clipRect`. This is how the chart pipeline
81
+ * tells the viewport about position-scale range reservations
82
+ * (`framePadding` + per-geom `prepareRange`) so `dataToScreen` matches
83
+ * where marks actually render. Defaults to zero insets — callers that
84
+ * don't reserve any space (test viewports, manual mounts) can omit it.
85
+ */
86
+ setFrame(frame: Frame, reserve?: AxisRangeReserve): void;
87
+ /** Convert a screen pixel to data coords. Returns null if outside the absolute frame. */
88
+ screenToData(sx: number, sy: number): {
89
+ x: X;
90
+ y: Y;
91
+ } | null;
92
+ /** Convert data coords to screen pixels (absolute). */
93
+ dataToScreen(dx: X, dy: Y): Vec2;
94
+ /**
95
+ * If both axes are linear continuous (not log, not time, not band), return a
96
+ * pre-computed slope+intercept projector for use in hot loops. Returns null
97
+ * for mixed or non-linear axis configurations. Recompute once per project
98
+ * call; do not call per-entry.
99
+ */
100
+ getLinearProjector(): LinearProjector | null;
101
+ /** Set the visible domain absolutely on one or both continuous axes. */
102
+ setVisibleDomain(domains: {
103
+ x?: VisibleDomainInput;
104
+ y?: VisibleDomainInput;
105
+ }): void;
106
+ /** Replace the pan-bound config (or clear it with `null`). */
107
+ setPanBounds(options: DataPanBoundsOptions | null): void;
108
+ /** Subscribe to any view-state change. */
109
+ onChange(cb: () => void): () => void;
110
+ /** @internal */
111
+ _state: DataInternal;
112
+ }
113
+ /** Anything that can be linked together — `linkViewports` ignores camera viewports. */
114
+ export type Viewport<X = unknown, Y = unknown> = DataViewport<X, Y> | CameraViewport;
115
+ interface MutableFrameRect {
116
+ x: number;
117
+ y: number;
118
+ width: number;
119
+ height: number;
120
+ }
121
+ interface ResolvedAxisMargin {
122
+ x: number;
123
+ y: number;
124
+ }
125
+ interface ResolvedMargins {
126
+ overshoot: ResolvedAxisMargin;
127
+ drift: ResolvedAxisMargin;
128
+ }
129
+ export interface DataInternal {
130
+ mode: "data";
131
+ frame: Frame;
132
+ parent: ViewportLike | null;
133
+ listeners: Set<() => void>;
134
+ notifying: boolean;
135
+ clipRect: MutableFrameRect;
136
+ xAxis: AxisState<unknown>;
137
+ yAxis: AxisState<unknown>;
138
+ minZoom: number;
139
+ maxZoom: number;
140
+ panBoundsMargins: ResolvedMargins | null;
141
+ rangeReserve: AxisRangeReserve;
142
+ }
143
+ /**
144
+ * CSS-px insets that shrink the axis pixel range without changing the frame
145
+ * extent. Mirrors the chart pipeline's `framePadding` + per-geom
146
+ * `prepareRange` reservations so `dataToScreen` lines up with where marks
147
+ * actually render.
148
+ */
149
+ export interface AxisRangeReserve {
150
+ readonly left: number;
151
+ readonly right: number;
152
+ readonly top: number;
153
+ readonly bottom: number;
154
+ }
155
+ export declare function createDataViewport<X = unknown, Y = unknown>(options: DataViewportOptions<X, Y>): DataViewport<X, Y>;
156
+ export interface LinkViewportsOptions {
157
+ /** Sync X axis state. Default: true. */
158
+ x?: boolean;
159
+ /** Sync Y axis state. Default: false. */
160
+ y?: boolean;
161
+ }
162
+ /**
163
+ * Keep multiple data viewports in sync on selected axes. Camera viewports in
164
+ * the array are skipped (link only makes sense between like-typed data axes).
165
+ *
166
+ * Any pan/zoom/reset on one data viewport propagates the current transformed-
167
+ * domain endpoints to the others on the linked axes. Requires linked axes to
168
+ * have matching `type` (e.g. both `linear`, or both `time`).
169
+ */
170
+ export declare function linkViewports(viewports: readonly Viewport<any, any>[], options?: LinkViewportsOptions): () => void;