@svgrid/grid 3.0.2 → 3.0.4

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 (281) hide show
  1. package/README.md +2 -1
  2. package/dist/GridMenus.svelte +44 -2
  3. package/dist/SvChartMenu.svelte +69 -0
  4. package/dist/SvChartMenu.svelte.d.ts +11 -0
  5. package/dist/SvChartPanes.svelte +113 -0
  6. package/dist/SvChartPanes.svelte.d.ts +20 -0
  7. package/dist/SvGrid.controller.svelte.d.ts +110 -39
  8. package/dist/SvGrid.controller.svelte.js +655 -200
  9. package/dist/SvGrid.css +105 -2
  10. package/dist/SvGrid.svelte +386 -514
  11. package/dist/SvGrid.types.d.ts +210 -9
  12. package/dist/SvGridCellEditor.svelte +17 -0
  13. package/dist/SvGridChart.svelte +2257 -329
  14. package/dist/SvGridChart.svelte.d.ts +6 -106
  15. package/dist/SvGridChart.types.d.ts +338 -0
  16. package/dist/SvGridChart.types.js +1 -0
  17. package/dist/SvGridChartBuilder.svelte +323 -0
  18. package/dist/SvGridChartBuilder.svelte.d.ts +34 -0
  19. package/dist/SvGridChartPanel.svelte +221 -275
  20. package/dist/SvGridChartPickers.svelte +417 -0
  21. package/dist/SvGridChartPickers.svelte.d.ts +70 -0
  22. package/dist/SvGridChartView.svelte +13 -14
  23. package/dist/SvModal.svelte +7 -1
  24. package/dist/SvModal.svelte.d.ts +5 -0
  25. package/dist/ai.d.ts +40 -8
  26. package/dist/ai.js +135 -24
  27. package/dist/aria.d.ts +11 -0
  28. package/dist/build-api.js +108 -22
  29. package/dist/cdn/GridMenus-7kbpnnBW.js +635 -0
  30. package/dist/cdn/GridMenus-BL7ZgQvU.js +636 -0
  31. package/dist/cdn/SvChartMenu-Bl6PBbkT.js +58 -0
  32. package/dist/cdn/SvChartMenu-FBSMINA6.js +59 -0
  33. package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-BeU_TkbQ.js} +15 -14
  34. package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-yX8vzleW.js} +38 -37
  35. package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-DQwt4UAs.js} +139 -180
  36. package/dist/cdn/SvDateTimePicker-vNU6bZ-q.js +775 -0
  37. package/dist/cdn/{SvGridCellEditor-CjEJMJKc.js → SvGridCellEditor-B1p-vCK5.js} +188 -178
  38. package/dist/cdn/{SvGridCellEditor-BWnTi2N7.js → SvGridCellEditor-D_0q4xAu.js} +98 -88
  39. package/dist/cdn/SvGridChart-C3EWAZaM.js +3574 -0
  40. package/dist/cdn/SvGridChart-DzLkSwCH.js +3572 -0
  41. package/dist/cdn/SvGridChartBuilder-BE2T1ykB.js +783 -0
  42. package/dist/cdn/SvGridChartBuilder-CfII62sZ.js +784 -0
  43. package/dist/cdn/SvGridChartPanel-CCX5_Wgd.js +675 -0
  44. package/dist/cdn/SvGridChartPanel-xf4sWVTo.js +699 -0
  45. package/dist/cdn/SvGridChartView-CfuXmY5I.js +48 -0
  46. package/dist/cdn/SvGridChartView-DYwabQWj.js +47 -0
  47. package/dist/cdn/SvMenuList-CLN8OuIK.js +452 -0
  48. package/dist/cdn/SvMenuList-DkGhNKjf.js +451 -0
  49. package/dist/cdn/SvModal-BAE-pjZX.js +396 -0
  50. package/dist/cdn/SvModal-CIZWCcad.js +397 -0
  51. package/dist/cdn/chart-Dz7SqMXH.js +4341 -0
  52. package/dist/cdn/chart-export-BXqueeWJ.js +135 -0
  53. package/dist/cdn/chart-export-pdf-DONFUnNY.js +110 -0
  54. package/dist/cdn/chart-panel-messages-CUbf2R4i.js +1052 -0
  55. package/dist/cdn/chart-panel-messages-CmNrMdsr.js +1053 -0
  56. package/dist/cdn/chart-summary-BJW_tg_X.js +138 -0
  57. package/dist/cdn/chart-trend-CaN9mDEV.js +50 -0
  58. package/dist/cdn/chart-validate-HcOVFAfJ.js +188 -0
  59. package/dist/cdn/{column-resize-DsfNXMom.js → column-resize-DpmOLfRp.js} +3 -3
  60. package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CtquV-p3.js} +583 -603
  61. package/dist/cdn/{date-format-BNii4zeD.js → date-format-D6KFzU_W.js} +347 -367
  62. package/dist/cdn/dismissable-DAHetSNk.js +44 -0
  63. package/dist/cdn/editor-contract-LXgQJAgd.js +22 -0
  64. package/dist/cdn/focus-trap-BBOQYBma.js +65 -0
  65. package/dist/cdn/{row-resize-BRcimkUT.js → row-resize-niQCp040.js} +44 -36
  66. package/dist/cdn/{src-DNbz94Ct.js → src-BKhZ6eXd.js} +10227 -9787
  67. package/dist/cdn/{src-BJ2opd0j.js → src-V1uu8iE9.js} +6568 -6128
  68. package/dist/cdn/svgrid.js +26 -15
  69. package/dist/cdn/svgrid.svelte-external.js +26 -15
  70. package/dist/chart-axes.d.ts +53 -0
  71. package/dist/chart-axes.js +351 -0
  72. package/dist/chart-cartesian.d.ts +88 -0
  73. package/dist/chart-cartesian.js +1862 -0
  74. package/dist/chart-decimate.d.ts +51 -0
  75. package/dist/chart-decimate.js +199 -0
  76. package/dist/chart-export-pdf.d.ts +47 -0
  77. package/dist/chart-export-pdf.js +187 -0
  78. package/dist/chart-export.d.ts +2 -1
  79. package/dist/chart-export.js +18 -3
  80. package/dist/chart-financial.d.ts +91 -0
  81. package/dist/chart-financial.js +175 -0
  82. package/dist/chart-flow.d.ts +10 -0
  83. package/dist/chart-flow.js +298 -0
  84. package/dist/chart-format.d.ts +18 -0
  85. package/dist/chart-format.js +65 -0
  86. package/dist/chart-grid.d.ts +8 -0
  87. package/dist/chart-grid.js +190 -0
  88. package/dist/chart-hierarchy.d.ts +17 -0
  89. package/dist/chart-hierarchy.js +146 -0
  90. package/dist/chart-indicators.d.ts +115 -0
  91. package/dist/chart-indicators.js +389 -0
  92. package/dist/chart-messages.d.ts +95 -0
  93. package/dist/chart-messages.js +92 -0
  94. package/dist/chart-motion.d.ts +16 -0
  95. package/dist/chart-motion.js +141 -0
  96. package/dist/chart-panel-messages.d.ts +265 -0
  97. package/dist/chart-panel-messages.js +285 -0
  98. package/dist/chart-pivot.d.ts +87 -0
  99. package/dist/chart-pivot.js +156 -0
  100. package/dist/chart-polar.d.ts +25 -0
  101. package/dist/chart-polar.js +700 -0
  102. package/dist/chart-samples.d.ts +16 -0
  103. package/dist/chart-samples.js +153 -0
  104. package/dist/chart-scale.d.ts +113 -0
  105. package/dist/chart-scale.js +405 -0
  106. package/dist/chart-stats.d.ts +111 -0
  107. package/dist/chart-stats.js +419 -0
  108. package/dist/chart-stream.d.ts +45 -0
  109. package/dist/chart-stream.js +91 -0
  110. package/dist/chart-summary.d.ts +27 -0
  111. package/dist/chart-summary.js +205 -0
  112. package/dist/chart-sync.svelte.d.ts +32 -0
  113. package/dist/chart-sync.svelte.js +23 -0
  114. package/dist/chart-table.d.ts +35 -0
  115. package/dist/chart-table.js +165 -0
  116. package/dist/chart-trend.d.ts +19 -0
  117. package/dist/chart-trend.js +61 -0
  118. package/dist/chart-types.d.ts +1456 -0
  119. package/dist/chart-types.js +6 -0
  120. package/dist/chart-validate.d.ts +43 -0
  121. package/dist/chart-validate.js +272 -0
  122. package/dist/chart-zoom.d.ts +40 -0
  123. package/dist/chart-zoom.js +145 -0
  124. package/dist/chart.d.ts +186 -865
  125. package/dist/chart.js +460 -2204
  126. package/dist/clipboard.d.ts +1 -0
  127. package/dist/clipboard.js +137 -18
  128. package/dist/column-resize.d.ts +3 -0
  129. package/dist/column-resize.js +4 -1
  130. package/dist/columns.js +3 -0
  131. package/dist/command-context.d.ts +19 -0
  132. package/dist/command-context.js +116 -0
  133. package/dist/core.d.ts +7 -0
  134. package/dist/createPopoverSelect.svelte.d.ts +2 -2
  135. package/dist/editing.d.ts +2 -8
  136. package/dist/editing.js +262 -33
  137. package/dist/fill-patterns.d.ts +0 -5
  138. package/dist/fill-patterns.js +56 -2
  139. package/dist/grid-messages.d.ts +10 -3
  140. package/dist/grid-messages.js +5 -0
  141. package/dist/history.d.ts +83 -0
  142. package/dist/history.js +133 -0
  143. package/dist/index.d.ts +24 -6
  144. package/dist/index.js +20 -2
  145. package/dist/keyboard-handlers.js +88 -31
  146. package/dist/keyboard.d.ts +55 -0
  147. package/dist/keyboard.js +132 -0
  148. package/dist/menus.d.ts +3 -1
  149. package/dist/menus.js +7 -2
  150. package/dist/merges.d.ts +97 -0
  151. package/dist/merges.js +147 -0
  152. package/dist/row-resize.d.ts +3 -0
  153. package/dist/row-resize.js +14 -0
  154. package/dist/scroll-sync.d.ts +1 -0
  155. package/dist/scroll-sync.js +24 -0
  156. package/dist/selection.d.ts +16 -0
  157. package/dist/selection.js +108 -16
  158. package/dist/shortcut-registry.d.ts +145 -0
  159. package/dist/shortcut-registry.js +44 -0
  160. package/dist/svgrid-wrapper.types.d.ts +213 -22
  161. package/dist/virtualization/virtualizer.js +38 -17
  162. package/package.json +6 -1
  163. package/src/GridMenus.svelte +44 -2
  164. package/src/SvChartMenu.svelte +69 -0
  165. package/src/SvChartPanes.svelte +113 -0
  166. package/src/SvChartPanes.test.ts +66 -0
  167. package/src/SvGrid.controller.svelte.ts +629 -172
  168. package/src/SvGrid.css +105 -2
  169. package/src/SvGrid.svelte +386 -514
  170. package/src/SvGrid.types.ts +188 -8
  171. package/src/SvGridCellEditor.svelte +17 -0
  172. package/src/SvGridChart.svelte +2257 -329
  173. package/src/SvGridChart.test.ts +1419 -1
  174. package/src/SvGridChart.types.ts +336 -0
  175. package/src/SvGridChartBuilder.svelte +323 -0
  176. package/src/SvGridChartPanel.svelte +221 -275
  177. package/src/SvGridChartPickers.svelte +417 -0
  178. package/src/SvGridChartView.svelte +13 -14
  179. package/src/SvModal.svelte +7 -1
  180. package/src/SvModal.test.ts +9 -0
  181. package/src/ai.test.ts +57 -2
  182. package/src/ai.ts +166 -22
  183. package/src/aria.d.ts +11 -0
  184. package/src/build-api.coverage.test.ts +184 -0
  185. package/src/build-api.ts +107 -18
  186. package/src/chart-axes.test.ts +573 -0
  187. package/src/chart-axes.ts +342 -0
  188. package/src/chart-cartesian.ts +1863 -0
  189. package/src/chart-decimate.test.ts +249 -0
  190. package/src/chart-decimate.ts +186 -0
  191. package/src/chart-export-pdf.test.ts +91 -0
  192. package/src/chart-export-pdf.ts +207 -0
  193. package/src/chart-export.test.ts +14 -0
  194. package/src/chart-export.ts +13 -3
  195. package/src/chart-financial.ts +208 -0
  196. package/src/chart-flow.ts +286 -0
  197. package/src/chart-format.ts +67 -0
  198. package/src/chart-grid.ts +179 -0
  199. package/src/chart-hierarchy.ts +135 -0
  200. package/src/chart-indicators.test.ts +443 -0
  201. package/src/chart-indicators.ts +384 -0
  202. package/src/chart-messages.test.ts +21 -0
  203. package/src/chart-messages.ts +184 -0
  204. package/src/chart-motion.test.ts +71 -0
  205. package/src/chart-motion.ts +148 -0
  206. package/src/chart-panel-messages.test.ts +29 -0
  207. package/src/chart-panel-messages.ts +550 -0
  208. package/src/chart-pivot.test.ts +54 -0
  209. package/src/chart-pivot.ts +225 -0
  210. package/src/chart-polar.ts +674 -0
  211. package/src/chart-samples.ts +106 -0
  212. package/src/chart-scale.ts +394 -0
  213. package/src/chart-series-types.test.ts +714 -0
  214. package/src/chart-stats.ts +409 -0
  215. package/src/chart-stream.test.ts +75 -0
  216. package/src/chart-stream.ts +114 -0
  217. package/src/chart-summary.test.ts +90 -0
  218. package/src/chart-summary.ts +197 -0
  219. package/src/chart-sync.svelte.ts +41 -0
  220. package/src/chart-table.test.ts +76 -0
  221. package/src/chart-table.ts +178 -0
  222. package/src/chart-trend.ts +47 -0
  223. package/src/chart-types.ts +1309 -0
  224. package/src/chart-validate.test.ts +100 -0
  225. package/src/chart-validate.ts +247 -0
  226. package/src/chart-zoom.test.ts +137 -0
  227. package/src/chart-zoom.ts +124 -0
  228. package/src/chart.coverage.test.ts +81 -0
  229. package/src/chart.test.ts +56 -3
  230. package/src/chart.ts +557 -2784
  231. package/src/clipboard.test.ts +151 -0
  232. package/src/clipboard.ts +145 -16
  233. package/src/column-resize.ts +7 -1
  234. package/src/columns.ts +2 -0
  235. package/src/command-context.test.ts +200 -0
  236. package/src/command-context.ts +137 -0
  237. package/src/core.ts +7 -0
  238. package/src/editing.test.ts +281 -3
  239. package/src/editing.ts +256 -38
  240. package/src/fill-patterns.test.ts +35 -0
  241. package/src/fill-patterns.ts +56 -2
  242. package/src/grid-messages.ts +16 -3
  243. package/src/history.test.ts +196 -0
  244. package/src/history.ts +162 -0
  245. package/src/icon-seam.test.ts +3 -2
  246. package/src/index.ts +150 -2
  247. package/src/keyboard-handlers.coverage.test.ts +126 -6
  248. package/src/keyboard-handlers.ts +91 -30
  249. package/src/keyboard-shortcuts.seam.test.ts +200 -0
  250. package/src/keyboard.test.ts +124 -1
  251. package/src/keyboard.ts +142 -0
  252. package/src/menus.test.ts +26 -0
  253. package/src/menus.ts +10 -3
  254. package/src/merges.test.ts +112 -0
  255. package/src/merges.ts +168 -0
  256. package/src/pivot.grid.test.ts +40 -5
  257. package/src/row-resize.test.ts +44 -0
  258. package/src/row-resize.ts +17 -0
  259. package/src/scroll-sync.test.ts +42 -1
  260. package/src/scroll-sync.ts +25 -0
  261. package/src/selection.multi-range.test.ts +34 -0
  262. package/src/selection.test.ts +30 -2
  263. package/src/selection.ts +111 -19
  264. package/src/shortcut-registry.test.ts +97 -0
  265. package/src/shortcut-registry.ts +181 -0
  266. package/src/svgrid-wrapper.types.ts +184 -13
  267. package/src/svgrid.behavior.test.ts +135 -0
  268. package/src/svgrid.charting.test.ts +513 -3
  269. package/src/svgrid.context-menu.test.ts +31 -0
  270. package/src/svgrid.new-features.wrapper.test.ts +10 -3
  271. package/src/virtualization/virtualizer.ts +39 -17
  272. package/dist/cdn/GridMenus-DyFA-QxA.js +0 -602
  273. package/dist/cdn/GridMenus-M2XP9t0_.js +0 -603
  274. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
  275. package/dist/cdn/SvGridChart-DAb9RbRs.js +0 -1480
  276. package/dist/cdn/SvGridChart-Yl-IXZFO.js +0 -1481
  277. package/dist/cdn/SvGridChartPanel-DayQpDsR.js +0 -834
  278. package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +0 -810
  279. package/dist/cdn/SvGridChartView-CxfakxR0.js +0 -56
  280. package/dist/cdn/SvGridChartView-Dm_YEx9L.js +0 -55
  281. package/dist/cdn/chart-CKJRCPOk.js +0 -1652
