@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,175 @@
1
+ import { bucketStart } from './chart-stats.js';
2
+ /**
3
+ * Heikin-Ashi ("average bar") candles from ordinary OHLC bars. Each close is
4
+ * the mean of the four prices, each open the midpoint of the previous
5
+ * Heikin-Ashi body, and the high / low stretch to include both. Runs of one
6
+ * colour then read as trends, at the cost of the true open and close.
7
+ * `null` bars (gaps) stay null and reset the chain.
8
+ */
9
+ export function heikinAshi(bars) {
10
+ const out = new Array(bars.length);
11
+ let prev = null;
12
+ for (let i = 0; i < bars.length; i += 1) {
13
+ const b = bars[i];
14
+ if (!b || ![b.o, b.h, b.l, b.c].every(Number.isFinite)) {
15
+ out[i] = null;
16
+ prev = null;
17
+ continue;
18
+ }
19
+ const c = (b.o + b.h + b.l + b.c) / 4;
20
+ const o = prev ? (prev.o + prev.c) / 2 : (b.o + b.c) / 2;
21
+ const ha = { o, c, h: Math.max(b.h, o, c), l: Math.min(b.l, o, c) };
22
+ out[i] = ha;
23
+ prev = ha;
24
+ }
25
+ return out;
26
+ }
27
+ /**
28
+ * Roll daily (or finer) bars up to a coarser calendar bucket: the first
29
+ * open, the highest high, the lowest low, the last close, the summed volume.
30
+ * Categories are ISO dates and come back as the bucket starts (`bucketStart`),
31
+ * sorted; bars whose date does not parse are dropped. Pass `volumes` to get
32
+ * them summed alongside.
33
+ */
34
+ export function resampleOhlc(categories, ohlc, opts) {
35
+ const groups = new Map();
36
+ const order = [];
37
+ for (let i = 0; i < categories.length; i += 1) {
38
+ const t = Date.parse(categories[i]);
39
+ if (!Number.isFinite(t))
40
+ continue;
41
+ const key = bucketStart(t, opts.bucket);
42
+ if (!key)
43
+ continue;
44
+ let g = groups.get(key);
45
+ if (!g) {
46
+ g = { bar: null, vol: 0, first: Infinity, last: -Infinity };
47
+ groups.set(key, g);
48
+ order.push([key, Date.parse(key)]);
49
+ }
50
+ const b = ohlc[i];
51
+ if (b) {
52
+ // Rows may arrive out of order: the open is the EARLIEST bar's and the
53
+ // close the LATEST bar's, by date, not by position.
54
+ if (!g.bar)
55
+ g.bar = { ...b };
56
+ else {
57
+ g.bar.h = Math.max(g.bar.h, b.h);
58
+ g.bar.l = Math.min(g.bar.l, b.l);
59
+ if (t >= g.last)
60
+ g.bar.c = b.c;
61
+ if (t <= g.first)
62
+ g.bar.o = b.o;
63
+ }
64
+ if (t > g.last)
65
+ g.last = t;
66
+ if (t < g.first)
67
+ g.first = t;
68
+ }
69
+ const v = opts.volumes?.[i];
70
+ if (Number.isFinite(v))
71
+ g.vol += v;
72
+ }
73
+ order.sort((a, b) => a[1] - b[1]);
74
+ const out = {
75
+ categories: order.map(([k]) => k),
76
+ ohlc: order.map(([k]) => groups.get(k).bar),
77
+ ...(opts.volumes ? { volumes: order.map(([k]) => groups.get(k).vol) } : {}),
78
+ };
79
+ return out;
80
+ }
81
+ /**
82
+ * Guess the price columns from their names: the first numeric column called
83
+ * open / high / low / close (or last, price) / volume. What the chart panel
84
+ * shows before the reader picks anything; a picked column always wins.
85
+ */
86
+ export function guessOhlcColumns(measures, picked) {
87
+ const guess = (re) => measures.find((m) => re.test(m.field) || re.test(m.label))?.id ?? null;
88
+ return {
89
+ open: picked?.open ?? guess(/open/i),
90
+ high: picked?.high ?? guess(/high/i),
91
+ low: picked?.low ?? guess(/low/i),
92
+ close: picked?.close ?? guess(/close|last|price/i),
93
+ volume: picked?.volume ?? guess(/vol/i),
94
+ };
95
+ }
96
+ /**
97
+ * The `rowsToDirectSpec` options for the grid panel's price chart, or null
98
+ * while a price column is still unpicked. `fieldOf` maps a column id to its
99
+ * field name; the date column is the category.
100
+ */
101
+ export function ohlcDirectOptions(fieldOf, dateId, cols, extra) {
102
+ const date = fieldOf(dateId);
103
+ const open = fieldOf(cols.open);
104
+ const high = fieldOf(cols.high);
105
+ const low = fieldOf(cols.low);
106
+ const close = fieldOf(cols.close);
107
+ if (!date || !open || !high || !low || !close)
108
+ return null;
109
+ const volume = fieldOf(cols.volume);
110
+ return {
111
+ category: date,
112
+ ohlc: {
113
+ open, high, low, close,
114
+ ...(volume ? { volume } : {}),
115
+ ...(extra.bucket ? { bucket: extra.bucket } : {}),
116
+ ...(extra.candleStyle ? { candleStyle: extra.candleStyle } : {}),
117
+ lastPriceLine: true,
118
+ },
119
+ };
120
+ }
121
+ /**
122
+ * Split the panel's indicator list into the panes that go under the price
123
+ * (in order) and the one overlay that rides on it (a series carries a single
124
+ * `overlay`, so the first overlay kind wins).
125
+ */
126
+ export function splitPanelIndicators(kinds) {
127
+ const OVERLAY = { sma: 'sma:20', ema: 'ema:20', bb: 'bb:20:2', vwap: 'vwap' };
128
+ const panes = [];
129
+ let overlay = null;
130
+ for (const k of kinds) {
131
+ const o = OVERLAY[k];
132
+ if (o) {
133
+ overlay ?? (overlay = o);
134
+ continue;
135
+ }
136
+ if (k === 'volume' || k === 'rsi' || k === 'macd' || k === 'stochastic' || k === 'atr' || k === 'obv')
137
+ panes.push({ kind: k });
138
+ }
139
+ return { panes, overlay };
140
+ }
141
+ /**
142
+ * A candlestick spec straight from rows: one bar per date (sorted), with the
143
+ * volume alongside when a field is named, optionally rolled up to a calendar
144
+ * bucket first. The x axis is ordinal-time, so weekends and holidays leave no
145
+ * gaps.
146
+ */
147
+ export function rowsToOhlcSpec(rows, opts) {
148
+ const num = (v) => (typeof v === 'number' ? v : Number(v));
149
+ const sorted = rows
150
+ .map((r) => ({ r, t: Date.parse(String(r[opts.date])) }))
151
+ .filter((x) => Number.isFinite(x.t))
152
+ .sort((a, b) => a.t - b.t);
153
+ let categories = sorted.map((x) => new Date(x.t).toISOString().slice(0, 10));
154
+ let ohlc = sorted.map(({ r }) => {
155
+ const b = { o: num(r[opts.open]), h: num(r[opts.high]), l: num(r[opts.low]), c: num(r[opts.close]) };
156
+ return [b.o, b.h, b.l, b.c].every(Number.isFinite) ? b : null;
157
+ });
158
+ let volumes = opts.volume ? sorted.map(({ r }) => num(r[opts.volume])) : undefined;
159
+ if (opts.bucket) {
160
+ const rolled = resampleOhlc(categories, ohlc, { bucket: opts.bucket, volumes });
161
+ categories = rolled.categories;
162
+ ohlc = rolled.ohlc;
163
+ volumes = rolled.volumes;
164
+ }
165
+ const label = opts.label ?? 'Price';
166
+ const series = { label, values: ohlc.map((b) => (b ? b.c : Number.NaN)), ohlc, ...(volumes ? { volumes } : {}) };
167
+ return {
168
+ type: opts.type ?? 'candlestick',
169
+ categories,
170
+ series: [series],
171
+ xType: 'ordinal-time',
172
+ ...(opts.candleStyle ? { candleStyle: opts.candleStyle } : {}),
173
+ ...(opts.lastPriceLine ? { lastPriceLine: true } : {}),
174
+ };
175
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Flow layouts: waterfall, funnel and sankey.
3
+ */
4
+ import type { ChartGeometry, LayoutCtx } from './chart-types.js';
5
+ /** @internal Lay out the waterfall family. Called by buildChart. */
6
+ export declare function layoutWaterfall(ctx: LayoutCtx): ChartGeometry;
7
+ /** @internal Lay out the funnel family. Called by buildChart. */
8
+ export declare function layoutFunnel(ctx: LayoutCtx): ChartGeometry;
9
+ /** @internal Lay out the sankey family. Called by buildChart. */
10
+ export declare function layoutSankey(ctx: LayoutCtx): ChartGeometry;
@@ -0,0 +1,298 @@
1
+ import { round, niceScale, formatChartValue, pickContrastText, DEFAULT_PALETTE } from './chart-scale.js';
2
+ // ---- Waterfall ----------------------------------------------------
3
+ // First series provides the values. Each non-total bar starts at the
4
+ // running cumulative sum; total bars (waterfallTotals[i]) span from 0 to
5
+ // that sum, and a total with a value of its own sets the sum to it first
6
+ // (the opening balance of a bridge, or a subtotal supplied by the caller).
7
+ // Color is derived from sign + total flag, with optional palette overrides
8
+ // via spec.waterfallColors.
9
+ /** @internal Lay out the waterfall family. Called by buildChart. */
10
+ export function layoutWaterfall(ctx) {
11
+ const { spec, series, width, height, empty, frame, axes } = ctx;
12
+ const src = series[0];
13
+ if (!src)
14
+ return { ...empty };
15
+ const colors = spec.waterfallColors ?? {};
16
+ const positive = colors.positive ?? '#16a34a';
17
+ const negative = colors.negative ?? '#ef4444';
18
+ const total = colors.total ?? '#475569';
19
+ const maxLabel = spec.categories.reduce((m, c) => Math.max(m, c.length), 0);
20
+ const padL = 48 + (axes.y.title ? 16 : 0);
21
+ const padR = 12;
22
+ const padT = 10 + frame.top;
23
+ const plotW = Math.max(1, width - padL - padR);
24
+ // The same rule as a bar chart: labels tilt when the widest does not fit
25
+ // its slot. Judged by count alone, nine categories on a 900px chart were
26
+ // flagged as rotated with no angle to go with it, and drew truncated.
27
+ const xLabelRotated = typeof axes.x.labelRotation === 'number' ? axes.x.labelRotation !== 0 : maxLabel * 6.2 + 8 > plotW / Math.max(1, spec.categories.length);
28
+ const xLabelAngle = typeof axes.x.labelRotation === 'number' ? axes.x.labelRotation : xLabelRotated ? -40 : 0;
29
+ const padB = (xLabelRotated ? 54 : 28) + (axes.x.title ? 16 : 0) + frame.bottom;
30
+ const plotH = Math.max(1, height - padT - padB);
31
+ const plot = { x: padL, y: padT, w: plotW, h: plotH };
32
+ // Compute the running cumulative + per-bar (from, to) pairs.
33
+ const totals = spec.waterfallTotals ?? [];
34
+ const pairs = [];
35
+ let cum = 0;
36
+ spec.categories.forEach((_, i) => {
37
+ const v = src.values[i] ?? 0;
38
+ const isTotal = !!totals[i];
39
+ if (isTotal) {
40
+ // An opening "Revenue 4300" is a total with a value: it anchors the
41
+ // running sum. Read as a computed subtotal it drew as zero and every
42
+ // bar after it hung below the axis.
43
+ if (Number.isFinite(v) && v !== 0)
44
+ cum = v;
45
+ pairs.push({ from: 0, to: cum, value: cum, isTotal: true });
46
+ }
47
+ else {
48
+ pairs.push({ from: cum, to: cum + v, value: v, isTotal: false });
49
+ cum += v;
50
+ }
51
+ });
52
+ // Y-axis domain spans every visited level (including 0).
53
+ let dMin = 0, dMax = 0;
54
+ for (const p of pairs) {
55
+ if (p.from < dMin)
56
+ dMin = p.from;
57
+ if (p.to < dMin)
58
+ dMin = p.to;
59
+ if (p.from > dMax)
60
+ dMax = p.from;
61
+ if (p.to > dMax)
62
+ dMax = p.to;
63
+ }
64
+ const dom = niceScale(dMin, dMax);
65
+ const yOfW = (v) => round(padT + plotH - ((v - dom.min) / (dom.max - dom.min || 1)) * plotH);
66
+ const slotW = plotW / Math.max(1, spec.categories.length);
67
+ const barPad = slotW * 0.2;
68
+ const barW = Math.max(1, slotW - barPad);
69
+ const bars = pairs.map((p, i) => {
70
+ const yTop = yOfW(Math.max(p.from, p.to));
71
+ const yBot = yOfW(Math.min(p.from, p.to));
72
+ const x = padL + slotW * i + barPad / 2;
73
+ const color = p.isTotal ? total : p.value >= 0 ? positive : negative;
74
+ return {
75
+ x: round(x), y: yTop, w: round(barW), h: Math.max(1, yBot - yTop),
76
+ color, label: spec.categories[i] ?? String(i), series: src.label, value: p.value, index: i,
77
+ };
78
+ });
79
+ // Thin connector lines between bar tops -> running total reads cleanly.
80
+ const connectors = [{
81
+ path: pairs
82
+ .map((p, i) => {
83
+ const x0 = padL + slotW * i + barPad / 2 + barW;
84
+ const y = yOfW(p.to);
85
+ const x1 = padL + slotW * (i + 1) + barPad / 2;
86
+ // Skip the final connector beyond the last bar.
87
+ return i < pairs.length - 1 ? `M${x0},${y} L${x1},${y}` : '';
88
+ })
89
+ .filter(Boolean)
90
+ .join(' '),
91
+ areaPath: '',
92
+ color: 'var(--sg-muted, #94a3b8)',
93
+ label: '',
94
+ points: [],
95
+ }];
96
+ const xTicks = spec.categories.map((label, i) => ({
97
+ label,
98
+ x: round(padL + slotW * i + slotW / 2),
99
+ }));
100
+ const yTicks = dom.ticks.map((value) => ({
101
+ value, y: yOfW(value), label: formatChartValue(value, spec.valueFormat, spec),
102
+ }));
103
+ return {
104
+ ...empty,
105
+ plot,
106
+ bars,
107
+ lines: connectors,
108
+ yTicks,
109
+ xTicks,
110
+ xLabelRotated,
111
+ xLabelAngle,
112
+ };
113
+ }
114
+ // ---- Funnel -------------------------------------------------------
115
+ // One series of strictly-decreasing values gets rendered as a stack
116
+ // of horizontal trapezoids: each level's width is proportional to its
117
+ // value relative to the largest, slope automatically links level N+1
118
+ // narrower than level N. Labels show value, conversion vs. top, and
119
+ // step drop-off.
120
+ /** @internal Lay out the funnel family. Called by buildChart. */
121
+ export function layoutFunnel(ctx) {
122
+ const { spec, series, width, height, empty, frame } = ctx;
123
+ const src = series[0];
124
+ if (!src || !src.values.length)
125
+ return { ...empty };
126
+ const padL = 20, padR = 20, padT = 16 + frame.top, padB = 16 + frame.bottom;
127
+ const plotW = Math.max(1, width - padL - padR);
128
+ const plotH = Math.max(1, height - padT - padB);
129
+ const plot = { x: padL, y: padT, w: plotW, h: plotH };
130
+ const n = src.values.length;
131
+ const stepH = plotH / n;
132
+ const valMax = Math.max(...src.values.map((v) => (Number.isFinite(v) ? v : 0)));
133
+ const top = src.values[0] ?? 0;
134
+ const widthAt = (v) => (valMax > 0 ? (v / valMax) * plotW : 0);
135
+ const cx = padL + plotW / 2;
136
+ const palette = spec.palette ?? DEFAULT_PALETTE;
137
+ const shape = spec.funnelShape ?? 'trapezoid';
138
+ const segments = src.values.map((v, i) => {
139
+ const next = src.values[i + 1] ?? v * 0.8; // taper to a point on the last level
140
+ const y0 = padT + stepH * i;
141
+ const y1 = y0 + stepH;
142
+ let path;
143
+ if (shape === 'pyramid') {
144
+ // Widest at the bottom: a pyramid of levels, each a slice of one big
145
+ // triangle. The top level is the point.
146
+ const wTop = (plotW * i) / n;
147
+ const wBot = (plotW * (i + 1)) / n;
148
+ path = `M${cx - wTop / 2},${y0} L${cx + wTop / 2},${y0} L${cx + wBot / 2},${y1} L${cx - wBot / 2},${y1} Z`;
149
+ }
150
+ else if (shape === 'cone') {
151
+ // One triangle, sliced: the width at each level follows its share of
152
+ // the height, so the whole funnel is a cone that tapers to a point.
153
+ const wTop = plotW * (1 - i / n);
154
+ const wBot = plotW * (1 - (i + 1) / n);
155
+ path = `M${cx - wTop / 2},${y0} L${cx + wTop / 2},${y0} L${cx + wBot / 2},${y1} L${cx - wBot / 2},${y1} Z`;
156
+ }
157
+ else {
158
+ const w0 = widthAt(v);
159
+ const w1 = widthAt(next);
160
+ path = `M${cx - w0 / 2},${y0} L${cx + w0 / 2},${y0} L${cx + w1 / 2},${y1} L${cx - w1 / 2},${y1} Z`;
161
+ }
162
+ const color = src.color ?? palette[i % palette.length];
163
+ return {
164
+ path, color,
165
+ label: spec.categories[i] ?? src.label,
166
+ value: v,
167
+ conversion: top > 0 ? v / top : 0,
168
+ dropoff: i === 0 ? 0 : (src.values[i - 1] ?? v) > 0 ? 1 - v / (src.values[i - 1] ?? v) : 0,
169
+ cx,
170
+ cy: (y0 + y1) / 2,
171
+ textColor: pickContrastText(color),
172
+ };
173
+ });
174
+ return {
175
+ ...empty,
176
+ plot,
177
+ funnelSegments: segments,
178
+ };
179
+ }
180
+ // ---- Sankey -------------------------------------------------------
181
+ // Multi-column flow layout. Each node assigned to a column by longest
182
+ // path from any source. Within a column, nodes are stacked vertically;
183
+ // height proportional to max(totalIn, totalOut). Links render as
184
+ // bezier ribbons whose width is the link value (in pixels).
185
+ /** @internal Lay out the sankey family. Called by buildChart. */
186
+ export function layoutSankey(ctx) {
187
+ const { spec, width, height, empty, frame } = ctx;
188
+ const nodes = spec.sankeyNodes ?? [];
189
+ const links = spec.sankeyLinks ?? [];
190
+ if (!nodes.length || !links.length)
191
+ return { ...empty };
192
+ const padL = 10, padR = 10, padT = 14 + frame.top, padB = 14 + frame.bottom;
193
+ const plotW = Math.max(1, width - padL - padR);
194
+ const plotH = Math.max(1, height - padT - padB);
195
+ const plot = { x: padL, y: padT, w: plotW, h: plotH };
196
+ const palette = spec.palette ?? DEFAULT_PALETTE;
197
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
198
+ // Column = longest path from any node with no incoming edges.
199
+ const targets = new Set(links.map((l) => l.target));
200
+ const sources = nodes.filter((n) => !targets.has(n.id));
201
+ const column = new Map();
202
+ function visit(id, depth, seen) {
203
+ if (seen.has(id))
204
+ return;
205
+ seen.add(id);
206
+ const cur = column.get(id) ?? 0;
207
+ if (depth > cur || !column.has(id))
208
+ column.set(id, depth);
209
+ for (const l of links)
210
+ if (l.source === id)
211
+ visit(l.target, depth + 1, seen);
212
+ seen.delete(id);
213
+ }
214
+ for (const s of sources)
215
+ visit(s.id, 0, new Set());
216
+ // Cover any nodes with no path from a source (orphan rings).
217
+ for (const n of nodes)
218
+ if (!column.has(n.id))
219
+ column.set(n.id, 0);
220
+ const maxCol = Math.max(...column.values());
221
+ const cols = maxCol + 1;
222
+ const nodeW = 14;
223
+ const gapBetweenColumns = cols > 1 ? (plotW - nodeW * cols) / (cols - 1) : 0;
224
+ // Totals per node.
225
+ const totalIn = new Map();
226
+ const totalOut = new Map();
227
+ for (const l of links) {
228
+ totalIn.set(l.target, (totalIn.get(l.target) ?? 0) + l.value);
229
+ totalOut.set(l.source, (totalOut.get(l.source) ?? 0) + l.value);
230
+ }
231
+ // Per-column groups + max total in that column.
232
+ const byCol = new Map();
233
+ for (const n of nodes) {
234
+ const c = column.get(n.id) ?? 0;
235
+ const arr = byCol.get(c) ?? [];
236
+ arr.push(n.id);
237
+ byCol.set(c, arr);
238
+ }
239
+ // Per-column total height + node height scale.
240
+ let maxColTotal = 0;
241
+ for (const ids of byCol.values()) {
242
+ const t = ids.reduce((s, id) => s + Math.max(totalIn.get(id) ?? 0, totalOut.get(id) ?? 0), 0);
243
+ if (t > maxColTotal)
244
+ maxColTotal = t;
245
+ }
246
+ if (maxColTotal === 0)
247
+ return { ...empty, plot };
248
+ const nodeGapPx = 8;
249
+ const heightScale = (plotH - nodeGapPx * 8) / maxColTotal; // leave gap room
250
+ const placed = [];
251
+ for (const [c, ids] of byCol) {
252
+ const heights = ids.map((id) => Math.max(8, Math.max(totalIn.get(id) ?? 0, totalOut.get(id) ?? 0) * heightScale));
253
+ const totalH = heights.reduce((s, h) => s + h, 0) + nodeGapPx * (ids.length - 1);
254
+ let yCursor = padT + (plotH - totalH) / 2;
255
+ const xCol = padL + c * (nodeW + gapBetweenColumns);
256
+ ids.forEach((id, idx) => {
257
+ const node = nodeById.get(id);
258
+ const h = heights[idx];
259
+ placed.push({
260
+ id,
261
+ label: node.label ?? id,
262
+ color: node.color ?? palette[(placed.length) % palette.length],
263
+ x: xCol, y: yCursor, w: nodeW, h,
264
+ column: c,
265
+ totalIn: totalIn.get(id) ?? 0,
266
+ totalOut: totalOut.get(id) ?? 0,
267
+ });
268
+ yCursor += h + nodeGapPx;
269
+ });
270
+ }
271
+ const placedById = new Map(placed.map((n) => [n.id, n]));
272
+ // Per-node sub-cursor so multiple links from one node stack vertically.
273
+ const inCursor = new Map();
274
+ const outCursor = new Map();
275
+ const builtLinks = [];
276
+ // Sort links so wider ribbons render first (so thin ribbons stack on top).
277
+ const sortedLinks = links.slice().sort((a, b) => b.value - a.value);
278
+ for (const link of sortedLinks) {
279
+ const a = placedById.get(link.source);
280
+ const b = placedById.get(link.target);
281
+ if (!a || !b)
282
+ continue;
283
+ const linkH = Math.max(1, link.value * heightScale);
284
+ const aY = a.y + (outCursor.get(a.id) ?? 0) + linkH / 2;
285
+ const bY = b.y + (inCursor.get(b.id) ?? 0) + linkH / 2;
286
+ outCursor.set(a.id, (outCursor.get(a.id) ?? 0) + linkH);
287
+ inCursor.set(b.id, (inCursor.get(b.id) ?? 0) + linkH);
288
+ const x0 = a.x + a.w;
289
+ const x1 = b.x;
290
+ const mid = (x0 + x1) / 2;
291
+ const path = `M${x0},${aY} C${mid},${aY} ${mid},${bY} ${x1},${bY}`;
292
+ builtLinks.push({
293
+ path, color: link.color ?? a.color, width: linkH,
294
+ source: link.source, target: link.target, value: link.value,
295
+ });
296
+ }
297
+ return { ...empty, plot, sankeyNodes: placed, sankeyLinks: builtLinks };
298
+ }
@@ -0,0 +1,18 @@
1
+ import type { ChartFormatLocale, ChartValueFormat } from './chart-types.js';
2
+ /** @internal Locale-free compact tick label (1.2k / 1.2M). */
3
+ export declare function fmtTick(n: number): string;
4
+ /**
5
+ * Format a numeric value for display, honouring an optional `valueFormat`.
6
+ *
7
+ * Two modes, on purpose. With no `locale` and no `currency` this is the original
8
+ * locale-free output: the compact `1.2k` / `1.2M` base, currency prefixed with
9
+ * `$` (sign outside), percent multiplied by 100 and suffixed `%`. Set either one
10
+ * and it switches to `Intl.NumberFormat`, so separators, the decimal mark and the
11
+ * compact suffixes all follow the locale.
12
+ *
13
+ * Keeping the old path as the default is deliberate rather than lazy. `Intl`'s
14
+ * compact notation is not the same string even for `en-US` (`1.2K`, capital),
15
+ * so formatting everything through it would silently restyle every axis in every
16
+ * existing chart. Opting in is the only version of this that is not a surprise.
17
+ */
18
+ export declare function formatChartValue(n: number, format?: ChartValueFormat, opts?: ChartFormatLocale): string;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Number formatting for chart values: the compact `1.2k` tick label and
3
+ * `formatChartValue`, which honours a spec's `valueFormat`, `locale` and
4
+ * `currency`.
5
+ *
6
+ * A leaf of its own, and deliberately small. The chart engine and the lazily
7
+ * loaded summary (chart-summary.ts) both need it, and a bundler hoists
8
+ * whatever the base chunk and a lazy chunk share into a third chunk. When
9
+ * that shared module was all of chart-scale.ts the hoist cost the base
10
+ * bundle a kilobyte of gzip locality for no new code; two functions cost a
11
+ * few dozen bytes. chart-scale.ts re-exports both, so importers of the old
12
+ * home keep working.
13
+ */
14
+ import { getNumberFormatter } from './cell-formatting.js';
15
+ /** @internal Locale-free compact tick label (1.2k / 1.2M). */
16
+ export function fmtTick(n) {
17
+ const abs = Math.abs(n);
18
+ if (abs >= 1000000)
19
+ return `${(n / 1000000).toFixed(abs % 1000000 ? 1 : 0)}M`;
20
+ if (abs >= 1000)
21
+ return `${(n / 1000).toFixed(abs % 1000 ? 1 : 0)}k`;
22
+ return String(Math.round(n * 100) / 100);
23
+ }
24
+ /**
25
+ * Format a numeric value for display, honouring an optional `valueFormat`.
26
+ *
27
+ * Two modes, on purpose. With no `locale` and no `currency` this is the original
28
+ * locale-free output: the compact `1.2k` / `1.2M` base, currency prefixed with
29
+ * `$` (sign outside), percent multiplied by 100 and suffixed `%`. Set either one
30
+ * and it switches to `Intl.NumberFormat`, so separators, the decimal mark and the
31
+ * compact suffixes all follow the locale.
32
+ *
33
+ * Keeping the old path as the default is deliberate rather than lazy. `Intl`'s
34
+ * compact notation is not the same string even for `en-US` (`1.2K`, capital),
35
+ * so formatting everything through it would silently restyle every axis in every
36
+ * existing chart. Opting in is the only version of this that is not a surprise.
37
+ */
38
+ export function formatChartValue(n, format, opts) {
39
+ if (!Number.isFinite(n))
40
+ return '';
41
+ const localized = opts && (opts.locale || opts.currency);
42
+ if (localized) {
43
+ // Compact notation because these are axis ticks and data labels, where a
44
+ // full-precision number is what makes an axis unreadable.
45
+ const style = format === 'currency' ? 'currency' : format === 'percent' ? 'percent' : 'decimal';
46
+ const o = { notation: 'compact', maximumFractionDigits: 1 };
47
+ if (style === 'currency') {
48
+ o.style = 'currency';
49
+ // `style: 'currency'` throws without a code, so fall back to the symbol
50
+ // this used to hard-code rather than refusing to draw the chart.
51
+ o.currency = opts.currency || 'USD';
52
+ }
53
+ else if (style === 'percent') {
54
+ o.style = 'percent';
55
+ }
56
+ return getNumberFormatter(opts.locale, o).format(n);
57
+ }
58
+ if (format === 'currency')
59
+ return `${n < 0 ? '-' : ''}$${fmtTick(Math.abs(n))}`;
60
+ if (format === 'percent') {
61
+ const p = n * 100;
62
+ return `${Math.round(p * 10) / 10}%`;
63
+ }
64
+ return fmtTick(n);
65
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Grid layouts: heatmap and the calendar heatmap.
3
+ */
4
+ import type { ChartGeometry, LayoutCtx } from './chart-types.js';
5
+ /** @internal Lay out the calendar family. Called by buildChart. */
6
+ export declare function layoutCalendar(ctx: LayoutCtx): ChartGeometry;
7
+ /** @internal Lay out the heatmap family. Called by buildChart. */
8
+ export declare function layoutHeatmap(ctx: LayoutCtx): ChartGeometry;