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,107 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, expect, test } from "vite-plus/test";
3
+ import { createLayer, type Layer } from "insomni";
4
+
5
+ import { pad, placeable, spacer, stack } from "./box.ts";
6
+
7
+ function box(
8
+ width: number,
9
+ height: number,
10
+ ): { item: ReturnType<typeof placeable>; calls: { x: number; y: number }[] } {
11
+ const calls: { x: number; y: number }[] = [];
12
+ const item = placeable({ width, height }, (_layer: Layer, origin) => {
13
+ calls.push({ x: origin.x, y: origin.y });
14
+ });
15
+ return { item, calls };
16
+ }
17
+
18
+ describe("layout/box", () => {
19
+ test("stack horizontal sums widths and uses max height", () => {
20
+ const a = box(10, 20);
21
+ const b = box(30, 5);
22
+ const s = stack([a.item, b.item], { direction: "horizontal", gap: 4 });
23
+ expect(s.measure()).toEqual({ width: 10 + 4 + 30, height: 20 });
24
+ });
25
+
26
+ test("stack vertical sums heights and uses max width", () => {
27
+ const a = box(10, 20);
28
+ const b = box(30, 5);
29
+ const s = stack([a.item, b.item], { direction: "vertical", gap: 4 });
30
+ expect(s.measure()).toEqual({ width: 30, height: 20 + 4 + 5 });
31
+ });
32
+
33
+ test("stack vertical with align=start places items flush left", () => {
34
+ const a = box(10, 20);
35
+ const b = box(30, 5);
36
+ const layer = createLayer({ space: "ui" });
37
+ const s = stack([a.item, b.item], { direction: "vertical", align: "start" });
38
+ s.addTo(layer, { x: 100, y: 200 });
39
+ expect(a.calls[0]).toEqual({ x: 100, y: 200 });
40
+ expect(b.calls[0]).toEqual({ x: 100, y: 220 });
41
+ });
42
+
43
+ test("stack vertical with align=center centers narrower items", () => {
44
+ const a = box(10, 20);
45
+ const b = box(30, 5);
46
+ const layer = createLayer({ space: "ui" });
47
+ const s = stack([a.item, b.item], { direction: "vertical", align: "center" });
48
+ s.addTo(layer, { x: 0, y: 0 });
49
+ // a (width 10) centered in 30 → x=10
50
+ expect(a.calls[0]).toEqual({ x: 10, y: 0 });
51
+ expect(b.calls[0]).toEqual({ x: 0, y: 20 });
52
+ });
53
+
54
+ test("stack vertical with align=end right-aligns narrower items", () => {
55
+ const a = box(10, 20);
56
+ const b = box(30, 5);
57
+ const layer = createLayer({ space: "ui" });
58
+ const s = stack([a.item, b.item], { direction: "vertical", align: "end" });
59
+ s.addTo(layer, { x: 0, y: 0 });
60
+ expect(a.calls[0]).toEqual({ x: 20, y: 0 });
61
+ expect(b.calls[0]).toEqual({ x: 0, y: 20 });
62
+ });
63
+
64
+ test("stack honors gap when placing", () => {
65
+ const a = box(10, 10);
66
+ const b = box(10, 10);
67
+ const layer = createLayer({ space: "ui" });
68
+ const s = stack([a.item, b.item], { direction: "vertical", gap: 7 });
69
+ s.addTo(layer, { x: 0, y: 0 });
70
+ expect(b.calls[0]).toEqual({ x: 0, y: 17 });
71
+ });
72
+
73
+ test("pad expands bbox and offsets child", () => {
74
+ const a = box(10, 10);
75
+ const p = pad(a.item, { top: 2, left: 3, right: 4, bottom: 5 });
76
+ expect(p.measure()).toEqual({ width: 17, height: 17 });
77
+ const layer = createLayer({ space: "ui" });
78
+ p.addTo(layer, { x: 100, y: 100 });
79
+ expect(a.calls[0]).toEqual({ x: 103, y: 102 });
80
+ });
81
+
82
+ test("spacer occupies space but draws nothing", () => {
83
+ const sp = spacer(20, 10);
84
+ expect(sp.measure()).toEqual({ width: 20, height: 10 });
85
+ const layer = createLayer({ space: "ui" });
86
+ sp.addTo(layer, { x: 0, y: 0 });
87
+ // no throw, no calls
88
+ });
89
+
90
+ test("nested stack composes correctly", () => {
91
+ // [title (40w 10h)]
92
+ // [bar (12w 80h)] ← stacked vertical, align=start
93
+ // overall bbox: 40 wide, 92 tall (gap=2)
94
+ const title = box(40, 10);
95
+ const bar = box(12, 80);
96
+ const s = stack([title.item, bar.item], {
97
+ direction: "vertical",
98
+ align: "start",
99
+ gap: 2,
100
+ });
101
+ expect(s.measure()).toEqual({ width: 40, height: 92 });
102
+ const layer = createLayer({ space: "ui" });
103
+ s.addTo(layer, { x: 0, y: 0 });
104
+ expect(title.calls[0]).toEqual({ x: 0, y: 0 });
105
+ expect(bar.calls[0]).toEqual({ x: 0, y: 12 });
106
+ });
107
+ });
@@ -0,0 +1,143 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Box layout primitive — Placeable + stack/pad/spacer
3
+ // ---------------------------------------------------------------------------
4
+ // Small, generic abstraction shared by guides (legend, colorBar), the chart
5
+ // title block, and any future overlay that needs "measured boxes arranged
6
+ // without overlap". Inspired by ggplot2's guide layout (legend.box,
7
+ // legend.box.just, legend.spacing) but kept deliberately minimal — we don't
8
+ // have a use case for a constraint solver, only for stacking measured items.
9
+ //
10
+ // Two operations cover every recurring shape in the codebase:
11
+ // stack(items, { direction, align, gap }) — flow children along an axis
12
+ // pad(item, { top, right, bottom, left }) — wrap with margin
13
+ //
14
+ // Plus two adapters: `placeable(measure, draw)` to lift any measure/draw pair
15
+ // into a Placeable, and `spacer(w, h)` for empty space.
16
+
17
+ import type { Layer } from "insomni";
18
+
19
+ export interface MeasuredBox {
20
+ width: number;
21
+ height: number;
22
+ }
23
+
24
+ export interface BoxOrigin {
25
+ x: number;
26
+ y: number;
27
+ }
28
+
29
+ /**
30
+ * A measure-then-place rectangle. Implementations must return a stable
31
+ * `measure()` (called by parents to size themselves) and an `addTo` that
32
+ * draws into a layer at the given top-left.
33
+ */
34
+ export interface Placeable {
35
+ measure(): MeasuredBox;
36
+ addTo(layer: Layer, origin: BoxOrigin): void;
37
+ }
38
+
39
+ export type StackDirection = "horizontal" | "vertical";
40
+ export type StackAlign = "start" | "center" | "end";
41
+
42
+ export interface StackOptions {
43
+ direction: StackDirection;
44
+ /** Cross-axis alignment. Default `"start"`. */
45
+ align?: StackAlign;
46
+ /** Pixel gap between items. Default `0`. */
47
+ gap?: number;
48
+ }
49
+
50
+ /**
51
+ * Lay out `items` along one axis. The stack's bbox is the sum of children
52
+ * along the main axis (plus gaps) and the max along the cross axis.
53
+ *
54
+ * Children are measured **once** at construction; `measure()` and `addTo()`
55
+ * are O(items) thereafter. If you need to refresh measurements, build a new
56
+ * stack.
57
+ */
58
+ export function stack(items: readonly Placeable[], opts: StackOptions): Placeable {
59
+ const direction = opts.direction;
60
+ const align: StackAlign = opts.align ?? "start";
61
+ const gap = opts.gap ?? 0;
62
+
63
+ const measured = items.map((i) => i.measure());
64
+ let main = 0;
65
+ let cross = 0;
66
+ for (let k = 0; k < measured.length; k++) {
67
+ const m = measured[k]!;
68
+ const mainExtent = direction === "horizontal" ? m.width : m.height;
69
+ const crossExtent = direction === "horizontal" ? m.height : m.width;
70
+ if (k > 0) main += gap;
71
+ main += mainExtent;
72
+ if (crossExtent > cross) cross = crossExtent;
73
+ }
74
+
75
+ const total: MeasuredBox =
76
+ direction === "horizontal" ? { width: main, height: cross } : { width: cross, height: main };
77
+
78
+ return {
79
+ measure: () => total,
80
+ addTo(layer, origin) {
81
+ let off = 0;
82
+ const crossSize = direction === "horizontal" ? total.height : total.width;
83
+ for (let k = 0; k < items.length; k++) {
84
+ const m = measured[k]!;
85
+ const mainExtent = direction === "horizontal" ? m.width : m.height;
86
+ const crossExtent = direction === "horizontal" ? m.height : m.width;
87
+ const crossOff =
88
+ align === "start"
89
+ ? 0
90
+ : align === "end"
91
+ ? crossSize - crossExtent
92
+ : (crossSize - crossExtent) / 2;
93
+ const x = origin.x + (direction === "horizontal" ? off : crossOff);
94
+ const y = origin.y + (direction === "horizontal" ? crossOff : off);
95
+ items[k]!.addTo(layer, { x, y });
96
+ off += mainExtent + gap;
97
+ }
98
+ },
99
+ };
100
+ }
101
+
102
+ export interface Padding {
103
+ top?: number;
104
+ right?: number;
105
+ bottom?: number;
106
+ left?: number;
107
+ }
108
+
109
+ /** Wrap `item` so its bbox includes uniform or per-side margin. */
110
+ export function pad(item: Placeable, p: Padding): Placeable {
111
+ const top = p.top ?? 0;
112
+ const right = p.right ?? 0;
113
+ const bottom = p.bottom ?? 0;
114
+ const left = p.left ?? 0;
115
+ const inner = item.measure();
116
+ const total: MeasuredBox = {
117
+ width: inner.width + left + right,
118
+ height: inner.height + top + bottom,
119
+ };
120
+ return {
121
+ measure: () => total,
122
+ addTo(layer, origin) {
123
+ item.addTo(layer, { x: origin.x + left, y: origin.y + top });
124
+ },
125
+ };
126
+ }
127
+
128
+ /** A degenerate Placeable that occupies space but draws nothing. */
129
+ export function spacer(width: number, height: number): Placeable {
130
+ const m: MeasuredBox = { width, height };
131
+ return {
132
+ measure: () => m,
133
+ addTo: () => {},
134
+ };
135
+ }
136
+
137
+ /** Lift a measure + draw pair into a Placeable. */
138
+ export function placeable(
139
+ measure: MeasuredBox,
140
+ draw: (layer: Layer, origin: BoxOrigin) => void,
141
+ ): Placeable {
142
+ return { measure: () => measure, addTo: draw };
143
+ }
@@ -0,0 +1,115 @@
1
+ import { type Color, type FrameRect, type GlyphAtlas, type Group } from "insomni";
2
+ import { type Placeable } from "./layout/box.ts";
3
+ import { type PointBorderStyle, type PointShapeKind } from "./marks.ts";
4
+ export interface PointSwatchSpec {
5
+ kind: "point";
6
+ fill?: Color;
7
+ stroke?: Color;
8
+ strokeWidth?: number;
9
+ shape?: PointShapeKind;
10
+ /** Default `5`. */
11
+ radius?: number;
12
+ /** Per-shape border treatment — matches the mark side. Default `"solid"`. */
13
+ borderStyle?: PointBorderStyle;
14
+ /** Optional secondary glyph drawn on the same anchor. `null`/undefined = none. */
15
+ overlayGlyph?: PointShapeKind | null;
16
+ /** Overlay radius as a fraction of `radius`. Default `0.6`. */
17
+ overlayScale?: number;
18
+ }
19
+ export interface LineSwatchSpec {
20
+ kind: "line";
21
+ stroke: Color;
22
+ strokeWidth?: number;
23
+ dashPattern?: readonly number[];
24
+ /** Default `18`. */
25
+ width?: number;
26
+ }
27
+ export interface BarSwatchSpec {
28
+ kind: "bar";
29
+ fill?: Color;
30
+ stroke?: Color;
31
+ strokeWidth?: number;
32
+ cornerRadius?: number;
33
+ /** Default `12`. */
34
+ size?: number;
35
+ }
36
+ export interface AreaSwatchSpec {
37
+ kind: "area";
38
+ fill?: Color;
39
+ stroke?: Color;
40
+ strokeWidth?: number;
41
+ /** Default `14` (slight rectangle, not a square). */
42
+ width?: number;
43
+ /** Default `8`. */
44
+ height?: number;
45
+ }
46
+ export type SwatchSpec = PointSwatchSpec | LineSwatchSpec | BarSwatchSpec | AreaSwatchSpec;
47
+ export declare function pointSwatch(opts: Omit<PointSwatchSpec, "kind">): PointSwatchSpec;
48
+ export declare function lineSwatch(opts: Omit<LineSwatchSpec, "kind">): LineSwatchSpec;
49
+ export declare function barSwatch(opts: Omit<BarSwatchSpec, "kind">): BarSwatchSpec;
50
+ export declare function areaSwatch(opts: Omit<AreaSwatchSpec, "kind">): AreaSwatchSpec;
51
+ export interface LegendEntry {
52
+ label: string;
53
+ swatch: SwatchSpec;
54
+ /** Optional hint for interactivity layers to render the entry as "off". */
55
+ hidden?: boolean;
56
+ }
57
+ export type LegendOrientation = "horizontal" | "vertical";
58
+ export type LegendAlign = "start" | "end";
59
+ export interface LegendOptions {
60
+ atlas: GlyphAtlas;
61
+ orientation?: LegendOrientation;
62
+ /** Anchor inside the layout box. Default `"start"` (left/top edge of box). */
63
+ align?: LegendAlign;
64
+ fontSize?: number;
65
+ fontStyle?: string;
66
+ labelColor?: Color;
67
+ /** Pixel gap between swatch and label. Default `6`. */
68
+ swatchGap?: number;
69
+ /** Pixel gap between entries. Default `14` (horizontal) or `4` (vertical). */
70
+ entryGap?: number;
71
+ /** Optional title rendered above the entries (left-aligned, bold). */
72
+ title?: string;
73
+ titleFontSize?: number;
74
+ titleColor?: Color;
75
+ /** Pixel gap between title and entries. Default `4`. */
76
+ titleGap?: number;
77
+ /** Alpha for hidden entries. Default `0.2`. */
78
+ dimAlpha?: number;
79
+ group?: Group;
80
+ }
81
+ export interface LegendBuilder extends Placeable {
82
+ /** Number of entries (or, for color bars, palette samples). */
83
+ readonly length: number;
84
+ /** Total laid-out width and height in layer-space pixels. */
85
+ measure(): {
86
+ width: number;
87
+ height: number;
88
+ };
89
+ /** Bounding boxes of each entry, relative to the legend's top-left. */
90
+ getEntryBboxes(): (FrameRect & {
91
+ label: string;
92
+ })[];
93
+ }
94
+ /**
95
+ * `Guide` is the shared shape every legend-like component (categorical
96
+ * `legend()`, continuous `colorBar()`, future `sizeGuide()` / `shapeGuide()`)
97
+ * returns. It's a `Placeable` so the chart layout engine can size and place
98
+ * it generically. Concrete builders may add extra fields (e.g. `length`).
99
+ */
100
+ export type Guide = Placeable;
101
+ /**
102
+ * Lay out a row (or column) of swatch + label pairs. Each mark contributes
103
+ * its own swatch shape via the `*Swatch(...)` helpers, so a dashed line shows
104
+ * a dashed swatch, a triangle point shows a triangle, etc.
105
+ *
106
+ * The legend is positioned by `origin` when added: `origin` is the *top-left*
107
+ * of the layout box. To right-align, measure first then offset:
108
+ *
109
+ * ```ts
110
+ * const lg = legend(entries, { atlas: a });
111
+ * const { width } = lg.measure();
112
+ * lg.addTo(layer, { x: outer.x + outer.width - width, y: outer.y });
113
+ * ```
114
+ */
115
+ export declare function legend(entries: readonly LegendEntry[], options: LegendOptions): LegendBuilder;