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,332 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, expect, it } from "vite-plus/test";
3
+
4
+ import { resolveAes } from "./aes.ts";
5
+ import type { PointSwatchSpec } from "../legend.ts";
6
+ import type { LegendMergeChannel } from "./chart.ts";
7
+ import { autoMergeChannels, composeMergedPointSwatch } from "./pipeline.ts";
8
+ import {
9
+ buildBorderStyleScale,
10
+ buildCategoricalColorScale,
11
+ buildOverlayGlyphScale,
12
+ buildShapeScale,
13
+ buildSizeScale,
14
+ DEFAULT_BORDER_STYLE_PALETTE,
15
+ DEFAULT_OVERLAY_GLYPH_PALETTE,
16
+ type ScaleBundle,
17
+ } from "./scales.ts";
18
+ import { themeDefault } from "./theme.ts";
19
+
20
+ interface Row {
21
+ category: string;
22
+ size: number;
23
+ }
24
+
25
+ const data: Row[] = [
26
+ { category: "a", size: 0 },
27
+ { category: "b", size: 5 },
28
+ { category: "c", size: 10 },
29
+ ];
30
+
31
+ function makeScales(): ScaleBundle {
32
+ const catAes = resolveAes<Row, unknown>("category");
33
+ const sizeAes = resolveAes<Row, number>("size");
34
+ return {
35
+ // x/y are unused by composeMergedPointSwatch — supply trivial position scales.
36
+ x: {
37
+ kind: "position",
38
+ type: "linear",
39
+ dataType: "number",
40
+ axisScale: {} as never,
41
+ fn: () => 0,
42
+ },
43
+ y: {
44
+ kind: "position",
45
+ type: "linear",
46
+ dataType: "number",
47
+ axisScale: {} as never,
48
+ fn: () => 0,
49
+ },
50
+ color: buildCategoricalColorScale(["a", "b", "c"], themeDefault),
51
+ shape: buildShapeScale(catAes, data),
52
+ borderStyle: buildBorderStyleScale(catAes, data),
53
+ overlayGlyph: buildOverlayGlyphScale(catAes, data),
54
+ size: buildSizeScale(sizeAes, data),
55
+ };
56
+ }
57
+
58
+ function mergeSet(...chs: LegendMergeChannel[]): ReadonlySet<LegendMergeChannel> {
59
+ return new Set(chs);
60
+ }
61
+
62
+ const RED = { r: 1, g: 0, b: 0, a: 1 };
63
+
64
+ describe("composeMergedPointSwatch", () => {
65
+ it("returns a point swatch with the given color and default radius when merge is empty", () => {
66
+ const s = makeScales();
67
+ const sw = composeMergedPointSwatch("a", RED, s, mergeSet(), themeDefault);
68
+ expect(sw.kind).toBe("point");
69
+ expect(sw.fill).toEqual(RED);
70
+ expect(sw.shape).toBeUndefined();
71
+ expect(sw.borderStyle).toBeUndefined();
72
+ expect(sw.overlayGlyph).toBeUndefined();
73
+ });
74
+
75
+ it("merges shape from the shape scale when 'shape' is listed", () => {
76
+ const s = makeScales();
77
+ const swA = composeMergedPointSwatch("a", RED, s, mergeSet("shape"), themeDefault);
78
+ const swB = composeMergedPointSwatch("b", RED, s, mergeSet("shape"), themeDefault);
79
+ expect(swA.shape).toBe(s.shape!.fn("a"));
80
+ expect(swB.shape).toBe(s.shape!.fn("b"));
81
+ expect(swA.shape).not.toBe(swB.shape);
82
+ });
83
+
84
+ it("merges borderStyle and sets stroke when the border is non-solid", () => {
85
+ const s = makeScales();
86
+ // Default palette index 1 is 'open' → stroke set, fill kept (legend draw layer
87
+ // treats 'open' by suppressing fill itself).
88
+ const sw = composeMergedPointSwatch("b", RED, s, mergeSet("borderStyle"), themeDefault);
89
+ expect(sw.borderStyle).toBe(DEFAULT_BORDER_STYLE_PALETTE[1]); // "open"
90
+ expect(sw.stroke).toEqual(RED);
91
+ });
92
+
93
+ it("does not set stroke when the merged borderStyle is 'solid'", () => {
94
+ const s = makeScales();
95
+ const sw = composeMergedPointSwatch("a", RED, s, mergeSet("borderStyle"), themeDefault);
96
+ expect(sw.borderStyle).toBe("solid");
97
+ expect(sw.stroke).toBeUndefined();
98
+ });
99
+
100
+ it("merges overlayGlyph from the overlay scale, skipping null slots", () => {
101
+ const s = makeScales();
102
+ const swA = composeMergedPointSwatch("a", RED, s, mergeSet("overlayGlyph"), themeDefault);
103
+ const swB = composeMergedPointSwatch("b", RED, s, mergeSet("overlayGlyph"), themeDefault);
104
+ // Default palette index 0 is null (most-common category has no overlay).
105
+ expect(DEFAULT_OVERLAY_GLYPH_PALETTE[0]).toBeNull();
106
+ expect(swA.overlayGlyph).toBeUndefined();
107
+ expect(swB.overlayGlyph).toBe(DEFAULT_OVERLAY_GLYPH_PALETTE[1]); // "plus"
108
+ });
109
+
110
+ it("merges size by sampling the scale at the numeric domain midpoint", () => {
111
+ const s = makeScales();
112
+ const sw = composeMergedPointSwatch("a", RED, s, mergeSet("size"), themeDefault);
113
+ const [d0, d1] = s.size!.domain;
114
+ const expected = s.size!.fn((d0 + d1) / 2) as number;
115
+ // Clamped to the legend swatch's [3, 9] range.
116
+ const clamped = Math.min(Math.max(expected, 3), 9);
117
+ expect(sw.radius).toBeCloseTo(clamped, 5);
118
+ });
119
+
120
+ it("composes all four categorical channels at once", () => {
121
+ const s = makeScales();
122
+ const sw: PointSwatchSpec = composeMergedPointSwatch(
123
+ "c",
124
+ RED,
125
+ s,
126
+ mergeSet("shape", "borderStyle", "overlayGlyph", "size"),
127
+ themeDefault,
128
+ );
129
+ expect(sw.shape).toBe(s.shape!.fn("c"));
130
+ expect(sw.borderStyle).toBe(s.borderStyle!.fn("c"));
131
+ expect(sw.overlayGlyph).toBe(s.overlayGlyph!.fn("c"));
132
+ expect(typeof sw.radius).toBe("number");
133
+ });
134
+
135
+ it("silently skips listed channels whose scale is absent on the bundle", () => {
136
+ const partial: ScaleBundle = {
137
+ x: {
138
+ kind: "position",
139
+ type: "linear",
140
+ dataType: "number",
141
+ axisScale: {} as never,
142
+ fn: () => 0,
143
+ },
144
+ y: {
145
+ kind: "position",
146
+ type: "linear",
147
+ dataType: "number",
148
+ axisScale: {} as never,
149
+ fn: () => 0,
150
+ },
151
+ color: buildCategoricalColorScale(["a"], themeDefault),
152
+ // shape/borderStyle/overlay/size all missing
153
+ };
154
+ const sw = composeMergedPointSwatch(
155
+ "a",
156
+ RED,
157
+ partial,
158
+ mergeSet("shape", "borderStyle", "overlayGlyph", "size"),
159
+ themeDefault,
160
+ );
161
+ expect(sw.shape).toBeUndefined();
162
+ expect(sw.borderStyle).toBeUndefined();
163
+ expect(sw.overlayGlyph).toBeUndefined();
164
+ // Default radius preserved.
165
+ expect(sw.radius).toBe(5);
166
+ });
167
+ });
168
+
169
+ describe("autoMergeChannels", () => {
170
+ it("returns shape/borderStyle/overlayGlyph when their domains match color's", () => {
171
+ const s = makeScales();
172
+ const out = autoMergeChannels(s.color!.domain, s);
173
+ expect(out).toEqual(["shape", "borderStyle", "overlayGlyph"]);
174
+ });
175
+
176
+ it("omits channels whose domain differs from color's", () => {
177
+ const s = makeScales();
178
+ // Swap shape scale for one with a different domain.
179
+ const shape = buildShapeScale(resolveAes<Row, unknown>("category"), data, {
180
+ domain: ["x", "y", "z"],
181
+ });
182
+ const out = autoMergeChannels(s.color!.domain, { ...s, shape });
183
+ expect(out).toEqual(["borderStyle", "overlayGlyph"]);
184
+ });
185
+
186
+ it("omits channels whose scale is absent", () => {
187
+ const s = makeScales();
188
+ const out = autoMergeChannels(s.color!.domain, { ...s, shape: undefined });
189
+ expect(out).toEqual(["borderStyle", "overlayGlyph"]);
190
+ });
191
+
192
+ it("returns empty when no non-color categorical channels are active", () => {
193
+ const s = makeScales();
194
+ const out = autoMergeChannels(s.color!.domain, {
195
+ ...s,
196
+ shape: undefined,
197
+ borderStyle: undefined,
198
+ overlayGlyph: undefined,
199
+ });
200
+ expect(out).toEqual([]);
201
+ });
202
+
203
+ it("skips empty-domain scales — no phantom merge when both color and channel are empty", () => {
204
+ const s = makeScales();
205
+ // Color scale with empty domain; shape scale also empty. domainsEqual([],[])
206
+ // would be true, but a merge with no entries is meaningless — guard returns [].
207
+ const emptyColor = buildCategoricalColorScale([], themeDefault);
208
+ const emptyShape = buildShapeScale(resolveAes<Row, unknown>("category"), []);
209
+ const out = autoMergeChannels(emptyColor.domain, {
210
+ ...s,
211
+ color: emptyColor,
212
+ shape: emptyShape,
213
+ borderStyle: undefined,
214
+ overlayGlyph: undefined,
215
+ });
216
+ expect(out).toEqual([]);
217
+ });
218
+
219
+ it("treats two Date arrays with equal instants as equal domains", () => {
220
+ const aes = resolveAes<{ d: Date }, unknown>("d");
221
+ const rows = [{ d: new Date(2026, 0, 1) }, { d: new Date(2026, 0, 2) }];
222
+ const colorDomain = [new Date(2026, 0, 1), new Date(2026, 0, 2)];
223
+ const shape = buildShapeScale(aes, rows);
224
+ const out = autoMergeChannels(colorDomain, {
225
+ color: buildCategoricalColorScale(colorDomain.map(String), themeDefault),
226
+ shape,
227
+ });
228
+ expect(out).toContain("shape");
229
+ });
230
+ });
231
+
232
+ describe("auto-merge end to end via plot.toSVG", () => {
233
+ it("merges color+shape automatically when both encode the same field", async () => {
234
+ const { plot } = await import("./chart.ts");
235
+ const { point } = await import("./geoms/index.ts");
236
+ // No explicit `.legend({ merge })`. Pipeline should detect the shared
237
+ // domain and emit one merged legend.
238
+ const svg = plot<Row>({ data, width: 400, height: 300 })
239
+ .layer(point<Row>({ x: "size", y: "size", color: "category", shape: "category" }))
240
+ .toSVG();
241
+ expect(svg).toBeInstanceOf(SVGSVGElement);
242
+ });
243
+
244
+ it("explicit merge:[] disables auto-merge", async () => {
245
+ const { plot } = await import("./chart.ts");
246
+ const { point } = await import("./geoms/index.ts");
247
+ // Passing an empty `merge` is the way to opt out of auto-detection.
248
+ const svg = plot<Row>({ data, width: 400, height: 300 })
249
+ .layer(point<Row>({ x: "size", y: "size", color: "category", shape: "category" }))
250
+ .legend({ merge: [] })
251
+ .toSVG();
252
+ expect(svg).toBeInstanceOf(SVGSVGElement);
253
+ });
254
+ });
255
+
256
+ describe("buildBorderStyleScale", () => {
257
+ it("maps a categorical domain through the default palette", () => {
258
+ const scale = buildBorderStyleScale(resolveAes<Row, unknown>("category"), data);
259
+ expect(scale.domain).toEqual(["a", "b", "c"]);
260
+ expect(scale.fn("a")).toBe(DEFAULT_BORDER_STYLE_PALETTE[0]); // "solid"
261
+ expect(scale.fn("b")).toBe(DEFAULT_BORDER_STYLE_PALETTE[1]); // "open"
262
+ expect(scale.fn("c")).toBe(DEFAULT_BORDER_STYLE_PALETTE[2]); // "dashed"
263
+ });
264
+
265
+ it("falls back to the palette's first slot for unknown values", () => {
266
+ const scale = buildBorderStyleScale(resolveAes<Row, unknown>("category"), data);
267
+ expect(scale.fn("nope")).toBe(DEFAULT_BORDER_STYLE_PALETTE[0]);
268
+ });
269
+
270
+ it("respects an explicit palette and domain override", () => {
271
+ const scale = buildBorderStyleScale(resolveAes<Row, unknown>("category"), data, {
272
+ domain: ["b", "a"],
273
+ palette: ["dotted", "dashed"],
274
+ });
275
+ expect(scale.fn("b")).toBe("dotted");
276
+ expect(scale.fn("a")).toBe("dashed");
277
+ });
278
+ });
279
+
280
+ describe("LegendSpec.merge — end to end via chart.toSVG", () => {
281
+ it("renders a merged color+shape legend without throwing", async () => {
282
+ const { plot } = await import("./chart.ts");
283
+ const { point } = await import("./geoms/index.ts");
284
+ const svg = plot<Row>({ data, width: 400, height: 300 })
285
+ .layer(point<Row>({ x: "size", y: "size", color: "category", shape: "category" }))
286
+ .legend({ merge: ["color", "shape"] })
287
+ .toSVG();
288
+ expect(svg).toBeInstanceOf(SVGSVGElement);
289
+ });
290
+
291
+ it("merged borderStyle picks up scale-resolved values for non-literal categorical input", async () => {
292
+ const { plot } = await import("./chart.ts");
293
+ const { point } = await import("./geoms/index.ts");
294
+ const svg = plot<Row>({ data, width: 400, height: 300 })
295
+ .layer(point<Row>({ x: "size", y: "size", color: "category", borderStyle: "category" }))
296
+ .legend({ merge: ["color", "borderStyle"] })
297
+ .toSVG();
298
+ expect(svg).toBeInstanceOf(SVGSVGElement);
299
+ });
300
+
301
+ it("default behavior (no merge) still renders a color-only legend", async () => {
302
+ const { plot } = await import("./chart.ts");
303
+ const { point } = await import("./geoms/index.ts");
304
+ const svg = plot<Row>({ data, width: 400, height: 300 })
305
+ .layer(point<Row>({ x: "size", y: "size", color: "category", shape: "category" }))
306
+ .toSVG();
307
+ expect(svg).toBeInstanceOf(SVGSVGElement);
308
+ });
309
+ });
310
+
311
+ describe("buildOverlayGlyphScale", () => {
312
+ it("maps a categorical domain through the default palette, allowing null entries", () => {
313
+ const scale = buildOverlayGlyphScale(resolveAes<Row, unknown>("category"), data);
314
+ expect(scale.fn("a")).toBeNull(); // first slot is null
315
+ expect(scale.fn("b")).toBe("plus");
316
+ expect(scale.fn("c")).toBe("cross");
317
+ });
318
+
319
+ it("returns null for unknown values", () => {
320
+ const scale = buildOverlayGlyphScale(resolveAes<Row, unknown>("category"), data);
321
+ expect(scale.fn("nope")).toBeNull();
322
+ });
323
+
324
+ it("respects an explicit palette with null suppressions", () => {
325
+ const scale = buildOverlayGlyphScale(resolveAes<Row, unknown>("category"), data, {
326
+ palette: ["star", null, "diamond"],
327
+ });
328
+ expect(scale.fn("a")).toBe("star");
329
+ expect(scale.fn("b")).toBeNull();
330
+ expect(scale.fn("c")).toBe("diamond");
331
+ });
332
+ });
@@ -0,0 +1,78 @@
1
+ import { type Frame } from "insomni";
2
+ import type { MountedPlot, MountPlotOptions } from "./chart.ts";
3
+ import { type ChartConfig } from "./pipeline.ts";
4
+ import { type DataViewport } from "../viewport.ts";
5
+ import type { Coord } from "./coord.ts";
6
+ import type { PanZoomConfig } from "./chart.ts";
7
+ import type { DataPanBoundsOptions } from "../viewport.ts";
8
+ import type { AxisSelection } from "../interactions.ts";
9
+ /**
10
+ * Internal entry. `chart.mount(canvas, opts)` calls this with its frozen
11
+ * config + the user-provided mount options.
12
+ */
13
+ export declare function mountChart<T>(config: ChartConfig<T>, canvas: HTMLCanvasElement, opts?: MountPlotOptions<T>): MountedPlot<T>;
14
+ interface ResolvedPanZoom {
15
+ minZoom: number;
16
+ maxZoom: number;
17
+ panBounds: DataPanBoundsOptions | undefined;
18
+ pan: AxisSelection;
19
+ zoom: AxisSelection;
20
+ /** Half-padding fraction applied around the visible-Y extent, or null when yFit is off. */
21
+ yFitPadding: number | null;
22
+ }
23
+ declare function resolvePanZoom(input: boolean | PanZoomConfig | undefined): ResolvedPanZoom | null;
24
+ /**
25
+ * Walk each layer's x/y accessors, find data points whose X falls inside
26
+ * `visibleX`, and return `[yMin - pad, yMax + pad]`. Returns `null` when no
27
+ * layer / no point falls in range — callers should keep the previous Y
28
+ * window rather than collapsing.
29
+ */
30
+ declare function computeVisibleYExtent<T>(layers: ReadonlyArray<{
31
+ channels: {
32
+ x?: unknown;
33
+ y?: unknown;
34
+ };
35
+ }>, data: readonly T[], visibleX: readonly [number, number], padding: number): readonly [number, number] | null;
36
+ /**
37
+ * Screen → data primitive backing `MountedPlot.pickAt`. Extracted so it can be
38
+ * unit-tested without spinning up a full mount (WebGPU device, layers, etc.).
39
+ *
40
+ * Returns `null` when the point is outside the frame, when `coord.unproject`
41
+ * rejects the point (polar outside the radius band), when scales are not yet
42
+ * available (pre-first-draw), or when either position scale lacks `invert`
43
+ * (band scales — band → continuous picking is a separate problem).
44
+ */
45
+ declare function screenToData(canvasX: number, canvasY: number, ctx: {
46
+ frame: Frame;
47
+ scales: import("./geoms/types.ts").ScaleBundle | null;
48
+ coord: Coord;
49
+ }): {
50
+ plotFrameX: number;
51
+ plotFrameY: number;
52
+ dataX: number;
53
+ dataY: number;
54
+ frame: Frame;
55
+ } | null;
56
+ /** @internal — exposed for unit tests only. */
57
+ export declare const __test__: {
58
+ resolvePanZoom: typeof resolvePanZoom;
59
+ computeVisibleYExtent: typeof computeVisibleYExtent;
60
+ wrapViewportThroughCoord: typeof wrapViewportThroughCoord;
61
+ screenToData: typeof screenToData;
62
+ };
63
+ /**
64
+ * Wrap a `DataViewport` so pan/zoom calls flow through `coord.handlePan` /
65
+ * `coord.handleZoom`. The wrapper is `Object.create(underlying)` — every
66
+ * untouched member (state, visibleXDomain, absoluteFrame, setFrame, …)
67
+ * resolves through the prototype, so the wrapper stays in lockstep with the
68
+ * underlying `DataViewport` API. Only `panBy` and `zoomAt` are overridden.
69
+ *
70
+ * `getCoord` is called at-the-event-time so `update()` swapping the chart
71
+ * spec (and its `coord`) takes effect on the next interaction without a
72
+ * remount. `invalidate` fires after each routed call because polar's
73
+ * `handlePan` may mutate its own `startAngle` without touching the viewport —
74
+ * which means the viewport's own `onChange` won't fire, and the rAF loop
75
+ * needs an explicit kick to re-draw the rotated chart.
76
+ */
77
+ declare function wrapViewportThroughCoord<X, Y>(underlying: DataViewport<X, Y>, getCoord: () => Coord, invalidate: () => void): DataViewport<X, Y>;
78
+ export {};