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
package/src/axis.d.ts ADDED
@@ -0,0 +1,184 @@
1
+ import { type Color, type GlyphAtlas, type Layer } from "insomni";
2
+ import { type AxisScale, type TickFormatter, type TimeIntervalUnit } from "./scales.ts";
3
+ /**
4
+ * Explicit time interval for axis ticks: render one tick per `step × unit`
5
+ * regardless of how many fit. `quarter` is sugar for `month, step: 3`. Pairs
6
+ * with {@link AxisOptions.ticks} on time scales — ignored on numeric / band
7
+ * scales (those fall back to a default tick count).
8
+ */
9
+ export interface TickIntervalSpec {
10
+ interval: TimeIntervalUnit;
11
+ step?: number;
12
+ }
13
+ /**
14
+ * Tick generation strategy on continuous scales:
15
+ * - `number` — target count (current default behavior).
16
+ * - `"auto"` — derive count from the axis's pixel span (~80px/tick horizontal,
17
+ * ~50px/tick vertical). Cheap proxy for "as many ticks as comfortably fit."
18
+ * - `TickIntervalSpec` — explicit time interval (time scales only).
19
+ *
20
+ * `undefined` keeps the previous default of {@link DEFAULT_TICKS}.
21
+ */
22
+ export type TickSpec = number | "auto" | TickIntervalSpec;
23
+ /**
24
+ * Loose tick formatter signature consumed by the grammar's `AxisSpec.format`.
25
+ * The runtime calls it with the active scale's tick value — `number` for
26
+ * linear/log/sqrt, `Date` for time, `string` for band scales. Typed loose so
27
+ * consumers can write `(v: number) => ...` or `(v: Date) => ...` directly
28
+ * without a wrapping cast; narrow inside the function to the type your scale
29
+ * produces.
30
+ */
31
+ export type AxisTickFormatter = (value: any) => string;
32
+ export interface AxisOrigin {
33
+ x?: number;
34
+ y?: number;
35
+ }
36
+ /**
37
+ * Configuration for axis tick labels. Currently only carries `maxWidth` —
38
+ * existing `labelFontSize` / `labelColor` / etc. live as flat keys on
39
+ * `AxisOptions` for back-compat. New per-label knobs go here.
40
+ */
41
+ export interface AxisLabelConfig {
42
+ /**
43
+ * Maximum width in pixels for any tick label. Labels exceeding this width
44
+ * are truncated to the longest prefix that fits with a trailing ellipsis
45
+ * (`…`). Requires `atlas` to measure text. When unset, labels render at
46
+ * their natural width.
47
+ */
48
+ maxWidth?: number;
49
+ }
50
+ /**
51
+ * Axis title — accepts either a plain string or a config object. The object
52
+ * form lets callers cap the title's rendered width with an ellipsis.
53
+ */
54
+ export type AxisTitle = string | {
55
+ text: string;
56
+ maxWidth?: number;
57
+ };
58
+ export interface AxisOptions<Value> {
59
+ ticks?: TickSpec;
60
+ tickValues?: readonly Value[];
61
+ /**
62
+ * Render shorter, unlabeled minor tick marks between the major ticks.
63
+ * - Number `N`: subdivide each major interval into `N` parts (so `N=2`
64
+ * places one minor halfway between every adjacent major pair).
65
+ * - Array: explicit minor tick values, used as-is.
66
+ *
67
+ * Minor ticks never render labels and don't affect axis measurement.
68
+ */
69
+ minorTicks?: number | readonly Value[];
70
+ /** Length of minor tick marks. Default `tickSize / 2`. */
71
+ minorTickSize?: number;
72
+ /**
73
+ * Render a tick label only every `N`th major tick. Default `1` (every
74
+ * major). Use `2` to label every other tick, etc. The hidden ticks still
75
+ * draw their tick mark and grid line. The first tick is always labeled.
76
+ */
77
+ labelStep?: number;
78
+ /**
79
+ * Automatic label collision avoidance. `"auto"` measures the
80
+ * formatted tick labels and inflates `labelStep` until no two visible
81
+ * labels overlap (with a small padding). Off by default — opt in when
82
+ * the axis density is data-driven (dense band axes, time axes whose
83
+ * domain rescales on zoom). Falls back to `labelStep` when no atlas
84
+ * is available.
85
+ */
86
+ labelCollision?: "auto";
87
+ format?: TickFormatter<Value>;
88
+ atlas?: GlyphAtlas;
89
+ labels?: boolean;
90
+ tickSize?: number;
91
+ tickWidth?: number;
92
+ tickColor?: Color;
93
+ axisLine?: boolean;
94
+ axisLineWidth?: number;
95
+ axisLineColor?: Color;
96
+ /**
97
+ * Override the axis-line extent in pixels along the axis direction. Defaults
98
+ * to the scale's range. Pass `[0, plotFrame.width]` (or `.height`) to draw
99
+ * the spine across the full plot frame even when the data range is inset
100
+ * (framePadding / `prepareRange` reservations).
101
+ */
102
+ axisLineExtent?: readonly [number, number];
103
+ labelPadding?: number;
104
+ labelColor?: Color;
105
+ labelFontSize?: number;
106
+ labelFontFamily?: string;
107
+ labelFontWeight?: string;
108
+ labelFontStyle?: string;
109
+ /** Per-label configuration (currently `maxWidth` for ellipsis truncation). */
110
+ label?: AxisLabelConfig;
111
+ gridLines?: boolean;
112
+ gridLength?: number;
113
+ gridWidth?: number;
114
+ gridColor?: Color;
115
+ /**
116
+ * Axis title. Rendered upright alongside the axis — outside the tick
117
+ * labels for horizontal axes, above the axis line for vertical axes.
118
+ * Requires `atlas`. Skipped when undefined / empty. Pass an object form
119
+ * `{ text, maxWidth }` to cap the title width with ellipsis.
120
+ */
121
+ title?: AxisTitle;
122
+ titlePadding?: number;
123
+ titleFontSize?: number;
124
+ titleFontFamily?: string;
125
+ titleFontWeight?: string;
126
+ titleFontStyle?: string;
127
+ titleColor?: Color;
128
+ }
129
+ export interface TruncateFontOptions {
130
+ fontSize: number;
131
+ fontFamily?: string;
132
+ fontWeight?: string;
133
+ fontStyle?: string;
134
+ }
135
+ /**
136
+ * Truncate `text` so its rendered width fits within `maxWidth`, appending an
137
+ * ellipsis when the original text is too wide. Returns the original text if
138
+ * it already fits or if measurement is impossible. Uses binary search over
139
+ * prefix lengths for O(log n) `measureText` calls.
140
+ */
141
+ export declare function truncateToWidth(atlas: GlyphAtlas | undefined, text: string, maxWidth: number | undefined, font: TruncateFontOptions): string;
142
+ export interface AxisMeasurement {
143
+ /**
144
+ * Total perpendicular space the axis needs. For horizontal axes this is
145
+ * tick size + label height + (title padding + title height when present),
146
+ * since the title sits below/above the labels. For vertical axes this is
147
+ * tick size + label width only — the title for vertical axes is rendered
148
+ * above the axis (parallel to the value range) and reported separately
149
+ * via `axialTitleOverhead` so callers can reserve vertical space for it.
150
+ */
151
+ readonly thickness: number;
152
+ /** Maximum measured width across all formatted tick labels. */
153
+ readonly maxLabelWidth: number;
154
+ /** Tick label height (font size). */
155
+ readonly labelHeight: number;
156
+ /** Whether a title is present and was measured. */
157
+ readonly hasTitle: boolean;
158
+ /** Measured title font size (0 if no title). */
159
+ readonly titleHeight: number;
160
+ /**
161
+ * Pixels the axis title needs *along* the value-range axis, on top of the
162
+ * axis itself. Non-zero only for vertical axes with a title (which is
163
+ * rendered above the axis line). Horizontal axes return 0 — their title's
164
+ * footprint is already included in `thickness`.
165
+ */
166
+ readonly axialTitleOverhead: number;
167
+ }
168
+ export interface AxisBuilder<Value> {
169
+ addTo(layer: Layer, origin?: AxisOrigin): Layer;
170
+ /**
171
+ * Measure how much room the axis will occupy on its perpendicular axis.
172
+ * Use the result as `AXIS_MARGIN.bottom/left/...` so callers don't have to
173
+ * hand-tune padding for long tick labels or axis titles.
174
+ *
175
+ * Requires an atlas — pass either `options.atlas` at construction or one
176
+ * via the `atlas` argument here.
177
+ */
178
+ measure(atlas?: GlyphAtlas): AxisMeasurement;
179
+ readonly tickValues: readonly Value[];
180
+ }
181
+ export declare function bottomAxis<Value>(scale: AxisScale<Value>, options?: AxisOptions<Value>): AxisBuilder<Value>;
182
+ export declare function leftAxis<Value>(scale: AxisScale<Value>, options?: AxisOptions<Value>): AxisBuilder<Value>;
183
+ export declare function topAxis<Value>(scale: AxisScale<Value>, options?: AxisOptions<Value>): AxisBuilder<Value>;
184
+ export declare function rightAxis<Value>(scale: AxisScale<Value>, options?: AxisOptions<Value>): AxisBuilder<Value>;
@@ -0,0 +1,131 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, expect, test } from "vite-plus/test";
3
+
4
+ import { bottomAxis, leftAxis } from "./axis.ts";
5
+ import { bandScale, linearScale, timeScale } from "./scales.ts";
6
+ import { createLayer } from "insomni";
7
+ import { INSTANCE_BYTES } from "insomni/advanced";
8
+
9
+ interface RecordedText {
10
+ text: string;
11
+ x: number;
12
+ y: number;
13
+ align?: "left" | "center" | "right";
14
+ }
15
+
16
+ function makeFakeTextAtlas(): any {
17
+ return {
18
+ measureText(text: string, opts: { fontSize: number }) {
19
+ return { width: text.length * (opts.fontSize ?? 12) * 0.6, height: opts.fontSize ?? 12 };
20
+ },
21
+ };
22
+ }
23
+
24
+ function makeRecordingLayer(): { layer: ReturnType<typeof createLayer>; texts: RecordedText[] } {
25
+ const texts: RecordedText[] = [];
26
+ // v3 has no `textPack` layer option — text shapes route through the layer's
27
+ // `pushText`, which requires an atlas. We mint a layer with a stub atlas and
28
+ // override `pushText` to record the call (and skip the real glyph shaping).
29
+ const layer = createLayer({ space: "ui", atlas: makeFakeTextAtlas() });
30
+ layer.pushText = ((shape: RecordedText) => {
31
+ texts.push({ text: shape.text, x: shape.x, y: shape.y, align: shape.align });
32
+ const bbox = { minX: shape.x, minY: shape.y, maxX: shape.x + 1, maxY: shape.y + 1 };
33
+ return { width: 0, height: 0, layoutHeight: 0, lineCount: 1, bbox, count: 1 };
34
+ }) as unknown as typeof layer.pushText;
35
+ return { layer, texts };
36
+ }
37
+
38
+ describe("axis helpers", () => {
39
+ test("emit axis, tick, and grid line shapes directly into a layer", () => {
40
+ const layer = createLayer({ space: "ui" });
41
+ bottomAxis(linearScale([0, 100], [0, 200]), {
42
+ ticks: 2,
43
+ labels: false,
44
+ gridLines: true,
45
+ gridLength: 50,
46
+ }).addTo(layer, { x: 10, y: 80 });
47
+
48
+ // 1 axis line + 2 ticks + (axis baseline grid?) → 7 stroked segments.
49
+ // v3's `Layer.shapeCount` only counts SDF closed shapes (rect/circle/
50
+ // ellipse); axis lines are segments, so count packed instances directly.
51
+ const instanceCount = layer.pack.byteLength / INSTANCE_BYTES;
52
+ expect(instanceCount).toBe(7);
53
+ });
54
+
55
+ test("draws axis geometry without an atlas (labels skipped, no throw)", () => {
56
+ const layer = createLayer({ space: "ui" });
57
+
58
+ // Without an atlas, labels are skipped but geometry (axis line + ticks +
59
+ // grid lines) still emits — no throw.
60
+ expect(() =>
61
+ bottomAxis(linearScale([0, 1], [0, 10]), {
62
+ ticks: 2,
63
+ gridLines: true,
64
+ gridLength: 50,
65
+ }).addTo(layer, { x: 0, y: 10 }),
66
+ ).not.toThrow();
67
+ expect(layer.pack.byteLength / INSTANCE_BYTES).toBeGreaterThan(0);
68
+ });
69
+
70
+ test("center categorical labels within each band", () => {
71
+ const { layer, texts } = makeRecordingLayer();
72
+ const atlas = makeFakeTextAtlas();
73
+ bottomAxis(bandScale(["A", "B"], [0, 100]), {
74
+ atlas,
75
+ ticks: 2,
76
+ }).addTo(layer, { x: 20, y: 50 });
77
+
78
+ expect(texts).toHaveLength(2);
79
+ expect(texts[0]?.text).toBe("A");
80
+ expect(texts[0]?.x).toBe(45);
81
+ expect(texts[0]?.align).toBe("center");
82
+ expect(texts[1]?.text).toBe("B");
83
+ expect(texts[1]?.x).toBe(95);
84
+ });
85
+
86
+ test("ticks: 'auto' picks count from the axis pixel span", () => {
87
+ const wide = bottomAxis(linearScale([0, 100], [0, 800]), { ticks: "auto" });
88
+ // 800px / 80px-per-tick ≈ 10 → ~10 ticks across the domain (snapped to nice steps).
89
+ expect(wide.tickValues.length).toBeGreaterThanOrEqual(8);
90
+
91
+ const narrow = bottomAxis(linearScale([0, 100], [0, 160]), { ticks: "auto" });
92
+ // 160px / 80px-per-tick ≈ 2 → handful of ticks (nice-snapped).
93
+ expect(narrow.tickValues.length).toBeLessThanOrEqual(wide.tickValues.length);
94
+
95
+ const tall = leftAxis(linearScale([0, 100], [400, 0]), { ticks: "auto" });
96
+ // Vertical orientation uses tighter spacing (~50px/tick).
97
+ expect(tall.tickValues.length).toBeGreaterThanOrEqual(5);
98
+ });
99
+
100
+ test("ticks: { interval } emits explicit time ticks on time scales", () => {
101
+ const scale = timeScale(
102
+ [new Date("2026-01-01T00:00:00Z"), new Date("2026-12-31T00:00:00Z")],
103
+ [0, 600],
104
+ );
105
+ const monthly = bottomAxis(scale, { ticks: { interval: "month" } });
106
+ expect(monthly.tickValues).toHaveLength(12);
107
+
108
+ const quarterly = bottomAxis(scale, { ticks: { interval: "quarter" } });
109
+ expect(quarterly.tickValues).toHaveLength(4);
110
+ });
111
+
112
+ test("ticks: { interval } falls back to default count on non-time scales", () => {
113
+ const numeric = bottomAxis(linearScale([0, 100], [0, 200]), {
114
+ ticks: { interval: "month" },
115
+ });
116
+ // Falls back to DEFAULT_TICKS (5) — numeric `[0, 100]` with step 20.
117
+ expect(numeric.tickValues).toEqual([0, 20, 40, 60, 80, 100]);
118
+ });
119
+
120
+ test("use end-aligned labels for left axes", () => {
121
+ const { layer, texts } = makeRecordingLayer();
122
+ const atlas = makeFakeTextAtlas();
123
+ leftAxis(linearScale([0, 100], [100, 0]), {
124
+ ticks: 2,
125
+ atlas,
126
+ }).addTo(layer, { x: 40, y: 10 });
127
+
128
+ expect(texts.length).toBeGreaterThan(0);
129
+ expect(texts[0]?.align).toBe("right");
130
+ });
131
+ });