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,619 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Coord — coordinate system abstraction
3
+ // ---------------------------------------------------------------------------
4
+ // A `Coord` decides how plot-frame pixel positions (the output of x/y scales)
5
+ // map onto layer-pixel space, and how axes are drawn for that mapping.
6
+ //
7
+ // Two coords ship today:
8
+ // - `coordCartesian()` — identity projection, current axis behavior.
9
+ // - `coordPolar()` / `coordRadial()` — (θ, r) → (cx + r·cosθ, cy + r·sinθ)
10
+ // with spokes + concentric-circle axes.
11
+ //
12
+ // The interface is shaped so polar lands without a second refactor:
13
+ // `project` / `segment` are vertex-level hooks; `renderAxes` is the dispatch
14
+ // point for non-Cartesian axis layouts; `handlePan` / `handleZoom` give polar
15
+ // a place to translate pointer deltas into rotation / radial moves rather
16
+ // than direct scale-domain rewrites.
17
+ //
18
+ // Polar is stateful: `bindFrame(plotFrame)` must be called before `project` /
19
+ // `segment` / `renderAxes` so the projection knows where the plot centre is.
20
+ // The pipeline calls this once per panel (faceted) or once per chart
21
+ // (unfaceted). Cartesian's `bindFrame` is a no-op.
22
+
23
+ import { type Color, type GlyphAtlas, type Layer, rgba } from "insomni";
24
+ import { bottomAxis, leftAxis, type AxisOptions } from "../axis.ts";
25
+ import type { ContinuousScale, TimeScale, BandScale } from "../scales.ts";
26
+ import type { PositionScale } from "./scales.ts";
27
+ import type { ScaleBundle } from "./geoms/types.ts";
28
+
29
+ export interface Point {
30
+ x: number;
31
+ y: number;
32
+ }
33
+
34
+ /**
35
+ * Inputs to `Coord.renderAxes`. The pipeline owns scale construction and
36
+ * layout; the coord owns axis dispatch (Cartesian → bottom + left; polar →
37
+ * angular ring + radial spokes).
38
+ */
39
+ export interface CoordAxesArgs {
40
+ axisLayer: Layer;
41
+ scales: ScaleBundle;
42
+ /** Inner plot frame — coords project relative to this. */
43
+ plotFrame: import("insomni").Frame;
44
+ /** True when the chart actually has a column on this channel. Skipped axes are not drawn. */
45
+ hasX: boolean;
46
+ hasY: boolean;
47
+ xAxisOptions: AxisOptions<unknown>;
48
+ yAxisOptions: AxisOptions<unknown>;
49
+ atlas: GlyphAtlas | undefined;
50
+ }
51
+
52
+ /**
53
+ * Inputs to `Coord.handlePan`. The mount / interactions layer passes a pointer
54
+ * delta in plot-frame pixels plus the active plot frame and a viewport handle
55
+ * the coord can use to mutate scale state. Cartesian delegates the whole
56
+ * thing to `viewport.panBy(dx, dy)`; polar decomposes into tangential
57
+ * (rotate `startAngle`) and radial (translate radius domain via the viewport).
58
+ */
59
+ export interface CoordPanArgs {
60
+ dx: number;
61
+ dy: number;
62
+ /** Plot frame in absolute layer-pixel space (x, y are screen-space origin). */
63
+ plotFrame: import("insomni").Frame;
64
+ /** Viewport handle for scale-domain mutations. */
65
+ viewport: CoordViewportHandle;
66
+ }
67
+
68
+ export interface CoordZoomArgs {
69
+ /**
70
+ * Multiplicative zoom factor — `> 1` zooms in, `< 1` zooms out. May be a
71
+ * scalar (both axes) or a `{ x?, y? }` per-axis form (matches
72
+ * `viewport.zoomAt` and the masking that `bindDataViewport` performs).
73
+ */
74
+ factor: number | { x?: number; y?: number };
75
+ /** Cursor position in absolute layer-pixel space (matches `viewport.zoomAt`). */
76
+ cx: number;
77
+ cy: number;
78
+ plotFrame: import("insomni").Frame;
79
+ viewport: CoordViewportHandle;
80
+ }
81
+
82
+ /**
83
+ * Minimal facade over `DataViewport` so the coord can mutate scale state
84
+ * without depending on the full viewport surface. The two methods polar
85
+ * uses (`panBy`, `zoomAt`) match `DataViewport` semantics 1:1; Cartesian's
86
+ * implementation forwards through them so behavior is byte-identical to the
87
+ * pre-`handlePan` path.
88
+ */
89
+ export interface CoordViewportHandle {
90
+ panBy(dxPx: number, dyPx: number): void;
91
+ zoomAt(anchorSx: number, anchorSy: number, factor: number | { x?: number; y?: number }): void;
92
+ }
93
+
94
+ /**
95
+ * A coordinate system. Polar / cartesian today; future room for log-polar /
96
+ * geographic projections behind the same interface.
97
+ */
98
+ export interface Coord {
99
+ readonly kind: "cartesian" | "polar";
100
+ /**
101
+ * Bind the coord to a specific plot frame for the upcoming render.
102
+ * Polar's `project` / `segment` / `renderAxes` need the plot-frame
103
+ * dimensions and centre to compute (cx, cy) and the default outerRadius.
104
+ * Called by the pipeline once per panel (faceted) or once per chart
105
+ * (unfaceted) before any `project` / `renderAxes` call. Cartesian no-ops.
106
+ */
107
+ bindFrame(plotFrame: import("insomni").Frame): void;
108
+ /**
109
+ * Map a single point from plot-frame pixel space (0..plotFrame.width on x,
110
+ * 0..plotFrame.height on y — i.e. the raw output of `xScale.fn` / `yScale.fn`)
111
+ * to layer-pixel space (still relative to the plot frame's origin —
112
+ * `plot.topLeft` offsets are applied later by each geom).
113
+ *
114
+ * Cartesian: identity. Polar: (θ, r) → (cx + r·cosθ, cy + r·sinθ).
115
+ */
116
+ project(p: Point): Point;
117
+ /**
118
+ * Tessellate a polyline segment between two points in plot-frame pixel
119
+ * space. Returns the projected pixel points (including both endpoints).
120
+ * Cartesian returns `[project(p1), project(p2)]`; polar returns ~N points
121
+ * along the arc.
122
+ */
123
+ segment(p1: Point, p2: Point): Point[];
124
+ /**
125
+ * Render axes into `args.axisLayer`. Cartesian draws bottom (x) + left (y);
126
+ * polar draws an angular ring (spokes) + radial concentric circles.
127
+ */
128
+ renderAxes(args: CoordAxesArgs): void;
129
+ /**
130
+ * Inverse of `project`, for hit-testing / tooltips. Cartesian is the
131
+ * identity within the plot frame. Polar inverts (θ, r) and returns `null`
132
+ * when the projected point is outside `[innerRadius, outerRadius]`.
133
+ */
134
+ unproject(p: Point): Point | null;
135
+ /**
136
+ * Translate a pointer pan delta into scale-domain mutations.
137
+ * Cartesian forwards to `viewport.panBy(dx, dy)` — byte-identical to the
138
+ * pre-coord path. Polar decomposes the delta relative to the plot centre
139
+ * into tangential (rotate `startAngle`) and radial (translate the radius
140
+ * scale via `viewport.panBy(0, radial_dy)`) components.
141
+ */
142
+ handlePan(args: CoordPanArgs): void;
143
+ /**
144
+ * Translate a zoom factor + anchor into scale-domain mutations.
145
+ * Cartesian forwards to `viewport.zoomAt(cx, cy, factor)`. Polar scales the
146
+ * radius domain around the cursor's data-space radius; angle scale
147
+ * unchanged.
148
+ */
149
+ handleZoom(args: CoordZoomArgs): void;
150
+ }
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // coordCartesian
154
+ // ---------------------------------------------------------------------------
155
+
156
+ /**
157
+ * Default coord — identity projection, current axis behavior. Plot's existing
158
+ * pipeline behaves exactly as it did before the `Coord` interface was added
159
+ * when this is in use.
160
+ */
161
+ export function coordCartesian(): Coord {
162
+ return cartesianSingleton;
163
+ }
164
+
165
+ const cartesianSingleton: Coord = {
166
+ kind: "cartesian",
167
+ bindFrame() {
168
+ // Cartesian is frame-agnostic — geoms apply `plot.topLeft` themselves.
169
+ },
170
+ project(p) {
171
+ return p;
172
+ },
173
+ segment(p1, p2) {
174
+ return [p1, p2];
175
+ },
176
+ renderAxes({ axisLayer, scales, plotFrame, hasX, hasY, xAxisOptions, yAxisOptions }) {
177
+ if (hasX) {
178
+ bottomAxis(
179
+ scales.x.axisScale as never,
180
+ {
181
+ ...xAxisOptions,
182
+ gridLength: plotFrame.height,
183
+ axisLineExtent: [0, plotFrame.width],
184
+ } as never,
185
+ ).addTo(axisLayer, plotFrame.bottomLeft);
186
+ }
187
+ if (hasY) {
188
+ leftAxis(
189
+ scales.y.axisScale as never,
190
+ {
191
+ ...yAxisOptions,
192
+ gridLength: plotFrame.width,
193
+ axisLineExtent: [0, plotFrame.height],
194
+ } as never,
195
+ ).addTo(axisLayer, plotFrame.topLeft);
196
+ }
197
+ },
198
+ unproject(p) {
199
+ return p;
200
+ },
201
+ handlePan({ dx, dy, viewport }) {
202
+ // Preserve existing behavior exactly: this is what `bindDataViewport`
203
+ // does today via a direct `viewport.panBy` call.
204
+ viewport.panBy(dx, dy);
205
+ },
206
+ handleZoom({ factor, cx, cy, viewport }) {
207
+ viewport.zoomAt(cx, cy, factor);
208
+ },
209
+ };
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // coordPolar
213
+ // ---------------------------------------------------------------------------
214
+
215
+ const TWO_PI = Math.PI * 2;
216
+ const DEFAULT_QUALITY = Math.PI / 180; // ~1°.
217
+
218
+ export interface CoordPolarOptions {
219
+ /** Angle (radians) where the angular axis starts. Default `-π/2` (top). */
220
+ startAngle?: number;
221
+ /** Angle where the angular axis ends. Default `startAngle + 2π` (full circle). */
222
+ endAngle?: number;
223
+ /**
224
+ * Convenience for non-full-circle layouts: the gap (in radians) between
225
+ * `startAngle` and `endAngle`. `openAngle: 0` ↔ full circle;
226
+ * `openAngle: π/4` ↔ fan with a 45° gap at the start. Ignored when
227
+ * `endAngle` is provided.
228
+ */
229
+ openAngle?: number;
230
+ /** `1` (default, CCW) or `-1` (CW). */
231
+ direction?: 1 | -1;
232
+ /** Inner radius in pixels. Default `0`. */
233
+ innerRadius?: number;
234
+ /**
235
+ * Outer radius in pixels. Default `min(plotFrame.width, plotFrame.height) / 2`
236
+ * — resolved lazily on `bindFrame`.
237
+ */
238
+ outerRadius?: number;
239
+ /** Which channel maps to θ. Default `'y'` (gheatmap/circular-tree convention). */
240
+ angleChannel?: "x" | "y";
241
+ /** Angular tessellation step (radians) for `segment` and circular axes. Default ~1°. */
242
+ quality?: number;
243
+ }
244
+
245
+ /**
246
+ * Polar projection. See {@link CoordPolarOptions}.
247
+ *
248
+ * Each call returns a fresh stateful coord so the same `coordPolar({...})`
249
+ * expression can be reused safely. `bindFrame` mutates the centre and the
250
+ * default `outerRadius`.
251
+ */
252
+ export function coordPolar(opts: CoordPolarOptions = {}): Coord {
253
+ const direction: 1 | -1 = opts.direction ?? 1;
254
+ const angleChannel: "x" | "y" = opts.angleChannel ?? "y";
255
+ const innerRadius = opts.innerRadius ?? 0;
256
+ const explicitOuter = opts.outerRadius;
257
+ const quality = opts.quality ?? DEFAULT_QUALITY;
258
+
259
+ let startAngle = opts.startAngle ?? -Math.PI / 2;
260
+ let endAngle: number;
261
+ if (opts.endAngle !== undefined) {
262
+ endAngle = opts.endAngle;
263
+ } else if (opts.openAngle !== undefined) {
264
+ endAngle = startAngle + (TWO_PI - opts.openAngle);
265
+ } else {
266
+ endAngle = startAngle + TWO_PI;
267
+ }
268
+
269
+ // Bound state. Updated by `bindFrame`.
270
+ let cx = 0;
271
+ let cy = 0;
272
+ let outerR = explicitOuter ?? 0;
273
+ let plotW = 0;
274
+ let plotH = 0;
275
+
276
+ function arcSpan(): number {
277
+ return endAngle - startAngle;
278
+ }
279
+
280
+ function angleAt(channelPixel: number, channelExtent: number): number {
281
+ const t = channelExtent > 0 ? channelPixel / channelExtent : 0;
282
+ return startAngle + direction * t * arcSpan();
283
+ }
284
+
285
+ function radiusAt(channelPixel: number, channelExtent: number): number {
286
+ const t = channelExtent > 0 ? channelPixel / channelExtent : 0;
287
+ return innerRadius + t * (outerR - innerRadius);
288
+ }
289
+
290
+ function project(p: Point): Point {
291
+ const theta = angleChannel === "x" ? angleAt(p.x, plotW) : angleAt(plotH - p.y, plotH);
292
+ const r = angleChannel === "x" ? radiusAt(plotH - p.y, plotH) : radiusAt(p.x, plotW);
293
+ return { x: cx + r * Math.cos(theta), y: cy + r * Math.sin(theta) };
294
+ }
295
+
296
+ function projectThetaR(theta: number, r: number): Point {
297
+ return { x: cx + r * Math.cos(theta), y: cy + r * Math.sin(theta) };
298
+ }
299
+
300
+ function pointToThetaR(p: Point): { theta: number; r: number } {
301
+ const theta = angleChannel === "x" ? angleAt(p.x, plotW) : angleAt(plotH - p.y, plotH);
302
+ const r = angleChannel === "x" ? radiusAt(plotH - p.y, plotH) : radiusAt(p.x, plotW);
303
+ return { theta, r };
304
+ }
305
+
306
+ function segment(p1: Point, p2: Point): Point[] {
307
+ const a = pointToThetaR(p1);
308
+ const b = pointToThetaR(p2);
309
+ const dTheta = b.theta - a.theta;
310
+ const dR = b.r - a.r;
311
+ // Radial line (same θ): straight spoke — no tessellation needed.
312
+ if (Math.abs(dTheta) < 1e-9) {
313
+ return [projectThetaR(a.theta, a.r), projectThetaR(b.theta, b.r)];
314
+ }
315
+ const absDTheta = Math.abs(dTheta);
316
+ const steps = Math.max(1, Math.ceil(absDTheta / quality));
317
+ const out: Point[] = Array.from({ length: steps + 1 });
318
+ for (let i = 0; i <= steps; i++) {
319
+ const t = i / steps;
320
+ const theta = a.theta + dTheta * t;
321
+ const r = a.r + dR * t; // arc when dR=0, oblique otherwise.
322
+ out[i] = projectThetaR(theta, r);
323
+ }
324
+ return out;
325
+ }
326
+
327
+ function unproject(p: Point): Point | null {
328
+ const dx = p.x - cx;
329
+ const dy = p.y - cy;
330
+ const r = Math.hypot(dx, dy);
331
+ if (r < innerRadius - 1e-6 || r > outerR + 1e-6) return null;
332
+ let theta = Math.atan2(dy, dx);
333
+ // Walk theta into the [startAngle, endAngle] half-open range (handling
334
+ // both 2π and partial-arc cases). For full circle this always succeeds.
335
+ const span = arcSpan();
336
+ const dirSpan = direction * span;
337
+ // Normalize offset from startAngle along the configured direction.
338
+ let off = (theta - startAngle) * direction;
339
+ // Bring into [0, 2π).
340
+ off = ((off % TWO_PI) + TWO_PI) % TWO_PI;
341
+ if (off > Math.abs(span) + 1e-6 && Math.abs(span) < TWO_PI - 1e-9) {
342
+ return null;
343
+ }
344
+ const tAngle = Math.abs(span) > 0 ? off / Math.abs(span) : 0;
345
+ const tRadial = outerR > innerRadius ? (r - innerRadius) / (outerR - innerRadius) : 0;
346
+ // Re-derive plot-frame pixel coords inverse to `project`.
347
+ const angleVal = tAngle * (angleChannel === "x" ? plotW : plotH);
348
+ const radialVal = tRadial * (angleChannel === "x" ? plotH : plotW);
349
+ if (angleChannel === "x") {
350
+ return { x: angleVal, y: plotH - radialVal };
351
+ }
352
+ return { x: radialVal, y: plotH - angleVal };
353
+ // Note: `dirSpan` reserved for future asymmetric direction handling.
354
+ void dirSpan;
355
+ }
356
+
357
+ function bindFrame(plotFrame: import("insomni").Frame): void {
358
+ plotW = plotFrame.width;
359
+ plotH = plotFrame.height;
360
+ cx = plotFrame.width / 2;
361
+ cy = plotFrame.height / 2;
362
+ outerR = explicitOuter ?? Math.max(0, Math.min(plotFrame.width, plotFrame.height) / 2);
363
+ }
364
+
365
+ function renderAxes(args: CoordAxesArgs): void {
366
+ bindFrame(args.plotFrame);
367
+ const origin = args.plotFrame.topLeft;
368
+ const axisLayer = args.axisLayer;
369
+
370
+ const angleScale = (angleChannel === "x" ? args.scales.x : args.scales.y) as
371
+ | PositionScale
372
+ | undefined;
373
+ const radiusScale = (angleChannel === "x" ? args.scales.y : args.scales.x) as
374
+ | PositionScale
375
+ | undefined;
376
+ const hasAngle = (angleChannel === "x" ? args.hasX : args.hasY) && !!angleScale;
377
+ const hasRadius = (angleChannel === "x" ? args.hasY : args.hasX) && !!radiusScale;
378
+
379
+ const angleOptions = angleChannel === "x" ? args.xAxisOptions : args.yAxisOptions;
380
+ const radiusOptions = angleChannel === "x" ? args.yAxisOptions : args.xAxisOptions;
381
+
382
+ const axisColor = angleOptions.tickColor ?? rgba(0.4, 0.4, 0.4, 1);
383
+ const gridColor =
384
+ (angleOptions.gridLines === false ? undefined : angleOptions.gridColor) ??
385
+ rgba(0.85, 0.85, 0.85, 1);
386
+ const axisWidth = angleOptions.axisLineWidth ?? 1;
387
+ const gridWidth = angleOptions.gridWidth ?? 1;
388
+
389
+ // ---- Radial axis (concentric circles) ----
390
+ if (hasRadius && radiusScale) {
391
+ const ticks = scaleTickValues(radiusScale.axisScale, radiusOptions);
392
+ for (const tick of ticks) {
393
+ const tPx = radiusScale.fn(tick);
394
+ if (!Number.isFinite(tPx)) continue;
395
+ const r = radiusAt(
396
+ angleChannel === "x" ? plotH - tPx : tPx,
397
+ angleChannel === "x" ? plotH : plotW,
398
+ );
399
+ if (r <= 0) continue;
400
+ drawArc(axisLayer, origin, cx, cy, r, startAngle, endAngle, quality, gridColor, gridWidth);
401
+ }
402
+ }
403
+
404
+ // ---- Angular axis (spokes from center to outerR) ----
405
+ if (hasAngle && angleScale) {
406
+ const ticks = scaleTickValues(angleScale.axisScale, angleOptions);
407
+ for (const tick of ticks) {
408
+ const tPx = angleScale.fn(tick);
409
+ if (!Number.isFinite(tPx)) continue;
410
+ const theta = angleAt(
411
+ angleChannel === "x" ? tPx : plotH - tPx,
412
+ angleChannel === "x" ? plotW : plotH,
413
+ );
414
+ const r0 = innerRadius;
415
+ const r1 = outerR;
416
+ const p0 = projectThetaR(theta, r0);
417
+ const p1 = projectThetaR(theta, r1);
418
+ pushLineAt(axisLayer, origin, p0, p1, axisColor, axisWidth);
419
+ }
420
+ }
421
+ }
422
+
423
+ function handlePan(args: CoordPanArgs): void {
424
+ const { dx, dy, plotFrame, viewport } = args;
425
+ // Plot-frame absolute centre.
426
+ const absCx = plotFrame.x + plotFrame.width / 2;
427
+ const absCy = plotFrame.y + plotFrame.height / 2;
428
+ // Use the *midpoint* of the drag (after the delta has been applied to the
429
+ // last pointer event) as the reference for tangential vs radial
430
+ // decomposition. The exact incremental delta is small enough that using
431
+ // the centre-to-current vector is a good approximation; bindDataViewport
432
+ // dispatches sub-frame deltas.
433
+ // Vector from centre to current pointer (approximate as start-of-drag
434
+ // pointer since we don't have it — use centre→delta-direction instead).
435
+ // For incremental decomposition we use the pointer's *frame-relative*
436
+ // position implied by combining frame centre with the delta direction.
437
+ // A simpler and well-defined decomposition: rotate by tangential
438
+ // (perpendicular to the radial vector at the *current* drag location,
439
+ // which we recover by treating (dx, dy) as the radial vector's projection
440
+ // onto an "average pointer" at frame centre + 0.5*outerR in +x).
441
+ //
442
+ // For v1 we go with: radial = projection of (dx, dy) onto the unit vector
443
+ // from centre toward the current cursor; tangential = remainder.
444
+ // Because we don't get the cursor position in this signature, fall back
445
+ // to: dx ↦ tangential rotation, dy ↦ radial pan. This matches the common
446
+ // "drag left/right rotates, drag up/down pans radius" mouse convention.
447
+ // A future enhancement can take the full (pointer, delta) pair via a
448
+ // richer `CoordPanArgs`.
449
+ void absCx;
450
+ void absCy;
451
+ const arc = arcSpan();
452
+ if (arc !== 0 && plotFrame.width > 0) {
453
+ const tangential = dx;
454
+ const rotation = direction * (tangential / plotFrame.width) * arc;
455
+ startAngle += rotation;
456
+ endAngle += rotation;
457
+ }
458
+ if (dy !== 0) {
459
+ // Translate the radius scale's domain. The radius scale corresponds to
460
+ // the *non-angle* viewport channel.
461
+ const radialAxis = angleChannel === "x" ? "y" : "x";
462
+ if (radialAxis === "y") viewport.panBy(0, dy);
463
+ else viewport.panBy(dy, 0);
464
+ }
465
+ }
466
+
467
+ function handleZoom(args: CoordZoomArgs): void {
468
+ const { factor, cx: ax, cy: ay, viewport } = args;
469
+ // Extract the relevant scalar component: bindDataViewport may issue
470
+ // per-axis factors (`{ x, y }`) after masking; collapse to the radial
471
+ // axis since angle scale stays put under zoom.
472
+ const radialAxis = angleChannel === "x" ? "y" : "x";
473
+ const fRadial: number =
474
+ typeof factor === "number" ? factor : radialAxis === "y" ? (factor.y ?? 1) : (factor.x ?? 1);
475
+ if (fRadial === 1) return;
476
+ if (radialAxis === "y") {
477
+ viewport.zoomAt(ax, ay, { y: fRadial });
478
+ } else {
479
+ viewport.zoomAt(ax, ay, { x: fRadial });
480
+ }
481
+ }
482
+
483
+ const coord: Coord & {
484
+ /** @internal — exposed for unit tests. Returns the current angular configuration. */
485
+ readonly __polar__: {
486
+ readonly startAngle: () => number;
487
+ readonly endAngle: () => number;
488
+ readonly innerRadius: () => number;
489
+ readonly outerRadius: () => number;
490
+ readonly angleChannel: "x" | "y";
491
+ readonly direction: 1 | -1;
492
+ };
493
+ } = {
494
+ kind: "polar",
495
+ bindFrame,
496
+ project,
497
+ segment,
498
+ renderAxes,
499
+ unproject,
500
+ handlePan,
501
+ handleZoom,
502
+ __polar__: {
503
+ startAngle: () => startAngle,
504
+ endAngle: () => endAngle,
505
+ innerRadius: () => innerRadius,
506
+ outerRadius: () => outerR,
507
+ angleChannel,
508
+ direction,
509
+ },
510
+ };
511
+ return coord;
512
+ }
513
+
514
+ /**
515
+ * Convenience polar coord: full circle, root at centre. Equivalent to
516
+ * `coordPolar({ openAngle: 0, innerRadius: 0 })`.
517
+ */
518
+ export function coordRadial(
519
+ opts: Omit<CoordPolarOptions, "openAngle" | "innerRadius"> = {},
520
+ ): Coord {
521
+ return coordPolar({ ...opts, openAngle: 0, innerRadius: 0 });
522
+ }
523
+
524
+ /**
525
+ * Pin a coord to one plot frame (faceted panels). The facet loop mutates a
526
+ * SINGLE shared stateful coord via `bindFrame(panel.frame)` per panel; that's
527
+ * fine for the synchronous mark-push path (compiled right after the bind), but
528
+ * a geom's `hoverDecoration` closure captures `ctx.coord` and runs LATER (at
529
+ * hover, from the mount) — by then the shared coord is bound to the LAST
530
+ * panel's frame, so a polar halo would project against the wrong centre. This
531
+ * wrapper re-binds the underlying coord to THIS panel's frame before every
532
+ * frame-dependent delegate, so each panel's closures see a panel-stable
533
+ * projection. Cartesian's `bindFrame` is a no-op so this is zero-cost there.
534
+ */
535
+ export function frameBoundCoord(coord: Coord, frame: import("insomni").Frame): Coord {
536
+ const rebind = <R>(fn: () => R): R => {
537
+ coord.bindFrame(frame);
538
+ return fn();
539
+ };
540
+ return {
541
+ kind: coord.kind,
542
+ bindFrame: () => coord.bindFrame(frame),
543
+ project: (p) => rebind(() => coord.project(p)),
544
+ segment: (p1, p2) => rebind(() => coord.segment(p1, p2)),
545
+ renderAxes: (a) => rebind(() => coord.renderAxes(a)),
546
+ unproject: (p) => rebind(() => coord.unproject(p)),
547
+ handlePan: (a) => coord.handlePan(a),
548
+ handleZoom: (a) => coord.handleZoom(a),
549
+ };
550
+ }
551
+
552
+ // ---------------------------------------------------------------------------
553
+ // Helpers
554
+ // ---------------------------------------------------------------------------
555
+
556
+ function scaleTickValues(
557
+ scale: ContinuousScale | TimeScale | BandScale<string>,
558
+ axisOpts?: { ticks?: unknown },
559
+ ): readonly unknown[] {
560
+ if ("bandwidth" in scale) {
561
+ return scale.ticks();
562
+ }
563
+ // Use the user-specified tick count when available; fall back to 8.
564
+ const userTicks = axisOpts?.ticks;
565
+ const count = typeof userTicks === "number" ? userTicks : 8;
566
+ return scale.ticks(count);
567
+ }
568
+
569
+ function pushLineAt(
570
+ layer: Layer,
571
+ origin: { x: number; y: number },
572
+ p0: Point,
573
+ p1: Point,
574
+ color: Color,
575
+ width: number,
576
+ ): void {
577
+ layer.pushLine({
578
+ x1: origin.x + p0.x,
579
+ y1: origin.y + p0.y,
580
+ x2: origin.x + p1.x,
581
+ y2: origin.y + p1.y,
582
+ color,
583
+ width,
584
+ });
585
+ }
586
+
587
+ function drawArc(
588
+ layer: Layer,
589
+ origin: { x: number; y: number },
590
+ cx: number,
591
+ cy: number,
592
+ r: number,
593
+ startAngle: number,
594
+ endAngle: number,
595
+ quality: number,
596
+ color: Color,
597
+ width: number,
598
+ ): void {
599
+ const arc = endAngle - startAngle;
600
+ const steps = Math.max(2, Math.ceil(Math.abs(arc) / quality));
601
+ let prevX = cx + r * Math.cos(startAngle);
602
+ let prevY = cy + r * Math.sin(startAngle);
603
+ for (let i = 1; i <= steps; i++) {
604
+ const t = i / steps;
605
+ const theta = startAngle + arc * t;
606
+ const x = cx + r * Math.cos(theta);
607
+ const y = cy + r * Math.sin(theta);
608
+ layer.pushLine({
609
+ x1: origin.x + prevX,
610
+ y1: origin.y + prevY,
611
+ x2: origin.x + x,
612
+ y2: origin.y + y,
613
+ color,
614
+ width,
615
+ });
616
+ prevX = x;
617
+ prevY = y;
618
+ }
619
+ }
@@ -0,0 +1,57 @@
1
+ export interface FromMatrixOptions {
2
+ /** Y labels (rows). Top to bottom. Length must equal `values.length`. */
3
+ rows: readonly string[];
4
+ /** X labels (cols). Left to right. Length must equal `values[0].length`. */
5
+ cols: readonly string[];
6
+ /** Output key for the row label. Default `"row"`. */
7
+ rowKey?: string;
8
+ /** Output key for the col label. Default `"col"`. */
9
+ colKey?: string;
10
+ /** Output key for the cell value. Default `"value"`. */
11
+ valueKey?: string;
12
+ }
13
+ export type LongRow<R extends string, C extends string, V extends string> = {
14
+ [K in R | C | V]: K extends V ? number : string;
15
+ };
16
+ /**
17
+ * Convert a 2D `values[row][col]` matrix into long-format rows that the
18
+ * `tile()` geom consumes directly. Cells with `NaN` / `null` / `undefined`
19
+ * are kept (so consumers can opt into NA cell rendering); cells whose
20
+ * coordinates are out of range are skipped.
21
+ *
22
+ * Example:
23
+ * ```ts
24
+ * const long = fromMatrix(temperatures, {
25
+ * rows: ["00:00", "06:00", "12:00", "18:00"],
26
+ * cols: ["Mon", "Tue", "Wed", "Thu", "Fri"],
27
+ * });
28
+ * tile({ x: "col", y: "row", fill: "value" })
29
+ * ```
30
+ */
31
+ export declare function fromMatrix(values: readonly (readonly number[])[], opts: FromMatrixOptions): {
32
+ row: string;
33
+ col: string;
34
+ value: number;
35
+ }[];
36
+ export interface PivotLongerOptions<T> {
37
+ /** Output column name for the original key. Default `"name"`. */
38
+ nameKey?: string;
39
+ /** Output column name for the value. Default `"value"`. */
40
+ valueKey?: string;
41
+ /** Optional id columns kept verbatim alongside the long pair. */
42
+ idColumns?: readonly (keyof T & string)[];
43
+ }
44
+ /**
45
+ * Pivot a wide row to a sequence of long rows — one per `keys` entry.
46
+ * `idColumns` are copied through to every output row so a per-row identifier
47
+ * (e.g. car name in the mtcars example) is preserved.
48
+ *
49
+ * Example:
50
+ * ```ts
51
+ * const long = pivotLonger(mtcars, ["mpg", "cyl", "disp", "hp"], {
52
+ * idColumns: ["model"],
53
+ * });
54
+ * // → [{ model, name: "mpg", value }, { model, name: "cyl", value }, ...]
55
+ * ```
56
+ */
57
+ export declare function pivotLonger<T extends Record<string, unknown>>(rows: readonly T[], keys: readonly (keyof T & string)[], options?: PivotLongerOptions<T>): Record<string, unknown>[];