@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,384 @@
1
+ /**
2
+ * Technical indicators for financial charts: the overlays that sit on the
3
+ * price (Bollinger bands, VWAP, weighted moving average) and the oscillators
4
+ * that get a pane of their own (RSI, MACD, stochastic, ATR, OBV, volume).
5
+ *
6
+ * Every function is pure, takes plain arrays and returns arrays of the same
7
+ * length, NaN-padded where the window is not full yet, so the result lines up
8
+ * with `categories` and draws as a gap rather than as a fake zero. Wilder's
9
+ * smoothing (RSI, ATR) seeds with a simple average over the first period and
10
+ * then smooths recursively, which is what every charting terminal shows.
11
+ */
12
+ import type { ChartReferenceLine, ChartSeries, ChartSpec, OhlcBar, SeriesOverlay } from './chart-types'
13
+ import { exponentialFit, exponentialMovingAverage, linearFit, logarithmicFit, polynomialFit, powerFit, simpleMovingAverage, type RegressionFit } from './chart-stats'
14
+
15
+ /** Which indicator a pane shows, and with what parameters. */
16
+ export type ChartIndicatorSpec = {
17
+ kind: 'rsi' | 'macd' | 'stochastic' | 'atr' | 'obv' | 'volume'
18
+ /** The series (by label) the indicator reads. Defaults to the first
19
+ * series with `ohlc`, else the first series. */
20
+ source?: string
21
+ /** Periods: `period` (RSI / ATR / stochastic %K), `fast` / `slow` /
22
+ * `signal` (MACD), `smooth` (stochastic %D). Each has the usual default. */
23
+ params?: { period?: number; fast?: number; slow?: number; signal?: number; smooth?: number }
24
+ /** Pane height in px; the default is a third of the main chart. */
25
+ height?: number
26
+ }
27
+
28
+ const nanArray = (n: number): number[] => new Array(n).fill(Number.NaN)
29
+
30
+ /** Weighted moving average: the latest value weighs `period`, the oldest 1. */
31
+ export function wma(values: ReadonlyArray<number>, period: number): number[] {
32
+ const n = values.length
33
+ const p = Math.max(1, Math.floor(period))
34
+ const out = nanArray(n)
35
+ const denom = (p * (p + 1)) / 2
36
+ for (let i = p - 1; i < n; i += 1) {
37
+ let sum = 0
38
+ let ok = true
39
+ for (let k = 0; k < p; k += 1) {
40
+ const v = values[i - k]!
41
+ if (!Number.isFinite(v)) { ok = false; break }
42
+ sum += v * (p - k)
43
+ }
44
+ if (ok) out[i] = sum / denom
45
+ }
46
+ return out
47
+ }
48
+
49
+ /**
50
+ * Bollinger bands: a simple moving average with a band `k` standard
51
+ * deviations either side (population deviation, as the terminals compute it).
52
+ */
53
+ export function bollingerBands(values: ReadonlyArray<number>, period = 20, k = 2): { middle: number[]; upper: number[]; lower: number[] } {
54
+ const n = values.length
55
+ const p = Math.max(1, Math.floor(period))
56
+ const middle = simpleMovingAverage(values as number[], p)
57
+ const upper = nanArray(n)
58
+ const lower = nanArray(n)
59
+ for (let i = p - 1; i < n; i += 1) {
60
+ const m = middle[i]!
61
+ if (!Number.isFinite(m)) continue
62
+ let ss = 0
63
+ let count = 0
64
+ for (let j = i - p + 1; j <= i; j += 1) {
65
+ const v = values[j]!
66
+ if (!Number.isFinite(v)) continue
67
+ ss += (v - m) * (v - m)
68
+ count += 1
69
+ }
70
+ const sd = count ? Math.sqrt(ss / count) : 0
71
+ upper[i] = m + k * sd
72
+ lower[i] = m - k * sd
73
+ }
74
+ return { middle, upper, lower }
75
+ }
76
+
77
+ /**
78
+ * Relative strength index over `period` bars, 0 to 100. The first value lands
79
+ * at index `period`; before it there is nothing to average.
80
+ */
81
+ export function rsi(values: ReadonlyArray<number>, period = 14): number[] {
82
+ const n = values.length
83
+ const p = Math.max(1, Math.floor(period))
84
+ const out = nanArray(n)
85
+ if (n <= p) return out
86
+ let gain = 0
87
+ let loss = 0
88
+ for (let i = 1; i <= p; i += 1) {
89
+ const d = values[i]! - values[i - 1]!
90
+ if (d > 0) gain += d
91
+ else loss -= d
92
+ }
93
+ let avgGain = gain / p
94
+ let avgLoss = loss / p
95
+ const rs = (g: number, l: number) => (l === 0 ? 100 : 100 - 100 / (1 + g / l))
96
+ out[p] = rs(avgGain, avgLoss)
97
+ for (let i = p + 1; i < n; i += 1) {
98
+ const d = values[i]! - values[i - 1]!
99
+ avgGain = (avgGain * (p - 1) + (d > 0 ? d : 0)) / p
100
+ avgLoss = (avgLoss * (p - 1) + (d < 0 ? -d : 0)) / p
101
+ out[i] = rs(avgGain, avgLoss)
102
+ }
103
+ return out
104
+ }
105
+
106
+ /**
107
+ * Moving average convergence / divergence: the fast EMA minus the slow one,
108
+ * its signal EMA, and the histogram between them.
109
+ */
110
+ export function macd(values: ReadonlyArray<number>, fast = 12, slow = 26, signal = 9): { macd: number[]; signal: number[]; histogram: number[] } {
111
+ const n = values.length
112
+ const f = exponentialMovingAverage(values as number[], fast)
113
+ const s = exponentialMovingAverage(values as number[], slow)
114
+ const line = nanArray(n)
115
+ // The slow EMA is only meaningful once it has seen `slow` values; before
116
+ // that both averages are still warming up and the difference is noise.
117
+ for (let i = slow - 1; i < n; i += 1) line[i] = f[i]! - s[i]!
118
+ const sig = nanArray(n)
119
+ const hist = nanArray(n)
120
+ const alpha = 2 / (Math.max(1, signal) + 1)
121
+ let prev: number | null = null
122
+ let seen = 0
123
+ for (let i = 0; i < n; i += 1) {
124
+ const v = line[i]!
125
+ if (!Number.isFinite(v)) continue
126
+ seen += 1
127
+ prev = prev == null ? v : alpha * v + (1 - alpha) * prev
128
+ if (seen >= signal) {
129
+ sig[i] = prev
130
+ hist[i] = v - prev
131
+ }
132
+ }
133
+ return { macd: line, signal: sig, histogram: hist }
134
+ }
135
+
136
+ /** Typical price of a bar: the mean of its high, low and close. */
137
+ const typical = (b: OhlcBar) => (b.h + b.l + b.c) / 3
138
+
139
+ /**
140
+ * Volume-weighted average price, cumulative from the first bar. Reset it per
141
+ * session by resampling or slicing before calling.
142
+ */
143
+ export function vwap(ohlc: ReadonlyArray<OhlcBar | null | undefined>, volumes: ReadonlyArray<number>): number[] {
144
+ const n = ohlc.length
145
+ const out = nanArray(n)
146
+ let pv = 0
147
+ let vol = 0
148
+ for (let i = 0; i < n; i += 1) {
149
+ const b = ohlc[i]
150
+ const v = volumes[i]
151
+ if (!b || !Number.isFinite(v)) { out[i] = vol > 0 ? pv / vol : Number.NaN; continue }
152
+ pv += typical(b) * v!
153
+ vol += v!
154
+ out[i] = vol > 0 ? pv / vol : Number.NaN
155
+ }
156
+ return out
157
+ }
158
+
159
+ /** True range of bar i: the widest of high-low, high-prevClose, low-prevClose. */
160
+ function trueRange(ohlc: ReadonlyArray<OhlcBar | null | undefined>, i: number): number {
161
+ const b = ohlc[i]
162
+ if (!b) return Number.NaN
163
+ const prev = i > 0 ? ohlc[i - 1] : null
164
+ if (!prev) return b.h - b.l
165
+ return Math.max(b.h - b.l, Math.abs(b.h - prev.c), Math.abs(b.l - prev.c))
166
+ }
167
+
168
+ /** Average true range with Wilder's smoothing. */
169
+ export function atr(ohlc: ReadonlyArray<OhlcBar | null | undefined>, period = 14): number[] {
170
+ const n = ohlc.length
171
+ const p = Math.max(1, Math.floor(period))
172
+ const out = nanArray(n)
173
+ if (n < p) return out
174
+ let sum = 0
175
+ for (let i = 0; i < p; i += 1) {
176
+ const tr = trueRange(ohlc, i)
177
+ if (!Number.isFinite(tr)) return out
178
+ sum += tr
179
+ }
180
+ let prev = sum / p
181
+ out[p - 1] = prev
182
+ for (let i = p; i < n; i += 1) {
183
+ const tr = trueRange(ohlc, i)
184
+ if (!Number.isFinite(tr)) { out[i] = Number.NaN; continue }
185
+ prev = (prev * (p - 1) + tr) / p
186
+ out[i] = prev
187
+ }
188
+ return out
189
+ }
190
+
191
+ /**
192
+ * Stochastic oscillator: %K is where the close sits in the last `period`
193
+ * bars' range (0 to 100), %D its `smooth`-bar simple average.
194
+ */
195
+ export function stochastic(ohlc: ReadonlyArray<OhlcBar | null | undefined>, period = 14, smooth = 3): { k: number[]; d: number[] } {
196
+ const n = ohlc.length
197
+ const p = Math.max(1, Math.floor(period))
198
+ const k = nanArray(n)
199
+ for (let i = p - 1; i < n; i += 1) {
200
+ let hi = -Infinity
201
+ let lo = Infinity
202
+ let ok = true
203
+ for (let j = i - p + 1; j <= i; j += 1) {
204
+ const b = ohlc[j]
205
+ if (!b) { ok = false; break }
206
+ if (b.h > hi) hi = b.h
207
+ if (b.l < lo) lo = b.l
208
+ }
209
+ const c = ohlc[i]?.c
210
+ if (!ok || c == null) continue
211
+ k[i] = hi === lo ? 50 : ((c - lo) / (hi - lo)) * 100
212
+ }
213
+ // %D is a strict average: it starts once `smooth` %K values exist, rather
214
+ // than averaging whatever is there (which is what the tolerant SMA does).
215
+ const sm = Math.max(1, Math.floor(smooth))
216
+ const d = nanArray(n)
217
+ for (let i = sm - 1; i < n; i += 1) {
218
+ let sum = 0
219
+ let ok = true
220
+ for (let j = i - sm + 1; j <= i; j += 1) {
221
+ if (!Number.isFinite(k[j])) { ok = false; break }
222
+ sum += k[j]!
223
+ }
224
+ if (ok) d[i] = sum / sm
225
+ }
226
+ return { k, d }
227
+ }
228
+
229
+ /** On-balance volume: a running total that adds volume on up closes and
230
+ * subtracts it on down closes. */
231
+ export function obv(closes: ReadonlyArray<number>, volumes: ReadonlyArray<number>): number[] {
232
+ const n = closes.length
233
+ const out = nanArray(n)
234
+ let total = 0
235
+ for (let i = 0; i < n; i += 1) {
236
+ const v = volumes[i]
237
+ if (!Number.isFinite(closes[i]) || !Number.isFinite(v)) { out[i] = total; continue }
238
+ if (i > 0 && Number.isFinite(closes[i - 1])) {
239
+ if (closes[i]! > closes[i - 1]!) total += v!
240
+ else if (closes[i]! < closes[i - 1]!) total -= v!
241
+ }
242
+ out[i] = total
243
+ }
244
+ return out
245
+ }
246
+
247
+ /**
248
+ * Compute overlay values for a series spec like 'sma:7' / 'ema:14' / 'wma:9' /
249
+ * 'linear'. `'bb:N:K'` returns the band's middle line (the engine draws the
250
+ * band from `bollingerBands` itself) and `'vwap'` needs bars and volumes, so
251
+ * it comes back as gaps here; pass `extra` for those two.
252
+ */
253
+ export function computeOverlay(values: number[], spec: SeriesOverlay, extra?: { ohlc?: ReadonlyArray<OhlcBar | null>; volumes?: ReadonlyArray<number> }): number[] {
254
+ return computeOverlayFit(values, spec, extra).values
255
+ }
256
+
257
+ /**
258
+ * {@link computeOverlay} plus what a regression knows about itself: its
259
+ * R-squared and equation. The moving averages, the bands and VWAP carry
260
+ * neither.
261
+ */
262
+ export function computeOverlayFit(values: number[], spec: SeriesOverlay, extra?: { ohlc?: ReadonlyArray<OhlcBar | null>; volumes?: ReadonlyArray<number> }): { values: number[]; r2?: number; equation?: string } {
263
+ const fit = (f: { fitted: number[]; r2: number; equation: string }) => (f.equation ? { values: f.fitted, r2: f.r2, equation: f.equation } : { values: f.fitted })
264
+ const reg = regressionFit(values, spec)
265
+ if (reg) return fit(reg)
266
+ return { values: computeOverlayPlain(values, spec, extra) }
267
+ }
268
+
269
+ /**
270
+ * The regression an overlay string names (`linear`, `poly:N`, `exp`, `log`,
271
+ * `power`), fitted to `values` on their index or on `xs`; `null` for the
272
+ * moving averages and bands, which are not regressions. A scatter passes its
273
+ * points' x and y so the curve is y on x, and reads `predict` to draw it.
274
+ */
275
+ export function regressionFit(values: ReadonlyArray<number>, spec: SeriesOverlay, xs?: ReadonlyArray<number>): RegressionFit | null {
276
+ if (spec === 'linear') return linearFit(values, xs)
277
+ if (spec === 'exp') return exponentialFit(values, xs)
278
+ if (spec === 'log') return logarithmicFit(values, xs)
279
+ if (spec === 'power') return powerFit(values, xs)
280
+ const poly = /^poly:(\d+)$/.exec(spec)
281
+ if (poly) return polynomialFit(values, Number(poly[1]), xs)
282
+ return null
283
+ }
284
+
285
+ /** The name a tooltip or legend gives an overlay: `sma:20` reads "SMA 20",
286
+ * `poly:3` "poly 3", `bb:20:2` "BB 20", the rest as written. */
287
+ export function overlayName(spec: SeriesOverlay): string {
288
+ const m = /^(sma|ema|wma|bb|poly):(\d+)/.exec(spec)
289
+ if (!m) return spec === 'linear' ? 'trend' : spec
290
+ return `${m[1] === 'poly' ? 'poly' : m[1]!.toUpperCase()} ${m[2]}`
291
+ }
292
+
293
+ function computeOverlayPlain(values: number[], spec: SeriesOverlay, extra?: { ohlc?: ReadonlyArray<OhlcBar | null>; volumes?: ReadonlyArray<number> }): number[] {
294
+ if (spec === 'vwap') return extra?.ohlc && extra.volumes ? vwap(extra.ohlc, extra.volumes) : values.map(() => NaN)
295
+ const bb = /^bb:(\d+):(\d+(?:\.\d+)?)$/.exec(spec)
296
+ if (bb) return bollingerBands(values, Number(bb[1]), Number(bb[2])).middle
297
+ const m = /^(sma|ema|wma):(\d+)$/.exec(spec)
298
+ if (!m) return values.map(() => NaN)
299
+ const period = Number(m[2])
300
+ if (m[1] === 'wma') return wma(values, period)
301
+ return m[1] === 'ema' ? exponentialMovingAverage(values, period) : simpleMovingAverage(values, period)
302
+ }
303
+
304
+ /** The series an indicator reads: by label, else the first with `ohlc`, else the first. */
305
+ function sourceSeries(spec: ChartSpec, ind: ChartIndicatorSpec): ChartSeries | null {
306
+ if (ind.source) return spec.series.find((s) => s.label === ind.source) ?? null
307
+ return spec.series.find((s) => s.ohlc) ?? spec.series[0] ?? null
308
+ }
309
+
310
+ /** Closes of a series: its `ohlc` closes when it has them, else its values. */
311
+ function closesOf(s: ChartSeries): number[] {
312
+ if (s.ohlc) return s.ohlc.map((b) => (b ? b.c : Number.NaN))
313
+ return s.values.slice()
314
+ }
315
+
316
+ /**
317
+ * Build the spec of an indicator pane from a price spec: the same categories
318
+ * and x axis, the indicator's lines or bars, and the reference lines the
319
+ * indicator is read against (30 / 70 for RSI, 20 / 80 for the stochastic, a
320
+ * zero line for MACD). `SvChartPanes` stacks these under the price chart and
321
+ * syncs them; a pane is also a complete spec for a standalone `SvChart`.
322
+ */
323
+ export function indicatorPane(spec: ChartSpec, ind: ChartIndicatorSpec): ChartSpec {
324
+ const src = sourceSeries(spec, ind)
325
+ const n = spec.categories.length
326
+ const closes = src ? closesOf(src) : nanArray(n)
327
+ const ohlc = src?.ohlc ?? closes.map((c) => (Number.isFinite(c) ? { o: c, h: c, l: c, c } : null))
328
+ const volumes = src?.volumes ?? nanArray(n)
329
+ const params = ind.params ?? {}
330
+ const base: Pick<ChartSpec, 'categories' | 'xType' | 'xAxis' | 'height' | 'palette' | 'valueFormat' | 'locale'> = {
331
+ categories: spec.categories,
332
+ xType: spec.xType,
333
+ xAxis: spec.xAxis,
334
+ height: ind.height ?? Math.max(90, Math.round((spec.height ?? 300) / 3)),
335
+ palette: spec.palette,
336
+ valueFormat: 'number',
337
+ locale: spec.locale,
338
+ }
339
+ const refs = (lines: Array<[number, string]>): ChartReferenceLine[] =>
340
+ lines.map(([value, label]) => ({ value, label, dashed: true }))
341
+ switch (ind.kind) {
342
+ case 'rsi': {
343
+ const p = params.period ?? 14
344
+ return { ...base, type: 'line', title: `RSI ${p}`, series: [{ label: `RSI ${p}`, values: rsi(closes, p), marker: 'none' }],
345
+ yAxis: { min: 0, max: 100, tickInterval: 50, nice: false }, referenceLines: refs([[30, '30'], [70, '70']]) }
346
+ }
347
+ case 'stochastic': {
348
+ const p = params.period ?? 14
349
+ const sm = params.smooth ?? 3
350
+ const { k, d } = stochastic(ohlc, p, sm)
351
+ return { ...base, type: 'line', title: `Stochastic ${p}, ${sm}`, series: [
352
+ { label: '%K', values: k, marker: 'none' }, { label: '%D', values: d, marker: 'none', dash: [4, 3] },
353
+ ], yAxis: { min: 0, max: 100, tickInterval: 50, nice: false }, referenceLines: refs([[20, '20'], [80, '80']]) }
354
+ }
355
+ case 'macd': {
356
+ const fast = params.fast ?? 12
357
+ const slow = params.slow ?? 26
358
+ const signal = params.signal ?? 9
359
+ const m = macd(closes, fast, slow, signal)
360
+ return { ...base, type: 'bar', title: `MACD ${fast}, ${slow}, ${signal}`, series: [
361
+ { label: 'Histogram', values: m.histogram, colors: m.histogram.map((v) => (v < 0 ? '#ef4444' : '#16a34a')) },
362
+ { label: 'MACD', values: m.macd, type: 'line', marker: 'none' },
363
+ { label: 'Signal', values: m.signal, type: 'line', marker: 'none', dash: [4, 3] },
364
+ ], referenceLines: refs([[0, '']]) }
365
+ }
366
+ case 'atr': {
367
+ const p = params.period ?? 14
368
+ return { ...base, type: 'line', title: `ATR ${p}`, series: [{ label: `ATR ${p}`, values: atr(ohlc, p), marker: 'none' }], yAxis: { min: 0 } }
369
+ }
370
+ case 'obv':
371
+ return { ...base, type: 'line', title: 'OBV', series: [{ label: 'OBV', values: obv(closes, volumes), marker: 'none' }], valueFormat: 'compact' }
372
+ case 'volume':
373
+ default: {
374
+ // Up bars in the up colour, down bars in the down colour, like the candles.
375
+ const colors = ohlc.map((b, i) => {
376
+ if (!b) return null
377
+ const prev = i > 0 ? ohlc[i - 1] : null
378
+ const up = prev ? b.c >= prev.c : b.c >= b.o
379
+ return up ? '#16a34a' : '#ef4444'
380
+ })
381
+ return { ...base, type: 'bar', title: 'Volume', series: [{ label: 'Volume', values: volumes.slice(), colors }], yAxis: { min: 0 }, valueFormat: 'compact' }
382
+ }
383
+ }
384
+ }
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { chartMessage, defaultChartMessages, resolveChartMessages } from './chart-messages'
3
+
4
+ describe('chart messages', () => {
5
+ it('resolves a partial map over the English defaults and ignores empty values', () => {
6
+ const m = resolveChartMessages({ png: 'Bild', noData: '' })
7
+ expect(m.png).toBe('Bild')
8
+ expect(m.noData).toBe(defaultChartMessages.noData)
9
+ expect(resolveChartMessages()).toBe(defaultChartMessages)
10
+ })
11
+
12
+ it('fills placeholders and leaves unknown ones alone', () => {
13
+ expect(chartMessage('{type} chart', { type: 'bar' })).toBe('bar chart')
14
+ expect(chartMessage('{series}: {value} at {category}', { series: 'S', value: 3, category: 'Q1' })).toBe('S: 3 at Q1')
15
+ expect(chartMessage('+{n} more {x}', { n: 4 })).toBe('+4 more {x}')
16
+ })
17
+
18
+ it('every default has a value', () => {
19
+ for (const [k, v] of Object.entries(defaultChartMessages)) expect(v, k).not.toBe('')
20
+ })
21
+ })
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Chart chrome localization. Every string `SvChart` renders on its own -
3
+ * toolbar buttons and their titles, the context menu, the legend's overflow
4
+ * and hint text, the empty state, and everything it says to assistive
5
+ * technology (the image label, the data table caption, the live region, the
6
+ * marks' labels) - lives here as a flat map with English defaults. Pass
7
+ * `localeText` on `<SvChart>` to override any subset; unset keys fall back
8
+ * to English, so omitting it is a no-op. The same `resolveMessages` idiom as
9
+ * the grid's `localeText` (grid-messages.ts) and the editors'.
10
+ *
11
+ * Placeholders in braces (`{type}`, `{n}`) are filled by {@link chartMessage}.
12
+ */
13
+ import { resolveMessages } from './editor-contract'
14
+
15
+ /** Every localizable chart string, grouped by area in the comments. */
16
+ export type ChartMessages = {
17
+ // The image and its data table
18
+ /** The SVG's accessible name. `{type}` is the chart type. */
19
+ chartLabel: string
20
+ /** The hidden data table's caption. `{type}` is the chart type. */
21
+ dataTable: string
22
+ /** Appended to the caption when the table is capped. `{shown}` / `{total}`. */
23
+ dataTableCapped: string
24
+ noData: string
25
+ /** A point with no value, in labels and the live region. */
26
+ noValue: string
27
+ // What the live region and the marks say
28
+ /** The live region while stepping through series: `{series}`, `{value}`, `{category}`. */
29
+ seriesAtCategory: string
30
+ /** A dense chart's single hit zone: `{count}`, `{from}`, `{to}`. */
31
+ densePoints: string
32
+ /** Appended to a slice or cell label that drills further. */
33
+ drillIn: string
34
+ /** A sankey node's label: `{label}`, `{in}`, `{out}`. */
35
+ sankeyNode: string
36
+ /** The donut's centre caption under the total. */
37
+ total: string
38
+ /** A regression overlay's goodness of fit in the tooltip. `{value}` is 0..1. */
39
+ rSquared: string
40
+ /** The context menu item that copies the chart's plain-language summary. */
41
+ describeChart: string
42
+ /** The live region after the summary was copied. */
43
+ describeCopied: string
44
+ // Toolbar
45
+ drillPath: string
46
+ drillRoot: string
47
+ dateRange: string
48
+ /** A preset button's title. `{preset}` is its label. */
49
+ showLast: string
50
+ pan: string
51
+ panTitle: string
52
+ resetZoom: string
53
+ resetZoomTitle: string
54
+ annotate: string
55
+ annotateOnTitle: string
56
+ annotateOffTitle: string
57
+ drawingTools: string
58
+ /** A drawing tool button's title. `{tool}` is the tool's label. */
59
+ draw: string
60
+ toolTrend: string
61
+ toolRay: string
62
+ toolFib: string
63
+ toolRect: string
64
+ toolArrow: string
65
+ toolText: string
66
+ /** A drawing's accessible name. `{tool}` is the tool's label. */
67
+ drawing: string
68
+ deleteDrawing: string
69
+ deleteDrawingTitle: string
70
+ clearDrawings: string
71
+ clearDrawingsTitle: string
72
+ noteText: string
73
+ /** A note marker's name while annotating. `{label}` is the note. */
74
+ removeNote: string
75
+ png: string
76
+ downloadPng: string
77
+ svg: string
78
+ downloadSvg: string
79
+ pdf: string
80
+ downloadPdf: string
81
+ downloadCsv: string
82
+ print: string
83
+ printTitle: string
84
+ copy: string
85
+ copied: string
86
+ copyTitle: string
87
+ copyAsImage: string
88
+ // Context menu
89
+ range: string
90
+ series: string
91
+ // Legend
92
+ showSeries: string
93
+ hideSeries: string
94
+ isolateHint: string
95
+ /** The overflow toggle. `{n}` is how many chips are folded away. */
96
+ showMore: string
97
+ showLess: string
98
+ // Brush
99
+ rangeBrush: string
100
+ /** The brush's value text. `{from}` / `{to}` are the window's ends. */
101
+ brushRange: string
102
+ }
103
+
104
+ /** The English defaults. */
105
+ export const defaultChartMessages: ChartMessages = {
106
+ chartLabel: '{type} chart',
107
+ dataTable: '{type} chart data',
108
+ dataTableCapped: ', first {shown} of {total} rows',
109
+ noData: 'No data',
110
+ noValue: 'no value',
111
+ seriesAtCategory: '{series}: {value} at {category}',
112
+ densePoints: '{count} points, {from} to {to}. Arrow keys step through values.',
113
+ drillIn: ', drill in',
114
+ sankeyNode: '{label}: in {in}, out {out}',
115
+ total: 'Total',
116
+ rSquared: 'R\u00b2 {value}',
117
+ describeChart: 'Describe chart',
118
+ describeCopied: 'Description copied',
119
+ drillPath: 'Drilldown path',
120
+ drillRoot: 'All',
121
+ dateRange: 'Date range',
122
+ showLast: 'Show the last {preset}',
123
+ pan: 'Pan',
124
+ panTitle: 'Drag the plot to pan the zoomed window',
125
+ resetZoom: 'Reset zoom',
126
+ resetZoomTitle: 'Reset zoom (or double-click the plot)',
127
+ annotate: 'Annotate',
128
+ annotateOnTitle: 'Click the plot to pin a note, or a marker to remove it',
129
+ annotateOffTitle: 'Pin notes on the chart',
130
+ drawingTools: 'Drawing tools',
131
+ draw: 'Draw: {tool}',
132
+ toolTrend: 'Trend',
133
+ toolRay: 'Ray',
134
+ toolFib: 'Fib',
135
+ toolRect: 'Rect',
136
+ toolArrow: 'Arrow',
137
+ toolText: 'Text',
138
+ drawing: '{tool} drawing',
139
+ deleteDrawing: 'Delete',
140
+ deleteDrawingTitle: 'Remove the selected drawing (Delete)',
141
+ clearDrawings: 'Clear',
142
+ clearDrawingsTitle: 'Remove every drawing',
143
+ noteText: 'Note text',
144
+ removeNote: 'Remove note: {label}',
145
+ png: 'PNG',
146
+ downloadPng: 'Download PNG',
147
+ svg: 'SVG',
148
+ downloadSvg: 'Download SVG',
149
+ pdf: 'PDF',
150
+ downloadPdf: 'Download PDF',
151
+ downloadCsv: 'Download CSV',
152
+ print: 'Print',
153
+ printTitle: 'Print the chart',
154
+ copy: 'Copy',
155
+ copied: '✓ Copied',
156
+ copyTitle: 'Copy chart as image',
157
+ copyAsImage: 'Copy as image',
158
+ range: 'Range',
159
+ series: 'Series',
160
+ showSeries: 'Show',
161
+ hideSeries: 'Hide',
162
+ isolateHint: 'double-click to isolate',
163
+ showMore: '+{n} more',
164
+ showLess: 'Show less',
165
+ rangeBrush: 'Range brush',
166
+ brushRange: '{from} to {to}',
167
+ }
168
+
169
+ /**
170
+ * Merge a consumer's `localeText` over the English defaults. Undefined or
171
+ * empty values fall back to the default, so a partial map only replaces the
172
+ * keys it sets.
173
+ */
174
+ export function resolveChartMessages(overrides?: Partial<ChartMessages> | null): ChartMessages {
175
+ return resolveMessages(defaultChartMessages, overrides)
176
+ }
177
+
178
+ /**
179
+ * Fill a message's `{name}` placeholders. Unknown placeholders are left as
180
+ * they are, so a translation that drops one loses nothing.
181
+ */
182
+ export function chartMessage(template: string, vars: Record<string, string | number>): string {
183
+ return template.replace(/\{(\w+)\}/g, (m, k: string) => (k in vars ? String(vars[k]) : m))
184
+ }
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { buildChart } from './chart'
3
+ import { interpolateGeometry } from './chart-motion'
4
+ import type { ChartSpec } from './chart-types'
5
+
6
+ const bars = (values: number[], categories = ['a', 'b', 'c']): ChartSpec => ({
7
+ type: 'bar', categories, series: [{ label: 's', values }], width: 400, height: 200,
8
+ })
9
+ const lines = (values: number[]): ChartSpec => ({
10
+ type: 'line', categories: ['a', 'b', 'c'], series: [{ label: 's', values }], width: 400, height: 200,
11
+ })
12
+
13
+ describe('interpolateGeometry', () => {
14
+ const a = buildChart(bars([1, 2, 3]), 'light')
15
+ const b = buildChart(bars([3, 2, 1]), 'light')
16
+
17
+ it('is the start at t = 0 and the end at t = 1, without copying', () => {
18
+ expect(interpolateGeometry(a, b, 0)).toBe(a)
19
+ expect(interpolateGeometry(a, b, 1)).toBe(b)
20
+ expect(interpolateGeometry(a, b, 1.5)).toBe(b)
21
+ })
22
+
23
+ it('moves bars halfway at t = 0.5, keyed by series and label', () => {
24
+ const mid = interpolateGeometry(a, b, 0.5)
25
+ expect(mid.bars).toHaveLength(3)
26
+ for (let i = 0; i < 3; i += 1) {
27
+ expect(mid.bars[i]!.y).toBeCloseTo((a.bars[i]!.y + b.bars[i]!.y) / 2, 1)
28
+ expect(mid.bars[i]!.h).toBeCloseTo((a.bars[i]!.h + b.bars[i]!.h) / 2, 1)
29
+ }
30
+ // Everything that is not a mark comes from the destination.
31
+ expect(mid.plot).toEqual(b.plot)
32
+ expect(mid.xTicks).toEqual(b.xTicks)
33
+ })
34
+
35
+ it('grows a new bar from the baseline and shrinks a removed one into it', () => {
36
+ const c = buildChart(bars([1, 2, 3, 4], ['a', 'b', 'c', 'd']), 'light')
37
+ const grow = interpolateGeometry(a, c, 0.5)
38
+ const fresh = grow.bars.find((x) => x.label === 'd')!
39
+ expect(fresh.h).toBeCloseTo(c.bars[3]!.h / 2, 1)
40
+ expect(fresh.y + fresh.h).toBeCloseTo(c.plot.y + c.plot.h, 1)
41
+ const shrink = interpolateGeometry(c, a, 0.5)
42
+ const gone = shrink.bars.find((x) => x.label === 'd')!
43
+ expect(gone).toBeTruthy()
44
+ expect(gone.h).toBeCloseTo(c.bars[3]!.h / 2, 1)
45
+ expect(shrink.bars).toHaveLength(4)
46
+ })
47
+
48
+ it('slides line points and rebuilds the path; a changed point count snaps', () => {
49
+ const l0 = buildChart(lines([1, 2, 3]), 'light')
50
+ const l1 = buildChart(lines([3, 2, 1]), 'light')
51
+ const mid = interpolateGeometry(l0, l1, 0.5)
52
+ expect(mid.lines[0]!.points[0]!.y).toBeCloseTo((l0.lines[0]!.points[0]!.y + l1.lines[0]!.points[0]!.y) / 2, 1)
53
+ expect(mid.lines[0]!.path).not.toBe(l1.lines[0]!.path)
54
+ const l2 = buildChart({ ...lines([1, 2]), categories: ['a', 'b'] }, 'light')
55
+ expect(interpolateGeometry(l0, l2, 0.5).lines[0]).toBe(l2.lines[0])
56
+ })
57
+
58
+ it('a type change is not tweened', () => {
59
+ const pie = buildChart({ ...bars([1, 2, 3]), type: 'pie' }, 'light')
60
+ expect(interpolateGeometry(a, pie, 0.5)).toBe(pie)
61
+ })
62
+
63
+ it('pie slices and sunburst arcs interpolate their angles', () => {
64
+ const p0 = buildChart({ ...bars([1, 1, 2]), type: 'pie' }, 'light')
65
+ const p1 = buildChart({ ...bars([2, 1, 1]), type: 'pie' }, 'light')
66
+ const mid = interpolateGeometry(p0, p1, 0.5)
67
+ expect(mid.slices).toHaveLength(3)
68
+ expect(mid.slices[0]!.arc!.a1).toBeCloseTo((p0.slices[0]!.arc!.a1 + p1.slices[0]!.arc!.a1) / 2, 6)
69
+ expect(mid.slices[0]!.path).not.toBe(p1.slices[0]!.path)
70
+ })
71
+ })