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,545 @@
1
+ // ---------------------------------------------------------------------------
2
+ // point geom
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import { lerpColor, type Color, type Layer } from "insomni";
6
+ import {
7
+ pointMark,
8
+ type MarkBuilder,
9
+ type PointBorderStyle,
10
+ type PointShapeKind,
11
+ } from "../../marks.ts";
12
+ import type { Aes } from "../aes.ts";
13
+ import { resolveAes } from "../aes.ts";
14
+ import { alphaize, withAlpha } from "../color-utils.ts";
15
+ import type {
16
+ CompileContext,
17
+ CompiledHitTest,
18
+ Geom,
19
+ GeomFrame,
20
+ GeomHoverDecorator,
21
+ HoveredHit,
22
+ ResolvedChannelMap,
23
+ } from "./types.ts";
24
+ import {
25
+ defaultMarkFill,
26
+ haloRing,
27
+ inlineMark,
28
+ resolveCoord,
29
+ selectedIndicesFor,
30
+ selectionActive,
31
+ SELECTION_DIM_ALPHA,
32
+ wrapMark,
33
+ } from "./_mark.ts";
34
+ import { DEFAULT_OVERLAY_SCALE } from "../constants.ts";
35
+
36
+ export interface PointChannels<T> {
37
+ x: Aes<T, number | Date>;
38
+ y: Aes<T, number | Date>;
39
+ color?: Aes<T, unknown>;
40
+ size?: Aes<T, number>;
41
+ /**
42
+ * Categorical shape mapping. Values are looked up in the active shape scale
43
+ * — by default a built-in palette (`POINT_SHAPE_PALETTE`). Pass a literal
44
+ * `PointShapeKind` accessor and `.scale("shape", { palette: [...] })` to
45
+ * customize. When the value is already a `PointShapeKind`, it's used as-is.
46
+ */
47
+ shape?: Aes<T, unknown>;
48
+ alpha?: Aes<T, number>;
49
+ /**
50
+ * Per-datum border treatment. The accessor may return either a resolved
51
+ * {@link PointBorderStyle} (used as-is) or any other categorical value that
52
+ * gets routed through the active `borderStyle` scale (default palette:
53
+ * `solid`, `open`, `dashed`, `dotted`). Configure a custom mapping with
54
+ * `.scale("borderStyle", { palette: [...] })`.
55
+ */
56
+ borderStyle?: Aes<T, unknown>;
57
+ /**
58
+ * Optional secondary glyph overlaid on the base shape at the same anchor.
59
+ * Accepts either a resolved {@link PointShapeKind}/`null`, or a categorical
60
+ * value routed through the `overlayGlyph` scale (default palette starts
61
+ * with `null` so the most-common category gets no overlay). Configure with
62
+ * `.scale("overlayGlyph", { palette: [...] })`.
63
+ */
64
+ overlayGlyph?: Aes<T, unknown>;
65
+ /** Overlay radius as a fraction of base radius. Default `0.6`. */
66
+ overlayScale?: Aes<T, number>;
67
+ }
68
+
69
+ export interface PointOptions {
70
+ /** Constant fill if color channel is absent. */
71
+ fill?: Color;
72
+ /** Constant point radius if size channel is absent. */
73
+ radius?: number;
74
+ /** Override theme stroke. */
75
+ stroke?: Color | null;
76
+ /** Override theme stroke width. */
77
+ strokeWidth?: number;
78
+ /** Constant shape if shape channel is absent. */
79
+ shape?: PointShapeKind;
80
+ /** Constant border style if border-style channel is absent. */
81
+ borderStyle?: PointBorderStyle;
82
+ /** Constant overlay glyph if overlay-glyph channel is absent. */
83
+ overlayGlyph?: PointShapeKind | null;
84
+ /** Constant overlay scale if overlay-scale channel is absent. */
85
+ overlayScale?: number;
86
+ /** Display label for legend (defaults to color column name). */
87
+ label?: string;
88
+ }
89
+
90
+ const isBorderStyleLiteral = (v: unknown): v is PointBorderStyle =>
91
+ v === "solid" || v === "dashed" || v === "dotted" || v === "open";
92
+ const isPointShapeLiteral = (v: unknown): v is PointShapeKind => {
93
+ if (typeof v !== "string") return false;
94
+ return (
95
+ v === "circle" ||
96
+ v === "square" ||
97
+ v === "triangle" ||
98
+ v === "diamond" ||
99
+ v === "circle-open" ||
100
+ v === "square-open" ||
101
+ v === "triangle-open" ||
102
+ v === "diamond-open" ||
103
+ v === "cross" ||
104
+ v === "plus" ||
105
+ v === "star"
106
+ );
107
+ };
108
+
109
+ /**
110
+ * Resolve the per-datum visual functions shared by `compile` (which wraps
111
+ * `radius` / `fill` / projection in transition lerps) and `hoverDecoration`
112
+ * (which re-emits a single glyph onto the overlay). Centralizing keeps the two
113
+ * paths from drifting on shape / border / overlay / fill resolution. The
114
+ * returned `radius` / `fill` are the *static* (non-animated) values; `compile`
115
+ * layers transition interpolation on top.
116
+ */
117
+ function pointVisuals<T>(
118
+ channels: PointChannels<T>,
119
+ options: PointOptions,
120
+ ctx: CompileContext<T>,
121
+ ) {
122
+ const { scales, theme } = ctx;
123
+ const coord = resolveCoord(ctx);
124
+ const xAes = resolveAes<T, unknown>(channels.x as Aes<T, unknown>);
125
+ const yAes = resolveAes<T, unknown>(channels.y as Aes<T, unknown>);
126
+ const colorAes = channels.color ? resolveAes<T, unknown>(channels.color) : undefined;
127
+ const sizeAes = channels.size ? resolveAes<T, number>(channels.size) : undefined;
128
+ const alphaAes = channels.alpha ? resolveAes<T, number>(channels.alpha) : undefined;
129
+ const shapeAes = channels.shape ? resolveAes<T, unknown>(channels.shape) : undefined;
130
+ const borderStyleAes = channels.borderStyle
131
+ ? resolveAes<T, unknown>(channels.borderStyle)
132
+ : undefined;
133
+ const overlayGlyphAes = channels.overlayGlyph
134
+ ? resolveAes<T, unknown>(channels.overlayGlyph)
135
+ : undefined;
136
+ const overlayScaleAes = channels.overlayScale
137
+ ? resolveAes<T, number>(channels.overlayScale)
138
+ : undefined;
139
+
140
+ const baseFill: Color = options.fill ?? defaultMarkFill(theme);
141
+ const baseRadius = options.radius ?? theme.marks.pointRadius;
142
+ const baseShape: PointShapeKind = options.shape ?? "circle";
143
+ const stroke =
144
+ options.stroke === null ? undefined : (options.stroke ?? theme.marks.pointStroke ?? undefined);
145
+ const strokeWidth = options.strokeWidth ?? theme.marks.pointStrokeWidth;
146
+
147
+ const xScale = scales.x.fn;
148
+ const yScale = scales.y.fn;
149
+ const colorScale = scales.color?.fn;
150
+ const sizeScale = scales.size?.fn;
151
+ const alphaScale = scales.alpha?.fn;
152
+ const shapeScale = scales.shape?.fn;
153
+ const borderStyleScale = scales.borderStyle?.fn;
154
+ const overlayGlyphScale = scales.overlayGlyph?.fn;
155
+
156
+ const fill = (d: T, i: number): Color => {
157
+ const base: Color = colorAes && colorScale ? colorScale(colorAes.fn(d, i)) : baseFill;
158
+ if (alphaAes && alphaScale) return withAlpha(base, alphaScale(alphaAes.fn(d, i)));
159
+ return alphaize(base, theme.marks.fillAlpha);
160
+ };
161
+ const radius = (d: T, i: number): number =>
162
+ sizeAes && sizeScale ? (sizeScale(sizeAes.fn(d, i)) as number) || baseRadius : baseRadius;
163
+ const shape: (d: T, i: number) => PointShapeKind =
164
+ shapeAes && shapeScale
165
+ ? (d, i) => shapeScale(shapeAes.fn(d, i)) as PointShapeKind
166
+ : shapeAes
167
+ ? (d, i) => shapeAes.fn(d, i) as PointShapeKind
168
+ : () => baseShape;
169
+ const borderStyle: (d: T, i: number) => PointBorderStyle = borderStyleAes
170
+ ? (d, i) => {
171
+ const val = borderStyleAes.fn(d, i);
172
+ if (isBorderStyleLiteral(val)) return val;
173
+ return borderStyleScale ? (borderStyleScale(val) as PointBorderStyle) : "solid";
174
+ }
175
+ : () => options.borderStyle ?? "solid";
176
+ const overlayGlyph: (d: T, i: number) => PointShapeKind | null = overlayGlyphAes
177
+ ? (d, i) => {
178
+ const val = overlayGlyphAes.fn(d, i);
179
+ if (val === null || val === undefined) return null;
180
+ if (isPointShapeLiteral(val)) return val;
181
+ return overlayGlyphScale ? (overlayGlyphScale(val) as PointShapeKind | null) : null;
182
+ }
183
+ : () => options.overlayGlyph ?? null;
184
+ const overlayScale: (d: T, i: number) => number = overlayScaleAes
185
+ ? (d, i) => overlayScaleAes.fn(d, i)
186
+ : () => options.overlayScale ?? DEFAULT_OVERLAY_SCALE;
187
+
188
+ return {
189
+ coord,
190
+ xAes,
191
+ yAes,
192
+ colorAes,
193
+ sizeAes,
194
+ colorScale,
195
+ sizeScale,
196
+ baseFill,
197
+ baseRadius,
198
+ stroke,
199
+ strokeWidth,
200
+ xScale,
201
+ yScale,
202
+ fill,
203
+ radius,
204
+ shape,
205
+ borderStyle,
206
+ overlayGlyph,
207
+ overlayScale,
208
+ };
209
+ }
210
+
211
+ export function point<T>(channels: PointChannels<T>, options: PointOptions = {}): Geom<T> {
212
+ return {
213
+ kind: "point",
214
+ channels,
215
+ label: options.label,
216
+ compile(ctx: CompileContext<T>) {
217
+ const { data, plot } = ctx;
218
+ const v = pointVisuals(channels, options, ctx);
219
+ const {
220
+ coord,
221
+ xAes,
222
+ yAes,
223
+ colorAes,
224
+ sizeAes,
225
+ colorScale,
226
+ sizeScale,
227
+ baseFill,
228
+ baseRadius,
229
+ stroke,
230
+ strokeWidth,
231
+ xScale,
232
+ yScale,
233
+ fill: baseFillFor,
234
+ shape: shapeFn,
235
+ borderStyle: borderStyleFn,
236
+ overlayGlyph: overlayGlyphFn,
237
+ overlayScale: overlayScaleFn,
238
+ } = v;
239
+
240
+ // Selection dimming — when any row is selected anywhere, non-matching
241
+ // rows on this geom drop to SELECTION_DIM_ALPHA. Rows in `selectedSet`
242
+ // keep their natural alpha and pick up a stroke ring further down.
243
+ const selectedSet = selectedIndicesFor(ctx, "point");
244
+ const dim = selectionActive(ctx);
245
+ const fillFn = dim
246
+ ? (d: T, i: number): Color => {
247
+ const c = baseFillFor(d, i);
248
+ return selectedSet?.has(i) ? c : alphaize(c, SELECTION_DIM_ALPHA);
249
+ }
250
+ : baseFillFor;
251
+
252
+ const isHidden = (d: T, i: number) => {
253
+ if (!ctx.hidden || !colorAes) return false;
254
+ const val = colorAes.fn(d, i);
255
+ return ctx.hidden.has(String(val));
256
+ };
257
+
258
+ const anim = ctx.activeTransition;
259
+ const fromIndexFor = (d: T, i: number) =>
260
+ anim
261
+ ? ctx.transitionKey
262
+ ? anim.matchIndex(ctx.transitionKey(d, i), i)
263
+ : anim.matchIndex(String(i), i)
264
+ : undefined;
265
+
266
+ // Animated fill: lerp from the stored RGBA toward the current fill.
267
+ const animFill = anim
268
+ ? (d: T, i: number): Color => {
269
+ const toFill = fillFn(d, i);
270
+ const fromIndex = fromIndexFor(d, i);
271
+ if (fromIndex === undefined) return { ...toFill, a: toFill.a * anim.t };
272
+ const fromFill: Color = {
273
+ r: anim.from.rgba[fromIndex * 4]!,
274
+ g: anim.from.rgba[fromIndex * 4 + 1]!,
275
+ b: anim.from.rgba[fromIndex * 4 + 2]!,
276
+ a: anim.from.rgba[fromIndex * 4 + 3]!,
277
+ };
278
+ return lerpColor(fromFill, toFill, anim.t);
279
+ }
280
+ : fillFn;
281
+
282
+ // Combined per-datum projection through the active coord. Under
283
+ // `coordCartesian()` `project` is the identity, so this returns the
284
+ // same plot-frame pixel pair we used to emit directly. Under polar
285
+ // (Phase 3) the (x, y) need to be projected as a single point — we
286
+ // compute both axes' lerped values and then project once per axis.
287
+ const projectXY = (d: T, i: number): { x: number; y: number } => {
288
+ if (isHidden(d, i)) return { x: NaN, y: NaN };
289
+ const toX = xScale(xAes.fn(d, i));
290
+ const toY = yScale(yAes.fn(d, i));
291
+ const fromIndex = fromIndexFor(d, i);
292
+ let px = toX;
293
+ let py = toY;
294
+ if (anim && fromIndex !== undefined) {
295
+ if (Number.isFinite(anim.from.x[fromIndex]!)) {
296
+ px = anim.from.x[fromIndex]! + (toX - anim.from.x[fromIndex]!) * anim.t;
297
+ }
298
+ if (Number.isFinite(anim.from.y[fromIndex]!)) {
299
+ py = anim.from.y[fromIndex]! + (toY - anim.from.y[fromIndex]!) * anim.t;
300
+ }
301
+ }
302
+ return coord.project({ x: px, y: py });
303
+ };
304
+ // Animated radius: lerp from the stored radius toward the current size.
305
+ const radiusFn: (d: T, i: number) => number =
306
+ sizeAes && sizeScale
307
+ ? (d, i) => {
308
+ const toR = v.radius(d, i);
309
+ if (!anim) return toR;
310
+ const fromIndex = fromIndexFor(d, i);
311
+ if (fromIndex === undefined) return toR * anim.t;
312
+ const fromR = anim.from.r?.[fromIndex] ?? toR;
313
+ return fromR + (toR - fromR) * anim.t;
314
+ }
315
+ : () => baseRadius;
316
+
317
+ const mark = pointMark(data, {
318
+ x: (d, i) => projectXY(d, i).x,
319
+ y: (d, i) => projectXY(d, i).y,
320
+ radius: radiusFn,
321
+ fill: animFill,
322
+ stroke,
323
+ strokeWidth,
324
+ shape: shapeFn,
325
+ borderStyle: borderStyleFn,
326
+ overlayGlyph: overlayGlyphFn,
327
+ overlayScale: overlayScaleFn,
328
+ });
329
+
330
+ const builders: MarkBuilder[] = [wrapMark(mark, plot.topLeft, data.length)];
331
+
332
+ // Hover focus (contrast halo + bring-to-front) is NOT drawn here — under
333
+ // partial redraw the marks layer is baked and never recompiles on hover,
334
+ // so it would never appear. It rides the cheap overlay path instead; see
335
+ // `hoverDecoration` below. Points also intentionally do not dim the rest
336
+ // on hover (sparse marks read fine with just the ring); only selection
337
+ // dims (see `dim` above).
338
+
339
+ // Selection rings — a stroke ring on each selected row.
340
+ if (selectedSet) {
341
+ for (const i of selectedSet) {
342
+ const d = data[i];
343
+ if (d === undefined) continue;
344
+ const xv = xAes.fn(d, i);
345
+ const yv = yAes.fn(d, i);
346
+ if (xv == null || yv == null) continue;
347
+ const rawX = xScale(xv);
348
+ const rawY = yScale(yv);
349
+ if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) continue;
350
+ const projected = coord.project({ x: rawX, y: rawY });
351
+ const r =
352
+ sizeAes && sizeScale ? Number(sizeScale(sizeAes.fn(d, i))) || baseRadius : baseRadius;
353
+ const ringColor: Color =
354
+ colorAes && colorScale ? colorScale(colorAes.fn(d, i)) : baseFill;
355
+ const cx = plot.topLeft.x + projected.x;
356
+ const cy = plot.topLeft.y + projected.y;
357
+ builders.push(inlineMark((layer) => haloRing(layer, cx, cy, r + 4, ringColor, 2)));
358
+ }
359
+ }
360
+
361
+ return builders;
362
+ },
363
+ hoverDecoration(ctx: CompileContext<T>): GeomHoverDecorator | null {
364
+ const { data, plot } = ctx;
365
+ if (data.length === 0) return null;
366
+ const v = pointVisuals(channels, options, ctx);
367
+ const hoverCfg = ctx.theme.interactions.hover;
368
+ const ringColor: Color = hoverCfg.haloColor ?? ctx.theme.text.color;
369
+ const origin = plot.topLeft;
370
+ return {
371
+ geomKind: "point",
372
+ data,
373
+ decorate(hit: HoveredHit, layer: Layer): void {
374
+ if (!hoverCfg.enabled || hit.data !== data) return;
375
+ const i = hit.dataIndex;
376
+ const d = data[i];
377
+ if (d === undefined) return;
378
+ const xv = v.xAes.fn(d, i);
379
+ const yv = v.yAes.fn(d, i);
380
+ if (xv == null || yv == null) return;
381
+ const rawX = v.xScale(xv);
382
+ const rawY = v.yScale(yv);
383
+ if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) return;
384
+ const p = v.coord.project({ x: rawX, y: rawY });
385
+ const r = v.radius(d, i);
386
+ // Contrast ring sits over the marks; the re-emitted glyph then draws
387
+ // on top so a point tucked under the trend line or behind a neighbor
388
+ // stays fully visible while focused. Un-dimmed fill keeps it vivid.
389
+ haloRing(
390
+ layer,
391
+ origin.x + p.x,
392
+ origin.y + p.y,
393
+ r + 4,
394
+ ringColor,
395
+ hoverCfg.haloStrokeWidth,
396
+ );
397
+ pointMark([d], {
398
+ x: () => p.x,
399
+ y: () => p.y,
400
+ radius: () => r,
401
+ fill: () => v.fill(d, i),
402
+ stroke: v.stroke,
403
+ strokeWidth: v.strokeWidth,
404
+ shape: () => v.shape(d, i),
405
+ borderStyle: () => v.borderStyle(d, i),
406
+ overlayGlyph: () => v.overlayGlyph(d, i),
407
+ overlayScale: () => v.overlayScale(d, i),
408
+ }).addTo(layer, origin);
409
+ },
410
+ };
411
+ },
412
+ compileHitTest(ctx: CompileContext<T>): CompiledHitTest<T> | null {
413
+ const { data, scales, plot, theme, hidden } = ctx;
414
+ if (data.length === 0) return null;
415
+ const coord = resolveCoord(ctx);
416
+
417
+ const xAes = resolveAes<T, unknown>(channels.x as Aes<T, unknown>);
418
+ const yAes = resolveAes<T, unknown>(channels.y as Aes<T, unknown>);
419
+ const colorAes = channels.color ? resolveAes<T, unknown>(channels.color) : undefined;
420
+ const sizeAes = channels.size
421
+ ? (resolveAes<T, number>(channels.size) as unknown as ResolvedAesUnknown<T>)
422
+ : undefined;
423
+ const shapeAes = channels.shape ? resolveAes<T, unknown>(channels.shape) : undefined;
424
+ const alphaAes = channels.alpha
425
+ ? (resolveAes<T, number>(channels.alpha) as unknown as ResolvedAesUnknown<T>)
426
+ : undefined;
427
+
428
+ const xScale = scales.x.fn;
429
+ const yScale = scales.y.fn;
430
+ const sizeScale = scales.size?.fn;
431
+ const baseRadius = options.radius ?? theme.marks.pointRadius;
432
+
433
+ const positions = new Float32Array(data.length * 2);
434
+ const dataIndex = new Int32Array(data.length);
435
+ const ox = plot.topLeft.x;
436
+ const oy = plot.topLeft.y;
437
+ let n = 0;
438
+ let maxRadius = baseRadius;
439
+ for (let i = 0; i < data.length; i++) {
440
+ const d = data[i]!;
441
+ const xv = xAes.fn(d, i);
442
+ const yv = yAes.fn(d, i);
443
+ if (xv == null || yv == null) continue;
444
+
445
+ if (hidden && colorAes && hidden.has(String(colorAes.fn(d, i)))) {
446
+ continue;
447
+ }
448
+
449
+ const rawX = xScale(xv);
450
+ const rawY = yScale(yv);
451
+ if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) continue;
452
+ const projected = coord.project({ x: rawX, y: rawY });
453
+ positions[n * 2] = ox + projected.x;
454
+ positions[n * 2 + 1] = oy + projected.y;
455
+ dataIndex[n] = i;
456
+ n++;
457
+ if (sizeAes && sizeScale) {
458
+ const r = sizeScale(sizeAes.fn(d, i));
459
+ if (typeof r === "number" && r > maxRadius) maxRadius = r;
460
+ }
461
+ }
462
+ if (n === 0) return null;
463
+
464
+ const channelsMap: ResolvedChannelMap<T> = {
465
+ x: xAes,
466
+ y: yAes,
467
+ color: colorAes,
468
+ size: sizeAes as ResolvedAesUnknown<T> | undefined,
469
+ shape: shapeAes,
470
+ alpha: alphaAes as ResolvedAesUnknown<T> | undefined,
471
+ };
472
+
473
+ return {
474
+ geomKind: "point",
475
+ label: options.label,
476
+ positions: positions.subarray(0, n * 2),
477
+ dataIndex: dataIndex.subarray(0, n),
478
+ pickRadius: Math.max(8, maxRadius + 4),
479
+ channels: channelsMap,
480
+ data,
481
+ };
482
+ },
483
+ captureFrame(ctx: CompileContext<T>): GeomFrame | null {
484
+ const { data, scales, theme } = ctx;
485
+ if (data.length === 0) return null;
486
+
487
+ const xAes = resolveAes<T, unknown>(channels.x as Aes<T, unknown>);
488
+ const yAes = resolveAes<T, unknown>(channels.y as Aes<T, unknown>);
489
+ const colorAes = channels.color ? resolveAes<T, unknown>(channels.color) : undefined;
490
+ const sizeAes = channels.size ? resolveAes<T, number>(channels.size) : undefined;
491
+ const alphaAes = channels.alpha ? resolveAes<T, number>(channels.alpha) : undefined;
492
+
493
+ const xScale = scales.x.fn;
494
+ const yScale = scales.y.fn;
495
+ const colorScale = scales.color?.fn;
496
+ const sizeScale = scales.size?.fn;
497
+ const alphaScale = scales.alpha?.fn;
498
+
499
+ const baseRadius = options.radius ?? theme.marks.pointRadius;
500
+ const baseFill: Color = options.fill ?? defaultMarkFill(theme);
501
+
502
+ const count = data.length;
503
+ const x = new Float32Array(count);
504
+ const y = new Float32Array(count);
505
+ const rgba = new Float32Array(count * 4);
506
+ const a = new Float32Array(count);
507
+ const r = new Float32Array(count);
508
+ const transitionKey = ctx.transitionKey;
509
+ const ids = transitionKey ? Array.from<string>({ length: count }) : undefined;
510
+
511
+ for (let i = 0; i < count; i++) {
512
+ const d = data[i]!;
513
+ const xv = xAes.fn(d, i);
514
+ const yv = yAes.fn(d, i);
515
+ const px = xScale(xv);
516
+ const py = yScale(yv);
517
+ x[i] = Number.isFinite(px) ? px : NaN;
518
+ y[i] = Number.isFinite(py) ? py : NaN;
519
+
520
+ // Radius
521
+ r[i] =
522
+ sizeAes && sizeScale ? (sizeScale(sizeAes.fn(d, i)) as number) || baseRadius : baseRadius;
523
+
524
+ // Color with alpha
525
+ const c: Color = colorAes && colorScale ? colorScale(colorAes.fn(d, i)) : baseFill;
526
+ const alpha =
527
+ alphaAes && alphaScale
528
+ ? (alphaScale(alphaAes.fn(d, i)) as number)
529
+ : theme.marks.fillAlpha;
530
+ rgba[i * 4] = c.r;
531
+ rgba[i * 4 + 1] = c.g;
532
+ rgba[i * 4 + 2] = c.b;
533
+ rgba[i * 4 + 3] = c.a * alpha;
534
+ a[i] = alpha;
535
+ if (ids && transitionKey) ids[i] = transitionKey(d, i);
536
+ }
537
+
538
+ return { count, x, y, rgba, a, r, ids };
539
+ },
540
+ };
541
+ }
542
+
543
+ // Local alias to keep the cast site readable. SizeAes/AlphaAes resolve as
544
+ // `ResolvedAes<T, number>` but the channel map types them as <T, unknown>.
545
+ type ResolvedAesUnknown<T> = import("../aes.ts").ResolvedAes<T, unknown>;