@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,409 @@
1
+ /**
2
+ * Statistics the chart engine computes from raw values: five-number summaries,
3
+ * regression and moving-average overlays. DOM-free, spec-free.
4
+ */
5
+ import type { BoxStats, ChartReducer } from './chart-types'
6
+
7
+ // The two trend primitives live in their own leaf (see chart-trend.ts for
8
+ // why) and are re-exported here so every importer of chart-stats keeps working.
9
+ export { linearTrend, pearson } from './chart-trend'
10
+
11
+ /**
12
+ * Five-number summary of a raw sample, with the 1.5 IQR whisker rule.
13
+ *
14
+ * Whiskers stop at the last observation INSIDE the fence rather than at the
15
+ * fence itself, which is what makes them read as real data; anything past them
16
+ * comes back in `outliers`. Quartiles use linear interpolation between the two
17
+ * neighbouring order statistics.
18
+ *
19
+ * Returns `null` for an empty sample, so a category with no observations is a
20
+ * gap rather than a box drawn at zero.
21
+ */
22
+ export function boxStats(sample: ReadonlyArray<number>, whisker = 1.5): BoxStats | null {
23
+ const v = sample.filter((n) => Number.isFinite(n)).slice().sort((a, b) => a - b)
24
+ if (!v.length) return null
25
+ const q = (p: number) => {
26
+ const pos = (v.length - 1) * p
27
+ const lo = Math.floor(pos)
28
+ const hi = Math.ceil(pos)
29
+ return lo === hi ? v[lo]! : v[lo]! + (v[hi]! - v[lo]!) * (pos - lo)
30
+ }
31
+ const q1 = q(0.25)
32
+ const median = q(0.5)
33
+ const q3 = q(0.75)
34
+ const fenceLo = q1 - whisker * (q3 - q1)
35
+ const fenceHi = q3 + whisker * (q3 - q1)
36
+ const inside = v.filter((n) => n >= fenceLo && n <= fenceHi)
37
+ const outliers = v.filter((n) => n < fenceLo || n > fenceHi)
38
+ return {
39
+ // `inside` can only be empty if every point is an outlier, which the fence
40
+ // rule makes impossible (q1 and q3 are always within it) - but a degenerate
41
+ // sample should still produce a box rather than `undefined` coordinates.
42
+ min: inside.length ? inside[0]! : v[0]!,
43
+ q1,
44
+ median,
45
+ q3,
46
+ max: inside.length ? inside[inside.length - 1]! : v[v.length - 1]!,
47
+ ...(outliers.length ? { outliers } : {}),
48
+ }
49
+ }
50
+
51
+ /**
52
+ * A regression's result: the fitted value at every index (NaN where the
53
+ * source was non-finite or the model cannot take the value), R-squared
54
+ * against the finite source values, the equation, and `predict`, the model
55
+ * at any x. With no `xs` the x of a value is its index, which is what a
56
+ * category chart's overlay wants; a scatter passes its points' x values, so
57
+ * the fit is y on x and `predict` draws the curve across the plot.
58
+ */
59
+ export type RegressionFit = { fitted: number[]; r2: number; equation: string; predict: (x: number) => number }
60
+
61
+ const NO_FIT = (values: ReadonlyArray<number>): RegressionFit => ({ fitted: values.map(() => NaN), r2: 0, equation: '', predict: () => NaN })
62
+ /** The x of each value: the caller's, else the index. */
63
+ const xsOf = (values: ReadonlyArray<number>, xs?: ReadonlyArray<number>): number[] => (xs ? values.map((_, i) => xs[i] ?? NaN) : values.map((_, i) => i))
64
+
65
+ /** R-squared of a fit: 1 minus residual over total variance across the
66
+ * indices where both the value and the fit are finite. 0 when there is no
67
+ * variance to explain. */
68
+ export function rSquared(values: ReadonlyArray<number>, fitted: ReadonlyArray<number>): number {
69
+ let n = 0
70
+ let mean = 0
71
+ for (let i = 0; i < values.length; i += 1) {
72
+ if (Number.isFinite(values[i]) && Number.isFinite(fitted[i])) { n += 1; mean += values[i]! }
73
+ }
74
+ if (n < 2) return 0
75
+ mean /= n
76
+ let ssRes = 0
77
+ let ssTot = 0
78
+ for (let i = 0; i < values.length; i += 1) {
79
+ if (!Number.isFinite(values[i]) || !Number.isFinite(fitted[i])) continue
80
+ ssRes += (values[i]! - fitted[i]!) ** 2
81
+ ssTot += (values[i]! - mean) ** 2
82
+ }
83
+ if (ssTot === 0) return ssRes === 0 ? 1 : 0
84
+ return Math.max(0, Math.min(1, 1 - ssRes / ssTot))
85
+ }
86
+
87
+ const fmtCoef = (c: number) => (Math.abs(c) >= 1000 || (Math.abs(c) < 0.001 && c !== 0) ? c.toExponential(2) : String(Math.round(c * 1000) / 1000))
88
+
89
+ /** Solve a small linear system by Gaussian elimination with partial pivoting;
90
+ * null when singular. */
91
+ function solve(a: number[][], b: number[]): number[] | null {
92
+ const n = b.length
93
+ const m = a.map((row, i) => [...row, b[i]!])
94
+ for (let c = 0; c < n; c += 1) {
95
+ let p = c
96
+ for (let r = c + 1; r < n; r += 1) if (Math.abs(m[r]![c]!) > Math.abs(m[p]![c]!)) p = r
97
+ if (Math.abs(m[p]![c]!) < 1e-12) return null
98
+ ;[m[c], m[p]] = [m[p]!, m[c]!]
99
+ for (let r = 0; r < n; r += 1) {
100
+ if (r === c) continue
101
+ const f = m[r]![c]! / m[c]![c]!
102
+ for (let k = c; k <= n; k += 1) m[r]![k]! -= f * m[c]![k]!
103
+ }
104
+ }
105
+ return m.map((row, i) => row[n]! / row[i]!)
106
+ }
107
+
108
+ /**
109
+ * Least-squares polynomial of `degree` (clamped to 1..6) through
110
+ * (x, values[i]), x the index or the caller's `xs`. The x values are
111
+ * normalised to [-1, 1] before the normal equations are solved, so a
112
+ * degree-6 fit over a thousand points does not lose the low coefficients to
113
+ * rounding. Non-finite values are skipped and fitted as NaN.
114
+ */
115
+ export function polynomialFit(values: ReadonlyArray<number>, degree: number, xs?: ReadonlyArray<number>): RegressionFit {
116
+ const d = Math.max(1, Math.min(6, Math.round(degree)))
117
+ const n = values.length
118
+ const nan = NO_FIT(values)
119
+ const X = xsOf(values, xs)
120
+ const idx: number[] = []
121
+ for (let i = 0; i < n; i += 1) if (Number.isFinite(values[i]) && Number.isFinite(X[i])) idx.push(i)
122
+ if (idx.length <= d) return nan
123
+ let lo = Infinity
124
+ let hi = -Infinity
125
+ for (const i of idx) { lo = Math.min(lo, X[i]!); hi = Math.max(hi, X[i]!) }
126
+ if (!xs) { lo = 0; hi = Math.max(1, n - 1) }
127
+ const scale = Math.max(hi - lo, 1e-12)
128
+ const nx = (x: number) => (2 * (x - lo)) / scale - 1
129
+ // Normal equations: sum x^(j+k) c_k = sum x^j y.
130
+ const a: number[][] = Array.from({ length: d + 1 }, () => new Array(d + 1).fill(0))
131
+ const b: number[] = new Array(d + 1).fill(0)
132
+ for (const i of idx) {
133
+ const x = nx(X[i]!)
134
+ const y = values[i]!
135
+ const pow: number[] = [1]
136
+ for (let k = 1; k <= 2 * d; k += 1) pow.push(pow[k - 1]! * x)
137
+ for (let j = 0; j <= d; j += 1) {
138
+ b[j]! += pow[j]! * y
139
+ for (let k = 0; k <= d; k += 1) a[j]![k]! += pow[j + k]!
140
+ }
141
+ }
142
+ const c = solve(a, b)
143
+ if (!c) return nan
144
+ const predict = (x: number) => {
145
+ const u = nx(x)
146
+ let y = 0
147
+ let p = 1
148
+ for (let k = 0; k <= d; k += 1) { y += c[k]! * p; p *= u }
149
+ return y
150
+ }
151
+ const fitted = values.map((v, i) => (Number.isFinite(v) && Number.isFinite(X[i]) ? predict(X[i]!) : NaN))
152
+ const terms = c.map((ck, k) => (k === 0 ? fmtCoef(ck) : `${fmtCoef(ck)} u^${k}`)).reverse()
153
+ const u = xs ? `u = 2(x - ${fmtCoef(lo)})/${fmtCoef(scale)} - 1` : `u = 2x/${scale} - 1`
154
+ return { fitted, r2: rSquared(values, fitted), equation: `y = ${terms.join(' + ')} (${u})`, predict }
155
+ }
156
+
157
+ /** Ordinary least squares of values on their index, or on `xs`, as a {@link RegressionFit}. */
158
+ export function linearFit(values: ReadonlyArray<number>, xs?: ReadonlyArray<number>): RegressionFit {
159
+ const X = xsOf(values, xs)
160
+ const { slope, intercept, ok } = lineThrough(X, values)
161
+ if (!ok) return NO_FIT(values)
162
+ const predict = (x: number) => slope * x + intercept
163
+ const fitted = values.map((v, i) => (Number.isFinite(v) && Number.isFinite(X[i]) ? predict(X[i]!) : NaN))
164
+ return { fitted, r2: rSquared(values, fitted), equation: `y = ${fmtCoef(slope)} x + ${fmtCoef(intercept)}`, predict }
165
+ }
166
+
167
+ /** Fit y = a * e^(b x) by least squares on ln y, x the index or `xs`;
168
+ * values at or below zero cannot be logged and are skipped (fitted as NaN). */
169
+ export function exponentialFit(values: ReadonlyArray<number>, xs?: ReadonlyArray<number>): RegressionFit {
170
+ const X = xsOf(values, xs)
171
+ const logs = values.map((v) => (Number.isFinite(v) && v > 0 ? Math.log(v) : NaN))
172
+ const { slope: b, intercept, ok } = lineThrough(X, logs)
173
+ if (!ok) return NO_FIT(values)
174
+ const a = Math.exp(intercept)
175
+ const predict = (x: number) => a * Math.exp(b * x)
176
+ const fitted = values.map((v, i) => (Number.isFinite(v) && v > 0 && Number.isFinite(X[i]) ? predict(X[i]!) : NaN))
177
+ return { fitted, r2: rSquared(values, fitted), equation: `y = ${fmtCoef(a)} e^(${fmtCoef(b)} x)`, predict }
178
+ }
179
+
180
+ /** Fit y = a + b ln(x + 1) by least squares (x the index, shifted by one so
181
+ * the first point has a logarithm), or y = a + b ln(x) on `xs`, where
182
+ * points with x at or below zero are skipped. */
183
+ export function logarithmicFit(values: ReadonlyArray<number>, xs?: ReadonlyArray<number>): RegressionFit {
184
+ const shift = xs ? 0 : 1
185
+ const lx = xsOf(values, xs).map((x) => (x + shift > 0 ? Math.log(x + shift) : NaN))
186
+ const { slope, intercept, ok } = lineThrough(lx, values)
187
+ if (!ok) return NO_FIT(values)
188
+ const predict = (x: number) => (x + shift > 0 ? intercept + slope * Math.log(x + shift) : NaN)
189
+ const fitted = values.map((v, i) => (Number.isFinite(v) && Number.isFinite(lx[i]) ? intercept + slope * lx[i]! : NaN))
190
+ const arg = xs ? 'x' : 'x + 1'
191
+ return { fitted, r2: rSquared(values, fitted), equation: `y = ${fmtCoef(intercept)} + ${fmtCoef(slope)} ln(${arg})`, predict }
192
+ }
193
+
194
+ /** Fit y = a (x + 1)^b by least squares on ln y against ln(x + 1), or
195
+ * y = a x^b on `xs`; values (and x on `xs`) at or below zero are skipped
196
+ * (fitted as NaN). */
197
+ export function powerFit(values: ReadonlyArray<number>, xs?: ReadonlyArray<number>): RegressionFit {
198
+ const shift = xs ? 0 : 1
199
+ const lx = xsOf(values, xs).map((x) => (x + shift > 0 ? Math.log(x + shift) : NaN))
200
+ const logs = values.map((v) => (Number.isFinite(v) && v > 0 ? Math.log(v) : NaN))
201
+ const { slope, intercept, ok } = lineThrough(lx, logs)
202
+ if (!ok) return NO_FIT(values)
203
+ const a = Math.exp(intercept)
204
+ const predict = (x: number) => (x + shift > 0 ? a * Math.pow(x + shift, slope) : NaN)
205
+ const fitted = values.map((v, i) => (Number.isFinite(v) && v > 0 && Number.isFinite(lx[i]) ? a * Math.exp(slope * lx[i]!) : NaN))
206
+ const arg = xs ? 'x' : '(x + 1)'
207
+ return { fitted, r2: rSquared(values, fitted), equation: `y = ${fmtCoef(a)} ${arg}^${fmtCoef(slope)}`, predict }
208
+ }
209
+
210
+ /** Least squares of y on arbitrary x, skipping non-finite pairs. */
211
+ function lineThrough(xs: ReadonlyArray<number>, ys: ReadonlyArray<number>): { slope: number; intercept: number; ok: boolean } {
212
+ let n = 0, sx = 0, sy = 0, sxx = 0, sxy = 0
213
+ for (let i = 0; i < ys.length; i += 1) {
214
+ const x = xs[i]!
215
+ const y = ys[i]!
216
+ if (!Number.isFinite(x) || !Number.isFinite(y)) continue
217
+ n += 1; sx += x; sy += y; sxx += x * x; sxy += x * y
218
+ }
219
+ if (n < 2) return { slope: 0, intercept: 0, ok: false }
220
+ const denom = n * sxx - sx * sx
221
+ const slope = denom === 0 ? 0 : (n * sxy - sx * sy) / denom
222
+ return { slope, intercept: (sy - slope * sx) / n, ok: true }
223
+ }
224
+
225
+ /** Simple moving average over a window of `period` values. Window centres
226
+ * trail to the right (typical for time-series). NaN for points before the
227
+ * window is full. */
228
+ export function simpleMovingAverage(values: number[], period: number): number[] {
229
+ if (period < 1) return values.slice()
230
+ const out: number[] = new Array(values.length).fill(NaN)
231
+ let sum = 0, count = 0
232
+ for (let i = 0; i < values.length; i += 1) {
233
+ const v = values[i]!
234
+ if (Number.isFinite(v)) { sum += v; count += 1 }
235
+ if (i >= period) {
236
+ const drop = values[i - period]!
237
+ if (Number.isFinite(drop)) { sum -= drop; count -= 1 }
238
+ }
239
+ if (i >= period - 1 && count > 0) out[i] = sum / count
240
+ }
241
+ return out
242
+ }
243
+
244
+ /** Exponential moving average. Smoothing factor alpha = 2 / (period + 1). */
245
+ export function exponentialMovingAverage(values: number[], period: number): number[] {
246
+ const alpha = 2 / (Math.max(1, period) + 1)
247
+ const out: number[] = new Array(values.length).fill(NaN)
248
+ let prev: number | null = null
249
+ for (let i = 0; i < values.length; i += 1) {
250
+ const v = values[i]!
251
+ if (!Number.isFinite(v)) { out[i] = prev ?? NaN; continue }
252
+ prev = prev == null ? v : alpha * v + (1 - alpha) * prev
253
+ out[i] = prev
254
+ }
255
+ return out
256
+ }
257
+
258
+
259
+ /** The reducers that need every observation, not a running sum and count. */
260
+ export function reducerNeedsSamples(reducer: ChartReducer): boolean {
261
+ return !(reducer === 'sum' || reducer === 'avg' || reducer === 'count')
262
+ }
263
+
264
+ /**
265
+ * Collapse a group's observations to one number. `sum` / `avg` / `count` are
266
+ * what `rowsToChartSpec` always did; the rest are the ones a pivot table
267
+ * offers and a chart panel was missing: `min`, `max`, `median`, `first`,
268
+ * `last`, `countDistinct`, and `pN` for the Nth percentile (`'p90'`). An
269
+ * empty group reduces to 0 for `sum` / `count` and to NaN otherwise, so a
270
+ * missing bucket draws as a gap rather than as a fake zero.
271
+ */
272
+ export function reduceValues(samples: ReadonlyArray<number>, reducer: ChartReducer): number {
273
+ const n = samples.length
274
+ switch (reducer) {
275
+ case 'count':
276
+ return n
277
+ case 'sum': {
278
+ let s = 0
279
+ for (const v of samples) s += v
280
+ return s
281
+ }
282
+ case 'avg': {
283
+ if (!n) return 0
284
+ let s = 0
285
+ for (const v of samples) s += v
286
+ return s / n
287
+ }
288
+ case 'min':
289
+ return n ? Math.min(...samples) : Number.NaN
290
+ case 'max':
291
+ return n ? Math.max(...samples) : Number.NaN
292
+ case 'first':
293
+ return n ? samples[0]! : Number.NaN
294
+ case 'last':
295
+ return n ? samples[n - 1]! : Number.NaN
296
+ case 'countDistinct':
297
+ return new Set(samples).size
298
+ case 'median':
299
+ return percentile(samples, 50)
300
+ default: {
301
+ const m = /^p(\d+(?:\.\d+)?)$/.exec(reducer)
302
+ return m ? percentile(samples, Number(m[1])) : Number.NaN
303
+ }
304
+ }
305
+ }
306
+
307
+ /** The p-th percentile (0..100) by linear interpolation between order statistics. */
308
+ export function percentile(samples: ReadonlyArray<number>, p: number): number {
309
+ const v = samples.filter((n) => Number.isFinite(n)).slice().sort((a, b) => a - b)
310
+ if (!v.length) return Number.NaN
311
+ const pos = (v.length - 1) * Math.max(0, Math.min(100, p)) / 100
312
+ const lo = Math.floor(pos)
313
+ const hi = Math.ceil(pos)
314
+ return lo === hi ? v[lo]! : v[lo]! + (v[hi]! - v[lo]!) * (pos - lo)
315
+ }
316
+
317
+ /** How `rowsToChartSpec` groups dates when `bucket` is set. */
318
+ export type ChartTimeBucket = 'day' | 'week' | 'month' | 'quarter' | 'year'
319
+
320
+ /**
321
+ * The ISO date (`YYYY-MM-DD`, UTC) that starts the bucket containing `date`:
322
+ * the day itself, the Monday of its week, the first of its month, quarter or
323
+ * year. Returns `''` for a value that is not a date, so the caller can drop
324
+ * the row rather than file it under 1970.
325
+ */
326
+ export function bucketStart(date: string | number | Date, bucket: ChartTimeBucket): string {
327
+ const t = date instanceof Date ? date.getTime() : typeof date === 'number' ? date : Date.parse(date)
328
+ if (!Number.isFinite(t)) return ''
329
+ const d = new Date(t)
330
+ const y = d.getUTCFullYear()
331
+ const m = d.getUTCMonth()
332
+ let out: Date
333
+ switch (bucket) {
334
+ case 'day':
335
+ out = new Date(Date.UTC(y, m, d.getUTCDate()))
336
+ break
337
+ case 'week': {
338
+ // Monday-based, which is what a business calendar reads.
339
+ const day = (d.getUTCDay() + 6) % 7
340
+ out = new Date(Date.UTC(y, m, d.getUTCDate() - day))
341
+ break
342
+ }
343
+ case 'month':
344
+ out = new Date(Date.UTC(y, m, 1))
345
+ break
346
+ case 'quarter':
347
+ out = new Date(Date.UTC(y, m - (m % 3), 1))
348
+ break
349
+ default:
350
+ out = new Date(Date.UTC(y, 0, 1))
351
+ }
352
+ return out.toISOString().slice(0, 10)
353
+ }
354
+
355
+ /** What {@link binValues} returns: one category per bin plus the edges. */
356
+ export type ChartBins = {
357
+ /** Bin midpoints as strings, ready to be the categories of a numeric axis. */
358
+ categories: string[]
359
+ /** Observations per bin. */
360
+ counts: number[]
361
+ /** Bin edges, one more than there are bins. */
362
+ edges: number[]
363
+ /** Width of every bin. */
364
+ width: number
365
+ }
366
+
367
+ /**
368
+ * Bin a raw sample for a histogram. The bin count comes from `bins`, from a
369
+ * `binWidth`, or from a rule: `'sturges'` (default, log2 n + 1), `'fd'`
370
+ * (Freedman-Diaconis, 2 IQR / cbrt n) or `'sqrt'`. The range is the data's
371
+ * extent unless `min` / `max` pin it; the top edge is inclusive. Edges are
372
+ * rounded to a sensible precision for the width, so labels read as `10, 20,
373
+ * 30` rather than `10.000000001`.
374
+ */
375
+ export function binValues(
376
+ values: ReadonlyArray<number>,
377
+ opts: { bins?: number; binWidth?: number; method?: 'sturges' | 'fd' | 'sqrt'; min?: number; max?: number } = {},
378
+ ): ChartBins {
379
+ const v = values.filter((n) => Number.isFinite(n))
380
+ if (!v.length) return { categories: [], counts: [], edges: [], width: 0 }
381
+ const lo = opts.min ?? Math.min(...v)
382
+ let hi = opts.max ?? Math.max(...v)
383
+ if (hi <= lo) hi = lo + 1
384
+ let count: number
385
+ if (opts.bins && opts.bins > 0) count = Math.round(opts.bins)
386
+ else if (opts.binWidth && opts.binWidth > 0) count = Math.max(1, Math.ceil((hi - lo) / opts.binWidth))
387
+ else if (opts.method === 'sqrt') count = Math.ceil(Math.sqrt(v.length))
388
+ else if (opts.method === 'fd') {
389
+ const iqr = percentile(v, 75) - percentile(v, 25)
390
+ const w = iqr > 0 ? (2 * iqr) / Math.cbrt(v.length) : 0
391
+ count = w > 0 ? Math.ceil((hi - lo) / w) : Math.ceil(Math.log2(v.length) + 1)
392
+ } else count = Math.ceil(Math.log2(v.length) + 1)
393
+ count = Math.max(1, Math.min(1000, count))
394
+ const width = opts.binWidth && opts.binWidth > 0 ? opts.binWidth : (hi - lo) / count
395
+ // Precision from the width: a 0.25 width keeps two decimals, a 10 width none.
396
+ const decimals = width >= 1 ? 0 : Math.min(6, Math.ceil(-Math.log10(width)) + 1)
397
+ const fix = (n: number) => Number(n.toFixed(decimals))
398
+ const edges: number[] = []
399
+ for (let i = 0; i <= count; i += 1) edges.push(fix(lo + i * width))
400
+ const counts = new Array<number>(count).fill(0)
401
+ for (const n of v) {
402
+ let i = Math.floor((n - lo) / width)
403
+ if (i >= count) i = count - 1
404
+ if (i < 0) i = 0
405
+ counts[i] = (counts[i] ?? 0) + 1
406
+ }
407
+ const categories = counts.map((_, i) => String(fix(lo + (i + 0.5) * width)))
408
+ return { categories, counts, edges, width }
409
+ }
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { appendPoints } from './chart-stream'
3
+ import { PER_CATEGORY_SERIES_KEYS } from './chart-decimate'
4
+ import type { ChartSpec } from './chart-types'
5
+
6
+ const base = (): ChartSpec => ({
7
+ type: 'line',
8
+ categories: ['t1', 't2'],
9
+ series: [
10
+ { label: 'a', values: [1, 2] },
11
+ { label: 'b', values: [10, 20] },
12
+ ],
13
+ })
14
+
15
+ describe('appendPoints', () => {
16
+ it('appends one category and one value per series, by index or by label', () => {
17
+ const next = appendPoints(base(), { category: 't3', values: [3, 30] })
18
+ expect(next.categories).toEqual(['t1', 't2', 't3'])
19
+ expect(next.series.map((s) => s.values)).toEqual([[1, 2, 3], [10, 20, 30]])
20
+ const byLabel = appendPoints(base(), { category: 't3', values: { b: 33 } })
21
+ expect(byLabel.series[0]!.values[2]).toBeNaN()
22
+ expect(byLabel.series[1]!.values[2]).toBe(33)
23
+ const many = appendPoints(base(), [{ category: 't3', values: [3, 30] }, { category: 't4', values: [4, 40] }])
24
+ expect(many.categories).toHaveLength(4)
25
+ expect(many.series[1]!.values).toEqual([10, 20, 30, 40])
26
+ })
27
+
28
+ it('keeps every per-category array parallel: bands, row ids, candles, volumes, errors, colours', () => {
29
+ const spec: ChartSpec = {
30
+ type: 'candlestick',
31
+ categories: ['d1'],
32
+ series: [
33
+ { label: 'px', values: [10], ohlc: [{ o: 9, h: 11, l: 8, c: 10 }], volumes: [100], rowIds: [[1]], colors: ['#f00'], errors: [1], upperValues: [11], lowerValues: [9] },
34
+ { label: 'sig', values: [5] },
35
+ ],
36
+ waterfallTotals: [false],
37
+ }
38
+ const next = appendPoints(spec, { category: 'd2', values: [12, 6], ohlc: { o: 10, h: 13, l: 10, c: 12 }, volume: 120, rowIds: [[2], [9]], upper: [13], lower: [11] })
39
+ const px = next.series[0]!
40
+ for (const key of PER_CATEGORY_SERIES_KEYS) {
41
+ const arr = px[key] as unknown[] | undefined
42
+ if (arr) expect(arr.length, key).toBe(2)
43
+ }
44
+ expect(px.ohlc![1]).toEqual({ o: 10, h: 13, l: 10, c: 12 })
45
+ expect(px.volumes).toEqual([100, 120])
46
+ expect(px.rowIds).toEqual([[1], [2]])
47
+ expect(px.upperValues).toEqual([11, 13])
48
+ expect(px.lowerValues).toEqual([9, 11])
49
+ // Not given for this point: a gap, not a shorter array.
50
+ expect(px.colors).toEqual(['#f00', null])
51
+ expect(px.errors![1]).toBeNull()
52
+ // A series without row ids grows them only because the point brings one.
53
+ expect(next.series[1]!.rowIds).toEqual([[], [9]])
54
+ expect(next.series[1]!.ohlc).toBeUndefined()
55
+ expect(next.waterfallTotals).toEqual([false, false])
56
+ })
57
+
58
+ it('trims to the window from the right and leaves a short spec alone', () => {
59
+ const s = appendPoints(base(), [{ category: 't3', values: [3, 30] }, { category: 't4', values: [4, 40] }], { window: 3 })
60
+ expect(s.categories).toEqual(['t2', 't3', 't4'])
61
+ expect(s.series[0]!.values).toEqual([2, 3, 4])
62
+ const short = appendPoints(base(), { category: 't3', values: [3, 30] }, { window: 10 })
63
+ expect(short.categories).toHaveLength(3)
64
+ expect(appendPoints(base(), [], { window: 1 }).categories).toEqual(['t2'])
65
+ })
66
+
67
+ it('does not mutate the input spec or its arrays', () => {
68
+ const spec = base()
69
+ const before = JSON.stringify(spec)
70
+ const next = appendPoints(spec, { category: 't3', values: [3, 30] }, { window: 2 })
71
+ expect(JSON.stringify(spec)).toBe(before)
72
+ expect(next.series[0]!.values).not.toBe(spec.series[0]!.values)
73
+ expect(next.categories).not.toBe(spec.categories)
74
+ })
75
+ })
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Live data. A feed hands a chart one new point every few hundred
3
+ * milliseconds; the spec is immutable state, so each tick wants a new spec
4
+ * with the point appended and the oldest dropped, with every per-category
5
+ * array kept in step. Demo 159 rebuilt the whole spec from a rows array on
6
+ * every tick; `appendPoints` does the append in one call and keeps the
7
+ * confidence bands, row ids, candles and volumes parallel by construction,
8
+ * because it walks the same key list the zoom window and decimation use.
9
+ */
10
+ import type { ChartSpec, ChartSeries, OhlcBar } from './chart-types'
11
+ import { PER_CATEGORY_SERIES_KEYS, PER_CATEGORY_SPEC_KEYS, pickCategories } from './chart-decimate'
12
+
13
+ /** One new category with its values, for {@link appendPoints}. */
14
+ export type ChartPoint = {
15
+ /** The category label (a date string on a time axis). */
16
+ category: string
17
+ /** One value per series in spec order, or values keyed by series label;
18
+ * a series not named gets a gap (NaN). */
19
+ values: ReadonlyArray<number> | Record<string, number>
20
+ /** The bar for a candlestick series (the first series with `ohlc`). */
21
+ ohlc?: OhlcBar | null
22
+ /** The volume for that series. */
23
+ volume?: number
24
+ /** Row ids behind the point, one array per series in spec order. */
25
+ rowIds?: ReadonlyArray<ReadonlyArray<string | number>>
26
+ /** Confidence band ends, one per series in spec order. */
27
+ upper?: ReadonlyArray<number>
28
+ lower?: ReadonlyArray<number>
29
+ /** Range low / bullet target, one per series in spec order. */
30
+ low?: ReadonlyArray<number>
31
+ target?: ReadonlyArray<number>
32
+ }
33
+
34
+ /** What {@link appendPoints} takes besides the points. */
35
+ export type AppendPointsOptions = {
36
+ /** Keep only the last `window` categories after appending. */
37
+ window?: number
38
+ }
39
+
40
+ /** The gap value each per-category array takes when a point does not set it. */
41
+ function gapFor(key: (typeof PER_CATEGORY_SERIES_KEYS)[number]): unknown {
42
+ switch (key) {
43
+ case 'rowIds': return []
44
+ case 'ohlc': case 'boxes': case 'markers': case 'colors': case 'errors': return null
45
+ default: return Number.NaN
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Append one or more points to a spec and return the new spec, trimmed to
51
+ * the trailing `window` categories when one is given. Every array a series
52
+ * carries per category (values, row ids, bands, candles, volumes, colours,
53
+ * markers, errors, lows, targets) grows by the same count, with a gap where
54
+ * the point says nothing; so do the spec-level ones (waterfall totals). The
55
+ * input is never mutated: only the arrays that grow are copied, which keeps
56
+ * Svelte's state diffing cheap at 4 Hz.
57
+ */
58
+ export function appendPoints(spec: ChartSpec, points: ChartPoint | ReadonlyArray<ChartPoint>, opts: AppendPointsOptions = {}): ChartSpec {
59
+ const list = Array.isArray(points) ? (points as ReadonlyArray<ChartPoint>) : [points as ChartPoint]
60
+ if (!list.length) return opts.window ? trim(spec, opts.window) : spec
61
+ const categories = [...spec.categories, ...list.map((p) => p.category)]
62
+ const ohlcOwner = spec.series.findIndex((s) => s.ohlc)
63
+ const series = spec.series.map((s, si) => {
64
+ const next: ChartSeries = { ...s, values: [...s.values] }
65
+ for (const p of list) {
66
+ const v = Array.isArray(p.values) ? (p.values as ReadonlyArray<number>)[si] : (p.values as Record<string, number>)[s.label]
67
+ next.values.push(typeof v === 'number' ? v : Number.NaN)
68
+ }
69
+ for (const key of PER_CATEGORY_SERIES_KEYS) {
70
+ if (key === 'values') continue
71
+ const has = s[key] !== undefined
72
+ // A key the series does not carry is only created when a point brings
73
+ // it (a candle, a volume), so a plain line stays a plain line.
74
+ const brings = list.some((p) =>
75
+ (key === 'ohlc' && p.ohlc !== undefined && si === (ohlcOwner < 0 ? 0 : ohlcOwner)) ||
76
+ (key === 'volumes' && p.volume !== undefined && si === (ohlcOwner < 0 ? 0 : ohlcOwner)) ||
77
+ (key === 'rowIds' && p.rowIds?.[si] !== undefined) ||
78
+ (key === 'upperValues' && p.upper?.[si] !== undefined) ||
79
+ (key === 'lowerValues' && p.lower?.[si] !== undefined) ||
80
+ (key === 'lowValues' && p.low?.[si] !== undefined) ||
81
+ (key === 'targets' && p.target?.[si] !== undefined))
82
+ if (!has && !brings) continue
83
+ const base: unknown[] = has ? [...(s[key] as unknown[])] : new Array(s.values.length).fill(gapFor(key))
84
+ for (const p of list) {
85
+ let v: unknown = undefined
86
+ if (key === 'ohlc' && si === (ohlcOwner < 0 ? 0 : ohlcOwner)) v = p.ohlc
87
+ else if (key === 'volumes' && si === (ohlcOwner < 0 ? 0 : ohlcOwner)) v = p.volume
88
+ else if (key === 'rowIds') v = p.rowIds?.[si]
89
+ else if (key === 'upperValues') v = p.upper?.[si]
90
+ else if (key === 'lowerValues') v = p.lower?.[si]
91
+ else if (key === 'lowValues') v = p.low?.[si]
92
+ else if (key === 'targets') v = p.target?.[si]
93
+ base.push(v === undefined ? gapFor(key) : v)
94
+ }
95
+ ;(next as unknown as Record<string, unknown>)[key] = base
96
+ }
97
+ return next
98
+ })
99
+ let out: ChartSpec = { ...spec, categories, series }
100
+ for (const key of PER_CATEGORY_SPEC_KEYS) {
101
+ const arr = spec[key] as unknown[] | undefined
102
+ if (arr) (out as unknown as Record<string, unknown>)[key] = [...arr, ...list.map(() => false)]
103
+ }
104
+ if (opts.window) out = trim(out, opts.window)
105
+ return out
106
+ }
107
+
108
+ /** The last `window` categories of a spec (the spec itself when it fits). */
109
+ function trim(spec: ChartSpec, window: number): ChartSpec {
110
+ const n = spec.categories.length
111
+ if (window <= 0 || n <= window) return spec
112
+ const indices = Array.from({ length: window }, (_, k) => n - window + k)
113
+ return pickCategories(spec, indices)
114
+ }