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,541 @@
1
+ import { describe, expect, test } from "vite-plus/test";
2
+
3
+ import { areaMark, barMark, lineMark, pointMark, stack } from "./marks.ts";
4
+ import { createGroup, createLayer, rgba } from "insomni";
5
+ import { bandScale, linearScale } from "./scales.ts";
6
+
7
+ interface Point {
8
+ x: number;
9
+ y: number;
10
+ value?: number;
11
+ category?: string;
12
+ }
13
+
14
+ describe("pointMark", () => {
15
+ test("emits one circle per datum at accessor-resolved coords", () => {
16
+ const layer = createLayer({ space: "ui" });
17
+ const data: Point[] = [
18
+ { x: 0, y: 0 },
19
+ { x: 1, y: 2 },
20
+ { x: 2, y: 4 },
21
+ ];
22
+
23
+ pointMark(data, {
24
+ x: (d) => d.x * 10,
25
+ y: (d) => d.y * 10,
26
+ }).addTo(layer);
27
+
28
+ // pushCircle → 1 shape per datum
29
+ expect(layer.shapeCount).toBe(3);
30
+ });
31
+
32
+ test("applies origin offset to accessor output", () => {
33
+ const layer = createLayer({ space: "ui" });
34
+ pointMark([{ x: 5, y: 5 }], {
35
+ x: (d) => d.x,
36
+ y: (d) => d.y,
37
+ radius: 3,
38
+ }).addTo(layer, { x: 100, y: 50 });
39
+
40
+ // circle at 105,55 — inspect pack to verify
41
+ // v3 packs into one UberPack ArrayBuffer (16-float stride); geom0 (the
42
+ // shape center for rect/circle) sits at float offset 0/1 of instance 0 —
43
+ // the same slot v1's `packedShapeFloats[0..1]` exposed.
44
+ const packed = new Float32Array(layer.pack.buffer);
45
+ expect(packed[0]).toBe(105);
46
+ expect(packed[1]).toBe(55);
47
+ });
48
+
49
+ test("resolves value-or-accessor for radius, fill, shape", () => {
50
+ const layer = createLayer({ space: "ui" });
51
+ const data: Point[] = [
52
+ { x: 0, y: 0, value: 1 },
53
+ { x: 1, y: 1, value: 2 },
54
+ ];
55
+ const rScale = linearScale([1, 2], [2, 10]);
56
+
57
+ pointMark(data, {
58
+ x: (d) => d.x,
59
+ y: (d) => d.y,
60
+ radius: (d) => rScale(d.value!),
61
+ fill: (_, i) => (i === 0 ? rgba(1, 0, 0, 1) : rgba(0, 1, 0, 1)),
62
+ shape: "square",
63
+ }).addTo(layer);
64
+
65
+ // squares use pushRect → still 1 shape per datum
66
+ expect(layer.shapeCount).toBe(2);
67
+ });
68
+
69
+ test("triangle and diamond shapes emit triangulated polygons", () => {
70
+ const layer = createLayer({ space: "ui" });
71
+ pointMark([{ x: 0, y: 0 }], {
72
+ x: (d) => d.x,
73
+ y: (d) => d.y,
74
+ shape: "triangle",
75
+ fill: rgba(1, 0, 0, 1),
76
+ }).addTo(layer);
77
+
78
+ // equilateral triangle → 1 triangle from earcut
79
+ expect(layer.triangleCount).toBe(1);
80
+ expect(layer.shapeCount).toBe(0);
81
+
82
+ pointMark([{ x: 10, y: 10 }], {
83
+ x: (d) => d.x,
84
+ y: (d) => d.y,
85
+ shape: "diamond",
86
+ fill: rgba(0, 1, 0, 1),
87
+ }).addTo(layer);
88
+
89
+ // diamond (4 points) → 2 triangles
90
+ expect(layer.triangleCount).toBe(3);
91
+ });
92
+
93
+ test("threads group reference through to the layer", () => {
94
+ const layer = createLayer({ space: "ui" });
95
+ const group = createGroup();
96
+ pointMark([{ x: 0, y: 0 }], {
97
+ x: (d) => d.x,
98
+ y: (d) => d.y,
99
+ group,
100
+ }).addTo(layer);
101
+
102
+ // v3 carries the group on the draw command (CPU-side metadata), not in a
103
+ // `Layer._groups` parallel array (v1).
104
+ expect(layer.pack.commands[0]?.group).toBe(group);
105
+ });
106
+
107
+ test("borderStyle 'open' suppresses fill and routes color to stroke", () => {
108
+ // 'open' = fill-alpha 0 + stroke. Geometry-wise, still one circle shape;
109
+ // the open behavior is observable on the encoded fill alpha (no fill ⇒
110
+ // pack stores fill alpha 0) while the stroke color is the fill input.
111
+ const solid = createLayer({ space: "ui" });
112
+ const open = createLayer({ space: "ui" });
113
+ const data: Point[] = [{ x: 0, y: 0 }];
114
+
115
+ pointMark(data, {
116
+ x: (d) => d.x,
117
+ y: (d) => d.y,
118
+ radius: 5,
119
+ fill: rgba(0.5, 0.5, 0.5, 1),
120
+ }).addTo(solid);
121
+ pointMark(data, {
122
+ x: (d) => d.x,
123
+ y: (d) => d.y,
124
+ radius: 5,
125
+ fill: rgba(0.5, 0.5, 0.5, 1),
126
+ strokeWidth: 1,
127
+ borderStyle: "open",
128
+ }).addTo(open);
129
+
130
+ expect(solid.shapeCount).toBe(1);
131
+ expect(open.shapeCount).toBe(1);
132
+ // 'open' shouldn't add a polyline ring (that's dashed/dotted only).
133
+ expect(open.triangleCount).toBe(0);
134
+ });
135
+
136
+ test("borderStyle 'dashed' adds a polyline ring on top of the SDF shape", () => {
137
+ const layer = createLayer({ space: "ui" });
138
+ pointMark([{ x: 0, y: 0 }], {
139
+ x: (d) => d.x,
140
+ y: (d) => d.y,
141
+ radius: 5,
142
+ fill: rgba(0.5, 0.5, 0.5, 1),
143
+ stroke: rgba(0, 0, 0, 1),
144
+ strokeWidth: 1,
145
+ borderStyle: "dashed",
146
+ }).addTo(layer);
147
+
148
+ // Base circle still goes through the SDF path.
149
+ expect(layer.shapeCount).toBe(1);
150
+ // Dashed border is a tessellated polyline → emits triangles.
151
+ expect(layer.triangleCount).toBeGreaterThan(0);
152
+ });
153
+
154
+ test("borderStyle 'dotted' emits more dash segments than 'dashed'", () => {
155
+ const dashed = createLayer({ space: "ui" });
156
+ const dotted = createLayer({ space: "ui" });
157
+ const opts = {
158
+ x: (d: Point) => d.x,
159
+ y: (d: Point) => d.y,
160
+ radius: 8,
161
+ fill: rgba(0.5, 0.5, 0.5, 1),
162
+ stroke: rgba(0, 0, 0, 1),
163
+ strokeWidth: 1,
164
+ };
165
+ pointMark([{ x: 0, y: 0 }], { ...opts, borderStyle: "dashed" }).addTo(dashed);
166
+ pointMark([{ x: 0, y: 0 }], { ...opts, borderStyle: "dotted" }).addTo(dotted);
167
+ // Dotted pattern [1, 3] produces ~2x as many on/off transitions vs [4, 4].
168
+ expect(dotted.triangleCount).toBeGreaterThan(dashed.triangleCount);
169
+ });
170
+
171
+ test("overlayGlyph emits a second shape at the same anchor with scaled radius", () => {
172
+ const layer = createLayer({ space: "ui" });
173
+ pointMark([{ x: 10, y: 10 }], {
174
+ x: (d) => d.x,
175
+ y: (d) => d.y,
176
+ radius: 5,
177
+ fill: rgba(0.5, 0.5, 0.5, 1),
178
+ overlayGlyph: "circle",
179
+ overlayScale: 0.5,
180
+ }).addTo(layer);
181
+
182
+ // Base circle + overlay circle → 2 SDF shapes.
183
+ expect(layer.shapeCount).toBe(2);
184
+ // v3 packs into one UberPack ArrayBuffer (16-float stride); geom0 (the
185
+ // shape center for rect/circle) sits at float offset 0/1 of instance 0 —
186
+ // the same slot v1's `packedShapeFloats[0..1]` exposed.
187
+ const packed = new Float32Array(layer.pack.buffer);
188
+ // Both anchored at (10, 10).
189
+ expect(packed[0]).toBe(10);
190
+ expect(packed[1]).toBe(10);
191
+ });
192
+
193
+ test("overlayGlyph null per-datum skips overlay", () => {
194
+ const layer = createLayer({ space: "ui" });
195
+ pointMark(
196
+ [
197
+ { x: 0, y: 0 },
198
+ { x: 1, y: 1 },
199
+ ],
200
+ {
201
+ x: (d) => d.x,
202
+ y: (d) => d.y,
203
+ radius: 5,
204
+ fill: rgba(0.5, 0.5, 0.5, 1),
205
+ overlayGlyph: (_, i) => (i === 0 ? "circle" : null),
206
+ },
207
+ ).addTo(layer);
208
+
209
+ // Datum 0: base + overlay = 2; datum 1: base only = 1. Total = 3.
210
+ expect(layer.shapeCount).toBe(3);
211
+ });
212
+
213
+ test("status → {active: solid, inactive: open+cross} via accessor mapping", () => {
214
+ // Integration test for the documented categorical-status pattern: combine
215
+ // a `borderStyle` accessor with an `overlayGlyph` accessor so the rejected
216
+ // markers visibly differ from accepted markers.
217
+ const layer = createLayer({ space: "ui" });
218
+ interface Reading {
219
+ x: number;
220
+ y: number;
221
+ status: "active" | "inactive";
222
+ }
223
+ const data: Reading[] = [
224
+ { x: 0, y: 0, status: "active" },
225
+ { x: 1, y: 1, status: "inactive" },
226
+ { x: 2, y: 2, status: "active" },
227
+ ];
228
+ pointMark(data, {
229
+ x: (d) => d.x,
230
+ y: (d) => d.y,
231
+ radius: 4,
232
+ fill: rgba(0.2, 0.6, 0.8, 1),
233
+ // Open border requires a non-zero strokeWidth — without one the SDF
234
+ // skips the shape (fill suppressed, stroke width 0).
235
+ strokeWidth: 1,
236
+ borderStyle: (d) => (d.status === "active" ? "solid" : "open"),
237
+ overlayGlyph: (d) => (d.status === "active" ? null : "cross"),
238
+ }).addTo(layer);
239
+
240
+ // 3 base circles + 1 overlay (cross is a polygon, triangulated)
241
+ expect(layer.shapeCount).toBe(3);
242
+ // Cross overlay produces triangulated polygon → triangles > 0.
243
+ expect(layer.triangleCount).toBeGreaterThan(0);
244
+ });
245
+
246
+ test("skips datums with non-finite x or y", () => {
247
+ // Geoms signal "skip this row" by returning NaN from x/y accessors
248
+ // (e.g. legend hide filters in point.ts). Without a guard, NaN
249
+ // positions corrupt the GPU batch and erase all points in the layer.
250
+ const layer = createLayer({ space: "ui" });
251
+ const data: Point[] = [
252
+ { x: 0, y: 0 },
253
+ { x: 1, y: 1 },
254
+ { x: 2, y: 2 },
255
+ { x: 3, y: 3 },
256
+ ];
257
+
258
+ pointMark(data, {
259
+ x: (d, i) => (i === 1 ? NaN : d.x),
260
+ y: (d, i) => (i === 2 ? NaN : d.y),
261
+ }).addTo(layer);
262
+
263
+ // 4 datums in, 2 skipped (NaN x at i=1, NaN y at i=2) → 2 circles
264
+ expect(layer.shapeCount).toBe(2);
265
+ });
266
+ });
267
+
268
+ describe("lineMark", () => {
269
+ test("emits one polyline covering all data points", () => {
270
+ const layer = createLayer({ space: "ui" });
271
+ lineMark(
272
+ [
273
+ { x: 0, y: 0 },
274
+ { x: 1, y: 1 },
275
+ { x: 2, y: 4 },
276
+ ],
277
+ { x: (d) => d.x, y: (d) => d.y, strokeWidth: 2 },
278
+ ).addTo(layer);
279
+
280
+ // polyline produces tessellated triangles; at minimum >0 triangles
281
+ expect(layer.triangleCount).toBeGreaterThan(0);
282
+ });
283
+
284
+ test("splits into segments on defined=false", () => {
285
+ const layerWhole = createLayer({ space: "ui" });
286
+ const layerSplit = createLayer({ space: "ui" });
287
+ const data = [
288
+ { x: 0, y: 0 },
289
+ { x: 1, y: 1 },
290
+ { x: 2, y: 4 },
291
+ { x: 3, y: 9 },
292
+ ];
293
+
294
+ lineMark(data, { x: (d) => d.x, y: (d) => d.y }).addTo(layerWhole);
295
+ lineMark(data, {
296
+ x: (d) => d.x,
297
+ y: (d) => d.y,
298
+ defined: (_, i) => i !== 2,
299
+ }).addTo(layerSplit);
300
+
301
+ // split version drops point 2 → left with 2-pt and 1-pt segments.
302
+ // 1-pt segment skipped, so fewer triangles than the whole polyline.
303
+ expect(layerSplit.triangleCount).toBeLessThan(layerWhole.triangleCount);
304
+ });
305
+
306
+ test("skips entirely when fewer than 2 points are defined", () => {
307
+ const layer = createLayer({ space: "ui" });
308
+ lineMark([{ x: 0, y: 0 }], { x: (d) => d.x, y: (d) => d.y }).addTo(layer);
309
+ expect(layer.triangleCount).toBe(0);
310
+ });
311
+
312
+ test("dashStyle maps to a dashed polyline (more triangles than solid)", () => {
313
+ const solid = createLayer({ space: "ui" });
314
+ const dashed = createLayer({ space: "ui" });
315
+ const data = [
316
+ { x: 0, y: 0 },
317
+ { x: 50, y: 50 },
318
+ { x: 100, y: 0 },
319
+ ];
320
+ lineMark(data, { x: (d) => d.x, y: (d) => d.y, strokeWidth: 2 }).addTo(solid);
321
+ lineMark(data, {
322
+ x: (d) => d.x,
323
+ y: (d) => d.y,
324
+ strokeWidth: 2,
325
+ dashStyle: "dashed",
326
+ }).addTo(dashed);
327
+
328
+ expect(dashed.triangleCount).toBeGreaterThan(solid.triangleCount);
329
+ });
330
+
331
+ test("explicit dashPattern wins over dashStyle", () => {
332
+ // When both are set, dashPattern is authoritative — confirmed by output
333
+ // matching a custom-pattern run rather than the dashStyle preset.
334
+ const customPattern = createLayer({ space: "ui" });
335
+ const both = createLayer({ space: "ui" });
336
+ const data = [
337
+ { x: 0, y: 0 },
338
+ { x: 100, y: 0 },
339
+ ];
340
+ lineMark(data, {
341
+ x: (d) => d.x,
342
+ y: (d) => d.y,
343
+ strokeWidth: 1,
344
+ dashPattern: [10, 2],
345
+ }).addTo(customPattern);
346
+ lineMark(data, {
347
+ x: (d) => d.x,
348
+ y: (d) => d.y,
349
+ strokeWidth: 1,
350
+ dashPattern: [10, 2],
351
+ dashStyle: "dotted",
352
+ }).addTo(both);
353
+
354
+ expect(both.triangleCount).toBe(customPattern.triangleCount);
355
+ });
356
+ });
357
+
358
+ describe("areaMark", () => {
359
+ test("builds a single polygon tracing top then bottom", () => {
360
+ const layer = createLayer({ space: "ui" });
361
+ const xScale = linearScale([0, 2], [0, 100]);
362
+ const yScale = linearScale([0, 4], [100, 0]);
363
+
364
+ areaMark(
365
+ [
366
+ { x: 0, y: 1 },
367
+ { x: 1, y: 3 },
368
+ { x: 2, y: 2 },
369
+ ],
370
+ {
371
+ x: (d) => xScale(d.x),
372
+ y0: () => yScale(0),
373
+ y1: (d) => yScale(d.y),
374
+ fill: rgba(0.2, 0.4, 0.8, 0.5),
375
+ },
376
+ ).addTo(layer);
377
+
378
+ // polygon with 6 vertices (3 top + 3 bottom) → 4 triangles via earcut
379
+ expect(layer.triangleCount).toBe(4);
380
+ });
381
+
382
+ test("splits into multiple polygons on defined=false", () => {
383
+ const layer = createLayer({ space: "ui" });
384
+
385
+ areaMark(
386
+ [
387
+ { x: 0, y: 1 },
388
+ { x: 1, y: 2 },
389
+ { x: 2, y: 3 },
390
+ { x: 3, y: 4 },
391
+ ],
392
+ {
393
+ x: (d) => d.x,
394
+ y0: () => 0,
395
+ y1: (d) => d.y,
396
+ defined: (_, i) => i !== 2,
397
+ fill: rgba(0, 0, 0, 1),
398
+ },
399
+ ).addTo(layer);
400
+
401
+ // One segment of 2 points (quad → 2 triangles) plus a trailing 1-point segment
402
+ // (skipped due to top.length < 2). Total 2 triangles.
403
+ expect(layer.triangleCount).toBe(2);
404
+ });
405
+ });
406
+
407
+ describe("barMark", () => {
408
+ test("emits one rect per datum via bandScale + linear y", () => {
409
+ const layer = createLayer({ space: "ui" });
410
+ const data = [
411
+ { category: "A", value: 10 },
412
+ { category: "B", value: 20 },
413
+ { category: "C", value: 15 },
414
+ ];
415
+ const x = bandScale(
416
+ data.map((d) => d.category),
417
+ [0, 300],
418
+ { padding: 0.1 },
419
+ );
420
+ const y = linearScale([0, 20], [200, 0]);
421
+ const baseline = 200;
422
+
423
+ barMark(data, {
424
+ x: (d) => x(d.category),
425
+ y: (d) => y(d.value),
426
+ width: x.bandwidth(),
427
+ height: (d) => baseline - y(d.value),
428
+ fill: rgba(0.1, 0.4, 0.9, 1),
429
+ }).addTo(layer);
430
+
431
+ expect(layer.shapeCount).toBe(3);
432
+ });
433
+
434
+ test("supports per-datum fill accessor", () => {
435
+ const layer = createLayer({ space: "ui" });
436
+ barMark([{ value: 1 }, { value: 2 }], {
437
+ x: (_, i) => i * 20,
438
+ y: () => 0,
439
+ width: 10,
440
+ height: (d) => d.value * 10,
441
+ fill: (d) => (d.value > 1 ? rgba(1, 0, 0, 1) : rgba(0, 1, 0, 1)),
442
+ }).addTo(layer);
443
+
444
+ expect(layer.shapeCount).toBe(2);
445
+ });
446
+
447
+ test("applies origin offset", () => {
448
+ const layer = createLayer({ space: "ui" });
449
+ barMark([{ value: 5 }], {
450
+ x: () => 0,
451
+ y: () => 0,
452
+ width: 10,
453
+ height: 20,
454
+ }).addTo(layer, { x: 50, y: 100 });
455
+
456
+ // packed rect stores center: x+w/2 = 55, y+h/2 = 110
457
+ // v3 packs into one UberPack ArrayBuffer (16-float stride); geom0 (the
458
+ // shape center for rect/circle) sits at float offset 0/1 of instance 0 —
459
+ // the same slot v1's `packedShapeFloats[0..1]` exposed.
460
+ const packed = new Float32Array(layer.pack.buffer);
461
+ expect(packed[0]).toBe(55);
462
+ expect(packed[1]).toBe(110);
463
+ });
464
+
465
+ test("borderStyle 'open' keeps one shape per bar with no extra geometry", () => {
466
+ const layer = createLayer({ space: "ui" });
467
+ barMark([{ value: 5 }, { value: 10 }], {
468
+ x: (_, i) => i * 20,
469
+ y: () => 0,
470
+ width: 10,
471
+ height: (d) => d.value * 4,
472
+ fill: rgba(0.2, 0.5, 0.8, 1),
473
+ strokeWidth: 1,
474
+ borderStyle: "open",
475
+ }).addTo(layer);
476
+
477
+ expect(layer.shapeCount).toBe(2);
478
+ // Open border on bars is fill-suppression only; no polyline ring.
479
+ expect(layer.triangleCount).toBe(0);
480
+ });
481
+
482
+ test("borderStyle 'dashed' adds a polyline ring around each bar", () => {
483
+ const layer = createLayer({ space: "ui" });
484
+ barMark([{ value: 5 }], {
485
+ x: () => 0,
486
+ y: () => 0,
487
+ width: 20,
488
+ height: 40,
489
+ fill: rgba(0.2, 0.5, 0.8, 1),
490
+ stroke: rgba(0, 0, 0, 1),
491
+ strokeWidth: 1,
492
+ borderStyle: "dashed",
493
+ }).addTo(layer);
494
+
495
+ // SDF rect (1) + tessellated polyline ring (>0 triangles).
496
+ expect(layer.shapeCount).toBe(1);
497
+ expect(layer.triangleCount).toBeGreaterThan(0);
498
+ });
499
+ });
500
+
501
+ describe("stack()", () => {
502
+ test("emits one segment per (datum, key) with cumulative base/top", () => {
503
+ const data = [
504
+ { category: "A", west: 10, east: 20, central: 5 },
505
+ { category: "B", west: 4, east: 8, central: 6 },
506
+ ];
507
+ const segments = stack(data, ["west", "east", "central"]);
508
+
509
+ expect(segments).toHaveLength(6);
510
+
511
+ // First datum, first key
512
+ expect(segments[0]).toMatchObject({
513
+ datum: data[0],
514
+ key: "west",
515
+ value: 10,
516
+ base: 0,
517
+ top: 10,
518
+ subIndex: 0,
519
+ datumIndex: 0,
520
+ });
521
+ // First datum, second key — base resumes from prior top
522
+ expect(segments[1]).toMatchObject({ key: "east", base: 10, top: 30 });
523
+ // First datum, third key
524
+ expect(segments[2]).toMatchObject({ key: "central", base: 30, top: 35 });
525
+ // Second datum resets base
526
+ expect(segments[3]).toMatchObject({
527
+ datum: data[1],
528
+ key: "west",
529
+ base: 0,
530
+ datumIndex: 1,
531
+ });
532
+ });
533
+
534
+ test("supports a custom value accessor for non-flat data shapes", () => {
535
+ const data = [{ parts: new Map([["a", 5]]) }, { parts: new Map([["a", 9]]) }];
536
+ const segments = stack(data, ["a"], {
537
+ value: (d, key) => d.parts.get(key) ?? 0,
538
+ });
539
+ expect(segments.map((s) => s.value)).toEqual([5, 9]);
540
+ });
541
+ });