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,54 @@
1
+ import { type PointShapeKind } from "../marks.ts";
2
+ import { type CategoricalPalette } from "../colors.ts";
3
+ /** Built-in categorical palette identifiers supported by `distinguishable`. */
4
+ export type DistinguishableScheme = "category10" | "tableau10" | "set1" | "set2" | "set3" | "dark2" | "paired" | "pastel";
5
+ export interface DistinguishableOptions {
6
+ /** Per-channel cardinality. Each count must be ≥ 1. */
7
+ for: {
8
+ color: number;
9
+ shape: number;
10
+ };
11
+ /**
12
+ * Categorical palette to source the color palette from. Default
13
+ * `"category10"`. Pass a `CategoricalPalette` directly for a custom set.
14
+ */
15
+ scheme?: DistinguishableScheme | CategoricalPalette;
16
+ /**
17
+ * Override the shape pool. Default: `POINT_SHAPE_PALETTE` from `marks`,
18
+ * which is already ordered for silhouette distinctness.
19
+ */
20
+ shapes?: readonly PointShapeKind[];
21
+ }
22
+ export interface DistinguishablePalettes {
23
+ /**
24
+ * A categorical palette sliced to exactly `for.color` colors from the
25
+ * requested scheme. Plug into `scale("color", { palette })`.
26
+ */
27
+ color: CategoricalPalette;
28
+ /**
29
+ * Shape palette sliced to exactly `for.shape` entries. Plug into
30
+ * `scale("shape", { palette })`.
31
+ */
32
+ shape: readonly PointShapeKind[];
33
+ }
34
+ /**
35
+ * Build a matched `{ color, shape }` pair sized for the two channels'
36
+ * cardinalities. Sugar over manual `palette.colors.slice(0, N)` +
37
+ * `POINT_SHAPE_PALETTE.slice(0, M)` — the goal is one declaration site for
38
+ * the visual contract of a multi-encoding chart.
39
+ *
40
+ * Counts that exceed the source pool wrap modulo length, matching the
41
+ * default `CategoricalPalette` / scale wrap semantics so the helper never
42
+ * silently drops categories. (The visual distinction degrades past the pool
43
+ * size — that's an authoring problem, not a library one — but the runtime
44
+ * stays consistent with the rest of the scale system.)
45
+ */
46
+ export declare function distinguishable(options: DistinguishableOptions): DistinguishablePalettes;
47
+ /**
48
+ * `palettes` namespace — currently the home of `distinguishable`. Future
49
+ * combinators (legend-paired schemes, perceptual-uniform slicing, …) live
50
+ * here so the surface stays small and predictable.
51
+ */
52
+ export declare const palettes: {
53
+ readonly distinguishable: typeof distinguishable;
54
+ };
@@ -0,0 +1,80 @@
1
+ import { describe, expect, test } from "vite-plus/test";
2
+
3
+ import { category10, set1, tableau10 } from "../colors.ts";
4
+ import { POINT_SHAPE_PALETTE } from "../marks.ts";
5
+ import { distinguishable, palettes } from "./palettes.ts";
6
+
7
+ describe("palettes.distinguishable", () => {
8
+ test("returns the matched color/shape pair sized to the requested counts", () => {
9
+ const { color, shape } = distinguishable({ for: { color: 4, shape: 3 } });
10
+ expect(color.kind).toBe("categorical");
11
+ expect(color.colors.length).toBe(4);
12
+ expect(shape.length).toBe(3);
13
+ // First N colors come from category10 (the default scheme).
14
+ for (let i = 0; i < 4; i++) {
15
+ expect(color.colors[i]).toEqual(category10.colors[i]);
16
+ }
17
+ // Shapes mirror POINT_SHAPE_PALETTE's ordering.
18
+ for (let i = 0; i < 3; i++) {
19
+ expect(shape[i]).toBe(POINT_SHAPE_PALETTE[i]);
20
+ }
21
+ });
22
+
23
+ test("named scheme picks the corresponding built-in palette", () => {
24
+ const { color } = distinguishable({ for: { color: 3, shape: 1 }, scheme: "tableau10" });
25
+ for (let i = 0; i < 3; i++) {
26
+ expect(color.colors[i]).toEqual(tableau10.colors[i]);
27
+ }
28
+ });
29
+
30
+ test("custom CategoricalPalette passed as scheme is honored", () => {
31
+ const { color } = distinguishable({ for: { color: 2, shape: 1 }, scheme: set1 });
32
+ expect(color.colors[0]).toEqual(set1.colors[0]);
33
+ expect(color.colors[1]).toEqual(set1.colors[1]);
34
+ });
35
+
36
+ test("custom shape pool is honored", () => {
37
+ const pool = ["square", "diamond"] as const;
38
+ const { shape } = distinguishable({ for: { color: 1, shape: 2 }, shapes: pool });
39
+ expect(shape).toEqual(["square", "diamond"]);
40
+ });
41
+
42
+ test("counts that exceed pool size wrap modulo the pool length", () => {
43
+ const { color, shape } = distinguishable({ for: { color: 12, shape: 14 } });
44
+ expect(color.colors.length).toBe(12);
45
+ expect(shape.length).toBe(14);
46
+ // category10 has 10 colors — index 10 wraps to index 0.
47
+ expect(color.colors[10]).toEqual(category10.colors[0]);
48
+ expect(color.colors[11]).toEqual(category10.colors[1]);
49
+ // POINT_SHAPE_PALETTE has 11 entries — index 11 wraps to index 0.
50
+ expect(shape[11]).toBe(POINT_SHAPE_PALETTE[0]);
51
+ });
52
+
53
+ test("returned color palette is callable and wraps like the built-ins", () => {
54
+ const { color } = distinguishable({ for: { color: 3, shape: 1 } });
55
+ // Modulo-wrap parity with the standard categorical-palette closure.
56
+ expect(color(0)).toEqual(color.colors[0]);
57
+ expect(color(3)).toEqual(color.colors[0]);
58
+ expect(color(-1)).toEqual(color.colors[2]);
59
+ });
60
+
61
+ test("invalid counts throw at construction", () => {
62
+ expect(() => distinguishable({ for: { color: 0, shape: 1 } })).toThrow(/color/);
63
+ expect(() => distinguishable({ for: { color: 2, shape: -1 } })).toThrow(/shape/);
64
+ expect(() => distinguishable({ for: { color: 1.5, shape: 1 } })).toThrow(/color/);
65
+ });
66
+
67
+ test("unknown scheme name throws", () => {
68
+ expect(() =>
69
+ distinguishable({
70
+ for: { color: 2, shape: 1 },
71
+ // Force an invalid string past the union — surface check on the runtime guard.
72
+ scheme: "not-a-scheme" as unknown as "category10",
73
+ }),
74
+ ).toThrow(/Unknown palette scheme/);
75
+ });
76
+
77
+ test("namespace re-export matches the function export", () => {
78
+ expect(palettes.distinguishable).toBe(distinguishable);
79
+ });
80
+ });
@@ -0,0 +1,167 @@
1
+ // ---------------------------------------------------------------------------
2
+ // palettes — categorical encoding combinators
3
+ // ---------------------------------------------------------------------------
4
+ // Companion to the round-1 auto-legend merge: that closes the case where
5
+ // `color` and `shape` encode the *same* dimension. This namespace closes
6
+ // the inverse case — when `color` and `shape` encode *different* dimensions
7
+ // and the consumer wants matched defaults instead of picking ad hoc.
8
+ //
9
+ // `distinguishable({ for: { color: N, shape: M } })` returns a `{ color,
10
+ // shape }` pair: a categorical palette sized for `N` (the first N entries
11
+ // of the requested scheme) and a shape palette sized for `M` (the first M
12
+ // entries of `POINT_SHAPE_PALETTE`, which is already ordered by silhouette
13
+ // distinctness — circle, triangle, square, diamond, …).
14
+ //
15
+ // The helper is sugar, not a heuristic: built-in categorical palettes
16
+ // (`category10`, `tableau10`, `set1`, …) are already authored to maximize
17
+ // inter-color distinction, and `POINT_SHAPE_PALETTE` is already ordered
18
+ // for maximum inter-shape distinction. The win is that callers stop
19
+ // hand-slicing them in module-level boilerplate.
20
+
21
+ import type { Color } from "insomni";
22
+ import { POINT_SHAPE_PALETTE, type PointShapeKind } from "../marks.ts";
23
+ import {
24
+ category10,
25
+ dark2,
26
+ paired,
27
+ pastel,
28
+ set1,
29
+ set2,
30
+ set3,
31
+ tableau10,
32
+ type CategoricalPalette,
33
+ } from "../colors.ts";
34
+
35
+ /** Built-in categorical palette identifiers supported by `distinguishable`. */
36
+ export type DistinguishableScheme =
37
+ | "category10"
38
+ | "tableau10"
39
+ | "set1"
40
+ | "set2"
41
+ | "set3"
42
+ | "dark2"
43
+ | "paired"
44
+ | "pastel";
45
+
46
+ const SCHEMES: Record<DistinguishableScheme, CategoricalPalette> = {
47
+ category10,
48
+ tableau10,
49
+ set1,
50
+ set2,
51
+ set3,
52
+ dark2,
53
+ paired,
54
+ pastel,
55
+ };
56
+
57
+ export interface DistinguishableOptions {
58
+ /** Per-channel cardinality. Each count must be ≥ 1. */
59
+ for: { color: number; shape: number };
60
+ /**
61
+ * Categorical palette to source the color palette from. Default
62
+ * `"category10"`. Pass a `CategoricalPalette` directly for a custom set.
63
+ */
64
+ scheme?: DistinguishableScheme | CategoricalPalette;
65
+ /**
66
+ * Override the shape pool. Default: `POINT_SHAPE_PALETTE` from `marks`,
67
+ * which is already ordered for silhouette distinctness.
68
+ */
69
+ shapes?: readonly PointShapeKind[];
70
+ }
71
+
72
+ export interface DistinguishablePalettes {
73
+ /**
74
+ * A categorical palette sliced to exactly `for.color` colors from the
75
+ * requested scheme. Plug into `scale("color", { palette })`.
76
+ */
77
+ color: CategoricalPalette;
78
+ /**
79
+ * Shape palette sliced to exactly `for.shape` entries. Plug into
80
+ * `scale("shape", { palette })`.
81
+ */
82
+ shape: readonly PointShapeKind[];
83
+ }
84
+
85
+ function resolveScheme(
86
+ scheme: DistinguishableScheme | CategoricalPalette | undefined,
87
+ ): CategoricalPalette {
88
+ if (!scheme) return category10;
89
+ if (typeof scheme === "string") {
90
+ const palette = SCHEMES[scheme];
91
+ if (!palette) {
92
+ throw new Error(`Unknown palette scheme "${scheme as string}".`);
93
+ }
94
+ return palette;
95
+ }
96
+ return scheme;
97
+ }
98
+
99
+ /**
100
+ * Build a matched `{ color, shape }` pair sized for the two channels'
101
+ * cardinalities. Sugar over manual `palette.colors.slice(0, N)` +
102
+ * `POINT_SHAPE_PALETTE.slice(0, M)` — the goal is one declaration site for
103
+ * the visual contract of a multi-encoding chart.
104
+ *
105
+ * Counts that exceed the source pool wrap modulo length, matching the
106
+ * default `CategoricalPalette` / scale wrap semantics so the helper never
107
+ * silently drops categories. (The visual distinction degrades past the pool
108
+ * size — that's an authoring problem, not a library one — but the runtime
109
+ * stays consistent with the rest of the scale system.)
110
+ */
111
+ export function distinguishable(options: DistinguishableOptions): DistinguishablePalettes {
112
+ const colorCount = options.for.color;
113
+ const shapeCount = options.for.shape;
114
+ if (!Number.isInteger(colorCount) || colorCount < 1) {
115
+ throw new Error(`distinguishable: 'for.color' must be an integer ≥ 1 (got ${colorCount}).`);
116
+ }
117
+ if (!Number.isInteger(shapeCount) || shapeCount < 1) {
118
+ throw new Error(`distinguishable: 'for.shape' must be an integer ≥ 1 (got ${shapeCount}).`);
119
+ }
120
+ const palette = resolveScheme(options.scheme);
121
+ const shapePool = options.shapes ?? POINT_SHAPE_PALETTE;
122
+ if (shapePool.length === 0) {
123
+ throw new Error("distinguishable: 'shapes' pool must contain at least one shape.");
124
+ }
125
+
126
+ const sourceColors = palette.colors;
127
+ const colors: Color[] = Array.from({ length: colorCount });
128
+ for (let i = 0; i < colorCount; i++) {
129
+ colors[i] = sourceColors[i % sourceColors.length]!;
130
+ }
131
+ const colorPalette = categoricalFromColors(colors);
132
+
133
+ const shape: PointShapeKind[] = Array.from({ length: shapeCount });
134
+ for (let i = 0; i < shapeCount; i++) {
135
+ shape[i] = shapePool[i % shapePool.length]!;
136
+ }
137
+
138
+ return { color: colorPalette, shape };
139
+ }
140
+
141
+ // Build a CategoricalPalette from already-resolved `Color` objects. Mirrors
142
+ // the closure in `colors.ts`'s `createCategoricalPalette` (which only
143
+ // accepts hex strings publicly). Kept private to this module to avoid
144
+ // growing the public colors surface for a single internal caller.
145
+ function categoricalFromColors(colors: readonly Color[]): CategoricalPalette {
146
+ if (colors.length === 0) {
147
+ throw new Error("Categorical palettes must contain at least one color.");
148
+ }
149
+ const palette = ((index: number) => {
150
+ const resolved = ((Math.trunc(index) % colors.length) + colors.length) % colors.length;
151
+ return colors[resolved]!;
152
+ }) as CategoricalPalette;
153
+ Object.defineProperties(palette, {
154
+ kind: { value: "categorical", enumerable: true },
155
+ colors: { value: colors, enumerable: true },
156
+ });
157
+ return palette;
158
+ }
159
+
160
+ /**
161
+ * `palettes` namespace — currently the home of `distinguishable`. Future
162
+ * combinators (legend-paired schemes, perceptual-uniform slicing, …) live
163
+ * here so the surface stays small and predictable.
164
+ */
165
+ export const palettes = {
166
+ distinguishable,
167
+ } as const;