@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,342 @@
1
+ /**
2
+ * Axis resolution shared by every cartesian family: which mark a series draws, the value domain of an axis, and the scale functions a custom mark uses.
3
+ */
4
+ import type {
5
+ ChartAxisConfig,
6
+ ChartFrame,
7
+ ChartGeometry,
8
+ ChartScales,
9
+ ChartSeries,
10
+ ChartSpec,
11
+ ChartType,
12
+ NiceScale,
13
+ ResolvedAxes,
14
+ ResolvedAxis,
15
+ ResolvedSeries,
16
+ } from './chart-types'
17
+ import { axisScale, niceLogScale, project, errorSpan } from './chart-scale'
18
+
19
+ /**
20
+ * @internal Which mark a series draws.
21
+ *
22
+ * A series counts as a candle when it says so OR when it carries `ohlc` data,
23
+ * so a spec typed `'candlestick'` can still hold a plain volume bar series
24
+ * beside the prices. Boxes work the same way. Anything unrecognised falls back
25
+ * to `'bar'`, which is what pie and scatter have always relied on.
26
+ */
27
+ export function kindOf(s: ChartSeries, specType: ChartType): ResolvedSeries['kind'] {
28
+ const t = s.type ?? specType
29
+ if (t === 'candlestick' || t === 'ohlc' || s.ohlc) return 'candle'
30
+ if (t === 'boxplot' || s.boxes) return 'box'
31
+ if (t === 'line' || t === 'area') return t
32
+ // A stream is a stacked area on a wiggle baseline; the offset is the spec's.
33
+ if (t === 'stream') return 'area'
34
+ if (t === 'range-bar' || t === 'range-area' || t === 'lollipop' || t === 'dumbbell') return t
35
+ // A per-series scatter on a category axis: markers with no line.
36
+ if (t === 'scatter' && s.type === 'scatter') return 'scatter'
37
+ return 'bar'
38
+ }
39
+
40
+
41
+ /** @internal Data domain for one axis, honoring stacking of its bar/area series.
42
+ * When `isLog` is true, non-positive values are discarded (log undefined)
43
+ * and the domain is rounded to decade boundaries instead of nice steps. */
44
+ export function axisDomain(
45
+ list: ResolvedSeries[],
46
+ categories: string[],
47
+ stacked: boolean,
48
+ extra: number[] = [],
49
+ isLog = false,
50
+ axis: ChartAxisConfig = {},
51
+ ): NiceScale {
52
+ let dMin = Infinity
53
+ let dMax = -Infinity
54
+ const note = (v: number) => {
55
+ if (!Number.isFinite(v)) return
56
+ if (isLog && v <= 0) return
57
+ if (v < dMin) dMin = v
58
+ if (v > dMax) dMax = v
59
+ }
60
+ for (const v of extra) note(v)
61
+ const stackable = list.filter((s) => s.kind === 'bar' || s.kind === 'area')
62
+ const lines = list.filter((s) => s.kind === 'line' || s.kind === 'scatter' || s.kind === 'lollipop')
63
+ // Range kinds have no zero baseline: a floating bar from 40 to 60 wants an
64
+ // axis that starts near 40, not at 0, so they are noted but never stacked.
65
+ for (const s of list) {
66
+ if (s.kind !== 'range-bar' && s.kind !== 'range-area' && s.kind !== 'dumbbell') continue
67
+ for (const v of s.values) note(v)
68
+ for (const v of s.lowValues ?? []) note(v)
69
+ }
70
+ // Totals per stack. Everything shares one stack when `stacked`; otherwise a
71
+ // bar or area series with a `stack` name joins that stack and every other
72
+ // series stands alone, so a lone series contributes its own values. Bars
73
+ // and areas never share a pile even under one name: they are drawn apart.
74
+ const stacks = new Map<string, ResolvedSeries[]>()
75
+ stackable.forEach((s, k) => {
76
+ const key = s.stack ? `${s.kind}:${s.stack}` : stacked ? s.kind : `#${k}`
77
+ const list = stacks.get(key)
78
+ if (list) list.push(s)
79
+ else stacks.set(key, [s])
80
+ })
81
+ for (const members of stacks.values()) {
82
+ if (members.length === 1) {
83
+ for (const v of members[0]!.values) note(v)
84
+ continue
85
+ }
86
+ for (let i = 0; i < categories.length; i += 1) {
87
+ let pos = 0
88
+ let neg = 0
89
+ for (const s of members) {
90
+ const v = s.values[i] ?? 0
91
+ if (v >= 0) pos += v
92
+ else neg += v
93
+ }
94
+ note(pos)
95
+ note(neg)
96
+ }
97
+ }
98
+ for (const s of lines) for (const v of s.values) note(v)
99
+ // Candles: note the HIGH and the LOW, not `values` (the closes), or every
100
+ // wick clips at the body. Note also that candles are deliberately absent
101
+ // from `stackable`, so the zero-baseline rule below does not fire for them:
102
+ // a price series running 180 to 195 keeps a readable domain instead of being
103
+ // flattened against zero.
104
+ for (const s of list) {
105
+ if (s.kind !== 'candle') continue
106
+ for (const k of s.ohlc ?? []) {
107
+ if (!k) continue
108
+ note(k.h)
109
+ note(k.l)
110
+ }
111
+ }
112
+ // Boxes: the whisker ends and any outlier, for the same reason - `values`
113
+ // holds the medians, so a domain built from those alone would clip half of
114
+ // every box. Boxes are also absent from `stackable`, so a sample that never
115
+ // goes near zero keeps a readable domain.
116
+ for (const s of list) {
117
+ if (s.kind !== 'box') continue
118
+ for (const b of s.boxes ?? []) {
119
+ if (!b) continue
120
+ note(b.min)
121
+ note(b.max)
122
+ for (const o of b.outliers ?? []) note(o)
123
+ }
124
+ }
125
+ // Error bars extend past their own mark, so a whisker that leaves the plot is
126
+ // the same defect as a clipped candle wick.
127
+ for (const s of list) {
128
+ if (!s.errors) continue
129
+ s.errors.forEach((e, i) => {
130
+ const span = errorSpan(e, s.values[i] ?? 0)
131
+ if (!span) return
132
+ note(span.lo)
133
+ note(span.hi)
134
+ })
135
+ }
136
+ if (dMin === Infinity) {
137
+ dMin = isLog ? 1 : 0
138
+ dMax = isLog ? 10 : 1
139
+ }
140
+ // Bar / area charts read against a zero baseline, so always include 0
141
+ // - but only on linear axes (0 is invalid in log). Lollipops grow from the
142
+ // axis the way bars do.
143
+ if ((stackable.length || list.some((s) => s.kind === 'lollipop')) && !isLog) {
144
+ dMin = Math.min(dMin, 0)
145
+ dMax = Math.max(dMax, 0)
146
+ }
147
+ if (isLog) {
148
+ const ls = niceLogScale(dMin, dMax)
149
+ // A pinned end on a log axis has to be positive to mean anything.
150
+ const lo = axis.min != null && axis.min > 0 ? axis.min : ls.min
151
+ const hi = axis.max != null && axis.max > lo ? axis.max : ls.max
152
+ if (lo === ls.min && hi === ls.max) return ls
153
+ return { min: lo, max: hi, step: ls.step, ticks: ls.ticks.filter((t) => t >= lo && t <= hi) }
154
+ }
155
+ return axisScale(dMin, dMax, axis)
156
+ }
157
+
158
+ /**
159
+ * Turn a laid-out chart's axes into functions, so a caller can draw its own
160
+ * marks in the same coordinates the built-in ones use.
161
+ *
162
+ * This is the custom-series seam. Rather than a registry of mark types, the
163
+ * chart hands over its geometry and its scales and lets the caller render
164
+ * whatever SVG it likes into the plot - which is the Svelte-shaped answer, and
165
+ * means a custom mark is ordinary markup rather than a plugin.
166
+ *
167
+ * Deriving the scale from `geo.axes` matters: the domain a chart drew against
168
+ * is the NICE-ROUNDED one, stretched to include zero for bar charts and any
169
+ * reference lines. Recomputing it from the data outside would land custom marks
170
+ * a few pixels off the built-in ones, in a way that looks like a rendering bug.
171
+ *
172
+ * Returns `null` for a chart with no cartesian axes (pie, gauge, treemap,
173
+ * sankey, calendar, radar, funnel), where plot coordinates mean nothing.
174
+ */
175
+ export function chartScales(geo: ChartGeometry): ChartScales | null {
176
+ const a = geo.axes
177
+ if (!a) return null
178
+ const { x: px, y: py, w: pw, h: ph } = geo.plot
179
+ type Dom = { min: number; max: number; log: boolean; reversed?: boolean }
180
+ const yFor = (dom: Dom) => (value: number) => {
181
+ let t = project(value, dom.min, dom.max, dom.log)
182
+ if (t === null) return Number.NaN
183
+ if (dom.reversed) t = 1 - t
184
+ return py + ph - t * ph
185
+ }
186
+ const left = yFor(a.y)
187
+ const right = a.y2 ? yFor(a.y2) : left
188
+ const invFor = (dom: Dom) => (y: number) => {
189
+ let t = ph === 0 ? 0 : (py + ph - y) / ph
190
+ if (dom.reversed) t = 1 - t
191
+ if (!dom.log) return dom.min + t * (dom.max - dom.min)
192
+ const lo = Math.log10(dom.min)
193
+ return 10 ** (lo + t * (Math.log10(dom.max) - lo))
194
+ }
195
+ // A continuous x axis (time / number) places by value; a category axis by
196
+ // index. `xOf(i)` on a continuous axis goes through the stored per-category
197
+ // values, so a custom mark at "category 3" lands where the built-in one did
198
+ // even when the categories are unevenly spaced in time.
199
+ const cx = a.x
200
+ const span = cx ? cx.max - cx.min || 1 : 1
201
+ const logSpan = cx?.log ? Math.log10(cx.max) - Math.log10(cx.min) || 1 : 1
202
+ const xOfNum = (v: number) => {
203
+ if (!cx || !Number.isFinite(v)) return Number.NaN
204
+ if (cx.log && v <= 0) return Number.NaN
205
+ let t = cx.log ? (Math.log10(v) - Math.log10(cx.min)) / logSpan : (v - cx.min) / span
206
+ if (cx.reversed) t = 1 - t
207
+ return px + t * pw
208
+ }
209
+ const xOfIndex = (i: number) => {
210
+ if (cx?.values) {
211
+ const v = cx.values[Math.round(i)]
212
+ if (v != null && Number.isFinite(v)) return xOfNum(v)
213
+ }
214
+ const t = px + a.slot * i + a.slot / 2
215
+ return a.xReversed ? px + pw - (t - px) : t
216
+ }
217
+ const xOfValue = (value: number | string | Date): number => {
218
+ if (cx) {
219
+ if (value instanceof Date) return xOfNum(value.getTime())
220
+ if (typeof value === 'number') return xOfNum(value)
221
+ const n = cx.type === 'time' ? Date.parse(value) : Number(value)
222
+ if (Number.isFinite(n)) return xOfNum(n)
223
+ // A label that did not parse can still be one of the categories.
224
+ const i = geo.xTicks.findIndex((t) => t.label === value)
225
+ return i >= 0 ? geo.xTicks[i]!.x : Number.NaN
226
+ }
227
+ if (typeof value === 'number') return xOfIndex(value)
228
+ const label = value instanceof Date ? value.toISOString() : value
229
+ const i = a.labels ? a.labels.indexOf(label) : -1
230
+ return i >= 0 ? xOfIndex(i) : Number.NaN
231
+ }
232
+ const xInvertValue = (x: number): number => {
233
+ let t = pw === 0 ? 0 : (x - px) / pw
234
+ if (cx ? cx.reversed : a.xReversed) t = 1 - t
235
+ if (cx?.log) return Math.pow(10, Math.log10(cx.min) + t * logSpan)
236
+ if (cx) return cx.min + t * span
237
+ return Math.max(0, Math.min(a.count - 1, t * a.count - 0.5))
238
+ }
239
+ return {
240
+ xOf: xOfIndex,
241
+ yOf: (value, axis) => (axis === 'right' ? right(value) : left(value)),
242
+ xInvert: (x) => {
243
+ if (cx?.values) {
244
+ // Nearest category by value, which is what a hover on a time axis means.
245
+ const v = xInvertValue(x)
246
+ let best = 0
247
+ let bestD = Infinity
248
+ cx.values.forEach((cv, i) => {
249
+ const d = Math.abs(cv - v)
250
+ if (d < bestD) { bestD = d; best = i }
251
+ })
252
+ return best
253
+ }
254
+ const raw = Math.floor((x - px) / (a.slot || 1))
255
+ const i = a.xReversed ? a.count - 1 - raw : raw
256
+ return Math.max(0, Math.min(a.count - 1, i))
257
+ },
258
+ yInvert: (y, axis) => (axis === 'right' && a.y2 ? invFor(a.y2)(y) : invFor(a.y)(y)),
259
+ xOfValue,
260
+ xInvertValue,
261
+ }
262
+ }
263
+
264
+ /**
265
+ * @internal Fold the flat spec shortcuts (`xType`, `yScale`, `xAxisTitle`, ...)
266
+ * into three axis objects with every default filled in, so a layout reads one
267
+ * shape and never has to know which spelling the caller used.
268
+ */
269
+ export function resolveAxes(spec: ChartSpec): ResolvedAxes {
270
+ const x = spec.xAxis ?? {}
271
+ const y = spec.yAxis ?? {}
272
+ const y2 = spec.y2Axis ?? {}
273
+ const fill = (
274
+ a: ChartAxisConfig,
275
+ defaults: { type: ResolvedAxis['type']; scale: ResolvedAxis['scale']; title?: string; gridLines: boolean },
276
+ ): ResolvedAxis => ({
277
+ ...a,
278
+ type: a.type ?? defaults.type,
279
+ scale: a.scale ?? defaults.scale,
280
+ title: a.title ?? defaults.title,
281
+ gridLines: a.gridLines ?? defaults.gridLines,
282
+ labels: a.labels !== false,
283
+ nice: a.nice !== false,
284
+ reversed: a.reversed === true,
285
+ })
286
+ return {
287
+ x: fill(x, { type: spec.xType ?? 'category', scale: 'linear', title: spec.xAxisTitle, gridLines: false }),
288
+ y: fill(y, { type: 'category', scale: spec.yScale ?? 'linear', title: spec.yAxisTitle, gridLines: true }),
289
+ y2: fill(y2, { type: 'category', scale: spec.y2Scale ?? 'linear', title: spec.y2AxisTitle, gridLines: false }),
290
+ }
291
+ }
292
+
293
+ /** Vertical room the frame text takes, per line. */
294
+ const TITLE_H = 22
295
+ const SUBTITLE_H = 16
296
+ const CAPTION_H = 16
297
+
298
+ /**
299
+ * @internal Place the title, subtitle and caption and measure the room they
300
+ * take. The title and subtitle sit centred above the plot; the caption sits
301
+ * left-aligned under it. A family adds `top` to its top pad and `bottom` to its
302
+ * bottom pad, and the text positions are already absolute.
303
+ */
304
+ export function chartFrame(spec: ChartSpec, width: number, height: number): ChartFrame {
305
+ const title = (spec.title ?? '').trim()
306
+ const subtitle = (spec.subtitle ?? '').trim()
307
+ const caption = (spec.caption ?? '').trim()
308
+ let top = 0
309
+ let titlePos: ChartFrame['title'] = null
310
+ let subtitlePos: ChartFrame['subtitle'] = null
311
+ if (title) {
312
+ top += TITLE_H
313
+ titlePos = { x: width / 2, y: top - 6, text: title }
314
+ }
315
+ if (subtitle) {
316
+ top += SUBTITLE_H
317
+ subtitlePos = { x: width / 2, y: top - 4, text: subtitle }
318
+ }
319
+ if (top) top += 4
320
+ let bottom = 0
321
+ let captionPos: ChartFrame['caption'] = null
322
+ if (caption) {
323
+ bottom = CAPTION_H
324
+ captionPos = { x: 8, y: height - 5, text: caption }
325
+ }
326
+ return { top, bottom, title: titlePos, subtitle: subtitlePos, caption: captionPos }
327
+ }
328
+
329
+ /**
330
+ * @internal Format one value-axis tick through the axis' own `formatter` or
331
+ * `format`, falling back to the spec's `valueFormat`.
332
+ */
333
+ export function axisTickLabel(
334
+ value: number,
335
+ index: number,
336
+ axis: ChartAxisConfig,
337
+ spec: ChartSpec,
338
+ fmt: (n: number, format: ChartSpec['valueFormat'], spec: ChartSpec) => string,
339
+ ): string {
340
+ if (axis.formatter) return axis.formatter(value, index)
341
+ return fmt(value, axis.format ?? spec.valueFormat, spec)
342
+ }