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/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "insomni-plot",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Plotting and charting for insomni.",
5
+ "license": "GPL-3.0",
6
+ "files": [
7
+ "dist",
8
+ "src"
9
+ ],
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "types": "./src/index.ts",
13
+ "exports": {
14
+ ".": "./dist/index.mjs",
15
+ "./core": "./dist/core.mjs",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "dependencies": {
22
+ "insomni": "0.2.0-alpha.0"
23
+ },
24
+ "peerDependencies": {
25
+ "typegpu": "^0.10.2"
26
+ },
27
+ "scripts": {
28
+ "build": "vp pack",
29
+ "dev": "vp pack --watch",
30
+ "test": "vp test",
31
+ "test:browser": "vp test -c vite.browser-test.config.ts",
32
+ "check": "vp check"
33
+ }
34
+ }
@@ -0,0 +1,121 @@
1
+ import { type Color, type GlyphAtlas, type Group } from "insomni";
2
+ import { type Accessor, type MarkBuilder, type ValueOrAccessor } from "./marks.ts";
3
+ export type RuleAnchor = "start" | "middle" | "end";
4
+ interface RuleStyle {
5
+ stroke?: Color;
6
+ strokeWidth?: number;
7
+ dashPattern?: readonly number[];
8
+ dashOffset?: number;
9
+ }
10
+ interface RuleLabel {
11
+ label?: string;
12
+ /** Where the label sits along the rule. Default `"end"`. */
13
+ labelAnchor?: RuleAnchor;
14
+ /** Perpendicular offset from the rule (positive = away from the data). Default `6`. */
15
+ labelOffset?: number;
16
+ /** Tangential offset from the chosen anchor (positive = inwards). Default `6`. */
17
+ labelInset?: number;
18
+ labelColor?: Color;
19
+ fontSize?: number;
20
+ fontStyle?: string;
21
+ }
22
+ export interface HorizontalRuleOptions extends RuleStyle, RuleLabel {
23
+ /** Value (in layer-space pixels, post-scale) of the horizontal rule. */
24
+ y: number;
25
+ x?: never;
26
+ /** Range of x covered. Required — typically `[0, plot.width]`. */
27
+ extent: readonly [number, number];
28
+ group?: Group;
29
+ }
30
+ export interface VerticalRuleOptions extends RuleStyle, RuleLabel {
31
+ /** Value (in layer-space pixels, post-scale) of the vertical rule. */
32
+ x: number;
33
+ y?: never;
34
+ /** Range of y covered. Required — typically `[0, plot.height]`. */
35
+ extent: readonly [number, number];
36
+ group?: Group;
37
+ }
38
+ export type RuleMarkOptions = HorizontalRuleOptions | VerticalRuleOptions;
39
+ export declare function ruleMark(options: RuleMarkOptions): MarkBuilder;
40
+ interface BandStyle {
41
+ fill?: Color;
42
+ stroke?: Color;
43
+ strokeWidth?: number;
44
+ /** Draw the two perpendicular edge lines (uses `stroke`). Default `true` when `stroke` set. */
45
+ edges?: boolean;
46
+ }
47
+ interface BandLabel {
48
+ label?: string;
49
+ /** Where the label sits along the band. Default `"middle"`. */
50
+ labelAnchor?: RuleAnchor;
51
+ /** Perpendicular offset into the perpendicular extent (positive = inwards). Default `4`. */
52
+ labelOffset?: number;
53
+ labelColor?: Color;
54
+ fontSize?: number;
55
+ fontStyle?: string;
56
+ }
57
+ export interface VerticalBandOptions extends BandStyle, BandLabel {
58
+ /** Range along the x axis (vertical band — fills between x0 and x1). */
59
+ x: readonly [number, number];
60
+ y?: never;
61
+ /** Required perpendicular extent (typically `[0, plot.height]`). */
62
+ extent: readonly [number, number];
63
+ group?: Group;
64
+ }
65
+ export interface HorizontalBandOptions extends BandStyle, BandLabel {
66
+ /** Range along the y axis (horizontal band). */
67
+ y: readonly [number, number];
68
+ x?: never;
69
+ /** Required perpendicular extent (typically `[0, plot.width]`). */
70
+ extent: readonly [number, number];
71
+ group?: Group;
72
+ }
73
+ export type BandMarkOptions = VerticalBandOptions | HorizontalBandOptions;
74
+ export declare function bandMark(options: BandMarkOptions): MarkBuilder;
75
+ export type ValueLabelAlign = "left" | "center" | "right";
76
+ /**
77
+ * Optional rounded-rectangle background drawn behind label text. When set
78
+ * (and an `atlas` is available for measurement), `valueLabelMark` measures
79
+ * each label and emits a `pushRect` underneath sized to the text bounds plus
80
+ * `padding`. The rect's anchor matches the label's `align` so the text stays
81
+ * visually centered on `(x, y) + offset`.
82
+ */
83
+ export interface LabelBoxStyle {
84
+ fill?: Color;
85
+ stroke?: Color;
86
+ strokeWidth?: number;
87
+ /** Pixels added on each side. Number = uniform; object = per-edge. Default `4`. */
88
+ padding?: number | {
89
+ x?: number;
90
+ y?: number;
91
+ };
92
+ cornerRadius?: number;
93
+ }
94
+ export interface ValueLabelMarkOptions<T> {
95
+ x: Accessor<T, number>;
96
+ y: Accessor<T, number>;
97
+ text: Accessor<T, string>;
98
+ /** Horizontal anchor of the text relative to (`x`,`y`). Default `"center"`. */
99
+ align?: ValueLabelAlign;
100
+ /** Pixel offset from (`x`,`y`). Default `{ x: 0, y: -4 }` (above). */
101
+ offset?: {
102
+ x?: number;
103
+ y?: number;
104
+ };
105
+ color?: ValueOrAccessor<T, Color>;
106
+ fontSize?: number;
107
+ fontStyle?: string;
108
+ /** Skip labels for which this returns false. */
109
+ filter?: Accessor<T, boolean>;
110
+ /** Optional rounded-rect background. */
111
+ box?: LabelBoxStyle;
112
+ /**
113
+ * Glyph atlas used to measure each label's true rendered width and height
114
+ * for `box` sizing. When omitted the box falls back to a coarse character-
115
+ * width estimate that visibly mismatches the glyphs at small font sizes.
116
+ */
117
+ atlas?: GlyphAtlas;
118
+ group?: Group;
119
+ }
120
+ export declare function valueLabelMark<T>(data: readonly T[], options: ValueLabelMarkOptions<T>): MarkBuilder;
121
+ export {};
@@ -0,0 +1,438 @@
1
+ import { BLACK, type Color, type GlyphAtlas, type Group, type Layer } from "insomni";
2
+ import {
3
+ resolveValueOrAccessor as resolve,
4
+ type Accessor,
5
+ type MarkBuilder,
6
+ type MarkOrigin,
7
+ type ValueOrAccessor,
8
+ } from "./marks.ts";
9
+ import { TEXT_WIDTH_FALLBACK_RATIO } from "./format.ts";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // ruleMark — horizontal or vertical reference line, optionally labelled
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export type RuleAnchor = "start" | "middle" | "end";
16
+
17
+ interface RuleStyle {
18
+ stroke?: Color;
19
+ strokeWidth?: number;
20
+ dashPattern?: readonly number[];
21
+ dashOffset?: number;
22
+ }
23
+
24
+ interface RuleLabel {
25
+ label?: string;
26
+ /** Where the label sits along the rule. Default `"end"`. */
27
+ labelAnchor?: RuleAnchor;
28
+ /** Perpendicular offset from the rule (positive = away from the data). Default `6`. */
29
+ labelOffset?: number;
30
+ /** Tangential offset from the chosen anchor (positive = inwards). Default `6`. */
31
+ labelInset?: number;
32
+ labelColor?: Color;
33
+ fontSize?: number;
34
+ fontStyle?: string;
35
+ }
36
+
37
+ export interface HorizontalRuleOptions extends RuleStyle, RuleLabel {
38
+ /** Value (in layer-space pixels, post-scale) of the horizontal rule. */
39
+ y: number;
40
+ x?: never;
41
+ /** Range of x covered. Required — typically `[0, plot.width]`. */
42
+ extent: readonly [number, number];
43
+ group?: Group;
44
+ }
45
+
46
+ export interface VerticalRuleOptions extends RuleStyle, RuleLabel {
47
+ /** Value (in layer-space pixels, post-scale) of the vertical rule. */
48
+ x: number;
49
+ y?: never;
50
+ /** Range of y covered. Required — typically `[0, plot.height]`. */
51
+ extent: readonly [number, number];
52
+ group?: Group;
53
+ }
54
+
55
+ export type RuleMarkOptions = HorizontalRuleOptions | VerticalRuleOptions;
56
+
57
+ function isHorizontalRule(opts: RuleMarkOptions): opts is HorizontalRuleOptions {
58
+ return typeof (opts as HorizontalRuleOptions).y === "number";
59
+ }
60
+
61
+ export function ruleMark(options: RuleMarkOptions): MarkBuilder {
62
+ return {
63
+ length: 1,
64
+ addTo(layer: Layer, origin: MarkOrigin = {}) {
65
+ const ox = origin.x ?? 0;
66
+ const oy = origin.y ?? 0;
67
+ const stroke = options.stroke ?? BLACK;
68
+ const width = options.strokeWidth ?? 1;
69
+ const [e0, e1] = options.extent;
70
+
71
+ const horizontal = isHorizontalRule(options);
72
+ const x1 = horizontal ? ox + e0 : ox + options.x;
73
+ const y1 = horizontal ? oy + options.y : oy + e0;
74
+ const x2 = horizontal ? ox + e1 : ox + options.x;
75
+ const y2 = horizontal ? oy + options.y : oy + e1;
76
+
77
+ if (options.dashPattern && options.dashPattern.length > 0) {
78
+ layer.pushPolyline({
79
+ points: [
80
+ { x: x1, y: y1 },
81
+ { x: x2, y: y2 },
82
+ ],
83
+ color: stroke,
84
+ width,
85
+ dashPattern: options.dashPattern,
86
+ dashOffset: options.dashOffset,
87
+ group: options.group,
88
+ });
89
+ } else {
90
+ layer.pushLine({ x1, y1, x2, y2, color: stroke, width, group: options.group });
91
+ }
92
+
93
+ if (options.label) {
94
+ const fontSize = options.fontSize ?? 11;
95
+ const labelOffset = options.labelOffset ?? 6;
96
+ const labelInset = options.labelInset ?? 6;
97
+ const anchor: RuleAnchor = options.labelAnchor ?? "end";
98
+ const labelColor = options.labelColor ?? stroke;
99
+ const e0o = horizontal ? ox + e0 : oy + e0;
100
+ const e1o = horizontal ? ox + e1 : oy + e1;
101
+ const along =
102
+ anchor === "start"
103
+ ? Math.min(e0o, e1o) + labelInset
104
+ : anchor === "middle"
105
+ ? (e0o + e1o) / 2
106
+ : Math.max(e0o, e1o) - labelInset;
107
+ const align = anchor === "start" ? "left" : anchor === "middle" ? "center" : "right";
108
+
109
+ if (layer.atlas != null) {
110
+ if (horizontal) {
111
+ layer.pushText({
112
+ simple: true,
113
+ text: options.label,
114
+ x: along,
115
+ y: oy + options.y - labelOffset - fontSize,
116
+ fontSize,
117
+ color: labelColor,
118
+ align,
119
+ group: options.group,
120
+ });
121
+ } else {
122
+ layer.pushText({
123
+ simple: true,
124
+ text: options.label,
125
+ x: ox + options.x + labelOffset,
126
+ y: along,
127
+ fontSize,
128
+ color: labelColor,
129
+ align: "left",
130
+ group: options.group,
131
+ });
132
+ }
133
+ }
134
+ }
135
+
136
+ return layer;
137
+ },
138
+ };
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // bandMark — highlighted x or y range, with optional label
143
+ // ---------------------------------------------------------------------------
144
+
145
+ interface BandStyle {
146
+ fill?: Color;
147
+ stroke?: Color;
148
+ strokeWidth?: number;
149
+ /** Draw the two perpendicular edge lines (uses `stroke`). Default `true` when `stroke` set. */
150
+ edges?: boolean;
151
+ }
152
+
153
+ interface BandLabel {
154
+ label?: string;
155
+ /** Where the label sits along the band. Default `"middle"`. */
156
+ labelAnchor?: RuleAnchor;
157
+ /** Perpendicular offset into the perpendicular extent (positive = inwards). Default `4`. */
158
+ labelOffset?: number;
159
+ labelColor?: Color;
160
+ fontSize?: number;
161
+ fontStyle?: string;
162
+ }
163
+
164
+ export interface VerticalBandOptions extends BandStyle, BandLabel {
165
+ /** Range along the x axis (vertical band — fills between x0 and x1). */
166
+ x: readonly [number, number];
167
+ y?: never;
168
+ /** Required perpendicular extent (typically `[0, plot.height]`). */
169
+ extent: readonly [number, number];
170
+ group?: Group;
171
+ }
172
+
173
+ export interface HorizontalBandOptions extends BandStyle, BandLabel {
174
+ /** Range along the y axis (horizontal band). */
175
+ y: readonly [number, number];
176
+ x?: never;
177
+ /** Required perpendicular extent (typically `[0, plot.width]`). */
178
+ extent: readonly [number, number];
179
+ group?: Group;
180
+ }
181
+
182
+ export type BandMarkOptions = VerticalBandOptions | HorizontalBandOptions;
183
+
184
+ function isVerticalBand(opts: BandMarkOptions): opts is VerticalBandOptions {
185
+ return Array.isArray((opts as VerticalBandOptions).x);
186
+ }
187
+
188
+ export function bandMark(options: BandMarkOptions): MarkBuilder {
189
+ return {
190
+ length: 1,
191
+ addTo(layer: Layer, origin: MarkOrigin = {}) {
192
+ const ox = origin.x ?? 0;
193
+ const oy = origin.y ?? 0;
194
+ const [eA, eB] = options.extent;
195
+ const vertical = isVerticalBand(options);
196
+ const [pA, pB] = vertical ? options.x : options.y;
197
+ const xMin = vertical ? ox + Math.min(pA, pB) : ox + Math.min(eA, eB);
198
+ const yMin = vertical ? oy + Math.min(eA, eB) : oy + Math.min(pA, pB);
199
+ const xW = vertical ? Math.abs(pB - pA) : Math.abs(eB - eA);
200
+ const yH = vertical ? Math.abs(eB - eA) : Math.abs(pB - pA);
201
+
202
+ if (options.fill) {
203
+ layer.pushRect({
204
+ x: xMin,
205
+ y: yMin,
206
+ width: xW,
207
+ height: yH,
208
+ fill: options.fill,
209
+ group: options.group,
210
+ });
211
+ }
212
+
213
+ if (options.stroke && (options.edges ?? true)) {
214
+ const sw = options.strokeWidth ?? 1;
215
+ if (vertical) {
216
+ layer.pushLine({
217
+ x1: ox + pA,
218
+ y1: yMin,
219
+ x2: ox + pA,
220
+ y2: yMin + yH,
221
+ color: options.stroke,
222
+ width: sw,
223
+ group: options.group,
224
+ });
225
+ layer.pushLine({
226
+ x1: ox + pB,
227
+ y1: yMin,
228
+ x2: ox + pB,
229
+ y2: yMin + yH,
230
+ color: options.stroke,
231
+ width: sw,
232
+ group: options.group,
233
+ });
234
+ } else {
235
+ layer.pushLine({
236
+ x1: xMin,
237
+ y1: oy + pA,
238
+ x2: xMin + xW,
239
+ y2: oy + pA,
240
+ color: options.stroke,
241
+ width: sw,
242
+ group: options.group,
243
+ });
244
+ layer.pushLine({
245
+ x1: xMin,
246
+ y1: oy + pB,
247
+ x2: xMin + xW,
248
+ y2: oy + pB,
249
+ color: options.stroke,
250
+ width: sw,
251
+ group: options.group,
252
+ });
253
+ }
254
+ }
255
+
256
+ if (options.label) {
257
+ const fontSize = options.fontSize ?? 10;
258
+ const labelOffset = options.labelOffset ?? 4;
259
+ const anchor: RuleAnchor = options.labelAnchor ?? "middle";
260
+ const labelColor = options.labelColor ?? options.stroke ?? BLACK;
261
+ const along0 = vertical ? ox + pA : oy + pA;
262
+ const along1 = vertical ? ox + pB : oy + pB;
263
+ const center =
264
+ anchor === "start"
265
+ ? Math.min(along0, along1)
266
+ : anchor === "middle"
267
+ ? (along0 + along1) / 2
268
+ : Math.max(along0, along1);
269
+
270
+ if (layer.atlas != null) {
271
+ if (vertical) {
272
+ layer.pushText({
273
+ simple: true,
274
+ text: options.label,
275
+ x: center,
276
+ y: yMin + labelOffset,
277
+ fontSize,
278
+ color: labelColor,
279
+ align: "center",
280
+ group: options.group,
281
+ });
282
+ } else {
283
+ layer.pushText({
284
+ simple: true,
285
+ text: options.label,
286
+ x: xMin + labelOffset,
287
+ y: center - fontSize / 2,
288
+ fontSize,
289
+ color: labelColor,
290
+ align: "left",
291
+ group: options.group,
292
+ });
293
+ }
294
+ }
295
+ }
296
+
297
+ return layer;
298
+ },
299
+ };
300
+ }
301
+
302
+ // ---------------------------------------------------------------------------
303
+ // valueLabelMark — text labels per datum (above/right of bars, beside points)
304
+ // ---------------------------------------------------------------------------
305
+
306
+ export type ValueLabelAlign = "left" | "center" | "right";
307
+
308
+ /**
309
+ * Optional rounded-rectangle background drawn behind label text. When set
310
+ * (and an `atlas` is available for measurement), `valueLabelMark` measures
311
+ * each label and emits a `pushRect` underneath sized to the text bounds plus
312
+ * `padding`. The rect's anchor matches the label's `align` so the text stays
313
+ * visually centered on `(x, y) + offset`.
314
+ */
315
+ export interface LabelBoxStyle {
316
+ fill?: Color;
317
+ stroke?: Color;
318
+ strokeWidth?: number;
319
+ /** Pixels added on each side. Number = uniform; object = per-edge. Default `4`. */
320
+ padding?: number | { x?: number; y?: number };
321
+ cornerRadius?: number;
322
+ }
323
+
324
+ export interface ValueLabelMarkOptions<T> {
325
+ x: Accessor<T, number>;
326
+ y: Accessor<T, number>;
327
+ text: Accessor<T, string>;
328
+ /** Horizontal anchor of the text relative to (`x`,`y`). Default `"center"`. */
329
+ align?: ValueLabelAlign;
330
+ /** Pixel offset from (`x`,`y`). Default `{ x: 0, y: -4 }` (above). */
331
+ offset?: { x?: number; y?: number };
332
+ color?: ValueOrAccessor<T, Color>;
333
+ fontSize?: number;
334
+ fontStyle?: string;
335
+ /** Skip labels for which this returns false. */
336
+ filter?: Accessor<T, boolean>;
337
+ /** Optional rounded-rect background. */
338
+ box?: LabelBoxStyle;
339
+ /**
340
+ * Glyph atlas used to measure each label's true rendered width and height
341
+ * for `box` sizing. When omitted the box falls back to a coarse character-
342
+ * width estimate that visibly mismatches the glyphs at small font sizes.
343
+ */
344
+ atlas?: GlyphAtlas;
345
+ group?: Group;
346
+ }
347
+
348
+ export function valueLabelMark<T>(
349
+ data: readonly T[],
350
+ options: ValueLabelMarkOptions<T>,
351
+ ): MarkBuilder {
352
+ return {
353
+ length: data.length,
354
+ addTo(layer: Layer, origin: MarkOrigin = {}) {
355
+ const ox = origin.x ?? 0;
356
+ const oy = origin.y ?? 0;
357
+ const align = options.align ?? "center";
358
+ const offsetX = options.offset?.x ?? 0;
359
+ const offsetY = options.offset?.y ?? -4;
360
+ const fontSize = options.fontSize ?? 11;
361
+ const box = options.box;
362
+ const padX = box ? boxPad(box.padding, "x") : 0;
363
+ const padY = box ? boxPad(box.padding, "y") : 0;
364
+
365
+ // valueLabelMark is text-only (optional box background + label). Both
366
+ // pushString and pushText require a glyph atlas; skip the entire mark
367
+ // when none is present (e.g. SVG export without an external atlas).
368
+ if (layer.atlas == null) return layer;
369
+
370
+ for (let i = 0; i < data.length; i++) {
371
+ const datum = data[i]!;
372
+ if (options.filter && !options.filter(datum, i)) continue;
373
+ const x = ox + options.x(datum, i) + offsetX;
374
+ const y = oy + options.y(datum, i) + offsetY;
375
+ const text = options.text(datum, i);
376
+ const color = options.color === undefined ? BLACK : resolve(options.color, datum, i);
377
+
378
+ if (box) {
379
+ // Prefer real glyph metrics when an atlas is available — the
380
+ // ratio-based fallback ignores per-glyph advance and visibly
381
+ // under/over-shoots for short labels.
382
+ let tw: number;
383
+ let th: number;
384
+ if (options.atlas) {
385
+ const m = options.atlas.measureText(text, { fontSize, simple: true });
386
+ tw = m.width;
387
+ th = m.height;
388
+ } else {
389
+ tw = text.length * fontSize * TEXT_WIDTH_FALLBACK_RATIO;
390
+ th = fontSize;
391
+ }
392
+ const rx =
393
+ align === "center" ? x - tw / 2 - padX : align === "right" ? x - tw - padX : x - padX;
394
+ const ry = y - padY;
395
+ layer.pushRect({
396
+ x: rx,
397
+ y: ry,
398
+ width: tw + padX * 2,
399
+ height: th + padY * 2,
400
+ fill: box.fill,
401
+ stroke: box.stroke,
402
+ strokeWidth: box.strokeWidth,
403
+ cornerRadius: box.cornerRadius,
404
+ group: options.group,
405
+ });
406
+ }
407
+
408
+ // Fast path: when the layer has a glyph atlas (v3: layer.atlas) and the
409
+ // caller hasn't requested any feature outside the fast path's support
410
+ // (no outline, shadow, gradient, wrap), route through the GPU expansion
411
+ // pipeline. valueLabelMark itself never sets those, so this is always
412
+ // safe here. v3 replaced the v1 `stringPack` sentinel with `atlas`:
413
+ // pushString calls _requireAtlas and throws without one, so checking
414
+ // atlas != null is the correct gate. When no atlas is present (e.g.
415
+ // SVG export without an external atlas), skip text emission entirely —
416
+ // pushText also throws without an atlas.
417
+ if (layer.atlas != null) {
418
+ layer.pushString({
419
+ text,
420
+ x,
421
+ y,
422
+ fontSize,
423
+ color,
424
+ align,
425
+ group: options.group,
426
+ });
427
+ }
428
+ }
429
+ return layer;
430
+ },
431
+ };
432
+ }
433
+
434
+ function boxPad(padding: LabelBoxStyle["padding"], axis: "x" | "y"): number {
435
+ if (padding === undefined) return 4;
436
+ if (typeof padding === "number") return padding;
437
+ return padding[axis] ?? 4;
438
+ }