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,85 @@
1
+ import type { Color } from "insomni";
2
+ import { type KdeBandwidth, type KdeKernel, type QuantileMethod, type WhiskerRule } from "../../stats/index.ts";
3
+ import type { Aes } from "../aes.ts";
4
+ import type { Geom } from "./types.ts";
5
+ import type { PointsMode } from "./boxplot.ts";
6
+ import { type DensityScale } from "./_distribution.ts";
7
+ export interface ViolinChannels<T> {
8
+ x: Aes<T, string | number | Date>;
9
+ /**
10
+ * Numeric value axis when vertical. For horizontal layouts
11
+ * (`orientation: "x"`) this carries the band category, so a string is
12
+ * accepted; the value axis then lives on `x`.
13
+ */
14
+ y: Aes<T, string | number | Date>;
15
+ color?: Aes<T, unknown>;
16
+ }
17
+ export type ViolinScale = DensityScale;
18
+ export type ViolinInner = "box" | "quartile" | "stick" | "none";
19
+ export interface ViolinOptions {
20
+ orientation?: "x" | "y";
21
+ /** Violin width as a fraction of the bandwidth (or grouped inner bandwidth). Default `0.9`. */
22
+ width?: number;
23
+ /** KDE bandwidth — number, `"silverman"` (default), or `"scott"`. */
24
+ bandwidth?: KdeBandwidth;
25
+ /** KDE evaluation grid size. Default `64`. */
26
+ gridSize?: number;
27
+ /** KDE kernel. Default `"gaussian"`. */
28
+ kernel?: KdeKernel;
29
+ /** Clip KDE to data range (true, default) or pad outward by ~3 bandwidths. */
30
+ trim?: boolean;
31
+ /**
32
+ * Width-normalization mode.
33
+ *
34
+ * - `"width"` (default) — each violin uses its full allotted width; shapes
35
+ * are easy to compare but **areas are misleading**.
36
+ * - `"area"` — density scaled relative to the global max so areas are
37
+ * comparable across groups (groups with low n look skinny).
38
+ * - `"count"` — area scales with sample size (bigger n → wider violin).
39
+ */
40
+ scale?: ViolinScale;
41
+ /**
42
+ * Inner annotation drawn on top of the violin.
43
+ *
44
+ * - `"none"` (default) — bare KDE polygon, like ggplot's `geom_violin`.
45
+ * - `"box"` — slim box plot with median, IQR, whiskers.
46
+ * - `"quartile"` — three horizontal/vertical lines at Q1, median, Q3.
47
+ * - `"stick"` — single line at the median only.
48
+ */
49
+ inner?: ViolinInner;
50
+ /** When to overlay raw jittered points. See `BoxplotOptions["points"]`. */
51
+ points?: PointsMode;
52
+ pointsThreshold?: number;
53
+ pointJitter?: number;
54
+ pointRadius?: number;
55
+ jitterSeed?: number;
56
+ /** Box stats: whisker rule when `inner: "box"`. Default `1.5`. */
57
+ whisker?: WhiskerRule;
58
+ /** Box stats: quantile method. Default `"type-7"`. */
59
+ quantile?: QuantileMethod;
60
+ fill?: Color;
61
+ stroke?: Color;
62
+ strokeWidth?: number;
63
+ /** Color of inner annotation marks. Defaults to `stroke` (or theme text). */
64
+ innerStroke?: Color;
65
+ /** Stroke width for the embedded mini-box body. Default `1`. */
66
+ innerStrokeWidth?: number;
67
+ /** Inner-band padding for grouped (color-split) layout. Default `0.05`. */
68
+ groupPadding?: number;
69
+ /**
70
+ * Render an `n=<count>` label per group, aligned with the band axis just
71
+ * outside the plot frame. Useful when the categorical labels alone hide
72
+ * sample-size differences.
73
+ */
74
+ showCounts?: boolean;
75
+ /**
76
+ * Pixel offset of the count labels from the plot frame edge along the
77
+ * band-axis perpendicular. Increase to clear longer tick labels. Default
78
+ * `28` (vertical) / `32` (horizontal).
79
+ */
80
+ countsOffset?: number;
81
+ countsFontSize?: number;
82
+ countsColor?: Color;
83
+ label?: string;
84
+ }
85
+ export declare function violin<T>(channels: ViolinChannels<T>, options?: ViolinOptions): Geom<T>;
@@ -0,0 +1,187 @@
1
+ // @vitest-environment jsdom
2
+ import { createFrame } from "insomni";
3
+ import { describe, expect, test } from "vite-plus/test";
4
+ import { plot } from "../chart.ts";
5
+ import { resolveAes } from "../aes.ts";
6
+ import { buildPositionScale, type ScaleBundle } from "../scales.ts";
7
+ import { themeDefault } from "../theme.ts";
8
+ import { violin } from "./violin.ts";
9
+ import { emphasisKeyFor, geomEmphasisBase } from "./emphasis.ts";
10
+ import type { CompileContext } from "./types.ts";
11
+
12
+ // Fake Layer recording the emphasisKey of every primitive a violin composites
13
+ // (KDE polygon + outline polyline + inner box rect/polylines + point overlay).
14
+ function makeKeyCapture() {
15
+ const keys: (number | undefined)[] = [];
16
+ const record = (s: { emphasisKey?: number }) => keys.push(s.emphasisKey);
17
+ const layer = {
18
+ pushRect: record,
19
+ pushCircle: record,
20
+ pushEllipse: record,
21
+ pushSegment: record,
22
+ pushLine: record,
23
+ pushPolyline: record,
24
+ pushPolygon: record,
25
+ pushText: () => layer,
26
+ pushString: () => layer,
27
+ };
28
+ return { layer, keys };
29
+ }
30
+
31
+ interface Obs {
32
+ group: string;
33
+ value: number;
34
+ color?: string;
35
+ }
36
+
37
+ // ============ Fixtures ============
38
+ const fixture = {
39
+ threeGroups: (): Obs[] => {
40
+ const out: Obs[] = [];
41
+ const seeds = { A: 0.1, B: 0.5, C: 0.9 };
42
+ let r = 0;
43
+ const rand = () => {
44
+ r = (r * 9301 + 49297) % 233280;
45
+ return r / 233280;
46
+ };
47
+ for (const [name, mu] of Object.entries(seeds)) {
48
+ for (let i = 0; i < 50; i++) {
49
+ out.push({ group: name, value: mu + (rand() - 0.5) * 0.3 });
50
+ }
51
+ }
52
+ return out;
53
+ },
54
+ bimodal: (): Obs[] => {
55
+ const out: Obs[] = [];
56
+ for (let i = 0; i < 30; i++) out.push({ group: "A", value: 0.2 + i * 0.005 });
57
+ for (let i = 0; i < 30; i++) out.push({ group: "A", value: 0.8 + i * 0.005 });
58
+ return out;
59
+ },
60
+ };
61
+
62
+ // ============ Tests ============
63
+ describe("violin()", () => {
64
+ test("renders a polygon per group to SVG", () => {
65
+ const svg = plot({ data: fixture.threeGroups(), width: 480, height: 320 })
66
+ .layer(violin({ x: "group", y: "value" }))
67
+ .toSVG();
68
+ expect(svg).toBeInstanceOf(SVGSVGElement);
69
+ // SVG renderer tessellates filled polygons into <path> elements.
70
+ const polygons = svg.querySelectorAll("polygon");
71
+ expect(polygons.length).toBeGreaterThan(0);
72
+ });
73
+
74
+ test("respects gridSize via point-count of polygon (2 * gridSize vertices)", () => {
75
+ // We don't have a direct vertex-count probe through SVG, but we can verify
76
+ // it doesn't crash on small grids and renders non-empty output.
77
+ const svg = plot({ data: fixture.bimodal(), width: 320, height: 240 })
78
+ .layer(violin({ x: "group", y: "value" }, { gridSize: 16 }))
79
+ .toSVG();
80
+ expect(svg.querySelectorAll("polygon").length).toBeGreaterThan(0);
81
+ });
82
+
83
+ test("'inner: none' suppresses the embedded box-and-stick", () => {
84
+ const svgWith = plot({ data: fixture.threeGroups(), width: 320, height: 240 })
85
+ .layer(violin({ x: "group", y: "value" }, { inner: "box", points: "none" }))
86
+ .toSVG();
87
+ const svgNone = plot({ data: fixture.threeGroups(), width: 320, height: 240 })
88
+ .layer(violin({ x: "group", y: "value" }, { inner: "none", points: "none" }))
89
+ .toSVG();
90
+ // Inner "box" mode draws additional <rect> + <line> elements.
91
+ const withRects = svgWith.querySelectorAll("rect").length;
92
+ const noneRects = svgNone.querySelectorAll("rect").length;
93
+ expect(withRects).toBeGreaterThan(noneRects);
94
+ });
95
+
96
+ test("does not throw on degenerate samples (n=1, all-equal)", () => {
97
+ expect(() =>
98
+ plot({ data: [{ group: "A", value: 5 }] as Obs[] })
99
+ .layer(violin({ x: "group", y: "value" }))
100
+ .toSVG({ width: 200, height: 200 }),
101
+ ).not.toThrow();
102
+ expect(() =>
103
+ plot({ data: [3, 3, 3, 3].map((v) => ({ group: "A", value: v })) as Obs[] })
104
+ .layer(violin({ x: "group", y: "value" }))
105
+ .toSVG({ width: 200, height: 200 }),
106
+ ).not.toThrow();
107
+ });
108
+
109
+ test("compileHitTest emits one hit per non-empty bucket at (bandCenter, median)", () => {
110
+ const data = fixture.threeGroups();
111
+ const xAes = resolveAes<Obs, unknown>("group");
112
+ const yAes = resolveAes<Obs, unknown>("value");
113
+ const xScale = buildPositionScale(xAes, data, [0, 300], { type: "band", padding: 0 });
114
+ const yScale = buildPositionScale(yAes, data, [200, 0]);
115
+ const scales: ScaleBundle = { x: xScale, y: yScale };
116
+ const ctx: CompileContext<Obs> = {
117
+ data,
118
+ scales,
119
+ plot: createFrame({ x: 50, y: 30, width: 300, height: 200 }),
120
+ theme: themeDefault,
121
+ atlas: undefined,
122
+ };
123
+ const geom = violin<Obs>({ x: "group", y: "value" });
124
+ const hits = geom.compileHitTest!(ctx)!;
125
+ expect(hits.geomKind).toBe("violin");
126
+ expect(hits.dataIndex.length).toBe(3);
127
+ expect(new Set(hits.seriesKey)).toEqual(new Set(["A", "B", "C"]));
128
+ });
129
+
130
+ test("scale: 'count' / 'area' / 'width' all render", () => {
131
+ for (const mode of ["width", "area", "count"] as const) {
132
+ const svg = plot({ data: fixture.threeGroups(), width: 400, height: 300 })
133
+ .layer(violin({ x: "group", y: "value" }, { scale: mode }))
134
+ .toSVG();
135
+ expect(svg).toBeInstanceOf(SVGSVGElement);
136
+ }
137
+ });
138
+ });
139
+
140
+ describe("violin() — GPU hover emphasis (P5-T3 Gap 1)", () => {
141
+ function makeCtx(data: Obs[], emphasis = true): CompileContext<Obs> {
142
+ const xAes = resolveAes<Obs, unknown>("group");
143
+ const yAes = resolveAes<Obs, unknown>("value");
144
+ const xScale = buildPositionScale(xAes, data, [0, 300], { type: "band", padding: 0 });
145
+ const yScale = buildPositionScale(yAes, data, [200, 0]);
146
+ const scales: ScaleBundle = { x: xScale, y: yScale };
147
+ return {
148
+ data,
149
+ scales,
150
+ plot: createFrame({ x: 50, y: 30, width: 300, height: 200 }),
151
+ theme: themeDefault,
152
+ atlas: undefined,
153
+ emphasisBase: emphasis ? geomEmphasisBase(0) : undefined,
154
+ };
155
+ }
156
+
157
+ test("tagging present + every primitive of a single violin shares ONE key", () => {
158
+ const data = fixture.bimodal(); // one bucket "A"
159
+ const geom = violin<Obs>({ x: "group", y: "value" }, { inner: "box", points: "always" });
160
+ const { layer, keys } = makeKeyCapture();
161
+ for (const b of geom.compile(makeCtx(data))) b.addTo(layer as never);
162
+ const nonzero = keys.filter((k): k is number => k !== undefined && k >= 1);
163
+ expect(nonzero.length).toBeGreaterThan(0);
164
+ const expected = emphasisKeyFor(geomEmphasisBase(0), 0);
165
+ expect(new Set(nonzero)).toEqual(new Set([expected]));
166
+ });
167
+
168
+ test("emphasisResolution maps a hit's dataIndex (bucketIndex) to the tagged key", () => {
169
+ const data = fixture.threeGroups();
170
+ const geom = violin<Obs>({ x: "group", y: "value" });
171
+ const res = geom.emphasisResolution!(makeCtx(data))!;
172
+ expect(res.geomKind).toBe("violin");
173
+ expect(res.resolve({ geomKind: "violin", dataIndex: 1, data, x: 0, y: 0 })).toBe(
174
+ emphasisKeyFor(geomEmphasisBase(0), 1),
175
+ );
176
+ expect(res.resolve({ geomKind: "boxplot", dataIndex: 0, data, x: 0, y: 0 })).toBeNull();
177
+ });
178
+
179
+ test("no emphasisBase (SSR/SVG) → violins untagged, no resolver key", () => {
180
+ const data = fixture.threeGroups();
181
+ const geom = violin<Obs>({ x: "group", y: "value" });
182
+ const { layer, keys } = makeKeyCapture();
183
+ for (const b of geom.compile(makeCtx(data, false))) b.addTo(layer as never);
184
+ expect(keys.every((k) => k === undefined)).toBe(true);
185
+ expect(geom.emphasisResolution!(makeCtx(data, false))).toBeNull();
186
+ });
187
+ });