@suzumiyaaoba/mdxr 0.1.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 (236) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/dist/cli.d.mts +1 -0
  4. package/dist/cli.mjs +2902 -0
  5. package/dist/components.d.mts +791 -0
  6. package/dist/components.mjs +2 -0
  7. package/dist/config-SI9IyFiC.mjs +184 -0
  8. package/dist/doc-context-CEqzMYKv.d.mts +568 -0
  9. package/dist/index.d.mts +20 -0
  10. package/dist/index.mjs +4 -0
  11. package/dist/ui-GKFD0mx5.mjs +15986 -0
  12. package/package.json +131 -0
  13. package/skill/SKILL.md +49 -0
  14. package/skill/references/components/charts.md +172 -0
  15. package/skill/references/components/document.md +98 -0
  16. package/skill/references/components/forms.md +35 -0
  17. package/skill/references/components/investigation.md +198 -0
  18. package/skill/references/components/layout.md +68 -0
  19. package/skill/references/components/output.md +158 -0
  20. package/skill/references/components/planning.md +155 -0
  21. package/skill/references/components/reports.md +252 -0
  22. package/skill/references/components/shadcn.md +19 -0
  23. package/skill/references/components.md +186 -0
  24. package/skill/references/extending.md +48 -0
  25. package/src/ask-sheet.ts +40 -0
  26. package/src/assets/css.ts +392 -0
  27. package/src/assets/scripts.ts +78 -0
  28. package/src/catalog.ts +287 -0
  29. package/src/cli.ts +178 -0
  30. package/src/client/doc-events.ts +1108 -0
  31. package/src/client/entry.ts +27 -0
  32. package/src/client-js.ts +47 -0
  33. package/src/component-map.ts +47 -0
  34. package/src/components/ui/accordion.tsx +77 -0
  35. package/src/components/ui/alert-dialog.tsx +185 -0
  36. package/src/components/ui/alert.tsx +76 -0
  37. package/src/components/ui/aspect-ratio.tsx +22 -0
  38. package/src/components/ui/attachment.tsx +208 -0
  39. package/src/components/ui/avatar.tsx +106 -0
  40. package/src/components/ui/badge.tsx +52 -0
  41. package/src/components/ui/breadcrumb.tsx +121 -0
  42. package/src/components/ui/bubble.tsx +128 -0
  43. package/src/components/ui/button-group.tsx +88 -0
  44. package/src/components/ui/button.tsx +58 -0
  45. package/src/components/ui/calendar.tsx +226 -0
  46. package/src/components/ui/card.tsx +102 -0
  47. package/src/components/ui/carousel.tsx +246 -0
  48. package/src/components/ui/chart.tsx +379 -0
  49. package/src/components/ui/checkbox.tsx +27 -0
  50. package/src/components/ui/collapsible.tsx +19 -0
  51. package/src/components/ui/combobox.tsx +298 -0
  52. package/src/components/ui/command.tsx +193 -0
  53. package/src/components/ui/context-menu.tsx +271 -0
  54. package/src/components/ui/dialog.tsx +159 -0
  55. package/src/components/ui/direction.tsx +4 -0
  56. package/src/components/ui/drawer.tsx +227 -0
  57. package/src/components/ui/dropdown-menu.tsx +269 -0
  58. package/src/components/ui/empty.tsx +104 -0
  59. package/src/components/ui/field.tsx +237 -0
  60. package/src/components/ui/fieldset.tsx +32 -0
  61. package/src/components/ui/frame.tsx +87 -0
  62. package/src/components/ui/hover-card.tsx +50 -0
  63. package/src/components/ui/input-group.tsx +159 -0
  64. package/src/components/ui/input-otp.tsx +83 -0
  65. package/src/components/ui/input.tsx +19 -0
  66. package/src/components/ui/item.tsx +202 -0
  67. package/src/components/ui/kbd.tsx +26 -0
  68. package/src/components/ui/label.tsx +19 -0
  69. package/src/components/ui/marker.tsx +71 -0
  70. package/src/components/ui/menubar.tsx +284 -0
  71. package/src/components/ui/message-scroller.tsx +128 -0
  72. package/src/components/ui/message.tsx +91 -0
  73. package/src/components/ui/meter.tsx +80 -0
  74. package/src/components/ui/native-select.tsx +64 -0
  75. package/src/components/ui/navigation-menu.tsx +170 -0
  76. package/src/components/ui/pagination.tsx +133 -0
  77. package/src/components/ui/popover.tsx +87 -0
  78. package/src/components/ui/progress.tsx +82 -0
  79. package/src/components/ui/questionnaire.tsx +328 -0
  80. package/src/components/ui/radio-group.tsx +35 -0
  81. package/src/components/ui/resizable.tsx +49 -0
  82. package/src/components/ui/scroll-area.tsx +50 -0
  83. package/src/components/ui/select.tsx +201 -0
  84. package/src/components/ui/separator.tsx +22 -0
  85. package/src/components/ui/sheet.tsx +135 -0
  86. package/src/components/ui/sidebar.tsx +730 -0
  87. package/src/components/ui/skeleton.tsx +13 -0
  88. package/src/components/ui/slider.tsx +51 -0
  89. package/src/components/ui/spinner.tsx +16 -0
  90. package/src/components/ui/switch.tsx +31 -0
  91. package/src/components/ui/table.tsx +113 -0
  92. package/src/components/ui/tabs.tsx +82 -0
  93. package/src/components/ui/textarea.tsx +17 -0
  94. package/src/components/ui/toast.tsx +229 -0
  95. package/src/components/ui/toggle-group.tsx +87 -0
  96. package/src/components/ui/toggle.tsx +43 -0
  97. package/src/components/ui/tooltip.tsx +65 -0
  98. package/src/components.ts +78 -0
  99. package/src/config.ts +56 -0
  100. package/src/define.ts +122 -0
  101. package/src/doc-context.ts +20 -0
  102. package/src/editor.ts +94 -0
  103. package/src/format-error.ts +78 -0
  104. package/src/guards.ts +84 -0
  105. package/src/hooks/use-mobile.ts +21 -0
  106. package/src/html.ts +91 -0
  107. package/src/hydrate/export-index.ts +232 -0
  108. package/src/hydrate/import-scan.ts +169 -0
  109. package/src/hydrate/plugins.ts +118 -0
  110. package/src/hydrate/runtime-module.ts +145 -0
  111. package/src/hydrate-runtime.ts +67 -0
  112. package/src/hydrate.ts +147 -0
  113. package/src/index.ts +16 -0
  114. package/src/init.ts +57 -0
  115. package/src/langs.ts +148 -0
  116. package/src/lines.ts +53 -0
  117. package/src/load-user-module.ts +191 -0
  118. package/src/mdx.ts +248 -0
  119. package/src/paths.ts +36 -0
  120. package/src/rehype/shiki.ts +533 -0
  121. package/src/remark/alerts.ts +53 -0
  122. package/src/remark/ast.ts +96 -0
  123. package/src/remark/callouts.ts +25 -0
  124. package/src/remark/code-file.ts +85 -0
  125. package/src/remark/code-meta.ts +21 -0
  126. package/src/remark/directives.ts +170 -0
  127. package/src/remark/file-paths.ts +64 -0
  128. package/src/remark/headings.ts +131 -0
  129. package/src/remark/no-js.ts +40 -0
  130. package/src/render.ts +337 -0
  131. package/src/serve.ts +322 -0
  132. package/src/styles/globals.css +134 -0
  133. package/src/styles/shadcn.css +641 -0
  134. package/src/tailwind.ts +119 -0
  135. package/src/ui/approvals.tsx +76 -0
  136. package/src/ui/ask-question.tsx +386 -0
  137. package/src/ui/ask.tsx +206 -0
  138. package/src/ui/attrs.ts +51 -0
  139. package/src/ui/audit.tsx +139 -0
  140. package/src/ui/bar-chart.tsx +334 -0
  141. package/src/ui/benchmarks.tsx +143 -0
  142. package/src/ui/bits.tsx +537 -0
  143. package/src/ui/board.tsx +173 -0
  144. package/src/ui/bridge.tsx +207 -0
  145. package/src/ui/bumps.tsx +178 -0
  146. package/src/ui/callout.tsx +106 -0
  147. package/src/ui/changes.tsx +89 -0
  148. package/src/ui/chart-bits.tsx +52 -0
  149. package/src/ui/chart.ts +577 -0
  150. package/src/ui/checks.tsx +203 -0
  151. package/src/ui/child-index.tsx +44 -0
  152. package/src/ui/children.ts +43 -0
  153. package/src/ui/chips.tsx +49 -0
  154. package/src/ui/cmd.tsx +27 -0
  155. package/src/ui/columns.tsx +31 -0
  156. package/src/ui/comments.tsx +544 -0
  157. package/src/ui/compare.tsx +67 -0
  158. package/src/ui/decision.tsx +79 -0
  159. package/src/ui/deps.tsx +78 -0
  160. package/src/ui/details.tsx +43 -0
  161. package/src/ui/diff-parse.ts +307 -0
  162. package/src/ui/diff.tsx +458 -0
  163. package/src/ui/diffstat.tsx +58 -0
  164. package/src/ui/due.tsx +65 -0
  165. package/src/ui/effort.tsx +29 -0
  166. package/src/ui/endpoints.tsx +118 -0
  167. package/src/ui/envvars.tsx +96 -0
  168. package/src/ui/figure.tsx +37 -0
  169. package/src/ui/file-icon.ts +1020 -0
  170. package/src/ui/file-link.ts +32 -0
  171. package/src/ui/file-ref.tsx +39 -0
  172. package/src/ui/files.tsx +112 -0
  173. package/src/ui/findings.tsx +85 -0
  174. package/src/ui/flow.tsx +73 -0
  175. package/src/ui/funnel.tsx +121 -0
  176. package/src/ui/gantt.tsx +443 -0
  177. package/src/ui/gauges.tsx +135 -0
  178. package/src/ui/glossary.tsx +29 -0
  179. package/src/ui/graph-layout.ts +149 -0
  180. package/src/ui/graph-specs.tsx +102 -0
  181. package/src/ui/graph.tsx +278 -0
  182. package/src/ui/grid.tsx +119 -0
  183. package/src/ui/hypothesis.tsx +94 -0
  184. package/src/ui/icon.tsx +80 -0
  185. package/src/ui/incident.tsx +130 -0
  186. package/src/ui/index.ts +342 -0
  187. package/src/ui/ins-del.tsx +41 -0
  188. package/src/ui/json.tsx +190 -0
  189. package/src/ui/layout.ts +9 -0
  190. package/src/ui/line-chart.tsx +251 -0
  191. package/src/ui/matrix.tsx +208 -0
  192. package/src/ui/meta.tsx +73 -0
  193. package/src/ui/option.tsx +64 -0
  194. package/src/ui/owner.tsx +40 -0
  195. package/src/ui/packages.tsx +104 -0
  196. package/src/ui/pathway.tsx +96 -0
  197. package/src/ui/phase.tsx +41 -0
  198. package/src/ui/pie-chart.tsx +170 -0
  199. package/src/ui/plan.tsx +77 -0
  200. package/src/ui/pre.tsx +175 -0
  201. package/src/ui/priority.tsx +51 -0
  202. package/src/ui/props.tsx +77 -0
  203. package/src/ui/quadrant.tsx +172 -0
  204. package/src/ui/radar.tsx +200 -0
  205. package/src/ui/ref.tsx +160 -0
  206. package/src/ui/release.tsx +178 -0
  207. package/src/ui/req.tsx +42 -0
  208. package/src/ui/review.tsx +133 -0
  209. package/src/ui/risk.tsx +67 -0
  210. package/src/ui/sankey.tsx +287 -0
  211. package/src/ui/scatter.tsx +237 -0
  212. package/src/ui/schema.tsx +113 -0
  213. package/src/ui/score.tsx +105 -0
  214. package/src/ui/search.tsx +120 -0
  215. package/src/ui/series.tsx +38 -0
  216. package/src/ui/severity.tsx +69 -0
  217. package/src/ui/shadcn.tsx +216 -0
  218. package/src/ui/spark.tsx +86 -0
  219. package/src/ui/stack.tsx +57 -0
  220. package/src/ui/stats.tsx +63 -0
  221. package/src/ui/status-badge.tsx +66 -0
  222. package/src/ui/statuspage.tsx +238 -0
  223. package/src/ui/steps.tsx +74 -0
  224. package/src/ui/summary.tsx +41 -0
  225. package/src/ui/symbol-ref.tsx +73 -0
  226. package/src/ui/terminal.tsx +117 -0
  227. package/src/ui/tests.tsx +218 -0
  228. package/src/ui/timeline.tsx +63 -0
  229. package/src/ui/toc.tsx +56 -0
  230. package/src/ui/tones.ts +187 -0
  231. package/src/ui/trace.tsx +69 -0
  232. package/src/ui/tree.tsx +281 -0
  233. package/src/ui/treemap.tsx +128 -0
  234. package/src/ui/venn.tsx +258 -0
  235. package/src/ui/verdict.tsx +78 -0
  236. package/src/ui/waterfall.tsx +142 -0