@@ -0,0 +1,700 @@
1
+ import { round, DEFAULT_PALETTE, axisScale, pickContrastText, sampleGradient } from './chart-scale.js';
2
+ // ---- Radar --------------------------------------------------------
3
+ // Polar coordinates: each `category` is a spoke (axis); each `series`
4
+ // contributes a polygon connecting its values across the spokes. All
5
+ // series share the same scale (max across every value). Concentric
6
+ // ring count derived from data, capped at 5 for legibility.
7
+ /** @internal Lay out the radar family. Called by buildChart. */
8
+ export function layoutRadar(ctx) {
9
+ const { spec, series, width, height, empty, frame } = ctx;
10
+ if (!series.length || !spec.categories.length)
11
+ return { ...empty };
12
+ const padL = 30, padR = 30, padT = 24 + frame.top, padB = 24 + frame.bottom;
13
+ const plotW = Math.max(1, width - padL - padR);
14
+ const plotH = Math.max(1, height - padT - padB);
15
+ const plot = { x: padL, y: padT, w: plotW, h: plotH };
16
+ const cx = padL + plotW / 2;
17
+ const cy = padT + plotH / 2;
18
+ const r = Math.max(20, Math.min(plotW, plotH) / 2 - 20);
19
+ const axes = spec.categories;
20
+ const k = axes.length;
21
+ let vMax = 0;
22
+ for (const s of series)
23
+ for (const v of s.values) {
24
+ if (Number.isFinite(v) && v > vMax)
25
+ vMax = v;
26
+ }
27
+ if (vMax === 0)
28
+ vMax = 1;
29
+ // The rim is the data maximum unless `yAxis.min` / `max` pin it, the way
30
+ // every value axis is pinned: scores out of 100 then fill the dial to the
31
+ // same rim on every chart, and a point past the rim sits on it.
32
+ const vMin = spec.yAxis?.min ?? 0;
33
+ const vTop = spec.yAxis?.max ?? Math.max(vMax, vMin + 1);
34
+ const span = vTop > vMin ? vTop - vMin : 1;
35
+ const ringCount = 5;
36
+ const ringValues = Array.from({ length: ringCount }, (_, i) => vMin + ((i + 1) / ringCount) * span);
37
+ /** Convert (axis index, value) to (x, y). Angles start at 12 o'clock,
38
+ * proceed clockwise so axes lay out left-to-right when k <= 4. */
39
+ const angleAt = (i) => -Math.PI / 2 + (i / k) * Math.PI * 2;
40
+ const pointAt = (i, v) => {
41
+ const t = Math.max(0, Math.min(1, (v - vMin) / span));
42
+ const a = angleAt(i);
43
+ return { x: round(cx + r * t * Math.cos(a)), y: round(cy + r * t * Math.sin(a)) };
44
+ };
45
+ const radarAxes = axes.map((label, i) => {
46
+ const p = pointAt(i, vTop);
47
+ return { label, x: p.x, y: p.y };
48
+ });
49
+ const radarSeriesGeo = series.map((s, si) => {
50
+ const pts = s.values.map((v, i) => {
51
+ const safe = Number.isFinite(v) ? v : vMin;
52
+ const p = pointAt(i, safe);
53
+ return { x: p.x, y: p.y, value: v, axis: axes[i] ?? '' };
54
+ });
55
+ const path = pts.length
56
+ ? pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ') + ' Z'
57
+ : '';
58
+ const palette = spec.palette ?? DEFAULT_PALETTE;
59
+ return { label: s.label, color: s.color ?? palette[si % palette.length], path, points: pts };
60
+ });
61
+ return {
62
+ ...empty,
63
+ plot,
64
+ radarRings: ringValues,
65
+ radarAxes,
66
+ radarSeries: radarSeriesGeo,
67
+ radarCenter: { cx, cy, r },
68
+ };
69
+ }
70
+ // ---- Gauge --------------------------------------------------------
71
+ // Semicircle dial: track arc + value arc + optional colored range bands
72
+ // + optional target tick. Reads spec.gaugeValue / gaugeMin / gaugeMax.
73
+ /** @internal Lay out the gauge family. Called by buildChart. */
74
+ export function layoutGauge(ctx) {
75
+ const { spec, width, height, empty, frame } = ctx;
76
+ const min = spec.gaugeMin ?? 0;
77
+ const max = spec.gaugeMax ?? 100;
78
+ const value = Math.max(min, Math.min(max, spec.gaugeValue ?? 0));
79
+ const target = spec.gaugeTarget;
80
+ const cx = width / 2;
81
+ const innerH = Math.max(1, height - frame.top - frame.bottom);
82
+ const cy = frame.top + innerH * 0.78;
83
+ const r = Math.min(width * 0.42, innerH * 0.65);
84
+ // Start angle 180deg, end 360deg (drawn clockwise from 9 o'clock to 3).
85
+ const A0 = Math.PI;
86
+ const A1 = 2 * Math.PI;
87
+ const angleAt = (v) => A0 + ((v - min) / (max - min || 1)) * (A1 - A0);
88
+ const arc = (a0, a1, radius) => {
89
+ const x1 = cx + radius * Math.cos(a0);
90
+ const y1 = cy + radius * Math.sin(a0);
91
+ const x2 = cx + radius * Math.cos(a1);
92
+ const y2 = cy + radius * Math.sin(a1);
93
+ const large = a1 - a0 > Math.PI ? 1 : 0;
94
+ return `M${x1},${y1} A${radius},${radius} 0 ${large} 1 ${x2},${y2}`;
95
+ };
96
+ const trackPath = arc(A0, A1, r);
97
+ const valuePath = arc(A0, angleAt(value), r);
98
+ // Bands sit on their own inner ring, well clear of the value arc's 16px
99
+ // stroke at `r`. They are context, not the reading: a band covering most
100
+ // of the scale (an error-rate dial where anything above 0.45 is red) used
101
+ // to out-shout the value arc completely, so the dial looked pegged at
102
+ // maximum when the actual value was 9 percent.
103
+ const rangePaths = (spec.gaugeRanges ?? []).map((band) => ({
104
+ path: arc(angleAt(band.from), angleAt(band.to), r - 16),
105
+ color: band.color, from: band.from, to: band.to,
106
+ }));
107
+ let targetPx = null;
108
+ if (target != null && Number.isFinite(target)) {
109
+ const a = angleAt(Math.max(min, Math.min(max, target)));
110
+ const inner = r - 12;
111
+ const outer = r + 4;
112
+ targetPx = {
113
+ x1: cx + inner * Math.cos(a), y1: cy + inner * Math.sin(a),
114
+ x2: cx + outer * Math.cos(a), y2: cy + outer * Math.sin(a),
115
+ };
116
+ }
117
+ // Tick marks just outside the track: a major tick every 1/4 of the scale,
118
+ // with 4 minor ticks between each. Gives the dial a measured, instrument feel.
119
+ const ticks = [];
120
+ const TICK_MAJOR = 4, TICK_MINOR = 5, TICK_TOTAL = TICK_MAJOR * TICK_MINOR;
121
+ for (let i = 0; i <= TICK_TOTAL; i++) {
122
+ const a = A0 + (i / TICK_TOTAL) * (A1 - A0);
123
+ const major = i % TICK_MINOR === 0;
124
+ const inner = r + 9;
125
+ const outer = r + (major ? 17 : 13);
126
+ ticks.push({
127
+ x1: cx + inner * Math.cos(a), y1: cy + inner * Math.sin(a),
128
+ x2: cx + outer * Math.cos(a), y2: cy + outer * Math.sin(a),
129
+ major,
130
+ });
131
+ }
132
+ // Pointer needle: a kite (long tip toward the value, short counterweight
133
+ // tail) pivoting on a center hub.
134
+ //
135
+ // The proportions matter more than they look, and the counterweight was the
136
+ // whole problem. A tail sticking out past the hub is a short, solid triangle;
137
+ // the pointer is a 130px taper that thins to nothing. The compact shape wins
138
+ // the eye, so on a near-full dial the needle read as an arrow pointing at the
139
+ // MINIMUM. Keeping the tail inside the hub radius removes the competing
140
+ // point without giving up the pivot, and the shoulders sit outside the hub so
141
+ // the needle actually has a visible base to taper from.
142
+ const aV = angleAt(value);
143
+ const tipR = r - 14, tailR = 5, baseR = 9;
144
+ const aPerp = aV + Math.PI / 2;
145
+ const pt = (rad, ang) => `${round(cx + rad * Math.cos(ang))},${round(cy + rad * Math.sin(ang))}`;
146
+ const needlePath = `M${pt(baseR, aPerp)} L${pt(tipR, aV)} L${pt(baseR, aPerp + Math.PI)} L${pt(tailR, aV + Math.PI)} Z`;
147
+ // Color the value arc by the band the value currently sits in.
148
+ // First match wins, and bands are half-open [from, to). Bands normally
149
+ // share endpoints - green 0..0.3, amber 0.3..0.45, red 0.45..5 - and with
150
+ // an inclusive `to` plus last-match-wins, a value sitting exactly ON a
151
+ // boundary took the colour of the band ABOVE it. An error rate of 0.45
152
+ // against a 0.45 amber ceiling read as red.
153
+ let valueColor = null;
154
+ const bands = spec.gaugeRanges ?? [];
155
+ for (const band of bands) {
156
+ if (value >= band.from && value < band.to) {
157
+ valueColor = band.color;
158
+ break;
159
+ }
160
+ }
161
+ // The very top of the scale belongs to the last band that reaches it,
162
+ // which the half-open test above would otherwise exclude.
163
+ if (valueColor == null) {
164
+ for (const band of bands)
165
+ if (value >= band.from && value <= band.to)
166
+ valueColor = band.color;
167
+ }
168
+ return {
169
+ ...empty,
170
+ plot: { x: 0, y: 0, w: width, h: height },
171
+ gauge: {
172
+ cx, cy, r, trackPath, valuePath, rangePaths, target: targetPx,
173
+ ticks, needle: { path: needlePath, hubR: 6 }, valueColor,
174
+ minLabel: { x: cx - r, y: cy + 20 },
175
+ maxLabel: { x: cx + r, y: cy + 20 },
176
+ value, min, max, unit: spec.gaugeUnit ?? '',
177
+ },
178
+ };
179
+ }
180
+ /** @internal Lay out the pie family. Called by buildChart. */
181
+ export function layoutPie(ctx) {
182
+ const { spec, series, empty, width, height, palette, frame } = ctx;
183
+ const s = series[0];
184
+ if (!s)
185
+ return empty;
186
+ const total = s.values.reduce((a, b) => a + Math.max(0, b), 0) || 1;
187
+ const innerH = Math.max(1, height - frame.top - frame.bottom);
188
+ const cy = frame.top + innerH / 2;
189
+ // Callout labels need a margin round the pie for the leaders and the text:
190
+ // 34px above and below, and on each side the leader's horizontal run plus
191
+ // the widest label the callouts will write THERE (the label truncated to 18
192
+ // characters, a space and the percentage, at about 6.3px a character). The
193
+ // side is the slice's mid angle, which depends on the values alone. Sized
194
+ // by the widest label on either side, a pie whose small slices all sat on
195
+ // the left kept the same room on the right for a "Chrome 62%" and drew at
196
+ // half the radius the pane had; the pie now sits centred in what the two
197
+ // gutters leave. Without the text in the sum at all, a pie in a narrow
198
+ // pane pushed its labels past the edge and the reader saw "%" and a letter.
199
+ const callouts = spec.dataLabels?.placement === 'outside' && spec.dataLabels.show !== false;
200
+ let leftW = 0;
201
+ let rightW = 0;
202
+ if (callouts) {
203
+ let a = -Math.PI / 2;
204
+ spec.categories.forEach((c, i) => {
205
+ const frac = Math.max(0, s.values[i] ?? 0) / total;
206
+ const mid = a + (frac * Math.PI * 2) / 2;
207
+ a += frac * Math.PI * 2;
208
+ if (frac < 0.015)
209
+ return;
210
+ const w = (Math.min(c.length, 18) + 4) * 6.3;
211
+ if (Math.cos(mid) >= 0)
212
+ rightW = Math.max(rightW, w);
213
+ else
214
+ leftW = Math.max(leftW, w);
215
+ });
216
+ }
217
+ const leftGutter = callouts ? 22 + 4 + leftW : 10;
218
+ const rightGutter = callouts ? 22 + 4 + rightW : 10;
219
+ // The pie keeps at least a quarter of the short side; when the labels want
220
+ // more than that leaves, they are truncated to what fits instead.
221
+ const minR = Math.max(12, Math.min(width, innerH) * 0.25);
222
+ const room = (width - leftGutter - rightGutter) / 2;
223
+ const r = Math.max(minR, Math.min(innerH / 2 - (callouts ? 34 : 10), room));
224
+ // Centred in the room the gutters leave. When even the floor radius does
225
+ // not fit between them, the pie sits in the middle and both sides truncate
226
+ // to what is left, rather than one side keeping its text and the other
227
+ // running off the edge.
228
+ const cx = callouts && room >= r ? leftGutter + room : width / 2;
229
+ const innerFrac = Math.min(0.9, Math.max(0, spec.innerRadius ?? 0));
230
+ const ir = r * innerFrac;
231
+ let angle = -Math.PI / 2;
232
+ const slices = s.values.map((v, i) => {
233
+ const frac = Math.max(0, v) / total;
234
+ const a0 = angle;
235
+ const a1 = angle + frac * Math.PI * 2;
236
+ angle = a1;
237
+ const large = a1 - a0 > Math.PI ? 1 : 0;
238
+ const mid = (a0 + a1) / 2;
239
+ const labelR = (r + ir) / 2 || r * 0.6;
240
+ const ox0 = cx + r * Math.cos(a0);
241
+ const oy0 = cy + r * Math.sin(a0);
242
+ const ox1 = cx + r * Math.cos(a1);
243
+ const oy1 = cy + r * Math.sin(a1);
244
+ let path;
245
+ if (frac >= 0.999) {
246
+ path = ir
247
+ ? `M${round(cx - r)},${round(cy)} A${r},${r} 0 1 1 ${round(cx + r)},${round(cy)} A${r},${r} 0 1 1 ${round(cx - r)},${round(cy)} Z` +
248
+ `M${round(cx - ir)},${round(cy)} A${ir},${ir} 0 1 0 ${round(cx + ir)},${round(cy)} A${ir},${ir} 0 1 0 ${round(cx - ir)},${round(cy)} Z`
249
+ : `M${round(cx - r)},${round(cy)} A${r},${r} 0 1 1 ${round(cx + r)},${round(cy)} A${r},${r} 0 1 1 ${round(cx - r)},${round(cy)} Z`;
250
+ }
251
+ else if (ir > 0) {
252
+ const ix0 = cx + ir * Math.cos(a0);
253
+ const iy0 = cy + ir * Math.sin(a0);
254
+ const ix1 = cx + ir * Math.cos(a1);
255
+ const iy1 = cy + ir * Math.sin(a1);
256
+ path =
257
+ `M${round(ox0)},${round(oy0)} A${r},${r} 0 ${large} 1 ${round(ox1)},${round(oy1)} ` +
258
+ `L${round(ix1)},${round(iy1)} A${ir},${ir} 0 ${large} 0 ${round(ix0)},${round(iy0)} Z`;
259
+ }
260
+ else {
261
+ path = `M${round(cx)},${round(cy)} L${round(ox0)},${round(oy0)} A${r},${r} 0 ${large} 1 ${round(ox1)},${round(oy1)} Z`;
262
+ }
263
+ const catLabel = spec.categories[i] ?? String(i);
264
+ return {
265
+ path,
266
+ // The same precedence as a bar: a category colour, then the series'
267
+ // per-point colour (a tree node's, say), then the palette.
268
+ color: spec.categoryColors?.[catLabel] ?? s.colors?.[i] ?? palette[i % palette.length],
269
+ label: catLabel,
270
+ value: v,
271
+ percent: frac * 100,
272
+ cx: round(cx + labelR * Math.cos(mid)),
273
+ cy: round(cy + labelR * Math.sin(mid)),
274
+ arc: { a0, a1, r, ir, cx, cy },
275
+ };
276
+ });
277
+ if (callouts)
278
+ layoutPieCallouts(slices, cx, cy, r, frame.top, frame.top + innerH, width);
279
+ return {
280
+ ...empty,
281
+ slices,
282
+ legend: spec.categories.map((label, i) => ({ label, color: spec.categoryColors?.[label] ?? s.colors?.[i] ?? palette[i % palette.length] })),
283
+ donut: ir > 0 ? { cx: round(cx), cy: round(cy), r: round(ir), total: s.values.reduce((a, b) => a + Math.max(0, b), 0) } : null,
284
+ };
285
+ }
286
+ /**
287
+ * Callout labels round a pie: a short leader from the slice's edge along its
288
+ * mid-angle, a horizontal run, then the text outside. Labels on each side are
289
+ * pushed apart from the top down so they never overlap, and a sliver under
290
+ * 1.5% gets no callout at all (its leader would land on a neighbour's).
291
+ */
292
+ function layoutPieCallouts(slices, cx, cy, r, top, bottom, width) {
293
+ const GAP = 13;
294
+ // Characters of label that fit between the leader's run and the edge on
295
+ // each side, less the " NN%" the renderer appends; at least four so a name
296
+ // is recognisable.
297
+ const charsFor = (room) => Math.max(4, Math.floor((room - r - 26) / 6.3) - 4);
298
+ const maxCharsRight = charsFor(width - cx);
299
+ const maxCharsLeft = charsFor(cx);
300
+ const sides = [];
301
+ for (const slice of slices) {
302
+ if (!slice.arc || slice.percent < 1.5)
303
+ continue;
304
+ const mid = (slice.arc.a0 + slice.arc.a1) / 2;
305
+ sides.push({ slice, mid, ty: cy + Math.sin(mid) * (r + 16), right: Math.cos(mid) >= 0 });
306
+ }
307
+ for (const right of [true, false]) {
308
+ const list = sides.filter((c) => c.right === right).sort((a, b) => a.ty - b.ty);
309
+ // Push down from the top, then back up from the bottom, so a crowded side
310
+ // spreads out rather than piling below the plot.
311
+ for (let k = 1; k < list.length; k += 1)
312
+ list[k].ty = Math.max(list[k].ty, list[k - 1].ty + GAP);
313
+ const floor = bottom - 6;
314
+ for (let k = list.length - 1; k >= 0; k -= 1) {
315
+ const limit = k === list.length - 1 ? floor : list[k + 1].ty - GAP;
316
+ list[k].ty = Math.min(list[k].ty, limit);
317
+ }
318
+ for (const c of list)
319
+ if (c.ty < top + 6)
320
+ c.ty = top + 6;
321
+ for (const c of list) {
322
+ const x1 = cx + Math.cos(c.mid) * (r + 2);
323
+ const y1 = cy + Math.sin(c.mid) * (r + 2);
324
+ const x2 = cx + Math.cos(c.mid) * (r + 12);
325
+ const y2 = c.ty;
326
+ const x3 = right ? cx + r + 22 : cx - r - 22;
327
+ c.slice.callout = {
328
+ x1: round(x1), y1: round(y1), x2: round(x2), y2: round(y2), x3: round(x3),
329
+ tx: round(x3 + (right ? 4 : -4)), ty: round(c.ty), anchor: right ? 'start' : 'end',
330
+ maxChars: right ? maxCharsRight : maxCharsLeft,
331
+ };
332
+ }
333
+ }
334
+ }
335
+ // ---- Arcs ---------------------------------------------------------
336
+ // The one path builder every new polar type shares. Angles are radians,
337
+ // clockwise, with 12 o'clock at -PI/2 (the convention the pie already uses).
338
+ /**
339
+ * An annular sector from angle `a0` to `a1` between radii `r0` and `r1`, as
340
+ * an SVG path. `r0 = 0` gives a wedge; a span of a full turn gives a ring.
341
+ * Exposed so a custom mark can match the built-in polar types exactly.
342
+ */
343
+ export function arcPath(cx, cy, r0, r1, a0, a1) {
344
+ const TAU = Math.PI * 2;
345
+ const span = Math.max(0, Math.min(TAU, a1 - a0));
346
+ if (span <= 0 || r1 <= 0)
347
+ return '';
348
+ const px = (r, a) => `${round(cx + r * Math.cos(a))},${round(cy + r * Math.sin(a))}`;
349
+ if (span >= TAU - 1e-6) {
350
+ // Two half-arcs, because a single arc cannot describe a full circle.
351
+ const outer = `M${px(r1, a0)} A${round(r1)},${round(r1)} 0 1 1 ${px(r1, a0 + Math.PI)} A${round(r1)},${round(r1)} 0 1 1 ${px(r1, a0)} Z`;
352
+ if (r0 <= 0)
353
+ return outer;
354
+ return `${outer} M${px(r0, a0)} A${round(r0)},${round(r0)} 0 1 0 ${px(r0, a0 + Math.PI)} A${round(r0)},${round(r0)} 0 1 0 ${px(r0, a0)} Z`;
355
+ }
356
+ const large = span > Math.PI ? 1 : 0;
357
+ if (r0 <= 0) {
358
+ return `M${round(cx)},${round(cy)} L${px(r1, a0)} A${round(r1)},${round(r1)} 0 ${large} 1 ${px(r1, a1)} Z`;
359
+ }
360
+ return (`M${px(r1, a0)} A${round(r1)},${round(r1)} 0 ${large} 1 ${px(r1, a1)} ` +
361
+ `L${px(r0, a1)} A${round(r0)},${round(r0)} 0 ${large} 0 ${px(r0, a0)} Z`);
362
+ }
363
+ /** The centre of the plot and the largest radius that fits, after the frame. */
364
+ function polarBox(ctx, pad = 24) {
365
+ const { width, height, frame } = ctx;
366
+ const innerH = Math.max(1, height - frame.top - frame.bottom);
367
+ const cx = width / 2;
368
+ const cy = frame.top + innerH / 2;
369
+ const r = Math.max(10, Math.min(width, innerH) / 2 - pad);
370
+ return { cx, cy, r, plot: { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r } };
371
+ }
372
+ /** A tint of `color` for a deeper ring: towards white on a light surface,
373
+ * towards the dark surface on a dark one. */
374
+ function tint(color, depth, theme) {
375
+ if (depth <= 0)
376
+ return color;
377
+ return sampleGradient([color, theme === 'dark' ? '#111827' : '#ffffff'], Math.min(0.6, depth * 0.22));
378
+ }
379
+ const START = -Math.PI / 2;
380
+ const TAU = Math.PI * 2;
381
+ // ---- Sunburst -----------------------------------------------------
382
+ // A radial treemap: the root's children share the first ring in proportion to
383
+ // their totals, each child's children share that child's angular span in the
384
+ // next ring out, and so on. Colour follows the top-level branch, tinted per
385
+ // depth, so a whole limb of the hierarchy reads as one hue.
386
+ /** @internal Lay out the sunburst family. Called by buildChart. */
387
+ export function layoutSunburst(ctx) {
388
+ const { spec, empty, palette, theme } = ctx;
389
+ const root = spec.tree ?? spec.treemap;
390
+ if (!root)
391
+ return { ...empty };
392
+ const { cx, cy, r, plot } = polarBox(ctx, 12);
393
+ const totalOf = (n) => n.children?.length ? n.children.reduce((s, c) => s + totalOf(c), 0) : Math.max(0, n.value ?? 0);
394
+ const depthOf = (n) => (n.children?.length ? 1 + Math.max(...n.children.map(depthOf)) : 0);
395
+ const seed = root.children ?? [root];
396
+ const total = seed.reduce((s, c) => s + totalOf(c), 0);
397
+ if (total <= 0)
398
+ return { ...empty, plot };
399
+ const depth = Math.max(1, depthOf({ name: '', children: seed }));
400
+ const innerFrac = Math.min(0.6, Math.max(0, spec.innerRadius ?? 0.18));
401
+ const r0 = r * innerFrac;
402
+ const ringW = (r - r0) / depth;
403
+ const arcs = [];
404
+ const walk = (nodes, a0, a1, d, color, path) => {
405
+ const sum = nodes.reduce((s, c) => s + totalOf(c), 0);
406
+ if (sum <= 0)
407
+ return;
408
+ let a = a0;
409
+ nodes.forEach((n, i) => {
410
+ const v = totalOf(n);
411
+ if (v <= 0)
412
+ return;
413
+ const span = ((a1 - a0) * v) / sum;
414
+ const c = n.color ?? (d === 0 ? palette[i % palette.length] : color);
415
+ const fill = d === 0 ? c : tint(c, d, theme);
416
+ const ri = r0 + ringW * d;
417
+ const ro = ri + ringW - 1;
418
+ const mid = a + span / 2;
419
+ const lr = (ri + ro) / 2;
420
+ const nodePath = [...path, n.name];
421
+ arcs.push({
422
+ path: arcPath(cx, cy, ri, ro, a, a + span),
423
+ color: fill,
424
+ textColor: pickContrastText(fill),
425
+ label: n.name,
426
+ series: path[path.length - 1] ?? '',
427
+ value: v,
428
+ a0: a,
429
+ a1: a + span,
430
+ r0: ri,
431
+ r1: ro,
432
+ cx,
433
+ cy,
434
+ depth: d,
435
+ nodePath,
436
+ lx: round(cx + lr * Math.cos(mid)),
437
+ ly: round(cy + lr * Math.sin(mid)),
438
+ });
439
+ if (n.children?.length)
440
+ walk(n.children, a, a + span, d + 1, c, nodePath);
441
+ a += span;
442
+ });
443
+ };
444
+ walk(seed, START, START + TAU, 0, null, []);
445
+ return {
446
+ ...empty,
447
+ plot,
448
+ arcs,
449
+ legend: seed.map((n, i) => ({ label: n.name, color: n.color ?? palette[i % palette.length] })),
450
+ donut: r0 > 0 ? { cx: round(cx), cy: round(cy), r: round(r0), total } : null,
451
+ };
452
+ }
453
+ // ---- Radial bar ---------------------------------------------------
454
+ // One ring per category (per series), sweeping clockwise from 12 o'clock by
455
+ // value over the largest value. A faint full track behind each ring shows
456
+ // the scale. The first category is the innermost ring.
457
+ /** @internal Lay out the radial bar family. Called by buildChart. */
458
+ export function layoutRadialBar(ctx) {
459
+ const { spec, series, empty, palette } = ctx;
460
+ if (!series.length || !spec.categories.length)
461
+ return { ...empty };
462
+ const { cx, cy, r, plot } = polarBox(ctx, 12);
463
+ let vMax = 0;
464
+ for (const s of series)
465
+ for (const v of s.values)
466
+ if (Number.isFinite(v) && v > vMax)
467
+ vMax = v;
468
+ if (vMax <= 0)
469
+ vMax = 1;
470
+ const n = spec.categories.length;
471
+ const rings = n * series.length;
472
+ const innerFrac = Math.min(0.6, Math.max(0, spec.innerRadius ?? 0.2));
473
+ const r0 = r * innerFrac;
474
+ const ringW = (r - r0) / Math.max(1, rings);
475
+ const gap = Math.min(3, ringW * 0.2);
476
+ const arcs = [];
477
+ for (let i = 0; i < n; i += 1) {
478
+ series.forEach((s, si) => {
479
+ const v = s.values[i];
480
+ // A zero (a hidden category, or no data) draws nothing rather than an
481
+ // empty path, so the ring simply disappears the way a pie slice does.
482
+ if (!Number.isFinite(v) || v <= 0)
483
+ return;
484
+ const k = i * series.length + si;
485
+ const ri = r0 + ringW * k + gap / 2;
486
+ const ro = ri + ringW - gap;
487
+ const span = Math.min(TAU * 0.9999, (Math.max(0, v) / vMax) * TAU);
488
+ const color = series.length === 1 ? (spec.categoryColors?.[spec.categories[i]] ?? s.colors?.[i] ?? palette[i % palette.length]) : s.colors?.[i] ?? s.color;
489
+ const mid = START + span / 2;
490
+ const lr = (ri + ro) / 2;
491
+ arcs.push({
492
+ path: arcPath(cx, cy, ri, ro, START, START + span),
493
+ trackPath: arcPath(cx, cy, ri, ro, START, START + TAU),
494
+ color,
495
+ textColor: pickContrastText(color),
496
+ label: spec.categories[i],
497
+ series: s.label,
498
+ value: v,
499
+ a0: START,
500
+ a1: START + span,
501
+ r0: ri,
502
+ r1: ro,
503
+ cx,
504
+ cy,
505
+ lx: round(cx + lr * Math.cos(mid)),
506
+ ly: round(cy + lr * Math.sin(mid)),
507
+ });
508
+ });
509
+ }
510
+ return {
511
+ ...empty,
512
+ plot,
513
+ arcs,
514
+ legend: series.length === 1
515
+ ? spec.categories.map((label, i) => ({ label, color: spec.categoryColors?.[label] ?? palette[i % palette.length] }))
516
+ : empty.legend,
517
+ };
518
+ }
519
+ // ---- Radial column / nightingale ----------------------------------
520
+ // Bars on a polar axis: each category owns an angular slot, each series a
521
+ // sub-slot (grouped) or a radial layer (stacked). Radius follows the value
522
+ // through nice rings, which the renderer draws as the axis. A nightingale
523
+ // (Florence's rose) is the same with area-proportional radii (sqrt of the
524
+ // value) and no gap between the petals, so a value twice as big looks twice
525
+ // as big rather than four times.
526
+ /** @internal Lay out the radial column and nightingale families. Called by buildChart. */
527
+ export function layoutRadialColumn(ctx) {
528
+ const { spec, series, empty, palette, stacked } = ctx;
529
+ if (!series.length || !spec.categories.length)
530
+ return { ...empty };
531
+ const rose = spec.type === 'nightingale';
532
+ const { cx, cy, r, plot } = polarBox(ctx, 22);
533
+ const n = spec.categories.length;
534
+ const stack = stacked || (rose && series.length > 1);
535
+ // Domain over values, or stacked totals.
536
+ let vMax = 0;
537
+ if (stack) {
538
+ for (let i = 0; i < n; i += 1) {
539
+ let t = 0;
540
+ for (const s of series)
541
+ t += Number.isFinite(s.values[i]) ? Math.max(0, s.values[i]) : 0;
542
+ if (t > vMax)
543
+ vMax = t;
544
+ }
545
+ }
546
+ else {
547
+ for (const s of series)
548
+ for (const v of s.values)
549
+ if (Number.isFinite(v) && v > vMax)
550
+ vMax = v;
551
+ }
552
+ const dom = axisScale(0, vMax > 0 ? vMax : 1, spec.yAxis ?? {});
553
+ const innerFrac = Math.min(0.5, Math.max(0, spec.innerRadius ?? (rose ? 0 : 0.12)));
554
+ const r0 = r * innerFrac;
555
+ const radiusOf = (v) => {
556
+ const t = Math.max(0, Math.min(1, (v - dom.min) / (dom.max - dom.min || 1)));
557
+ return r0 + (r - r0) * (rose ? Math.sqrt(t) : t);
558
+ };
559
+ const slot = TAU / n;
560
+ const pad = rose ? 0 : slot * 0.15;
561
+ const arcs = [];
562
+ for (let i = 0; i < n; i += 1) {
563
+ const s0 = START + slot * i + pad / 2;
564
+ const s1 = START + slot * (i + 1) - pad / 2;
565
+ if (stack) {
566
+ let cum = 0;
567
+ series.forEach((s) => {
568
+ const v = s.values[i];
569
+ if (!Number.isFinite(v) || v <= 0)
570
+ return;
571
+ const ri = radiusOf(cum);
572
+ cum += v;
573
+ const ro = radiusOf(cum);
574
+ const color = series.length === 1 ? (spec.categoryColors?.[spec.categories[i]] ?? s.colors?.[i] ?? palette[i % palette.length]) : s.colors?.[i] ?? s.color;
575
+ const mid = (s0 + s1) / 2;
576
+ const lr = (ri + ro) / 2;
577
+ arcs.push({ path: arcPath(cx, cy, ri, ro, s0, s1), color, textColor: pickContrastText(color), label: spec.categories[i], series: s.label, value: v, a0: s0, a1: s1, r0: ri, r1: ro, cx, cy, lx: round(cx + lr * Math.cos(mid)), ly: round(cy + lr * Math.sin(mid)) });
578
+ });
579
+ }
580
+ else {
581
+ const sub = (s1 - s0) / series.length;
582
+ series.forEach((s, si) => {
583
+ const v = s.values[i];
584
+ if (!Number.isFinite(v) || v <= 0)
585
+ return;
586
+ const a0 = s0 + sub * si;
587
+ const a1 = a0 + sub * (series.length > 1 ? 0.92 : 1);
588
+ const ro = radiusOf(v);
589
+ const color = series.length === 1 ? (spec.categoryColors?.[spec.categories[i]] ?? s.colors?.[i] ?? palette[i % palette.length]) : s.colors?.[i] ?? s.color;
590
+ const mid = (a0 + a1) / 2;
591
+ const lr = (r0 + ro) / 2;
592
+ arcs.push({ path: arcPath(cx, cy, r0, ro, a0, a1), color, textColor: pickContrastText(color), label: spec.categories[i], series: s.label, value: v, a0, a1, r0, r1: ro, cx, cy, lx: round(cx + lr * Math.cos(mid)), ly: round(cy + lr * Math.sin(mid)) });
593
+ });
594
+ }
595
+ }
596
+ const polarAxes = !ctx.axes.x.labels ? [] : spec.categories.map((label, i) => {
597
+ const a = START + slot * i + slot / 2;
598
+ const lr = r + 10;
599
+ return { label, x: round(cx + lr * Math.cos(a)), y: round(cy + lr * Math.sin(a)) };
600
+ });
601
+ return {
602
+ ...empty,
603
+ plot,
604
+ arcs,
605
+ polarAxes,
606
+ polarRings: dom.ticks.filter((t) => t > dom.min).map((t) => radiusOf(t)),
607
+ radarCenter: { cx, cy, r },
608
+ legend: series.length === 1
609
+ ? spec.categories.map((label, i) => ({ label, color: spec.categoryColors?.[label] ?? palette[i % palette.length] }))
610
+ : empty.legend,
611
+ };
612
+ }
613
+ // ---- Chord --------------------------------------------------------
614
+ // Flows between groups round a circle. Each node gets an arc proportional to
615
+ // everything flowing in or out of it; each link a ribbon from its sub-span on
616
+ // the source arc to its sub-span on the target arc, coloured by the source.
617
+ /** @internal Lay out the chord family. Called by buildChart. */
618
+ export function layoutChord(ctx) {
619
+ const { spec, empty, palette } = ctx;
620
+ const nodes = spec.sankeyNodes ?? [];
621
+ const links = (spec.sankeyLinks ?? []).filter((l) => Number.isFinite(l.value) && l.value > 0);
622
+ if (!nodes.length || !links.length)
623
+ return { ...empty };
624
+ const { cx, cy, r, plot } = polarBox(ctx, 28);
625
+ const totals = new Map();
626
+ for (const l of links) {
627
+ totals.set(l.source, (totals.get(l.source) ?? 0) + l.value);
628
+ if (l.target !== l.source)
629
+ totals.set(l.target, (totals.get(l.target) ?? 0) + l.value);
630
+ }
631
+ const ordered = nodes.filter((n) => (totals.get(n.id) ?? 0) > 0);
632
+ const grand = ordered.reduce((s, n) => s + (totals.get(n.id) ?? 0), 0);
633
+ if (grand <= 0)
634
+ return { ...empty, plot };
635
+ const padAngle = Math.min(0.06, TAU / (ordered.length * 8));
636
+ const usable = TAU - padAngle * ordered.length;
637
+ const ringW = Math.max(8, r * 0.09);
638
+ const rInner = r - ringW;
639
+ const arcs = [];
640
+ const span = new Map();
641
+ let a = START;
642
+ ordered.forEach((n, i) => {
643
+ const t = totals.get(n.id) ?? 0;
644
+ const s = (usable * t) / grand;
645
+ const color = n.color ?? palette[i % palette.length];
646
+ span.set(n.id, { a0: a, a1: a + s, cursor: a, color });
647
+ const mid = a + s / 2;
648
+ const lr = r + 12;
649
+ arcs.push({
650
+ path: arcPath(cx, cy, rInner, r, a, a + s),
651
+ color,
652
+ textColor: pickContrastText(color),
653
+ label: n.label ?? n.id,
654
+ series: '',
655
+ value: t,
656
+ a0: a,
657
+ a1: a + s,
658
+ r0: rInner,
659
+ r1: r,
660
+ cx,
661
+ cy,
662
+ lx: round(cx + lr * Math.cos(mid)),
663
+ ly: round(cy + lr * Math.sin(mid)),
664
+ });
665
+ a += s + padAngle;
666
+ });
667
+ // Sub-spans: outgoing links first on the source, then incoming on the target,
668
+ // each consuming an angle proportional to its value.
669
+ const px = (rad, ang) => `${round(cx + rad * Math.cos(ang))},${round(cy + rad * Math.sin(ang))}`;
670
+ const chordRibbons = [];
671
+ const take = (id, value) => {
672
+ const g = span.get(id);
673
+ if (!g)
674
+ return null;
675
+ const w = ((g.a1 - g.a0) * value) / (totals.get(id) ?? 1);
676
+ const out = { a0: g.cursor, a1: g.cursor + w };
677
+ g.cursor += w;
678
+ return out;
679
+ };
680
+ const sorted = links.slice().sort((x, y) => y.value - x.value);
681
+ for (const l of sorted) {
682
+ const s = take(l.source, l.value);
683
+ const t = l.target === l.source ? s : take(l.target, l.value);
684
+ if (!s || !t)
685
+ continue;
686
+ const largeS = s.a1 - s.a0 > Math.PI ? 1 : 0;
687
+ const largeT = t.a1 - t.a0 > Math.PI ? 1 : 0;
688
+ const path = `M${px(rInner, s.a0)} A${round(rInner)},${round(rInner)} 0 ${largeS} 1 ${px(rInner, s.a1)} ` +
689
+ `Q${round(cx)},${round(cy)} ${px(rInner, t.a0)} A${round(rInner)},${round(rInner)} 0 ${largeT} 1 ${px(rInner, t.a1)} ` +
690
+ `Q${round(cx)},${round(cy)} ${px(rInner, s.a0)} Z`;
691
+ chordRibbons.push({ path, color: l.color ?? span.get(l.source).color, source: l.source, target: l.target, value: l.value });
692
+ }
693
+ return {
694
+ ...empty,
695
+ plot,
696
+ arcs,
697
+ chordRibbons,
698
+ legend: ordered.map((n, i) => ({ label: n.label ?? n.id, color: n.color ?? palette[i % palette.length] })),
699
+ };
700
+ }