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,27 @@
1
+ import type { Aes } from "../aes.ts";
2
+ import { type LineOptions } from "./line.ts";
3
+ import { type PointOptions } from "./point.ts";
4
+ import type { Geom } from "./types.ts";
5
+ export interface ConnectedScatterChannels<T> {
6
+ x: Aes<T, number | Date>;
7
+ y: Aes<T, number | Date>;
8
+ color?: Aes<T, unknown>;
9
+ order: Aes<T, number | Date>;
10
+ size?: Aes<T, number>;
11
+ shape?: Aes<T, unknown>;
12
+ alpha?: Aes<T, number>;
13
+ }
14
+ export interface ConnectedScatterOptions {
15
+ /** Line-layer options. */
16
+ line?: LineOptions;
17
+ /** Point-layer options. Pass `false` to render only the connecting path. */
18
+ point?: PointOptions | false;
19
+ }
20
+ /**
21
+ * Grammar helper for the common "connected scatterplot" recipe:
22
+ * a path ordered by a third variable, optionally topped with points.
23
+ *
24
+ * Returns plain geoms so callers still compose with `.layer(text(...))`,
25
+ * `.annotate(...)`, facets, transitions, and the normal interaction stack.
26
+ */
27
+ export declare function connectedScatter<T>(channels: ConnectedScatterChannels<T>, options?: ConnectedScatterOptions): readonly Geom<T>[];
@@ -0,0 +1,157 @@
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 { connectedScatter } from "./connected-scatter.ts";
8
+ import type { CompileContext } from "./types.ts";
9
+
10
+ interface Row {
11
+ x: number;
12
+ y: number;
13
+ series: string;
14
+ order: number;
15
+ }
16
+
17
+ const data: Row[] = [
18
+ { x: 0, y: 0, series: "a", order: 1 },
19
+ { x: 5, y: 10, series: "a", order: 2 },
20
+ { x: 10, y: 20, series: "a", order: 3 },
21
+ ];
22
+
23
+ function makeCtx(rows: readonly Row[]): CompileContext<Row> {
24
+ const xAes = resolveAes<Row, unknown>("x");
25
+ const yAes = resolveAes<Row, unknown>("y");
26
+ const xScale = buildPositionScale(xAes, rows, [0, 100]);
27
+ const yScale = buildPositionScale(yAes, rows, [200, 0]);
28
+ const scales: ScaleBundle = { x: xScale, y: yScale };
29
+ return {
30
+ data: rows,
31
+ scales,
32
+ plot: createFrame({ x: 50, y: 30, width: 100, height: 200 }),
33
+ theme: themeDefault,
34
+ atlas: undefined,
35
+ };
36
+ }
37
+
38
+ /** Collect the polylines and circles emitted across a geom's compiled builders. */
39
+ function captureShapes(
40
+ geom: { compile: (ctx: CompileContext<Row>) => readonly { addTo: (l: never) => void }[] },
41
+ ctx: CompileContext<Row>,
42
+ ): { polylines: Array<{ x: number; y: number }[]>; circles: Array<{ cx: number; cy: number }> } {
43
+ const polylines: Array<{ x: number; y: number }[]> = [];
44
+ const circles: Array<{ cx: number; cy: number }> = [];
45
+ const layer = {
46
+ pushPolyline({ points }: { points: Array<{ x: number; y: number }> }) {
47
+ polylines.push(points);
48
+ },
49
+ pushCircle(shape: { cx: number; cy: number }) {
50
+ circles.push(shape);
51
+ },
52
+ };
53
+ for (const builder of geom.compile(ctx)) builder.addTo(layer as never);
54
+ return { polylines, circles };
55
+ }
56
+
57
+ describe("connectedScatter geom — factory composition", () => {
58
+ test("default returns a line geom followed by a point geom", () => {
59
+ const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" });
60
+ expect(geoms).toHaveLength(2);
61
+ expect(geoms[0]!.kind).toBe("line");
62
+ expect(geoms[1]!.kind).toBe("point");
63
+ });
64
+
65
+ test("point: false renders only the connecting path", () => {
66
+ const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" }, { point: false });
67
+ expect(geoms).toHaveLength(1);
68
+ expect(geoms[0]!.kind).toBe("line");
69
+ });
70
+
71
+ test("threads x/y channels into both layers", () => {
72
+ const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" });
73
+ for (const g of geoms) {
74
+ expect(g.channels.x).toBe("x");
75
+ expect(g.channels.y).toBe("y");
76
+ }
77
+ });
78
+
79
+ test("color channel propagates to both line and point", () => {
80
+ const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order", color: "series" });
81
+ expect(geoms[0]!.channels.color).toBe("series");
82
+ expect(geoms[1]!.channels.color).toBe("series");
83
+ });
84
+
85
+ test("size/shape/alpha channels go only to the point layer", () => {
86
+ const geoms = connectedScatter<Row>({
87
+ x: "x",
88
+ y: "y",
89
+ order: "order",
90
+ size: "y",
91
+ shape: "series",
92
+ alpha: "y",
93
+ });
94
+ const pointGeom = geoms[1]!;
95
+ expect(pointGeom.channels.size).toBe("y");
96
+ expect(pointGeom.channels.shape).toBe("series");
97
+ expect(pointGeom.channels.alpha).toBe("y");
98
+ // The line layer carries none of these point-only channels.
99
+ expect(geoms[0]!.channels.size).toBeUndefined();
100
+ expect(geoms[0]!.channels.shape).toBeUndefined();
101
+ expect(geoms[0]!.channels.alpha).toBeUndefined();
102
+ });
103
+ });
104
+
105
+ describe("connectedScatter geom — compile output", () => {
106
+ test("line layer draws one ordered polyline through the scaled vertices", () => {
107
+ const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" });
108
+ const ctx = makeCtx(data);
109
+ const { polylines } = captureShapes(geoms[0]!, ctx);
110
+ expect(polylines).toHaveLength(1);
111
+ // x: 0,5,10 over domain [0,10] → range [0,100] → 0,50,100; +plot.x(50).
112
+ expect(polylines[0]!.map((p) => p.x)).toEqual([50, 100, 150]);
113
+ // y: 0,10,20 over domain [0,20] → range [200,0] → 200,100,0; +plot.y(30).
114
+ expect(polylines[0]!.map((p) => p.y)).toEqual([230, 130, 30]);
115
+ });
116
+
117
+ test("ordering aesthetic sorts the path even when rows arrive unordered", () => {
118
+ const shuffled: Row[] = [
119
+ { x: 10, y: 20, series: "a", order: 3 },
120
+ { x: 0, y: 0, series: "a", order: 1 },
121
+ { x: 5, y: 10, series: "a", order: 2 },
122
+ ];
123
+ const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" });
124
+ const { polylines } = captureShapes(geoms[0]!, makeCtx(shuffled));
125
+ expect(polylines).toHaveLength(1);
126
+ expect(polylines[0]!.map((p) => p.x)).toEqual([50, 100, 150]);
127
+ });
128
+
129
+ test("point layer emits one circle per datum at the scaled anchor", () => {
130
+ const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" });
131
+ const { circles } = captureShapes(geoms[1]!, makeCtx(data));
132
+ expect(circles).toHaveLength(3);
133
+ expect(circles.map((c) => c.cx)).toEqual([50, 100, 150]);
134
+ expect(circles.map((c) => c.cy)).toEqual([230, 130, 30]);
135
+ });
136
+ });
137
+
138
+ describe("connectedScatter geom — edge cases", () => {
139
+ test("empty data: both layers compile to no drawn shapes", () => {
140
+ const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" });
141
+ const ctx = makeCtx([]);
142
+ const line = captureShapes(geoms[0]!, ctx);
143
+ const points = captureShapes(geoms[1]!, ctx);
144
+ expect(line.polylines).toHaveLength(0);
145
+ expect(points.circles).toHaveLength(0);
146
+ });
147
+
148
+ test("single point: no polyline (one vertex), one circle", () => {
149
+ const single: Row[] = [{ x: 5, y: 10, series: "a", order: 1 }];
150
+ const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" });
151
+ const ctx = makeCtx(single);
152
+ const line = captureShapes(geoms[0]!, ctx);
153
+ const points = captureShapes(geoms[1]!, ctx);
154
+ expect(line.polylines).toHaveLength(0);
155
+ expect(points.circles).toHaveLength(1);
156
+ });
157
+ });
@@ -0,0 +1,63 @@
1
+ import type { Aes } from "../aes.ts";
2
+ import { line, type LineOptions } from "./line.ts";
3
+ import { point, type PointOptions } from "./point.ts";
4
+ import type { Geom } from "./types.ts";
5
+
6
+ export interface ConnectedScatterChannels<T> {
7
+ x: Aes<T, number | Date>;
8
+ y: Aes<T, number | Date>;
9
+ color?: Aes<T, unknown>;
10
+ order: Aes<T, number | Date>;
11
+ size?: Aes<T, number>;
12
+ shape?: Aes<T, unknown>;
13
+ alpha?: Aes<T, number>;
14
+ }
15
+
16
+ export interface ConnectedScatterOptions {
17
+ /** Line-layer options. */
18
+ line?: LineOptions;
19
+ /** Point-layer options. Pass `false` to render only the connecting path. */
20
+ point?: PointOptions | false;
21
+ }
22
+
23
+ /**
24
+ * Grammar helper for the common "connected scatterplot" recipe:
25
+ * a path ordered by a third variable, optionally topped with points.
26
+ *
27
+ * Returns plain geoms so callers still compose with `.layer(text(...))`,
28
+ * `.annotate(...)`, facets, transitions, and the normal interaction stack.
29
+ */
30
+ export function connectedScatter<T>(
31
+ channels: ConnectedScatterChannels<T>,
32
+ options: ConnectedScatterOptions = {},
33
+ ): readonly Geom<T>[] {
34
+ const out: Geom<T>[] = [
35
+ line(
36
+ {
37
+ x: channels.x,
38
+ y: channels.y,
39
+ color: channels.color,
40
+ order: channels.order,
41
+ },
42
+ options.line,
43
+ ),
44
+ ];
45
+
46
+ if (options.point !== false) {
47
+ out.push(
48
+ point(
49
+ {
50
+ x: channels.x,
51
+ y: channels.y,
52
+ color: channels.color,
53
+ size: channels.size,
54
+ shape: channels.shape,
55
+ alpha: channels.alpha,
56
+ },
57
+ options.point,
58
+ ),
59
+ );
60
+ }
61
+
62
+ return out;
63
+ }
@@ -0,0 +1,76 @@
1
+ /** Per-geom ordinal capacity / band width. See module header. */
2
+ export declare const EMPHASIS_GEOM_STRIDE: number;
3
+ /**
4
+ * Geom kinds whose hover dim-others treatment rides the core's ANIMATED GPU
5
+ * emphasis uniform (P5-T3): they tag each mark instance with a stable emphasis
6
+ * key at compile time (see {@link emphasisContext}), and the mount drives
7
+ * `renderer.setEmphasis({ focusedKey, dimAlpha, t })` from the hover signal —
8
+ * zero marks recompile per hover frame. A chart containing ANY of these (with
9
+ * hover enabled) pins its marks layer to `cache:"never"` (a bake would freeze
10
+ * the no-op emphasis; live marks let the uniform dim them — see the mount's
11
+ * `marksCacheHint`).
12
+ *
13
+ * `boxplot` / `violin` / `ridgeline` / `rug` joined this set once the core's
14
+ * emphasis dim reached `pushPolygon` (polygon-fill keys) — they tag a whole
15
+ * logical entity (box / violin / ridge row / tick) with ONE key so it dims as a
16
+ * unit. The remaining hover-INERT geoms are the `nearestX` curve geoms
17
+ * (`rolling`, `area`, whose compile-time halo updates only on a full frame) and
18
+ * `point` (its focus halo rides the overlay decorator). bar/histogram/tile/line
19
+ * ALSO carry overlay focus-halo decorators — dim + halo coexist: the dim is the
20
+ * global uniform, the halo is a `cache:"never"` overlay shape left at key 0
21
+ * (exempt) so it stays full-strength.
22
+ */
23
+ export declare const GPU_DIM_GEOM_KINDS: ReadonlySet<string>;
24
+ /**
25
+ * Largest geom index whose key band stays inside u32. `geomEmphasisBase(gi)` is
26
+ * `(gi+1) * 2^20`; `emphasisKeyFor` adds up to `(2^20 - 1) + 1 = 2^20`. So the
27
+ * max key for geom `gi` is `(gi+2) * 2^20`. The u32 ceiling is `2^32 - 1`, i.e.
28
+ * `4096 * 2^20`, so we need `(gi+2) * 2^20 <= 4096 * 2^20`, i.e. `gi <= 4094`.
29
+ * At `gi = 4094` the max key is exactly `4096 * 2^20 = 2^32`, which overflows
30
+ * u32 → wraps to 0 (the silent EXEMPT sentinel — a non-dimming hole). So the
31
+ * last SAFE geom index is `4093` and `gi >= 4094` is rejected by
32
+ * {@link geomEmphasisBase}.
33
+ */
34
+ export declare const EMPHASIS_MAX_GEOM_INDEX = 4093;
35
+ /**
36
+ * Disjoint emphasis-key band base for the geom at `geomIndex` in the chart's
37
+ * layer list (the pipeline's `gi`). `geomIndex + 1` so geom 0 starts at one
38
+ * full stride (key 0 stays the opt-out sentinel for axis / grid / overlay).
39
+ */
40
+ export declare function geomEmphasisBase(geomIndex: number): number;
41
+ /**
42
+ * Emphasis key for the `ordinal`-th participating instance of a geom whose band
43
+ * starts at `base`. `ordinal + 1` keeps the band's first instance off key 0.
44
+ * `ordinal` is taken mod the stride so a runaway count can never spill into the
45
+ * next geom's band (soundness floor — see module header).
46
+ */
47
+ export declare function emphasisKeyFor(base: number, ordinal: number): number;
48
+ /**
49
+ * A geom's hover-dim emphasis-key resolver, captured at compile time so the
50
+ * mount can map an active {@link HoveredHit} to the namespaced key it tagged
51
+ * its focused instance(s) with — WITHOUT recompiling. `geomKind` + `data`
52
+ * identity match the resolver to the hit (the same keys that route a hit to its
53
+ * geom, mirroring {@link GeomHoverDecorator}). `resolve` returns the focused
54
+ * key, or `null` when this hit focuses nothing in the geom (the mount then
55
+ * leaves emphasis settled). The geom computes the SAME ordinal it used to tag.
56
+ */
57
+ export interface EmphasisResolver {
58
+ readonly geomKind: import("./types.ts").GeomKind;
59
+ readonly data: readonly unknown[];
60
+ resolve(hit: import("./types.ts").HoveredHit): number | null;
61
+ }
62
+ /**
63
+ * Build the per-frame emphasis context a dim-participating geom uses to tag and
64
+ * resolve. Returns `null` when emphasis is off (`emphasisBase` absent, e.g.
65
+ * SSR/SVG, or `theme.interactions.hover.enabled === false`) so callers cheaply
66
+ * skip all tagging. `ordinalOf` maps a hit (or an instance's identity) to the
67
+ * geom's ordinal — by default the hit's `dataIndex`, which every single-series
68
+ * dim geom (bar-single/histogram/boxplot/violin/tile/ridgeline/rug) already
69
+ * uses as its compile index. Multi-series geoms pass a custom `ordinalOf`.
70
+ */
71
+ export declare function emphasisContext<T>(ctx: import("./types.ts").CompileContext<T>, kind: import("./types.ts").GeomKind, ordinalOf?: (hit: import("./types.ts").HoveredHit) => number | null): {
72
+ /** Namespaced key for the instance at `ordinal`. Tag marks with this. */
73
+ keyFor(ordinal: number): number;
74
+ /** Resolver to register so the mount can map a hit → focused key. */
75
+ resolver(): EmphasisResolver;
76
+ } | null;
@@ -0,0 +1,135 @@
1
+ import { describe, expect, test } from "vite-plus/test";
2
+
3
+ import {
4
+ EMPHASIS_GEOM_STRIDE,
5
+ EMPHASIS_MAX_GEOM_INDEX,
6
+ GPU_DIM_GEOM_KINDS,
7
+ emphasisContext,
8
+ emphasisKeyFor,
9
+ geomEmphasisBase,
10
+ } from "./emphasis.ts";
11
+ import { themeDefault } from "../theme.ts";
12
+ import type { CompileContext, HoveredHit } from "./types.ts";
13
+
14
+ describe("emphasis-key namespacing (P5-T3)", () => {
15
+ test("geom bands are disjoint full strides; geom 0 starts at one stride", () => {
16
+ expect(geomEmphasisBase(0)).toBe(EMPHASIS_GEOM_STRIDE);
17
+ expect(geomEmphasisBase(1)).toBe(2 * EMPHASIS_GEOM_STRIDE);
18
+ expect(geomEmphasisBase(2)).toBe(3 * EMPHASIS_GEOM_STRIDE);
19
+ });
20
+
21
+ test("keyFor offsets ordinal+1 so ordinal 0 never collides with the sentinel 0", () => {
22
+ const base = geomEmphasisBase(0);
23
+ expect(emphasisKeyFor(base, 0)).toBe(base + 1);
24
+ expect(emphasisKeyFor(base, 5)).toBe(base + 6);
25
+ // No geom's key is ever 0 (the EXEMPT/opt-out sentinel).
26
+ expect(emphasisKeyFor(geomEmphasisBase(0), 0)).toBeGreaterThan(0);
27
+ });
28
+
29
+ test("two distinct geoms never share a key for the same ordinal", () => {
30
+ const a = emphasisKeyFor(geomEmphasisBase(0), 3);
31
+ const b = emphasisKeyFor(geomEmphasisBase(1), 3);
32
+ expect(a).not.toBe(b);
33
+ });
34
+
35
+ test("ordinal at/over the stride wraps (stays inside the geom's band)", () => {
36
+ const base = geomEmphasisBase(0);
37
+ // ordinal === stride wraps to ordinal 0's key (mod), never spilling into geom 1.
38
+ expect(emphasisKeyFor(base, EMPHASIS_GEOM_STRIDE)).toBe(emphasisKeyFor(base, 0));
39
+ expect(emphasisKeyFor(base, EMPHASIS_GEOM_STRIDE)).toBeLessThan(geomEmphasisBase(1));
40
+ });
41
+
42
+ // Fix E — the key-band ceiling. At gi=4094 the MAX key reaches exactly 2^32 and
43
+ // wraps to 0 (the silent EXEMPT sentinel). So the last SAFE index is 4093.
44
+ test("the last safe geom index keeps its max key inside u32", () => {
45
+ expect(EMPHASIS_MAX_GEOM_INDEX).toBe(4093);
46
+ const base = geomEmphasisBase(EMPHASIS_MAX_GEOM_INDEX);
47
+ const maxKey = emphasisKeyFor(base, EMPHASIS_GEOM_STRIDE - 1);
48
+ // The largest key for the last safe geom is < 2^32 (no wrap).
49
+ expect(maxKey).toBeLessThan(2 ** 32);
50
+ });
51
+
52
+ test("geomEmphasisBase throws past the ceiling (would overflow u32 → exempt-0 hole)", () => {
53
+ expect(() => geomEmphasisBase(EMPHASIS_MAX_GEOM_INDEX)).not.toThrow();
54
+ expect(() => geomEmphasisBase(EMPHASIS_MAX_GEOM_INDEX + 1)).toThrow(RangeError);
55
+ expect(() => geomEmphasisBase(10000)).toThrow(/ceiling/);
56
+ });
57
+ });
58
+
59
+ describe("GPU_DIM_GEOM_KINDS (P5-T3 — single source of truth)", () => {
60
+ test("includes the four polygon-fill geoms plus the original four", () => {
61
+ expect([...GPU_DIM_GEOM_KINDS].sort()).toEqual(
62
+ ["bar", "boxplot", "histogram", "line", "ridgeline", "rug", "tile", "violin"].sort(),
63
+ );
64
+ });
65
+
66
+ test("excludes the deliberately hover-inert geoms (point / area / rolling)", () => {
67
+ expect(GPU_DIM_GEOM_KINDS.has("point")).toBe(false);
68
+ expect(GPU_DIM_GEOM_KINDS.has("area")).toBe(false);
69
+ expect(GPU_DIM_GEOM_KINDS.has("rolling")).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe("emphasisContext", () => {
74
+ function ctx(over: Partial<CompileContext<unknown>> = {}): CompileContext<unknown> {
75
+ return {
76
+ data: [{}, {}, {}],
77
+ scales: {} as never,
78
+ plot: {} as never,
79
+ theme: themeDefault,
80
+ atlas: undefined,
81
+ emphasisBase: geomEmphasisBase(0),
82
+ ...over,
83
+ };
84
+ }
85
+ const hit = (dataIndex: number): HoveredHit => ({
86
+ geomKind: "bar",
87
+ dataIndex,
88
+ data: ctx().data,
89
+ x: 0,
90
+ y: 0,
91
+ });
92
+
93
+ test("returns null when emphasisBase is absent (SSR/SVG)", () => {
94
+ expect(emphasisContext(ctx({ emphasisBase: undefined }), "bar")).toBeNull();
95
+ });
96
+
97
+ test("returns null when hover emphasis is disabled in the theme", () => {
98
+ const theme = {
99
+ ...themeDefault,
100
+ interactions: {
101
+ ...themeDefault.interactions,
102
+ hover: { ...themeDefault.interactions.hover, enabled: false },
103
+ },
104
+ };
105
+ expect(emphasisContext(ctx({ theme }), "bar")).toBeNull();
106
+ });
107
+
108
+ test("keyFor uses the geom's band; default ordinalOf = hit.dataIndex", () => {
109
+ const data = [{}, {}, {}];
110
+ const c = ctx({ data });
111
+ const emph = emphasisContext(c, "bar")!;
112
+ const base = geomEmphasisBase(0);
113
+ expect(emph.keyFor(2)).toBe(emphasisKeyFor(base, 2));
114
+ const res = emph.resolver();
115
+ expect(res.geomKind).toBe("bar");
116
+ expect(res.data).toBe(data);
117
+ expect(res.resolve({ geomKind: "bar", dataIndex: 1, data, x: 0, y: 0 })).toBe(
118
+ emphasisKeyFor(base, 1),
119
+ );
120
+ });
121
+
122
+ test("resolver rejects hits from a different geom kind or data array", () => {
123
+ const data = [{}, {}];
124
+ const emph = emphasisContext(ctx({ data }), "bar")!;
125
+ const res = emph.resolver();
126
+ expect(res.resolve({ geomKind: "tile", dataIndex: 0, data, x: 0, y: 0 })).toBeNull();
127
+ expect(res.resolve({ geomKind: "bar", dataIndex: 0, data: [{}], x: 0, y: 0 })).toBeNull();
128
+ });
129
+
130
+ test("custom ordinalOf returning null → resolver returns null (focuses nothing)", () => {
131
+ const data = [{}, {}];
132
+ const emph = emphasisContext(ctx({ data }), "bar", () => null)!;
133
+ expect(emph.resolver().resolve(hit(0))).toBeNull();
134
+ });
135
+ });
@@ -0,0 +1,162 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Emphasis-key namespacing (P5-T3 — animated GPU hover dim)
3
+ // ---------------------------------------------------------------------------
4
+ // Dim-participating geoms (bar / histogram / boxplot / violin / tile /
5
+ // ridgeline / rug / line) tag each mark instance with a STABLE per-instance
6
+ // emphasis key at compile time (hover-independent). The core renderer writes
7
+ // that key into the instance's `lane4`; `renderer.setEmphasis({ focusedKey,
8
+ // dimAlpha, t })` then dims — on the transparent/OIT path only — every TAGGED
9
+ // instance whose key differs from `focusedKey`, animated by `t` 0→1. Zero marks
10
+ // recompile per hover frame (the old "snap" rebake is gone).
11
+ //
12
+ // KEY NAMESPACING (the whole-frame budget). Keys are GLOBAL per frame across
13
+ // every layer (one emphasis uniform), so two geoms must never collide. Each
14
+ // geom gets a disjoint BASE band; the geom adds `ordinal + 1` within its band.
15
+ //
16
+ // base(geomIndex) = (geomIndex + 1) * EMPHASIS_GEOM_STRIDE
17
+ // key(base, ordinal) = base + (ordinal mod EMPHASIS_GEOM_STRIDE) + 1
18
+ //
19
+ // The `+ 1` keeps key 0 reserved as the EXEMPT/opt-out sentinel (core contract:
20
+ // a key-0 instance never dims). `geomIndex + 1` keeps geom 0 off the sentinel
21
+ // band too. With a u32 emphasis key:
22
+ //
23
+ // • EMPHASIS_GEOM_STRIDE = 2^20 → up to 1,048,575 distinct ordinals per geom
24
+ // (more than enough for per-row / per-segment marks; a chart with a million
25
+ // visible marks is already past the SDF instance budget).
26
+ // • geom band index up to 4093 (inclusive) per chart — the max key for geom
27
+ // `gi` is `(gi+2) * 2^20`, which must stay <= u32 max `2^32 - 1 < 4096*2^20`,
28
+ // so `gi <= 4094` keeps the BASE in range but `gi = 4094` makes the max KEY
29
+ // exactly `2^32` (wraps to 0, the silent EXEMPT sentinel). Hence the last
30
+ // SAFE index is 4093; `geomEmphasisBase` throws on `gi >= 4094` (which no
31
+ // real chart reaches). See `EMPHASIS_MAX_GEOM_INDEX`.
32
+ //
33
+ // An ordinal at or beyond the stride wraps (mod), trading a (cosmetic) dim
34
+ // collision for never escaping the geom's band — soundness over fidelity at a
35
+ // scale no real chart reaches. `ordinal` is whatever index the geom reports as
36
+ // the hit's `dataIndex` (per row for bars/tiles/box/violin/ridgeline, the flat
37
+ // cell counter for histogram) — so tagging and hit-resolution share one source.
38
+
39
+ /** Per-geom ordinal capacity / band width. See module header. */
40
+ export const EMPHASIS_GEOM_STRIDE = 2 ** 20;
41
+
42
+ /**
43
+ * Geom kinds whose hover dim-others treatment rides the core's ANIMATED GPU
44
+ * emphasis uniform (P5-T3): they tag each mark instance with a stable emphasis
45
+ * key at compile time (see {@link emphasisContext}), and the mount drives
46
+ * `renderer.setEmphasis({ focusedKey, dimAlpha, t })` from the hover signal —
47
+ * zero marks recompile per hover frame. A chart containing ANY of these (with
48
+ * hover enabled) pins its marks layer to `cache:"never"` (a bake would freeze
49
+ * the no-op emphasis; live marks let the uniform dim them — see the mount's
50
+ * `marksCacheHint`).
51
+ *
52
+ * `boxplot` / `violin` / `ridgeline` / `rug` joined this set once the core's
53
+ * emphasis dim reached `pushPolygon` (polygon-fill keys) — they tag a whole
54
+ * logical entity (box / violin / ridge row / tick) with ONE key so it dims as a
55
+ * unit. The remaining hover-INERT geoms are the `nearestX` curve geoms
56
+ * (`rolling`, `area`, whose compile-time halo updates only on a full frame) and
57
+ * `point` (its focus halo rides the overlay decorator). bar/histogram/tile/line
58
+ * ALSO carry overlay focus-halo decorators — dim + halo coexist: the dim is the
59
+ * global uniform, the halo is a `cache:"never"` overlay shape left at key 0
60
+ * (exempt) so it stays full-strength.
61
+ */
62
+ export const GPU_DIM_GEOM_KINDS: ReadonlySet<string> = new Set([
63
+ "bar",
64
+ "histogram",
65
+ "tile",
66
+ "line",
67
+ "boxplot",
68
+ "violin",
69
+ "ridgeline",
70
+ "rug",
71
+ ]);
72
+
73
+ /**
74
+ * Largest geom index whose key band stays inside u32. `geomEmphasisBase(gi)` is
75
+ * `(gi+1) * 2^20`; `emphasisKeyFor` adds up to `(2^20 - 1) + 1 = 2^20`. So the
76
+ * max key for geom `gi` is `(gi+2) * 2^20`. The u32 ceiling is `2^32 - 1`, i.e.
77
+ * `4096 * 2^20`, so we need `(gi+2) * 2^20 <= 4096 * 2^20`, i.e. `gi <= 4094`.
78
+ * At `gi = 4094` the max key is exactly `4096 * 2^20 = 2^32`, which overflows
79
+ * u32 → wraps to 0 (the silent EXEMPT sentinel — a non-dimming hole). So the
80
+ * last SAFE geom index is `4093` and `gi >= 4094` is rejected by
81
+ * {@link geomEmphasisBase}.
82
+ */
83
+ export const EMPHASIS_MAX_GEOM_INDEX = 4093;
84
+
85
+ /**
86
+ * Disjoint emphasis-key band base for the geom at `geomIndex` in the chart's
87
+ * layer list (the pipeline's `gi`). `geomIndex + 1` so geom 0 starts at one
88
+ * full stride (key 0 stays the opt-out sentinel for axis / grid / overlay).
89
+ */
90
+ export function geomEmphasisBase(geomIndex: number): number {
91
+ // Defensive guard: at `geomIndex >= 4094` the max emphasis KEY reaches/exceeds
92
+ // 2^32 and wraps to 0 (the EXEMPT sentinel) — a silent non-dimming hole. No
93
+ // real chart has 4094 geoms; reject it loudly rather than emit corrupt keys.
94
+ if (geomIndex >= EMPHASIS_MAX_GEOM_INDEX + 1) {
95
+ throw new RangeError(
96
+ `geomEmphasisBase: geomIndex ${geomIndex} exceeds the emphasis key-band ceiling ` +
97
+ `(${EMPHASIS_MAX_GEOM_INDEX}); keys would overflow u32 and wrap to the exempt sentinel.`,
98
+ );
99
+ }
100
+ return (geomIndex + 1) * EMPHASIS_GEOM_STRIDE;
101
+ }
102
+
103
+ /**
104
+ * Emphasis key for the `ordinal`-th participating instance of a geom whose band
105
+ * starts at `base`. `ordinal + 1` keeps the band's first instance off key 0.
106
+ * `ordinal` is taken mod the stride so a runaway count can never spill into the
107
+ * next geom's band (soundness floor — see module header).
108
+ */
109
+ export function emphasisKeyFor(base: number, ordinal: number): number {
110
+ return base + (ordinal % EMPHASIS_GEOM_STRIDE) + 1;
111
+ }
112
+
113
+ /**
114
+ * A geom's hover-dim emphasis-key resolver, captured at compile time so the
115
+ * mount can map an active {@link HoveredHit} to the namespaced key it tagged
116
+ * its focused instance(s) with — WITHOUT recompiling. `geomKind` + `data`
117
+ * identity match the resolver to the hit (the same keys that route a hit to its
118
+ * geom, mirroring {@link GeomHoverDecorator}). `resolve` returns the focused
119
+ * key, or `null` when this hit focuses nothing in the geom (the mount then
120
+ * leaves emphasis settled). The geom computes the SAME ordinal it used to tag.
121
+ */
122
+ export interface EmphasisResolver {
123
+ readonly geomKind: import("./types.ts").GeomKind;
124
+ readonly data: readonly unknown[];
125
+ resolve(hit: import("./types.ts").HoveredHit): number | null;
126
+ }
127
+
128
+ /**
129
+ * Build the per-frame emphasis context a dim-participating geom uses to tag and
130
+ * resolve. Returns `null` when emphasis is off (`emphasisBase` absent, e.g.
131
+ * SSR/SVG, or `theme.interactions.hover.enabled === false`) so callers cheaply
132
+ * skip all tagging. `ordinalOf` maps a hit (or an instance's identity) to the
133
+ * geom's ordinal — by default the hit's `dataIndex`, which every single-series
134
+ * dim geom (bar-single/histogram/boxplot/violin/tile/ridgeline/rug) already
135
+ * uses as its compile index. Multi-series geoms pass a custom `ordinalOf`.
136
+ */
137
+ export function emphasisContext<T>(
138
+ ctx: import("./types.ts").CompileContext<T>,
139
+ kind: import("./types.ts").GeomKind,
140
+ ordinalOf: (hit: import("./types.ts").HoveredHit) => number | null = (hit) => hit.dataIndex,
141
+ ): {
142
+ /** Namespaced key for the instance at `ordinal`. Tag marks with this. */
143
+ keyFor(ordinal: number): number;
144
+ /** Resolver to register so the mount can map a hit → focused key. */
145
+ resolver(): EmphasisResolver;
146
+ } | null {
147
+ const base = ctx.emphasisBase;
148
+ if (base === undefined || !ctx.theme.interactions.hover.enabled) return null;
149
+ const data = ctx.data as readonly unknown[];
150
+ return {
151
+ keyFor: (ordinal) => emphasisKeyFor(base, ordinal),
152
+ resolver: () => ({
153
+ geomKind: kind,
154
+ data,
155
+ resolve(hit) {
156
+ if (hit.geomKind !== kind || hit.data !== data) return null;
157
+ const ordinal = ordinalOf(hit);
158
+ return ordinal === null ? null : emphasisKeyFor(base, ordinal);
159
+ },
160
+ }),
161
+ };
162
+ }