@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,205 @@
1
+ import { formatChartValue } from './chart-format.js';
2
+ import { linearTrend, pearson } from './chart-trend.js';
3
+ const finite = (v) => typeof v === 'number' && Number.isFinite(v);
4
+ /** First and last finite value of a series with their indices, or null. */
5
+ function ends(values) {
6
+ let i0 = -1;
7
+ let i1 = -1;
8
+ for (let i = 0; i < values.length; i += 1)
9
+ if (finite(values[i])) {
10
+ if (i0 < 0)
11
+ i0 = i;
12
+ i1 = i;
13
+ }
14
+ if (i0 < 0)
15
+ return null;
16
+ return { i0, v0: values[i0], i1, v1: values[i1] };
17
+ }
18
+ function extremes(values) {
19
+ let max = -Infinity, min = Infinity, iMax = -1, iMin = -1;
20
+ values.forEach((v, i) => {
21
+ if (!finite(v))
22
+ return;
23
+ if (v > max) {
24
+ max = v;
25
+ iMax = i;
26
+ }
27
+ if (v < min) {
28
+ min = v;
29
+ iMin = i;
30
+ }
31
+ });
32
+ return iMax < 0 ? null : { max, iMax, min, iMin };
33
+ }
34
+ const pct = (from, to) => {
35
+ if (from === 0)
36
+ return '';
37
+ const p = Math.abs(((to - from) / Math.abs(from)) * 100);
38
+ return `${p >= 10 ? Math.round(p) : Math.round(p * 10) / 10}%`;
39
+ };
40
+ function describeSeries(s, categories, fmt) {
41
+ const values = s.ohlc ? s.ohlc.map((b) => (b ? b.c : Number.NaN)) : s.values;
42
+ const e = ends(values);
43
+ if (!e)
44
+ return `${s.label} has no values.`;
45
+ const at = (i) => categories[i] ?? `#${i + 1}`;
46
+ if (e.i0 === e.i1)
47
+ return `${s.label} is ${fmt(e.v0)} at ${at(e.i0)}.`;
48
+ const slope = linearTrend(values);
49
+ const rise = slope[slope.length - 1] - slope[0];
50
+ const span = Math.abs(e.v1 - e.v0);
51
+ const scale = Math.max(Math.abs(e.v0), Math.abs(e.v1), 1e-9);
52
+ const flat = span / scale < 0.02 && Math.abs(rise) / scale < 0.05;
53
+ const dir = flat ? 'holds at about' : e.v1 > e.v0 ? 'rises' : 'falls';
54
+ const change = pct(e.v0, e.v1);
55
+ let out = flat
56
+ ? `${s.label} holds at about ${fmt(e.v1)} from ${at(e.i0)} to ${at(e.i1)}`
57
+ : `${s.label} ${dir}${change ? ` ${change}` : ''} from ${fmt(e.v0)} at ${at(e.i0)} to ${fmt(e.v1)} at ${at(e.i1)}`;
58
+ const x = extremes(values);
59
+ if (x && !flat) {
60
+ const peakInside = x.iMax !== e.i0 && x.iMax !== e.i1;
61
+ const troughInside = x.iMin !== e.i0 && x.iMin !== e.i1;
62
+ if (peakInside)
63
+ out += `, peaking at ${fmt(x.max)} at ${at(x.iMax)}`;
64
+ if (troughInside)
65
+ out += `, lowest at ${fmt(x.min)} at ${at(x.iMin)}`;
66
+ }
67
+ return out + '.';
68
+ }
69
+ function shares(labels, values, fmt, noun) {
70
+ const total = values.reduce((a, v) => a + (finite(v) ? Math.abs(v) : 0), 0);
71
+ if (!total)
72
+ return `No ${noun} has a value.`;
73
+ const ranked = labels.map((l, i) => ({ l, v: finite(values[i]) ? Math.abs(values[i]) : 0 })).sort((a, b) => b.v - a.v);
74
+ const top = ranked.slice(0, 3).map((r) => `${r.l} ${Math.round((r.v / total) * 100)}%`);
75
+ const rest = ranked.length > 3 ? `, ${ranked.length - 3} more share the rest` : '';
76
+ return `${labels.length} ${noun}s totalling ${fmt(total)}: ${top.join(', ')}${rest}.`;
77
+ }
78
+ /**
79
+ * Describe a spec in plain words. Never throws: an empty spec of any type
80
+ * comes back as "No data." so a live region always has something to say.
81
+ */
82
+ export function chartSummary(spec, opts = {}) {
83
+ const fmt = opts.formatValue ?? ((v) => formatChartValue(v, spec.valueFormat, spec));
84
+ const max = opts.maxSeries ?? 4;
85
+ const cats = spec.categories ?? [];
86
+ const series = (spec.series ?? []).filter((s) => s.visible !== false);
87
+ const type = spec.type;
88
+ try {
89
+ if (type === 'pie' || type === 'radial-bar' || type === 'nightingale') {
90
+ const s = series[0];
91
+ if (!s || !s.values.some(finite))
92
+ return 'No data.';
93
+ return shares(cats, s.values, fmt, 'slice');
94
+ }
95
+ if (type === 'treemap' || type === 'sunburst') {
96
+ const root = spec.treemap ?? spec.tree;
97
+ const kids = root?.children ?? [];
98
+ if (!kids.length)
99
+ return 'No data.';
100
+ const total = (n) => n.value ?? (n.children ?? []).reduce((a, c) => a + total(c), 0);
101
+ return shares(kids.map((k) => k.name), kids.map((k) => total(k)), fmt, 'node');
102
+ }
103
+ if (type === 'gauge') {
104
+ if (!finite(spec.gaugeValue))
105
+ return 'No data.';
106
+ const lo = spec.gaugeMin ?? 0;
107
+ const hi = spec.gaugeMax ?? 100;
108
+ const share = hi > lo ? Math.round(((spec.gaugeValue - lo) / (hi - lo)) * 100) : 0;
109
+ const target = finite(spec.gaugeTarget) ? `, ${spec.gaugeValue >= spec.gaugeTarget ? 'above' : 'below'} the target of ${fmt(spec.gaugeTarget)}` : '';
110
+ return `${fmt(spec.gaugeValue)}${spec.gaugeUnit ? ' ' + spec.gaugeUnit : ''}, ${share}% of the way from ${fmt(lo)} to ${fmt(hi)}${target}.`;
111
+ }
112
+ if (type === 'scatter') {
113
+ const pts = series.flatMap((s) => s.points ?? []).filter((p) => finite(p.x) && finite(p.y));
114
+ if (!pts.length)
115
+ return 'No data.';
116
+ const r = pearson(pts.map((p) => p.x), pts.map((p) => p.y));
117
+ const strength = Math.abs(r) >= 0.7 ? 'strong' : Math.abs(r) >= 0.4 ? 'moderate' : Math.abs(r) >= 0.2 ? 'weak' : 'no';
118
+ const sign = r > 0 ? 'positive' : 'negative';
119
+ const rel = strength === 'no' ? 'no clear correlation' : `a ${strength} ${sign} correlation (r ${r.toFixed(2)})`;
120
+ return `${pts.length} points in ${series.length} series with ${rel} between x and y.`;
121
+ }
122
+ if (type === 'candlestick' || type === 'ohlc') {
123
+ const s = series.find((x) => x.ohlc) ?? series[0];
124
+ const bars = (s?.ohlc ?? []).filter((b) => !!b);
125
+ if (!s || !bars.length)
126
+ return 'No data.';
127
+ const last = bars[bars.length - 1];
128
+ const prev = bars.length > 1 ? bars[bars.length - 2] : null;
129
+ const hi = Math.max(...bars.map((b) => b.h));
130
+ const lo = Math.min(...bars.map((b) => b.l));
131
+ const move = prev ? `, ${last.c >= prev.c ? 'up' : 'down'} ${pct(prev.c, last.c)} on the session` : '';
132
+ return `${s.label}: last close ${fmt(last.c)}${move}, ranging ${fmt(lo)} to ${fmt(hi)} over ${bars.length} bars.`;
133
+ }
134
+ if (type === 'heatmap') {
135
+ let best = null;
136
+ for (const s of series)
137
+ s.values.forEach((v, i) => { if (finite(v) && (!best || v > best.v))
138
+ best = { v, row: s.label, col: cats[i] ?? String(i) }; });
139
+ if (!best)
140
+ return 'No data.';
141
+ const b = best;
142
+ return `${series.length} rows by ${cats.length} columns; the highest cell is ${fmt(b.v)} at ${b.row} and ${b.col}.`;
143
+ }
144
+ if (type === 'calendar') {
145
+ const days = (spec.calendarValues ?? []).filter((d) => finite(d.value));
146
+ if (!days.length)
147
+ return 'No data.';
148
+ const top = days.reduce((a, d) => (d.value > a.value ? d : a), days[0]);
149
+ const total = days.reduce((a, d) => a + d.value, 0);
150
+ return `${days.length} days totalling ${fmt(total)}; the busiest is ${top.date} at ${fmt(top.value)}.`;
151
+ }
152
+ if (type === 'funnel') {
153
+ const s = series[0];
154
+ const vals = (s?.values ?? []).filter(finite);
155
+ if (!s || vals.length < 2)
156
+ return s && vals.length === 1 ? `${cats[0] ?? s.label}: ${fmt(vals[0])}.` : 'No data.';
157
+ const first = vals[0];
158
+ const last = vals[vals.length - 1];
159
+ const conv = first ? Math.round((last / first) * 100) : 0;
160
+ let worstDrop = 0;
161
+ let worstAt = '';
162
+ for (let i = 1; i < vals.length; i += 1) {
163
+ const drop = vals[i - 1] ? 1 - vals[i] / vals[i - 1] : 0;
164
+ if (drop > worstDrop) {
165
+ worstDrop = drop;
166
+ worstAt = cats[i] ?? `step ${i + 1}`;
167
+ }
168
+ }
169
+ return `${conv}% of ${fmt(first)} at ${cats[0] ?? 'the top'} reach ${cats[vals.length - 1] ?? 'the bottom'}${worstAt ? `; the biggest drop is into ${worstAt} (${Math.round(worstDrop * 100)}% lost)` : ''}.`;
170
+ }
171
+ if (type === 'boxplot') {
172
+ let widest = null;
173
+ for (const s of series)
174
+ for (const b of s.boxes ?? []) {
175
+ if (!b)
176
+ continue;
177
+ const spread = b.q3 - b.q1;
178
+ if (!widest || spread > widest.spread)
179
+ widest = { label: s.label, spread };
180
+ }
181
+ if (!widest)
182
+ return 'No data.';
183
+ return `${series.length} distributions over ${cats.length} categories; ${widest.label} has the widest interquartile range at ${fmt(widest.spread)}.`;
184
+ }
185
+ if (type === 'sankey' || type === 'chord') {
186
+ const links = spec.sankeyLinks ?? [];
187
+ if (!links.length)
188
+ return 'No data.';
189
+ const total = links.reduce((a, l) => a + (finite(l.value) ? l.value : 0), 0);
190
+ const top = [...links].sort((a, b) => b.value - a.value)[0];
191
+ return `${links.length} flows totalling ${fmt(total)}; the largest is ${top.source} to ${top.target} at ${fmt(top.value)}.`;
192
+ }
193
+ // Cartesian families: one sentence per series, up to the cap.
194
+ const usable = series.filter((s) => s.values.some(finite) || s.ohlc);
195
+ if (!usable.length)
196
+ return 'No data.';
197
+ const shown = usable.slice(0, max);
198
+ const parts = shown.map((s) => describeSeries(s, cats, fmt));
199
+ const more = usable.length > shown.length ? ` And ${usable.length - shown.length} more series.` : '';
200
+ return `${parts.join(' ')}${more}`;
201
+ }
202
+ catch {
203
+ return 'No data.';
204
+ }
205
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared hover and zoom between charts in the same `syncGroup`.
3
+ *
4
+ * A registry of groups whose entries are `$state`, so every chart that reads
5
+ * its group's entry re-renders when another chart in the group writes to it. Each write
6
+ * carries the writer's token so a chart can ignore its own echoes.
7
+ */
8
+ import type { ChartZoomWindow } from './chart-types.js';
9
+ /** What one chart publishes for the others: the hovered category (its label,
10
+ * index and, when the category parses as a date, its time) and the window. */
11
+ export type ChartSyncState = {
12
+ hover: {
13
+ label: string;
14
+ index: number;
15
+ time: number | null;
16
+ from: symbol;
17
+ } | null;
18
+ zoom: {
19
+ window: ChartZoomWindow | null;
20
+ count: number;
21
+ from: symbol;
22
+ labels: [string, string] | null;
23
+ } | null;
24
+ };
25
+ /** The live entry for a group, created on first use. */
26
+ export declare function syncGroupState(name: string): ChartSyncState;
27
+ /** Publish a hover (or clear it with null). */
28
+ export declare function publishSyncHover(name: string, hover: ChartSyncState['hover']): void;
29
+ /** Publish a zoom window (null = the whole axis) with the axis length it
30
+ * was made against and the category labels at its two ends, so a chart with
31
+ * a different axis can map it by date. */
32
+ export declare function publishSyncZoom(name: string, window: ChartZoomWindow | null, count: number, from: symbol, labels?: [string, string] | null): void;
@@ -0,0 +1,23 @@
1
+ // A plain map of reactive entries: looking a group up (which a chart does
2
+ // inside a $derived) must not itself be a state mutation, so the map is not
3
+ // reactive and each entry is.
4
+ const groups = new Map();
5
+ /** The live entry for a group, created on first use. */
6
+ export function syncGroupState(name) {
7
+ const existing = groups.get(name);
8
+ if (existing)
9
+ return existing;
10
+ const fresh = $state({ hover: null, zoom: null });
11
+ groups.set(name, fresh);
12
+ return fresh;
13
+ }
14
+ /** Publish a hover (or clear it with null). */
15
+ export function publishSyncHover(name, hover) {
16
+ syncGroupState(name).hover = hover;
17
+ }
18
+ /** Publish a zoom window (null = the whole axis) with the axis length it
19
+ * was made against and the category labels at its two ends, so a chart with
20
+ * a different axis can map it by date. */
21
+ export function publishSyncZoom(name, window, count, from, labels = null) {
22
+ syncGroupState(name).zoom = { window, count, from, labels };
23
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Chart spec -> grid table.
3
+ *
4
+ * The rows and columns a `ChartSpec` amounts to, in the shape `SvGrid` takes:
5
+ * one row per category with a column per series for the category types, one
6
+ * row per point for a scatter, per link for a sankey or chord, per leaf for
7
+ * a tree map or sunburst, per day for a calendar, and one row for a gauge.
8
+ * Number columns carry `cellDataType: 'number'` and the chart's value format,
9
+ * so a grid of the chart reads its numbers the way the chart does.
10
+ *
11
+ * This is the grid-shaped sibling of `chartSpecToCsv`: the CSV keeps its
12
+ * header contract byte for byte, this one adds types and formats and covers
13
+ * the shapes the CSV leaves out. A "Chart | Grid" switch on a chart is this
14
+ * function behind an `SvGrid`.
15
+ */
16
+ import type { CellFormatConfig } from './core.js';
17
+ import type { ChartSpec } from './chart-types.js';
18
+ /** One column of a chart's table: what `SvGrid` needs to draw it. */
19
+ export type ChartTableColumn = {
20
+ field: string;
21
+ header: string;
22
+ cellDataType?: 'number' | 'text' | 'date';
23
+ format?: CellFormatConfig;
24
+ width?: number;
25
+ };
26
+ /** A chart's data as a table: grid columns and plain row objects. */
27
+ export type ChartTable = {
28
+ columns: ChartTableColumn[];
29
+ rows: Array<Record<string, unknown>>;
30
+ };
31
+ /**
32
+ * The rows and columns a chart spec amounts to. Never throws: a spec with
33
+ * nothing to tabulate comes back with no rows and no columns.
34
+ */
35
+ export declare function chartSpecToTable(spec: ChartSpec): ChartTable;
@@ -0,0 +1,165 @@
1
+ /** A finite number as itself, anything else as an empty cell. */
2
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
3
+ /** The cell format a chart value format maps to, for the number columns. */
4
+ function formatOf(format, spec) {
5
+ const locales = typeof spec.locale === 'string' ? spec.locale : spec.locale ? [...spec.locale] : undefined;
6
+ if (format === 'currency')
7
+ return { type: 'currency', currency: spec.currency ?? 'USD', ...(locales ? { locales } : {}) };
8
+ if (format === 'percent')
9
+ return { type: 'percent', ...(locales ? { locales } : {}) };
10
+ if (format === 'number' || format === 'compact')
11
+ return { type: 'number', ...(locales ? { locales } : {}) };
12
+ return undefined;
13
+ }
14
+ /**
15
+ * The rows and columns a chart spec amounts to. Never throws: a spec with
16
+ * nothing to tabulate comes back with no rows and no columns.
17
+ */
18
+ export function chartSpecToTable(spec) {
19
+ const fmt = formatOf(spec.valueFormat, spec);
20
+ // A series on the right axis reads in that axis's format: a margin of 0.29
21
+ // beside a revenue in dollars is "29%", not "$0.29".
22
+ const fmt2 = formatOf(spec.y2Axis?.format, spec);
23
+ const numCol = (field, header, right = false) => {
24
+ const f = right ? fmt2 : fmt;
25
+ return { field, header, cellDataType: 'number', ...(f ? { format: f } : {}) };
26
+ };
27
+ const textCol = (field, header) => ({ field, header });
28
+ const series = spec.series ?? [];
29
+ // Scatter and bubble: one row per point.
30
+ if (series.some((s) => s.points?.length)) {
31
+ const rows = [];
32
+ for (const s of series)
33
+ for (const p of s.points ?? [])
34
+ rows.push({ series: s.label, x: num(p.x), y: num(p.y), size: num(p.r), label: p.label ?? '' });
35
+ return { columns: [textCol('series', 'Series'), numCol('x', 'X'), numCol('y', 'Y'), numCol('size', 'Size'), textCol('label', 'Label')], rows };
36
+ }
37
+ // Sankey and chord: one row per link.
38
+ if (spec.sankeyLinks?.length) {
39
+ const names = new Map((spec.sankeyNodes ?? []).map((n) => [n.id, n.label ?? n.id]));
40
+ const rows = spec.sankeyLinks.map((l) => ({ source: names.get(l.source) ?? l.source, target: names.get(l.target) ?? l.target, value: num(l.value) }));
41
+ return { columns: [textCol('source', 'Source'), textCol('target', 'Target'), numCol('value', 'Value')], rows };
42
+ }
43
+ // Tree map and sunburst: one row per leaf, a column per level.
44
+ const tree = spec.tree ?? spec.treemap;
45
+ if (tree && (spec.type === 'treemap' || spec.type === 'sunburst' || !spec.categories?.length)) {
46
+ const rows = [];
47
+ let depth = 0;
48
+ const walk = (n, path) => {
49
+ if (n.children?.length) {
50
+ for (const c of n.children)
51
+ walk(c, [...path, c.name]);
52
+ return;
53
+ }
54
+ depth = Math.max(depth, path.length);
55
+ const row = { value: num(n.value) };
56
+ path.forEach((seg, i) => (row[`level${i + 1}`] = seg));
57
+ rows.push(row);
58
+ };
59
+ walk(tree, []);
60
+ const columns = [];
61
+ for (let i = 1; i <= depth; i += 1)
62
+ columns.push(textCol(`level${i}`, depth === 1 ? 'Name' : `Level ${i}`));
63
+ columns.push(numCol('value', 'Value'));
64
+ return { columns, rows };
65
+ }
66
+ // Calendar: one row per day.
67
+ if (spec.calendarValues?.length) {
68
+ return {
69
+ columns: [{ field: 'date', header: 'Date', cellDataType: 'date' }, numCol('value', 'Value')],
70
+ rows: spec.calendarValues.map((d) => ({ date: d.date, value: num(d.value) })),
71
+ };
72
+ }
73
+ // Gauge: the one reading and its scale.
74
+ if (spec.type === 'gauge') {
75
+ return {
76
+ columns: [textCol('metric', 'Metric'), numCol('value', spec.gaugeUnit ? `Value (${spec.gaugeUnit})` : 'Value'), numCol('min', 'Min'), numCol('max', 'Max'), numCol('target', 'Target')],
77
+ rows: [{ metric: spec.title ?? spec.series[0]?.label ?? 'Value', value: num(spec.gaugeValue), min: num(spec.gaugeMin ?? 0), max: num(spec.gaugeMax ?? 100), target: num(spec.gaugeTarget) }],
78
+ };
79
+ }
80
+ // Everything else reads categories x series: one row per category, one
81
+ // column per number a series carries.
82
+ const cats = spec.categories ?? [];
83
+ if (!cats.length || !series.length)
84
+ return { columns: [], rows: [] };
85
+ const columns = [];
86
+ const groups = spec.categoryGroups;
87
+ if (groups?.length)
88
+ columns.push(textCol('group', 'Group'));
89
+ const edges = spec.binEdges;
90
+ const xType = spec.xAxis?.type ?? spec.xType;
91
+ const dated = xType === 'time' || xType === 'ordinal-time';
92
+ const catHeader = spec.xAxis?.title ?? spec.xAxisTitle ?? (dated ? 'Date' : 'Category');
93
+ if (edges && edges.length === cats.length + 1)
94
+ columns.push(numCol('from', 'From'), numCol('to', 'To'));
95
+ else
96
+ columns.push(dated ? { field: 'category', header: catHeader, cellDataType: 'date' } : textCol('category', catHeader));
97
+ const fields = [];
98
+ series.forEach((s, si) => {
99
+ const key = (suffix) => `s${si}${suffix}`;
100
+ const add = (suffix, header, pick) => {
101
+ columns.push(numCol(key(suffix), header, s.axis === 'right'));
102
+ fields.push({ field: key(suffix), pick });
103
+ };
104
+ if (s.ohlc) {
105
+ add('o', `${s.label} Open`, (i) => num(s.ohlc[i]?.o));
106
+ add('h', `${s.label} High`, (i) => num(s.ohlc[i]?.h));
107
+ add('l', `${s.label} Low`, (i) => num(s.ohlc[i]?.l));
108
+ add('c', `${s.label} Close`, (i) => num(s.ohlc[i]?.c));
109
+ if (s.volumes)
110
+ add('v', `${s.label} Volume`, (i) => num(s.volumes[i]));
111
+ }
112
+ else if (s.boxes) {
113
+ add('min', `${s.label} Min`, (i) => num(s.boxes[i]?.min));
114
+ add('q1', `${s.label} Q1`, (i) => num(s.boxes[i]?.q1));
115
+ add('med', `${s.label} Median`, (i) => num(s.boxes[i]?.median));
116
+ add('q3', `${s.label} Q3`, (i) => num(s.boxes[i]?.q3));
117
+ add('max', `${s.label} Max`, (i) => num(s.boxes[i]?.max));
118
+ columns.push(textCol(key('out'), `${s.label} Outliers`));
119
+ fields.push({ field: key('out'), pick: (i) => s.boxes[i]?.outliers?.join(' ') ?? '' });
120
+ }
121
+ else if (s.errors) {
122
+ add('', s.label, (i) => num(s.values[i]));
123
+ add('lo', `${s.label} Low`, (i) => {
124
+ const v = s.values[i], e = s.errors[i];
125
+ return e == null || !Number.isFinite(v) ? null : typeof e === 'number' ? v - Math.abs(e) : Math.min(e.lo, e.hi);
126
+ });
127
+ add('hi', `${s.label} High`, (i) => {
128
+ const v = s.values[i], e = s.errors[i];
129
+ return e == null || !Number.isFinite(v) ? null : typeof e === 'number' ? v + Math.abs(e) : Math.max(e.lo, e.hi);
130
+ });
131
+ }
132
+ else if (s.lowValues) {
133
+ add('lo', `${s.label} Low`, (i) => num(s.lowValues[i]));
134
+ add('hi', `${s.label} High`, (i) => num(s.values[i]));
135
+ }
136
+ else if (s.targets) {
137
+ add('', s.label, (i) => num(s.values[i]));
138
+ add('t', `${s.label} Target`, (i) => num(s.targets[i]));
139
+ }
140
+ else {
141
+ add('', s.label, (i) => num(s.values[i]));
142
+ }
143
+ });
144
+ // A category's group, from the spans.
145
+ const groupOf = [];
146
+ if (groups?.length)
147
+ for (const g of groups)
148
+ for (let k = 0; k < g.span; k += 1)
149
+ groupOf.push(g.label);
150
+ const rows = cats.map((cat, i) => {
151
+ const row = {};
152
+ if (groups?.length)
153
+ row.group = groupOf[i] ?? '';
154
+ if (edges && edges.length === cats.length + 1) {
155
+ row.from = num(edges[i]);
156
+ row.to = num(edges[i + 1]);
157
+ }
158
+ else
159
+ row.category = cat;
160
+ for (const f of fields)
161
+ row[f.field] = f.pick(i);
162
+ return row;
163
+ });
164
+ return { columns, rows };
165
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The two trend primitives a chart is read with: the least-squares line
3
+ * through a series and Pearson's correlation of two samples.
4
+ *
5
+ * A leaf of its own, and deliberately small, for the same reason as
6
+ * chart-format.ts: the engine and the lazily loaded summary
7
+ * (chart-summary.ts) share these two functions, a bundler hoists what the
8
+ * base chunk and a lazy chunk share into a third chunk, and hoisting all of
9
+ * chart-stats.ts for two functions cost the base bundle gzip locality for no
10
+ * new code. chart-stats.ts re-exports both.
11
+ */
12
+ /** Ordinary least-squares regression on (i, values[i]) pairs (i = x index).
13
+ * Returns the fitted value at each x index, or NaN where the source value
14
+ * was non-finite. */
15
+ export declare function linearTrend(values: number[]): number[];
16
+ /** Pearson's correlation coefficient of two equal-length samples, skipping
17
+ * pairs where either side is non-finite; 0 when fewer than two pairs remain
18
+ * or either side has no variance. */
19
+ export declare function pearson(xs: ReadonlyArray<number>, ys: ReadonlyArray<number>): number;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The two trend primitives a chart is read with: the least-squares line
3
+ * through a series and Pearson's correlation of two samples.
4
+ *
5
+ * A leaf of its own, and deliberately small, for the same reason as
6
+ * chart-format.ts: the engine and the lazily loaded summary
7
+ * (chart-summary.ts) share these two functions, a bundler hoists what the
8
+ * base chunk and a lazy chunk share into a third chunk, and hoisting all of
9
+ * chart-stats.ts for two functions cost the base bundle gzip locality for no
10
+ * new code. chart-stats.ts re-exports both.
11
+ */
12
+ /** Ordinary least-squares regression on (i, values[i]) pairs (i = x index).
13
+ * Returns the fitted value at each x index, or NaN where the source value
14
+ * was non-finite. */
15
+ export function linearTrend(values) {
16
+ let n = 0, sumX = 0, sumY = 0, sumXX = 0, sumXY = 0;
17
+ for (let i = 0; i < values.length; i += 1) {
18
+ const y = values[i];
19
+ if (!Number.isFinite(y))
20
+ continue;
21
+ n += 1;
22
+ sumX += i;
23
+ sumY += y;
24
+ sumXX += i * i;
25
+ sumXY += i * y;
26
+ }
27
+ if (n < 2)
28
+ return values.map(() => NaN);
29
+ const denom = n * sumXX - sumX * sumX;
30
+ if (denom === 0)
31
+ return values.map(() => sumY / n);
32
+ const slope = (n * sumXY - sumX * sumY) / denom;
33
+ const intercept = (sumY - slope * sumX) / n;
34
+ return values.map((_, i) => slope * i + intercept);
35
+ }
36
+ /** Pearson's correlation coefficient of two equal-length samples, skipping
37
+ * pairs where either side is non-finite; 0 when fewer than two pairs remain
38
+ * or either side has no variance. */
39
+ export function pearson(xs, ys) {
40
+ let n = 0, sx = 0, sy = 0;
41
+ const len = Math.min(xs.length, ys.length);
42
+ for (let i = 0; i < len; i += 1)
43
+ if (Number.isFinite(xs[i]) && Number.isFinite(ys[i])) {
44
+ n += 1;
45
+ sx += xs[i];
46
+ sy += ys[i];
47
+ }
48
+ if (n < 2)
49
+ return 0;
50
+ const mx = sx / n, my = sy / n;
51
+ let sxy = 0, sxx = 0, syy = 0;
52
+ for (let i = 0; i < len; i += 1) {
53
+ if (!Number.isFinite(xs[i]) || !Number.isFinite(ys[i]))
54
+ continue;
55
+ const dx = xs[i] - mx, dy = ys[i] - my;
56
+ sxy += dx * dy;
57
+ sxx += dx * dx;
58
+ syy += dy * dy;
59
+ }
60
+ return sxx === 0 || syy === 0 ? 0 : sxy / Math.sqrt(sxx * syy);
61
+ }