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,63 @@
1
+ import { type Color, type Frame, type Layer } from "insomni";
2
+ import type { Aes } from "./aes.ts";
3
+ import type { Theme } from "./theme.ts";
4
+ export type FacetScales = "fixed" | "free";
5
+ export interface FacetStripStyle {
6
+ height?: number;
7
+ fontSize?: number;
8
+ color?: Color;
9
+ background?: Color;
10
+ }
11
+ export interface FacetSpec<T> {
12
+ /** Accessor / column name producing the facet key per datum. */
13
+ by: Aes<T, unknown>;
14
+ /** Number of columns. Default: ceil(sqrt(n)). */
15
+ ncol?: number;
16
+ /** Number of rows. Default: ceil(n / ncol). */
17
+ nrow?: number;
18
+ /**
19
+ * Scale handling across panels.
20
+ * - `"fixed"` (default) — every panel shares the same x/y/color/… scales.
21
+ * - `"free"` — each panel computes its own scales from its data.
22
+ */
23
+ scales?: FacetScales;
24
+ /** Spacing between panels in pixels. Default `12`. */
25
+ gap?: number;
26
+ /** Strip header style. */
27
+ strip?: FacetStripStyle;
28
+ /** Optional formatter for the strip label. */
29
+ format?: (key: unknown) => string;
30
+ }
31
+ export interface FacetPanel<T> {
32
+ key: unknown;
33
+ rows: T[];
34
+ /** Index into the column-major grid. */
35
+ col: number;
36
+ row: number;
37
+ /** Sub-frame within the chart's plot frame. */
38
+ frame: Frame;
39
+ /** True for the leftmost column — render y-axis only here. */
40
+ isLeftCol: boolean;
41
+ /** True for the bottom row of *populated* panels — render x-axis only here. */
42
+ isBottomRow: boolean;
43
+ }
44
+ export interface FacetLayout<T> {
45
+ panels: FacetPanel<T>[];
46
+ ncol: number;
47
+ nrow: number;
48
+ stripHeight: number;
49
+ }
50
+ export declare function groupForFacet<T>(spec: FacetSpec<T>, data: readonly T[]): {
51
+ key: unknown;
52
+ rows: T[];
53
+ }[];
54
+ /**
55
+ * Slice the chart's plot frame into a grid of panel sub-frames. Each sub-frame
56
+ * contains a strip header at the top and a panel body underneath.
57
+ */
58
+ export declare function computeFacetLayout<T>(spec: FacetSpec<T>, groups: readonly {
59
+ key: unknown;
60
+ rows: T[];
61
+ }[], plot: Frame): FacetLayout<T>;
62
+ /** Draw a strip header above a panel frame. */
63
+ export declare function renderFacetStrip(layer: Layer, panel: FacetPanel<unknown>, spec: FacetSpec<unknown>, theme: Theme): void;
@@ -0,0 +1,60 @@
1
+ import { describe, expect, test } from "vite-plus/test";
2
+ import { viewportFrame } from "insomni";
3
+
4
+ import { computeFacetLayout, groupForFacet, type FacetSpec } from "./facet.ts";
5
+
6
+ interface Row {
7
+ group: string;
8
+ v: number;
9
+ }
10
+
11
+ const DATA: Row[] = [
12
+ { group: "a", v: 1 },
13
+ { group: "b", v: 2 },
14
+ { group: "a", v: 3 },
15
+ { group: "c", v: 4 },
16
+ { group: "b", v: 5 },
17
+ ];
18
+
19
+ describe("facet", () => {
20
+ test("groupForFacet preserves first-occurrence order", () => {
21
+ const groups = groupForFacet({ by: "group" }, DATA);
22
+ expect(groups.map((g) => g.key)).toEqual(["a", "b", "c"]);
23
+ expect(groups[0]!.rows).toHaveLength(2);
24
+ expect(groups[1]!.rows).toHaveLength(2);
25
+ expect(groups[2]!.rows).toHaveLength(1);
26
+ });
27
+
28
+ test("computeFacetLayout splits the plot frame into ncol×nrow panels", () => {
29
+ const groups = groupForFacet({ by: "group" }, DATA);
30
+ const plot = viewportFrame(400, 300);
31
+ const spec: FacetSpec<Row> = { by: "group", ncol: 2, gap: 10, scales: "fixed" };
32
+ const layout = computeFacetLayout(spec, groups, plot);
33
+ expect(layout.ncol).toBe(2);
34
+ expect(layout.nrow).toBe(2);
35
+ expect(layout.panels).toHaveLength(3);
36
+
37
+ // Expected panel sizes — the gap takes 10px per inner row/col, strip eats
38
+ // the top of every panel.
39
+ const panelW = (400 - 10) / 2;
40
+ expect(layout.panels[0]!.frame.width).toBeCloseTo(panelW, 5);
41
+ expect(layout.panels[0]!.frame.x).toBeCloseTo(0, 5);
42
+ expect(layout.panels[1]!.frame.x).toBeCloseTo(panelW + 10, 5);
43
+ });
44
+
45
+ test("isBottomRow flags the last populated row in each column", () => {
46
+ // Five panels in a 2-col grid → row 2 has only column 0.
47
+ const data = ["a", "b", "c", "d", "e"].map((k) => ({ group: k, v: 0 }));
48
+ const groups = groupForFacet({ by: "group" }, data);
49
+ const plot = viewportFrame(400, 300);
50
+ const layout = computeFacetLayout({ by: "group", ncol: 2 }, groups, plot);
51
+ const flags = layout.panels.map((p) => p.isBottomRow);
52
+ // Column 0's bottom panel is index 4 (row 2). Column 1's bottom panel is
53
+ // index 3 (row 1).
54
+ expect(flags[0]).toBe(false);
55
+ expect(flags[1]).toBe(false);
56
+ expect(flags[2]).toBe(false);
57
+ expect(flags[3]).toBe(true);
58
+ expect(flags[4]).toBe(true);
59
+ });
60
+ });
@@ -0,0 +1,175 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Faceting — split data into a grid of panels
3
+ // ---------------------------------------------------------------------------
4
+ // `chart.facet({ by })` produces one panel per unique value of `by`. Panels
5
+ // are arranged in `ncol × nrow` (defaulting to a near-square grid) and share
6
+ // scales by default. Each panel reuses the chart's geoms; only data and the
7
+ // containing sub-frame change between panels.
8
+
9
+ import { type Color, type Frame, type Layer, viewportFrame } from "insomni";
10
+ import type { Aes } from "./aes.ts";
11
+ import { resolveAes } from "./aes.ts";
12
+ import type { Theme } from "./theme.ts";
13
+ import {
14
+ DEFAULT_FACET_GAP,
15
+ DEFAULT_FACET_STRIP_FONT_SIZE,
16
+ DEFAULT_FACET_STRIP_HEIGHT,
17
+ } from "./constants.ts";
18
+
19
+ export type FacetScales = "fixed" | "free";
20
+
21
+ export interface FacetStripStyle {
22
+ height?: number;
23
+ fontSize?: number;
24
+ color?: Color;
25
+ background?: Color;
26
+ }
27
+
28
+ export interface FacetSpec<T> {
29
+ /** Accessor / column name producing the facet key per datum. */
30
+ by: Aes<T, unknown>;
31
+ /** Number of columns. Default: ceil(sqrt(n)). */
32
+ ncol?: number;
33
+ /** Number of rows. Default: ceil(n / ncol). */
34
+ nrow?: number;
35
+ /**
36
+ * Scale handling across panels.
37
+ * - `"fixed"` (default) — every panel shares the same x/y/color/… scales.
38
+ * - `"free"` — each panel computes its own scales from its data.
39
+ */
40
+ scales?: FacetScales;
41
+ /** Spacing between panels in pixels. Default `12`. */
42
+ gap?: number;
43
+ /** Strip header style. */
44
+ strip?: FacetStripStyle;
45
+ /** Optional formatter for the strip label. */
46
+ format?: (key: unknown) => string;
47
+ }
48
+
49
+ export interface FacetPanel<T> {
50
+ key: unknown;
51
+ rows: T[];
52
+ /** Index into the column-major grid. */
53
+ col: number;
54
+ row: number;
55
+ /** Sub-frame within the chart's plot frame. */
56
+ frame: Frame;
57
+ /** True for the leftmost column — render y-axis only here. */
58
+ isLeftCol: boolean;
59
+ /** True for the bottom row of *populated* panels — render x-axis only here. */
60
+ isBottomRow: boolean;
61
+ }
62
+
63
+ export interface FacetLayout<T> {
64
+ panels: FacetPanel<T>[];
65
+ ncol: number;
66
+ nrow: number;
67
+ stripHeight: number;
68
+ }
69
+
70
+ export function groupForFacet<T>(
71
+ spec: FacetSpec<T>,
72
+ data: readonly T[],
73
+ ): { key: unknown; rows: T[] }[] {
74
+ const aes = resolveAes<T, unknown>(spec.by);
75
+ const keyOrder: unknown[] = [];
76
+ const buckets = new Map<unknown, T[]>();
77
+ for (let i = 0; i < data.length; i++) {
78
+ const datum = data[i]!;
79
+ const k = aes.fn(datum, i);
80
+ let bucket = buckets.get(k);
81
+ if (!bucket) {
82
+ bucket = [];
83
+ buckets.set(k, bucket);
84
+ keyOrder.push(k);
85
+ }
86
+ bucket.push(datum);
87
+ }
88
+ return keyOrder.map((key) => ({ key, rows: buckets.get(key)! }));
89
+ }
90
+
91
+ /**
92
+ * Slice the chart's plot frame into a grid of panel sub-frames. Each sub-frame
93
+ * contains a strip header at the top and a panel body underneath.
94
+ */
95
+ export function computeFacetLayout<T>(
96
+ spec: FacetSpec<T>,
97
+ groups: readonly { key: unknown; rows: T[] }[],
98
+ plot: Frame,
99
+ ): FacetLayout<T> {
100
+ const n = groups.length;
101
+ const ncol = Math.max(1, spec.ncol ?? Math.ceil(Math.sqrt(n)));
102
+ const nrow = Math.max(1, spec.nrow ?? Math.ceil(n / ncol));
103
+ const gap = spec.gap ?? DEFAULT_FACET_GAP;
104
+ const stripHeight = spec.strip?.height ?? DEFAULT_FACET_STRIP_HEIGHT;
105
+
106
+ const panelWidth = (plot.width - gap * (ncol - 1)) / ncol;
107
+ const panelHeight = (plot.height - gap * (nrow - 1)) / nrow;
108
+
109
+ const panels: FacetPanel<T>[] = [];
110
+ // Determine the bottom-row index per column — for an n that doesn't fill
111
+ // the last row, panels in earlier columns still need x-axes drawn.
112
+ const colMaxRow = Array.from({ length: ncol }, () => -1);
113
+ for (let i = 0; i < n; i++) {
114
+ const col = i % ncol;
115
+ const row = Math.floor(i / ncol);
116
+ if (row > colMaxRow[col]!) colMaxRow[col] = row;
117
+ }
118
+
119
+ for (let i = 0; i < n; i++) {
120
+ const col = i % ncol;
121
+ const row = Math.floor(i / ncol);
122
+ const x = plot.x + col * (panelWidth + gap);
123
+ const y = plot.y + row * (panelHeight + gap) + stripHeight;
124
+ const frame = viewportFrame(panelWidth, Math.max(0, panelHeight - stripHeight)).translated(
125
+ x,
126
+ y,
127
+ );
128
+ panels.push({
129
+ key: groups[i]!.key,
130
+ rows: groups[i]!.rows,
131
+ col,
132
+ row,
133
+ frame,
134
+ isLeftCol: col === 0,
135
+ isBottomRow: row === colMaxRow[col]!,
136
+ });
137
+ }
138
+
139
+ return { panels, ncol, nrow, stripHeight };
140
+ }
141
+
142
+ /** Draw a strip header above a panel frame. */
143
+ export function renderFacetStrip(
144
+ layer: Layer,
145
+ panel: FacetPanel<unknown>,
146
+ spec: FacetSpec<unknown>,
147
+ theme: Theme,
148
+ ): void {
149
+ const stripHeight = spec.strip?.height ?? DEFAULT_FACET_STRIP_HEIGHT;
150
+ const fontSize = spec.strip?.fontSize ?? DEFAULT_FACET_STRIP_FONT_SIZE;
151
+ const color = spec.strip?.color ?? theme.text.color;
152
+ const bg = spec.strip?.background;
153
+ const text = spec.format ? spec.format(panel.key) : String(panel.key);
154
+ const x = panel.frame.x;
155
+ const y = panel.frame.y - stripHeight;
156
+
157
+ if (bg) {
158
+ layer.pushRect({
159
+ x,
160
+ y,
161
+ width: panel.frame.width,
162
+ height: stripHeight,
163
+ fill: bg,
164
+ });
165
+ }
166
+ layer.pushText({
167
+ simple: true,
168
+ text,
169
+ x: x + panel.frame.width / 2,
170
+ y: y + (stripHeight - fontSize) / 2,
171
+ fontSize,
172
+ color,
173
+ align: "center",
174
+ });
175
+ }
@@ -0,0 +1,94 @@
1
+ import type { Color } from "insomni";
2
+ import type { Aes } from "../aes.ts";
3
+ import { type ResolvedAes } from "../aes.ts";
4
+ import { type BandScale, type ContinuousScale, type GroupedBandScale } from "../../scales.ts";
5
+ import type { MarkBuilder } from "../../marks.ts";
6
+ import type { CompileContext } from "./types.ts";
7
+ /** Default inner-band padding for dodged categorical layouts (fraction of band). */
8
+ export declare const DEFAULT_GROUP_PADDING = 0.05;
9
+ export interface CategoricalChannels<T> {
10
+ x: Aes<T, string | number | Date>;
11
+ y: Aes<T, number>;
12
+ color?: Aes<T, unknown>;
13
+ }
14
+ export interface CategoricalLayoutOptions {
15
+ /** Force orientation; defaults to detecting which axis is `band`. */
16
+ orientation?: "x" | "y";
17
+ /** Inner-band padding for grouped (color-split) layout. Default `0.05`. */
18
+ groupPadding?: number;
19
+ /** Fallback fill (used when there's no color channel). */
20
+ fill?: Color;
21
+ }
22
+ export interface CategoricalBucket {
23
+ /** Stringified category key (band-axis value). */
24
+ category: string;
25
+ /** Stringified inner-group key — undefined when not dodging. */
26
+ groupKey: string | undefined;
27
+ /** Numeric samples in this bucket, in input order. */
28
+ values: number[];
29
+ }
30
+ export interface CategoricalLayout {
31
+ orientation: "x" | "y";
32
+ bandAxis: BandScale<string>;
33
+ valueAxis: ContinuousScale;
34
+ /** Present when a color channel splits the band into sub-bands. */
35
+ inner: GroupedBandScale<string, string> | undefined;
36
+ /** Color column != band column (and color column is set). */
37
+ dodging: boolean;
38
+ /** Step on the band axis: inner bandwidth if dodging, else outer bandwidth. */
39
+ cellSize: number;
40
+ /** Buckets in first-encounter order. */
41
+ buckets: CategoricalBucket[];
42
+ /** Resolved fill (theme alpha already applied) for a bucket. */
43
+ resolveFill(bucket: CategoricalBucket): Color;
44
+ /** Plot-frame origin; cached to avoid `ctx.plot.topLeft` re-reads. */
45
+ ox: number;
46
+ oy: number;
47
+ }
48
+ export declare function prepareCategoricalLayout<T>(ctx: CompileContext<T>, channels: CategoricalChannels<T>, options?: CategoricalLayoutOptions): CategoricalLayout;
49
+ /**
50
+ * Position of the band-axis center for a bucket. Returns `NaN` when the
51
+ * category isn't on the scale (caller should skip the bucket).
52
+ */
53
+ export declare function bucketBandCenter(layout: CategoricalLayout, bucket: CategoricalBucket): number;
54
+ /**
55
+ * Stable per-bucket seed for jitter / random offsets. Same bucket always
56
+ * yields the same value so re-renders don't dance.
57
+ */
58
+ export declare function bucketSeed(bucket: CategoricalBucket, baseSeed: number): number;
59
+ /**
60
+ * Aggregating geoms (boxplot / violin / histogram / ridgeline) emit one hit
61
+ * per bucket — not per source row — so their `compileHitTest` can't expose
62
+ * the user's column accessors meaningfully. Instead they `synthAes` channel
63
+ * accessors that ignore their datum and return a precomputed per-bucket
64
+ * value keyed by the hit's `dataIndex`. The `column` string flows through
65
+ * to the tooltip's row label.
66
+ */
67
+ export declare function synthAes<T>(column: string, fn: (datum: T, index: number) => unknown): ResolvedAes<T, unknown>;
68
+ export interface CountsLabelOptions {
69
+ /** Pixel offset from the band-axis edge of the plot frame. */
70
+ offset: number;
71
+ fontSize: number;
72
+ color: Color;
73
+ /** "Sample size" prefix. Default `"n="`. */
74
+ prefix?: string;
75
+ /**
76
+ * Where to anchor the labels:
77
+ *
78
+ * - `"outside"` (default) — outside the plot frame on the band-axis
79
+ * perpendicular. Used by boxplot / violin so labels never collide with
80
+ * the marks.
81
+ * - `"inline"` — at the band-axis center, inside the plot frame at the
82
+ * row baseline (offset by `offset` along the value axis). Used by
83
+ * ridgeline-style row charts where each row owns its baseline.
84
+ */
85
+ anchor?: "outside" | "inline";
86
+ }
87
+ /**
88
+ * Build a `MarkBuilder` that draws `n=<count>` labels for each band-axis
89
+ * category, just outside the plot frame on the band-axis perpendicular.
90
+ *
91
+ * Counts are summed across dodge groups so a band with `A: 10` and
92
+ * `B: 15` displays `n=25`.
93
+ */
94
+ export declare function countsLabelMark(layout: CategoricalLayout, options: CountsLabelOptions, plotWidth: number, plotHeight: number): MarkBuilder;
@@ -0,0 +1,52 @@
1
+ import { type BoxStats, type BoxStatsOptions, type KdeOptions, type KdeResult } from "../../stats/index.ts";
2
+ import type { CategoricalBucket } from "./_categorical.ts";
3
+ /**
4
+ * Compute the value-axis extent across all buckets' KDEs. Used by `violin`
5
+ * and `ridgeline` to extend their value-axis domain so smoothed tails don't
6
+ * render past the plot frame. Returns `null` when no bucket yields a usable
7
+ * KDE (empty input, or all values non-finite).
8
+ */
9
+ export declare function kdeValueExtent(buckets: Iterable<readonly number[]>, options: KdeOptions): readonly [number, number] | null;
10
+ /** How per-row width relates to density across groups. */
11
+ export type DensityScale = "width" | "area" | "count";
12
+ export interface GroupedKdeResult {
13
+ bucket: CategoricalBucket;
14
+ /** KDE evaluated on a fixed grid (`null` only when bucket has no finite values). */
15
+ kde: KdeResult;
16
+ /** Max raw density on the KDE grid, before any normalization. */
17
+ maxDensity: number;
18
+ /** Sample size that produced the KDE. */
19
+ n: number;
20
+ /** Box stats for the same sample — used by inner annotations. */
21
+ stats: BoxStats;
22
+ }
23
+ export interface ComputeGroupedKdeOptions {
24
+ kde: KdeOptions;
25
+ box: BoxStatsOptions;
26
+ /**
27
+ * Width-scaling mode. Affects only `globalMaxDensity`, which is then folded
28
+ * into `densityWidthFn`. The per-bucket `maxDensity` stays raw so callers
29
+ * that need it (e.g. for `width` mode) get an unscaled reference.
30
+ */
31
+ scale: DensityScale;
32
+ }
33
+ /**
34
+ * Compute a KDE + box stats for each bucket. Buckets that yield no usable KDE
35
+ * (zero finite samples) are dropped from the output. Returns the global max
36
+ * density used for cross-group `area` / `count` normalization.
37
+ */
38
+ export declare function computeGroupedKde(buckets: readonly CategoricalBucket[], options: ComputeGroupedKdeOptions): {
39
+ groups: GroupedKdeResult[];
40
+ globalMaxDensity: number;
41
+ };
42
+ /**
43
+ * Build the `density → pixel width` mapping for one group, given the global
44
+ * max density (for `area` / `count` modes) and the slot half-width in pixels.
45
+ *
46
+ * - `"width"` — each group fills its slot. Densities are not comparable.
47
+ * - `"area"` — each group is normalized by the global max density. Areas
48
+ * are comparable across groups.
49
+ * - `"count"` — like area, but multiplied by `n` so groups with more samples
50
+ * render larger.
51
+ */
52
+ export declare function densityWidthFn(group: GroupedKdeResult, globalMaxDensity: number, scale: DensityScale, halfWidth: number): (density: number) => number;
@@ -0,0 +1,125 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Per-group distribution helpers
3
+ // ---------------------------------------------------------------------------
4
+ // Pure (no render deps) helpers shared between distribution geoms — violin and
5
+ // ridgeline. Centralizes:
6
+ // - per-bucket KDE evaluation
7
+ // - per-bucket box stats (used for inner annotations)
8
+ // - cross-group max-density bookkeeping for `area` / `count` width modes
9
+ // - the width-from-density function each geom plugs into its layout
10
+
11
+ import {
12
+ boxStats,
13
+ kde,
14
+ type BoxStats,
15
+ type BoxStatsOptions,
16
+ type KdeOptions,
17
+ type KdeResult,
18
+ } from "../../stats/index.ts";
19
+ import type { CategoricalBucket } from "./_categorical.ts";
20
+
21
+ /**
22
+ * Compute the value-axis extent across all buckets' KDEs. Used by `violin`
23
+ * and `ridgeline` to extend their value-axis domain so smoothed tails don't
24
+ * render past the plot frame. Returns `null` when no bucket yields a usable
25
+ * KDE (empty input, or all values non-finite).
26
+ */
27
+ export function kdeValueExtent(
28
+ buckets: Iterable<readonly number[]>,
29
+ options: KdeOptions,
30
+ ): readonly [number, number] | null {
31
+ let lo = Number.POSITIVE_INFINITY;
32
+ let hi = Number.NEGATIVE_INFINITY;
33
+ for (const values of buckets) {
34
+ const k = kde(values, options);
35
+ if (!k || k.x.length === 0) continue;
36
+ const x0 = k.x[0]!;
37
+ const xn = k.x[k.x.length - 1]!;
38
+ if (x0 < lo) lo = x0;
39
+ if (xn > hi) hi = xn;
40
+ }
41
+ if (!Number.isFinite(lo) || !Number.isFinite(hi)) return null;
42
+ return [lo, hi];
43
+ }
44
+
45
+ /** How per-row width relates to density across groups. */
46
+ export type DensityScale = "width" | "area" | "count";
47
+
48
+ export interface GroupedKdeResult {
49
+ bucket: CategoricalBucket;
50
+ /** KDE evaluated on a fixed grid (`null` only when bucket has no finite values). */
51
+ kde: KdeResult;
52
+ /** Max raw density on the KDE grid, before any normalization. */
53
+ maxDensity: number;
54
+ /** Sample size that produced the KDE. */
55
+ n: number;
56
+ /** Box stats for the same sample — used by inner annotations. */
57
+ stats: BoxStats;
58
+ }
59
+
60
+ export interface ComputeGroupedKdeOptions {
61
+ kde: KdeOptions;
62
+ box: BoxStatsOptions;
63
+ /**
64
+ * Width-scaling mode. Affects only `globalMaxDensity`, which is then folded
65
+ * into `densityWidthFn`. The per-bucket `maxDensity` stays raw so callers
66
+ * that need it (e.g. for `width` mode) get an unscaled reference.
67
+ */
68
+ scale: DensityScale;
69
+ }
70
+
71
+ /**
72
+ * Compute a KDE + box stats for each bucket. Buckets that yield no usable KDE
73
+ * (zero finite samples) are dropped from the output. Returns the global max
74
+ * density used for cross-group `area` / `count` normalization.
75
+ */
76
+ export function computeGroupedKde(
77
+ buckets: readonly CategoricalBucket[],
78
+ options: ComputeGroupedKdeOptions,
79
+ ): { groups: GroupedKdeResult[]; globalMaxDensity: number } {
80
+ const groups: GroupedKdeResult[] = [];
81
+ let globalMaxDensity = 0;
82
+ for (const bucket of buckets) {
83
+ const k = kde(bucket.values, options.kde);
84
+ if (!k) continue;
85
+ const stats = boxStats(bucket.values, options.box);
86
+ if (!stats) continue;
87
+ let max = 0;
88
+ for (const v of k.y) if (v > max) max = v;
89
+ const scaledMax = options.scale === "count" ? max * stats.n : max;
90
+ if (scaledMax > globalMaxDensity) globalMaxDensity = scaledMax;
91
+ groups.push({ bucket, kde: k, maxDensity: max, n: stats.n, stats });
92
+ }
93
+ return { groups, globalMaxDensity };
94
+ }
95
+
96
+ /**
97
+ * Build the `density → pixel width` mapping for one group, given the global
98
+ * max density (for `area` / `count` modes) and the slot half-width in pixels.
99
+ *
100
+ * - `"width"` — each group fills its slot. Densities are not comparable.
101
+ * - `"area"` — each group is normalized by the global max density. Areas
102
+ * are comparable across groups.
103
+ * - `"count"` — like area, but multiplied by `n` so groups with more samples
104
+ * render larger.
105
+ */
106
+ export function densityWidthFn(
107
+ group: GroupedKdeResult,
108
+ globalMaxDensity: number,
109
+ scale: DensityScale,
110
+ halfWidth: number,
111
+ ): (density: number) => number {
112
+ if (scale === "width") {
113
+ if (group.maxDensity <= 0) return () => 0;
114
+ const k = halfWidth / group.maxDensity;
115
+ return (d) => d * k;
116
+ }
117
+ const ref = globalMaxDensity > 0 ? globalMaxDensity : 1;
118
+ if (scale === "count") {
119
+ const k = (halfWidth * group.n) / ref;
120
+ return (d) => d * k;
121
+ }
122
+ // area
123
+ const k = halfWidth / ref;
124
+ return (d) => d * k;
125
+ }
@@ -0,0 +1,69 @@
1
+ import type { Color, Layer } from "insomni";
2
+ import type { MarkBuilder, MarkOrigin } from "../../marks.ts";
3
+ import { type Coord } from "../coord.ts";
4
+ import type { Theme } from "../theme.ts";
5
+ import type { CompileContext, GeomKind } from "./types.ts";
6
+ /**
7
+ * Resolve `ctx.coord` to a concrete {@link Coord}. The interface flags `coord`
8
+ * as optional in {@link CompileContext} during the Phase 1 migration to the
9
+ * `Coord` API — geoms call this once at the top of their compile path to get
10
+ * a non-null projection, falling back to {@link coordCartesian} when the
11
+ * pipeline didn't supply one (e.g. legacy direct-compile call sites). Under
12
+ * Cartesian both `project` and `segment` are the identity, so this is a
13
+ * zero-pixel-diff drop-in for every existing geom.
14
+ */
15
+ export declare function resolveCoord<T>(ctx: CompileContext<T>): Coord;
16
+ /**
17
+ * Default alpha applied to non-selected marks while a selection is active.
18
+ * Lifted into a constant so geoms stay consistent without each importing the
19
+ * theme's selection config; per-mount overrides happen via the selection
20
+ * config (see chart.ts:SelectionConfig.dimAlpha).
21
+ */
22
+ export declare const SELECTION_DIM_ALPHA = 0.3;
23
+ /**
24
+ * Returns the set of selected `dataIndex`es that belong to *this* geom (kind
25
+ * matches and the data array reference matches), or `null` when no selection
26
+ * row matches. Geoms use this to push stroke rings on the matching rows.
27
+ */
28
+ export declare function selectedIndicesFor<T>(ctx: CompileContext<T>, kind: GeomKind): Set<number> | null;
29
+ /** True when any selection is active anywhere in the chart. */
30
+ export declare function selectionActive<T>(ctx: CompileContext<T>): boolean;
31
+ /**
32
+ * Multi-series equivalent of {@link selectedIndicesFor}: returns the list of
33
+ * `(dataIndex, seriesKey)` pairs selected for *this* geom. Used by stacked /
34
+ * dodged geoms where a single source row produces multiple visible segments
35
+ * and the ring must distinguish them by series key.
36
+ */
37
+ export declare function selectedSegmentsFor<T>(ctx: CompileContext<T>, kind: GeomKind): Array<{
38
+ dataIndex: number;
39
+ seriesKey?: string;
40
+ }> | null;
41
+ /**
42
+ * Wrap a `MarkBuilder` so it composes into the `compile()` builder list with
43
+ * a fixed origin (typically the plot frame's top-left). Geoms collect these
44
+ * into the array returned from `compile`. Pass `length` to override the
45
+ * builder's reported length — used for label sub-marks where the visual
46
+ * count differs from the underlying mark's vertex count.
47
+ */
48
+ export declare function wrapMark(mark: MarkBuilder, origin: MarkOrigin, length?: number): {
49
+ length: number;
50
+ addTo: (layer: Layer) => Layer;
51
+ };
52
+ /**
53
+ * Build a `MarkBuilder` from a draw callback. Used for one-off shapes that
54
+ * don't fit a mark factory — currently the hover-highlight halo each geom
55
+ * pushes for `ctx.hovered`. Coordinates passed to `draw` are layer-local
56
+ * (the caller has already applied any plot-frame offset).
57
+ */
58
+ export declare function inlineMark(draw: (layer: Layer) => void, length?: number): MarkBuilder;
59
+ export declare function resolveFillAlpha(selected: boolean, theme: Theme): number;
60
+ export declare function defaultMarkFill(theme: Theme): Color;
61
+ export declare function selectChannels<X, Y>(orientation: "x" | "y", channels: {
62
+ x: X;
63
+ y: Y;
64
+ }): {
65
+ categoryChannel: X | Y;
66
+ valueChannel: X | Y;
67
+ };
68
+ /** Highlight ring used by point/line/area on `ctx.hovered`. */
69
+ export declare function haloRing(layer: Layer, cx: number, cy: number, radius: number, color: Color, width?: number): void;