@@ -0,0 +1,577 @@
1
+ import { numOf } from "./attrs.js";
2
+ import { TONE, TONE_TEXT } from "./tones.js";
3
+ import type { Tone } from "./tones.js";
4
+
5
+ /**
6
+ * Shared chart machinery — series palette, document-attribute number parsing,
7
+ * "nice" axis scales, and pure SVG/layout geometry for the catalog's
8
+ * quantitative components (BarChart, LineChart, PieChart, Scatter, Radar,
9
+ * Funnel, Quadrant, Bridge, Treemap, Sankey, Venn). All layouts are computed
10
+ * at render time; no client JS is involved.
11
+ */
12
+
13
+ /** Tones usable as an explicit `tone=` override on data items. */
14
+ export const CHART_TONES = [
15
+ "sky",
16
+ "emerald",
17
+ "amber",
18
+ "violet",
19
+ "teal",
20
+ "indigo",
21
+ "orange",
22
+ "red",
23
+ ] as const satisfies readonly Tone[];
24
+
25
+ export type ChartTone = (typeof CHART_TONES)[number];
26
+
27
+ /** `fill-{c}-500` — solid SVG fill per tone (literal classes for the scanner). */
28
+ const FILL: Record<Tone, string> = {
29
+ amber: "fill-amber-500",
30
+ emerald: "fill-emerald-500",
31
+ indigo: "fill-indigo-500",
32
+ neutral: "fill-neutral-400",
33
+ orange: "fill-orange-500",
34
+ red: "fill-red-500",
35
+ sky: "fill-sky-500",
36
+ teal: "fill-teal-500",
37
+ violet: "fill-violet-500",
38
+ };
39
+
40
+ /** `fill-{c}-500/20` — soft SVG fill for areas, ribbons, venn circles. */
41
+ const SOFT_FILL: Record<Tone, string> = {
42
+ amber: "fill-amber-500/20",
43
+ emerald: "fill-emerald-500/20",
44
+ indigo: "fill-indigo-500/20",
45
+ neutral: "fill-neutral-400/20",
46
+ orange: "fill-orange-500/20",
47
+ red: "fill-red-500/20",
48
+ sky: "fill-sky-500/20",
49
+ teal: "fill-teal-500/20",
50
+ violet: "fill-violet-500/20",
51
+ };
52
+
53
+ /** `stroke-{c}-500` — SVG stroke per tone. */
54
+ const STROKE: Record<Tone, string> = {
55
+ amber: "stroke-amber-500",
56
+ emerald: "stroke-emerald-500",
57
+ indigo: "stroke-indigo-500",
58
+ neutral: "stroke-neutral-400",
59
+ orange: "stroke-orange-500",
60
+ red: "stroke-red-500",
61
+ sky: "stroke-sky-500",
62
+ teal: "stroke-teal-500",
63
+ violet: "stroke-violet-500",
64
+ };
65
+
66
+ /** `bg-{c}-500` — solid HTML fill for bars/dots. */
67
+ const BG: Record<Tone, string> = {
68
+ amber: "bg-amber-500",
69
+ emerald: "bg-emerald-500",
70
+ indigo: "bg-indigo-500",
71
+ neutral: "bg-neutral-400",
72
+ orange: "bg-orange-500",
73
+ red: "bg-red-500",
74
+ sky: "bg-sky-500",
75
+ teal: "bg-teal-500",
76
+ violet: "bg-violet-500",
77
+ };
78
+
79
+ /** `bg-{c}-500/25` — translucent HTML fill for floating range segments. */
80
+ const SOFT_BG: Record<Tone, string> = {
81
+ amber: "bg-amber-500/25",
82
+ emerald: "bg-emerald-500/25",
83
+ indigo: "bg-indigo-500/25",
84
+ neutral: "bg-neutral-400/25",
85
+ orange: "bg-orange-500/25",
86
+ red: "bg-red-500/25",
87
+ sky: "bg-sky-500/25",
88
+ teal: "bg-teal-500/25",
89
+ violet: "bg-violet-500/25",
90
+ };
91
+
92
+ export interface SeriesStyle {
93
+ /** Solid HTML background (`bg-{c}-500`) — bars, dots. */
94
+ bg: string;
95
+ /** Solid SVG fill (`fill-{c}-500`). */
96
+ fill: string;
97
+ /** Translucent SVG fill (`fill-{c}-500/20`) — areas, ribbons. */
98
+ soft: string;
99
+ /** Translucent HTML background — range segments. */
100
+ softBg: string;
101
+ /** SVG stroke (`stroke-{c}-500`). */
102
+ stroke: string;
103
+ /** Text accent (`text-{c}-600 dark:text-{c}-400`). */
104
+ text: string;
105
+ /** Toned tile surface (soft bg + readable text) — treemap regions. */
106
+ tile: string;
107
+ }
108
+
109
+ const styleOf = (tone: Tone): SeriesStyle => ({
110
+ bg: BG[tone],
111
+ fill: FILL[tone],
112
+ soft: SOFT_FILL[tone],
113
+ softBg: SOFT_BG[tone],
114
+ stroke: STROKE[tone],
115
+ text: TONE_TEXT[tone],
116
+ tile: TONE[tone],
117
+ });
118
+
119
+ /** Cyclic series palette — index `i` mod length picks a series color. */
120
+ export const SERIES: readonly SeriesStyle[] = CHART_TONES.map(styleOf);
121
+
122
+ export const seriesOf = (i: number): SeriesStyle =>
123
+ SERIES[((i % SERIES.length) + SERIES.length) % SERIES.length];
124
+
125
+ /** Style for an explicit `tone` prop; falls back to the cyclic palette. */
126
+ export const toneOf = (tone: string | undefined, i: number): SeriesStyle => {
127
+ const t = CHART_TONES.find((c) => c === tone);
128
+ return t === undefined ? seriesOf(i) : styleOf(t);
129
+ };
130
+
131
+ /**
132
+ * Comma/space-separated number list from a document attribute
133
+ * (`values="12, 8, 4"`). Non-numeric entries are dropped.
134
+ */
135
+ export const numList = (x: unknown): number[] => {
136
+ if (typeof x === "number") {
137
+ return Number.isFinite(x) ? [x] : [];
138
+ }
139
+ if (typeof x !== "string") {
140
+ return [];
141
+ }
142
+ return x
143
+ .split(/[,\s]+/u)
144
+ .map(numOf)
145
+ .filter((n): n is number => n !== undefined);
146
+ };
147
+
148
+ /**
149
+ * Comma-separated label list (`labels="Q1, Q2"`). Whitespace around each
150
+ * label is trimmed; empty entries are dropped.
151
+ */
152
+ export const textList = (x: unknown): string[] =>
153
+ typeof x === "string"
154
+ ? x
155
+ .split(",")
156
+ .map((s) => s.trim())
157
+ .filter((s) => s !== "")
158
+ : [];
159
+
160
+ const round2 = (n: number): number => Math.round(n * 100) / 100;
161
+
162
+ /** Compact tick/label number — `1.2k`, `3M`, plain integers under 1000. */
163
+ export const fmtNum = (n: number): string => {
164
+ if (!Number.isFinite(n)) {
165
+ return "0";
166
+ }
167
+ const abs = Math.abs(n);
168
+ if (abs >= 1_000_000) {
169
+ return `${round2(n / 1_000_000)}M`;
170
+ }
171
+ if (abs >= 10_000) {
172
+ return `${Math.round(n / 1000)}k`;
173
+ }
174
+ if (abs >= 1000) {
175
+ return `${round2(n / 1000)}k`;
176
+ }
177
+ return `${round2(n)}`;
178
+ };
179
+
180
+ const STEPS = [1, 2, 2.5, 5, 10] as const;
181
+
182
+ /** Round a span up to a 1/2/2.5/5×10^k step. */
183
+ const niceStep = (rough: number): number => {
184
+ const mag = 10 ** Math.floor(Math.log10(rough));
185
+ return STEPS.map((m) => m * mag).find((s) => s >= rough - 1e-9) ?? 10 * mag;
186
+ };
187
+
188
+ export interface AxisScale {
189
+ /** Scale top — `max` rounded up to a tick multiple. */
190
+ ceil: number;
191
+ /** Tick step (a "nice" 1/2/2.5/5×10^k value). */
192
+ step: number;
193
+ /** Tick values including `ceil` (0 excluded — the baseline marks it). */
194
+ ticks: number[];
195
+ }
196
+
197
+ /**
198
+ * Zero-based "nice" scale covering [0, max]: the top is a round multiple of
199
+ * a 1/2/2.5/5 step and interior ticks land on step boundaries.
200
+ */
201
+ export const niceScale = (max: number, count = 4): AxisScale => {
202
+ if (!(max > 0)) {
203
+ return { ceil: 1, step: 1, ticks: [1] };
204
+ }
205
+ const step = niceStep(max / count);
206
+ const ceil = Math.ceil(max / step - 1e-9) * step;
207
+ const ticks: number[] = [];
208
+ for (let t = step; t <= ceil + 1e-9; t += step) {
209
+ ticks.push(round2(t));
210
+ }
211
+ return { ceil, step, ticks };
212
+ };
213
+
214
+ export interface ExtentScale {
215
+ max: number;
216
+ min: number;
217
+ /** Tick values snapped to step boundaries within [min, max]. */
218
+ ticks: number[];
219
+ }
220
+
221
+ /**
222
+ * "Nice" scale covering an arbitrary [min, max] extent — bounds snap outward
223
+ * to step boundaries so ticks stay round. A zero-width extent expands ±1.
224
+ */
225
+ export const niceBounds = (lo: number, hi: number, count = 4): ExtentScale => {
226
+ let min = lo;
227
+ let max = hi;
228
+ if (!(min <= max)) {
229
+ min = 0;
230
+ max = 1;
231
+ }
232
+ if (min === max) {
233
+ min -= 1;
234
+ max += 1;
235
+ }
236
+ const step = niceStep((max - min) / count);
237
+ const lo2 = Math.floor(min / step + 1e-9) * step;
238
+ const hi2 = Math.ceil(max / step - 1e-9) * step;
239
+ const ticks: number[] = [];
240
+ for (let t = lo2; t <= hi2 + 1e-9; t += step) {
241
+ ticks.push(round2(t));
242
+ }
243
+ return { max: hi2, min: lo2, ticks };
244
+ };
245
+
246
+ /** Point on a circle — angle in radians measured from +x, y grows downward. */
247
+ export const polar = (
248
+ cx: number,
249
+ cy: number,
250
+ r: number,
251
+ angle: number
252
+ ): { x: number; y: number } => ({
253
+ x: cx + r * Math.cos(angle),
254
+ y: cy + r * Math.sin(angle),
255
+ });
256
+
257
+ const TWO_PI = Math.PI * 2;
258
+
259
+ /**
260
+ * SVG path for a pie (r0 = 0) or annular sector between angles a0→a1
261
+ * (radians, +x = 0, clockwise as y grows). A full turn is split into two
262
+ * half arcs — a single `A` command cannot close on itself.
263
+ */
264
+ export const sectorPath = (
265
+ cx: number,
266
+ cy: number,
267
+ r0: number,
268
+ r1: number,
269
+ a0: number,
270
+ a1: number
271
+ ): string => {
272
+ const span = Math.min(a1 - a0, TWO_PI);
273
+ if (span <= 0 || r1 <= 0) {
274
+ return "";
275
+ }
276
+ const segs = span >= TWO_PI - 1e-6 ? 2 : 1;
277
+ const each = span / segs;
278
+ const parts: string[] = [];
279
+ for (let i = 0; i < segs; i += 1) {
280
+ const s = a0 + each * i;
281
+ const e = s + each;
282
+ const p0 = polar(cx, cy, r1, s);
283
+ const p1 = polar(cx, cy, r1, e);
284
+ const large = each > Math.PI ? 1 : 0;
285
+ if (r0 <= 0) {
286
+ parts.push(
287
+ `M${cx} ${cy}L${p0.x} ${p0.y}A${r1} ${r1} 0 ${large} 1 ${p1.x} ${p1.y}Z`
288
+ );
289
+ } else {
290
+ const q0 = polar(cx, cy, r0, e);
291
+ const q1 = polar(cx, cy, r0, s);
292
+ parts.push(
293
+ `M${p0.x} ${p0.y}A${r1} ${r1} 0 ${large} 1 ${p1.x} ${p1.y}L${q0.x} ${q0.y}A${r0} ${r0} 0 ${large} 0 ${q1.x} ${q1.y}Z`
294
+ );
295
+ }
296
+ }
297
+ return parts.join("");
298
+ };
299
+
300
+ /** Rounded coordinate for emitted SVG — trims float noise in `d` strings. */
301
+ const c = (n: number): number => Math.round(n * 100) / 100;
302
+
303
+ /** `M x0 y0 L x1 y1 …` polyline path from scaled points. */
304
+ export const linePath = (pts: readonly { x: number; y: number }[]): string =>
305
+ pts.map((p, i) => `${i === 0 ? "M" : "L"}${c(p.x)} ${c(p.y)}`).join("");
306
+
307
+ /** Closed polygon from `linePath` extended to a baseline (area fills). */
308
+ export const areaPath = (
309
+ pts: readonly { x: number; y: number }[],
310
+ baseY: number
311
+ ): string => {
312
+ if (pts.length === 0) {
313
+ return "";
314
+ }
315
+ const [first] = pts;
316
+ const last = pts.at(-1);
317
+ return `${linePath(pts)}L${c(last?.x ?? 0)} ${c(baseY)}L${c(first?.x ?? 0)} ${c(baseY)}Z`;
318
+ };
319
+
320
+ export interface Rect {
321
+ h: number;
322
+ w: number;
323
+ x: number;
324
+ y: number;
325
+ }
326
+
327
+ /**
328
+ * Worst aspect ratio in a candidate row — `w` is the length of the side the
329
+ * row is laid along (Bruls et al. §4). Squarify closes a row when adding
330
+ * another item would raise this ratio.
331
+ */
332
+ const worst = (row: readonly number[], w: number): number => {
333
+ const s = row.reduce((a, b) => a + b, 0);
334
+ if (s <= 0 || w <= 0) {
335
+ return Number.POSITIVE_INFINITY;
336
+ }
337
+ const s2 = s * s;
338
+ const w2 = w * w;
339
+ let m = 0;
340
+ for (const r of row) {
341
+ m = Math.max(m, (w2 * r) / s2, s2 / (w2 * r));
342
+ }
343
+ return m;
344
+ };
345
+
346
+ /**
347
+ * Squarified treemap layout (Bruls et al.) — returns one rect per input
348
+ * value in the same order, packed inside `rect`. Non-positive values are
349
+ * dropped (they get a zero rect).
350
+ */
351
+ export const squarify = (values: readonly number[], rect: Rect): Rect[] => {
352
+ const pending = values
353
+ .map((v, i) => ({ i, v }))
354
+ .filter((e) => e.v > 0)
355
+ .toSorted((a, b) => b.v - a.v);
356
+ const placed = new Map<number, Rect>();
357
+ let rest = { ...rect };
358
+ let remaining = pending.reduce((a, e) => a + e.v, 0);
359
+ let row: { i: number; v: number }[] = [];
360
+
361
+ const closeRow = () => {
362
+ if (row.length === 0 || remaining <= 0) {
363
+ return;
364
+ }
365
+ const sum = row.reduce((a, e) => a + e.v, 0);
366
+ const frac = Math.min(1, sum / remaining);
367
+ // The row strip occupies `frac` of the remaining area along the short
368
+ // side: a vertical strip when wider than tall, horizontal otherwise.
369
+ const wide = rest.w >= rest.h;
370
+ const strip = wide
371
+ ? { h: rest.h, w: rest.w * frac, x: rest.x, y: rest.y }
372
+ : { h: rest.h * frac, w: rest.w, x: rest.x, y: rest.y };
373
+ let offset = 0;
374
+ for (const e of row) {
375
+ const share = sum === 0 ? 0 : e.v / sum;
376
+ placed.set(
377
+ e.i,
378
+ wide
379
+ ? { h: strip.h * share, w: strip.w, x: strip.x, y: strip.y + offset }
380
+ : { h: strip.h, w: strip.w * share, x: strip.x + offset, y: strip.y }
381
+ );
382
+ offset += wide ? strip.h * share : strip.w * share;
383
+ }
384
+ rest = wide
385
+ ? {
386
+ h: rest.h,
387
+ w: rest.w * (1 - frac),
388
+ x: rest.x + strip.w,
389
+ y: rest.y,
390
+ }
391
+ : {
392
+ h: rest.h * (1 - frac),
393
+ w: rest.w,
394
+ x: rest.x,
395
+ y: rest.y + strip.h,
396
+ };
397
+ remaining -= sum;
398
+ row = [];
399
+ };
400
+
401
+ for (const item of pending) {
402
+ const side = Math.min(rest.w, rest.h);
403
+ const withItem = [...row, item];
404
+ if (
405
+ row.length > 0 &&
406
+ worst(
407
+ withItem.map((e) => e.v),
408
+ side
409
+ ) >
410
+ worst(
411
+ row.map((e) => e.v),
412
+ side
413
+ )
414
+ ) {
415
+ closeRow();
416
+ row = [item];
417
+ } else {
418
+ row = withItem;
419
+ }
420
+ }
421
+ closeRow();
422
+ return values.map((_, i) => placed.get(i) ?? { h: 0, w: 0, x: 0, y: 0 });
423
+ };
424
+
425
+ export interface SankeySpec {
426
+ links: { from: string; to: string; value: number }[];
427
+ nodes: { id: string; stage?: number }[];
428
+ }
429
+
430
+ export interface SankeyLayout {
431
+ /** Pixels per unit — ribbon thickness is `value * k`. */
432
+ k: number;
433
+ /** Link endpoints in layout space (y offsets within source/target bars). */
434
+ links: { from: string; sy: number; to: string; ty: number; value: number }[];
435
+ nodes: { h: number; id: string; stage: number; value: number; y: number }[];
436
+ stages: number;
437
+ }
438
+
439
+ /** Every node id mentioned by a <Node> spec or a link endpoint. */
440
+ const sankeyIds = (spec: SankeySpec): Set<string> => {
441
+ const ids = new Set<string>();
442
+ for (const n of spec.nodes) {
443
+ ids.add(n.id);
444
+ }
445
+ for (const l of spec.links) {
446
+ ids.add(l.from);
447
+ ids.add(l.to);
448
+ }
449
+ return ids;
450
+ };
451
+
452
+ /** Longest-path layering: stage = 1 + max stage of any incoming node. */
453
+ const resolveStage = (
454
+ id: string,
455
+ trail: Set<string>,
456
+ incoming: Map<string, string[]>,
457
+ explicit: Map<string, number | undefined>,
458
+ stageOf: Map<string, number>
459
+ ): number => {
460
+ const fixed = explicit.get(id);
461
+ if (fixed !== undefined) {
462
+ return fixed;
463
+ }
464
+ const memo = stageOf.get(id);
465
+ if (memo !== undefined) {
466
+ return memo;
467
+ }
468
+ if (trail.has(id)) {
469
+ // Cycle — break at the first repeated node.
470
+ return 0;
471
+ }
472
+ trail.add(id);
473
+ const deps = incoming.get(id) ?? [];
474
+ const stage =
475
+ deps.length === 0
476
+ ? 0
477
+ : Math.max(
478
+ ...deps.map((d) =>
479
+ resolveStage(d, trail, incoming, explicit, stageOf)
480
+ )
481
+ ) + 1;
482
+ trail.delete(id);
483
+ stageOf.set(id, stage);
484
+ return stage;
485
+ };
486
+
487
+ /** Node value = max(in-flow, out-flow). */
488
+ const sankeyValues = (links: SankeySpec["links"]): ((id: string) => number) => {
489
+ const flow = new Map<string, { in: number; out: number }>();
490
+ const add = (id: string, key: "in" | "out", v: number): void => {
491
+ const f = flow.get(id) ?? { in: 0, out: 0 };
492
+ f[key] += v;
493
+ flow.set(id, f);
494
+ };
495
+ for (const l of links) {
496
+ add(l.from, "out", l.value);
497
+ add(l.to, "in", l.value);
498
+ }
499
+ return (id) => {
500
+ const f = flow.get(id);
501
+ return f === undefined ? 0 : Math.max(f.in, f.out);
502
+ };
503
+ };
504
+
505
+ /**
506
+ * Sankey layout: nodes are vertical bars stacked per stage column, one
507
+ * px-per-unit scale across the whole diagram (column totals equalize at the
508
+ * tallest column). Stage comes from `node.stage` or longest-path layering
509
+ * from the source nodes. Link `sy`/`ty` are offsets inside each bar where
510
+ * the ribbon attaches.
511
+ */
512
+ export const layoutSankey = (
513
+ spec: SankeySpec,
514
+ height: number,
515
+ gap: number
516
+ ): SankeyLayout => {
517
+ const explicit = new Map(spec.nodes.map((n) => [n.id, n.stage]));
518
+ const stageOf = new Map<string, number>();
519
+ const incoming = new Map<string, string[]>();
520
+ for (const l of spec.links) {
521
+ incoming.set(l.to, [...(incoming.get(l.to) ?? []), l.from]);
522
+ }
523
+ const nodes = [...sankeyIds(spec)].map((id) => ({
524
+ id,
525
+ stage: resolveStage(id, new Set(), incoming, explicit, stageOf),
526
+ }));
527
+ const stages = Math.max(0, ...nodes.map((n) => n.stage)) + 1;
528
+ const valueOf = sankeyValues(spec.links);
529
+
530
+ const columns: string[][] = Array.from({ length: stages }, () => []);
531
+ for (const n of nodes) {
532
+ columns[n.stage]?.push(n.id);
533
+ }
534
+ const maxRows = Math.max(1, ...columns.map((col) => col.length));
535
+ const maxTotal = Math.max(
536
+ 0,
537
+ ...columns.map((col) => col.reduce((a, id) => a + valueOf(id), 0))
538
+ );
539
+ const k =
540
+ maxTotal <= 0 ? 0 : (height - gap * Math.max(0, maxRows - 1)) / maxTotal;
541
+
542
+ const laid = new Map<string, { h: number; y: number }>();
543
+ for (const col of columns) {
544
+ let y = 0;
545
+ for (const id of col) {
546
+ const h = Math.max(2, valueOf(id) * k);
547
+ laid.set(id, { h, y });
548
+ y += h + gap;
549
+ }
550
+ }
551
+
552
+ // Ribbon attach offsets: links leave a source bar top-down in link order,
553
+ // and arrive at target bars the same way — matching orders keep the
554
+ // crossing count low without a full barycenter pass.
555
+ const sourceUsed = new Map<string, number>();
556
+ const targetUsed = new Map<string, number>();
557
+ const links = spec.links.map((l) => {
558
+ const sOff = sourceUsed.get(l.from) ?? 0;
559
+ const tOff = targetUsed.get(l.to) ?? 0;
560
+ sourceUsed.set(l.from, sOff + l.value * k);
561
+ targetUsed.set(l.to, tOff + l.value * k);
562
+ return { from: l.from, sy: sOff, to: l.to, ty: tOff, value: l.value };
563
+ });
564
+
565
+ return {
566
+ k,
567
+ links,
568
+ nodes: nodes.map((n) => ({
569
+ h: laid.get(n.id)?.h ?? 0,
570
+ id: n.id,
571
+ stage: n.stage,
572
+ value: valueOf(n.id),
573
+ y: laid.get(n.id)?.y ?? 0,
574
+ })),
575
+ stages,
576
+ };
577
+ };