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,247 @@
1
+ import { createFrame } from "insomni";
2
+ import { describe, expect, test } from "vite-plus/test";
3
+
4
+ import { resolveAes } from "../aes.ts";
5
+ import { buildPositionScale, type ScaleBundle } from "../scales.ts";
6
+ import { themeDefault } from "../theme.ts";
7
+ import { line } from "./line.ts";
8
+ import { emphasisKeyFor, geomEmphasisBase } from "./emphasis.ts";
9
+ import type { CompileContext } from "./types.ts";
10
+
11
+ interface Row {
12
+ x: number;
13
+ y: number;
14
+ series: string;
15
+ }
16
+
17
+ const data: Row[] = [
18
+ { x: 0, y: 0, series: "a" },
19
+ { x: 5, y: 10, series: "b" },
20
+ { x: 10, y: 20, series: "a" },
21
+ ];
22
+
23
+ interface OrderedRow extends Row {
24
+ order: number;
25
+ }
26
+
27
+ function makeCtx<T extends Row>(rows: readonly T[]): CompileContext<T> {
28
+ const xAes = resolveAes<T, unknown>("x");
29
+ const yAes = resolveAes<T, unknown>("y");
30
+ const xScale = buildPositionScale(xAes, rows, [0, 100]);
31
+ const yScale = buildPositionScale(yAes, rows, [200, 0]);
32
+ const scales: ScaleBundle = { x: xScale, y: yScale };
33
+ return {
34
+ data: rows,
35
+ scales,
36
+ plot: createFrame({ x: 50, y: 30, width: 100, height: 200 }),
37
+ theme: themeDefault,
38
+ atlas: undefined,
39
+ emphasisBase: geomEmphasisBase(0),
40
+ };
41
+ }
42
+
43
+ function capturePolylines(builders: ReturnType<ReturnType<typeof line<Row>>["compile"]>) {
44
+ const polys: Array<{ emphasisKey?: number }> = [];
45
+ const layer = {
46
+ pushPolyline: (s: { points: unknown; emphasisKey?: number }) => polys.push(s),
47
+ pushCircle: () => {},
48
+ };
49
+ for (const b of builders) b.addTo(layer as never);
50
+ return polys;
51
+ }
52
+
53
+ describe("line geom — emphasis-key tagging (P5-T3)", () => {
54
+ test("multi-line tags each color group's strokes with a stable per-series key", () => {
55
+ // Two points per series so each group emits a polyline. First-seen: a, b.
56
+ const rows: Row[] = [
57
+ { x: 0, y: 0, series: "a" },
58
+ { x: 5, y: 10, series: "b" },
59
+ { x: 10, y: 20, series: "a" },
60
+ { x: 15, y: 5, series: "b" },
61
+ ];
62
+ const geom = line<Row>({ x: "x", y: "y", color: "series" });
63
+ const base = geomEmphasisBase(0);
64
+ const polys = capturePolylines(geom.compile(makeCtx(rows)));
65
+ const keys = polys.map((p) => p.emphasisKey);
66
+ expect(keys).toContain(emphasisKeyFor(base, 0)); // series "a"
67
+ expect(keys).toContain(emphasisKeyFor(base, 1)); // series "b"
68
+ expect(keys.every((k) => k !== undefined)).toBe(true);
69
+ });
70
+
71
+ test("single-line tags nothing (no sibling series to dim against)", () => {
72
+ const geom = line<Row>({ x: "x", y: "y" });
73
+ const polys = capturePolylines(geom.compile(makeCtx(data)));
74
+ expect(polys.every((p) => p.emphasisKey === undefined)).toBe(true);
75
+ // No resolver for single-line either.
76
+ expect(geom.emphasisResolution!(makeCtx(data))).toBeNull();
77
+ });
78
+
79
+ test("resolver maps a hovered vertex to its color group's key", () => {
80
+ const geom = line<Row>({ x: "x", y: "y", color: "series" });
81
+ const res = geom.emphasisResolution!(makeCtx(data))!;
82
+ const base = geomEmphasisBase(0);
83
+ // data[1].series === "b" → group ordinal 1 (first-seen: a, b).
84
+ expect(res.resolve({ geomKind: "line", dataIndex: 1, data, x: 0, y: 0 })).toBe(
85
+ emphasisKeyFor(base, 1),
86
+ );
87
+ // data[0].series === "a" → ordinal 0.
88
+ expect(res.resolve({ geomKind: "line", dataIndex: 0, data, x: 0, y: 0 })).toBe(
89
+ emphasisKeyFor(base, 0),
90
+ );
91
+ });
92
+
93
+ test("no compile-time hover dim: strokes stable across hover", () => {
94
+ const geom = line<Row>({ x: "x", y: "y", color: "series" });
95
+ const plain = capturePolylines(geom.compile(makeCtx(data)));
96
+ const hovered = capturePolylines(
97
+ geom.compile({
98
+ ...makeCtx(data),
99
+ hovered: { geomKind: "line", dataIndex: 1, data, x: 0, y: 0 },
100
+ }),
101
+ );
102
+ expect(hovered.map((p) => p.emphasisKey)).toEqual(plain.map((p) => p.emphasisKey));
103
+ });
104
+ });
105
+
106
+ describe("line geom — compileHitTest", () => {
107
+ test("emits one hit per vertex at scaled (x,y) absolute px", () => {
108
+ const geom = line<Row>({ x: "x", y: "y" });
109
+ const hits = geom.compileHitTest!(makeCtx(data))!;
110
+ expect(hits.geomKind).toBe("line");
111
+ expect(hits.dataIndex.length).toBe(3);
112
+ // x=0 → 0 + plot.x(50) = 50; y=0 → 200 + plot.y(30) = 230
113
+ expect(hits.positions[0]).toBeCloseTo(50, 3);
114
+ expect(hits.positions[1]).toBeCloseTo(230, 3);
115
+ // x=10 → 100 + 50 = 150; y=20 → 0 + 30 = 30
116
+ expect(hits.positions[4]).toBeCloseTo(150, 3);
117
+ expect(hits.positions[5]).toBeCloseTo(30, 3);
118
+ });
119
+
120
+ test("filters non-finite positions", () => {
121
+ const dirty: Row[] = [
122
+ { x: 0, y: 0, series: "a" },
123
+ { x: NaN, y: 5, series: "b" },
124
+ { x: 10, y: 20, series: "a" },
125
+ ];
126
+ const geom = line<Row>({ x: "x", y: "y" });
127
+ const hits = geom.compileHitTest!(makeCtx(dirty))!;
128
+ expect(Array.from(hits.dataIndex)).toEqual([0, 2]);
129
+ });
130
+
131
+ test("emits per-vertex hits even with color channel (multi-series)", () => {
132
+ const geom = line<Row>({ x: "x", y: "y", color: "series" });
133
+ const hits = geom.compileHitTest!(makeCtx(data))!;
134
+ expect(hits.dataIndex.length).toBe(3);
135
+ expect(hits.channels.color?.column).toBe("series");
136
+ });
137
+
138
+ test("returns null for empty data", () => {
139
+ const geom = line<Row>({ x: "x", y: "y" });
140
+ expect(geom.compileHitTest!(makeCtx<Row>([]))).toBeNull();
141
+ });
142
+
143
+ test("pickRadius scales with stroke width", () => {
144
+ const geom = line<Row>({ x: "x", y: "y" });
145
+ const hits = geom.compileHitTest!(makeCtx(data))!;
146
+ expect(hits.pickRadius).toBeGreaterThanOrEqual(8);
147
+ });
148
+
149
+ test("nearestX: emits pickAxis 'x' and pickRadius spanning plot", () => {
150
+ const geom = line<Row>({ x: "x", y: "y" }, { nearestX: true });
151
+ const hits = geom.compileHitTest!(makeCtx(data))!;
152
+ expect(hits.pickAxis).toBe("x");
153
+ // Radius is at least the plot's larger dimension so the cursor anywhere
154
+ // inside the plot picks a vertex by x.
155
+ expect(hits.pickRadius).toBeGreaterThanOrEqual(200);
156
+ });
157
+
158
+ test("default mode leaves pickAxis undefined", () => {
159
+ const geom = line<Row>({ x: "x", y: "y" });
160
+ const hits = geom.compileHitTest!(makeCtx(data))!;
161
+ expect(hits.pickAxis).toBeUndefined();
162
+ });
163
+ });
164
+
165
+ describe("line geom — ordered path rendering", () => {
166
+ test("connects a single line in ascending order aesthetic", () => {
167
+ const rows: OrderedRow[] = [
168
+ { x: 10, y: 20, series: "a", order: 3 },
169
+ { x: 0, y: 0, series: "a", order: 1 },
170
+ { x: 5, y: 10, series: "a", order: 2 },
171
+ ];
172
+ const geom = line<OrderedRow>({ x: "x", y: "y", order: "order" });
173
+ const builders = geom.compile(makeCtx(rows));
174
+ const polylines: { x: number; y: number }[][] = [];
175
+ const layer = {
176
+ pushPolyline({ points }: { points: { x: number; y: number }[] }) {
177
+ polylines.push(points);
178
+ },
179
+ };
180
+ for (const builder of builders) builder.addTo(layer as never);
181
+ expect(polylines).toHaveLength(1);
182
+ expect(polylines[0]?.map((p) => p.x)).toEqual([50, 100, 150]);
183
+ expect(polylines[0]?.map((p) => p.y)).toEqual([230, 130, 30]);
184
+ });
185
+
186
+ test("sorts within each color-grouped series", () => {
187
+ const rows: OrderedRow[] = [
188
+ { x: 10, y: 18, series: "b", order: 2 },
189
+ { x: 2, y: 2, series: "a", order: 2 },
190
+ { x: 0, y: 0, series: "a", order: 1 },
191
+ { x: 8, y: 16, series: "b", order: 1 },
192
+ ];
193
+ const geom = line<OrderedRow>({ x: "x", y: "y", color: "series", order: "order" });
194
+ const builders = geom.compile(makeCtx(rows));
195
+ const polylines: { x: number; y: number }[][] = [];
196
+ const layer = {
197
+ pushPolyline({ points }: { points: { x: number; y: number }[] }) {
198
+ polylines.push(points);
199
+ },
200
+ };
201
+ for (const builder of builders) builder.addTo(layer as never);
202
+ expect(polylines).toHaveLength(2);
203
+ expect(polylines.map((line) => line.map((p) => p.x))).toEqual([
204
+ [130, 150],
205
+ [50, 70],
206
+ ]);
207
+ });
208
+ });
209
+
210
+ describe("line geom — focus halo decorator (Gap 2)", () => {
211
+ // The halo is a stroke-only circle (haloRing) at the hovered vertex. Mirrors
212
+ // point.test.ts: re-derived geometry; theme hover stroke width; key default
213
+ // (exempt) so the ring stays full-strength while sibling series dim.
214
+ function decorate(dataIndex: number, hitData: readonly unknown[] = data) {
215
+ const geom = line<Row>({ x: "x", y: "y", color: "series" });
216
+ const deco = geom.hoverDecoration!(makeCtx(data))!;
217
+ const circles: Array<{
218
+ cx: number;
219
+ cy: number;
220
+ radius: number;
221
+ fill?: unknown;
222
+ stroke?: unknown;
223
+ strokeWidth?: number;
224
+ emphasisKey?: number;
225
+ }> = [];
226
+ const layer = { pushCircle: (s: never) => circles.push(s) };
227
+ deco.decorate({ geomKind: "line", dataIndex, data: hitData, x: 0, y: 0 }, layer as never);
228
+ return { deco, circles };
229
+ }
230
+
231
+ test("emits a stroke-only ring at the hovered vertex, exempt from dim", () => {
232
+ const { deco, circles } = decorate(1);
233
+ expect(deco.geomKind).toBe("line");
234
+ expect(circles.length).toBe(1);
235
+ // haloRing pushes a stroke-only circle (no fill).
236
+ expect(circles[0]!.fill).toBeUndefined();
237
+ expect(circles[0]!.stroke).toBeDefined();
238
+ expect(circles[0]!.strokeWidth).toBe(themeDefault.interactions.hover.haloStrokeWidth);
239
+ expect(circles[0]!.emphasisKey).toBeUndefined();
240
+ // Vertex 1 is (x=5, y=10) → x scale [0,100] over domain [0,10] → 50; +plot.x(50)=100.
241
+ expect(circles[0]!.cx).toBeCloseTo(100, 3);
242
+ });
243
+
244
+ test("hover on a foreign data array emits nothing", () => {
245
+ expect(decorate(0, []).circles.length).toBe(0);
246
+ });
247
+ });