@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.
- package/README.md +2 -1
- package/dist/GridMenus.svelte +44 -2
- package/dist/SvChartMenu.svelte +69 -0
- package/dist/SvChartMenu.svelte.d.ts +11 -0
- package/dist/SvChartPanes.svelte +113 -0
- package/dist/SvChartPanes.svelte.d.ts +20 -0
- package/dist/SvGrid.controller.svelte.d.ts +110 -39
- package/dist/SvGrid.controller.svelte.js +655 -200
- package/dist/SvGrid.css +105 -2
- package/dist/SvGrid.svelte +386 -514
- package/dist/SvGrid.types.d.ts +210 -9
- package/dist/SvGridCellEditor.svelte +17 -0
- package/dist/SvGridChart.svelte +2257 -329
- package/dist/SvGridChart.svelte.d.ts +6 -106
- package/dist/SvGridChart.types.d.ts +338 -0
- package/dist/SvGridChart.types.js +1 -0
- package/dist/SvGridChartBuilder.svelte +323 -0
- package/dist/SvGridChartBuilder.svelte.d.ts +34 -0
- package/dist/SvGridChartPanel.svelte +221 -275
- package/dist/SvGridChartPickers.svelte +417 -0
- package/dist/SvGridChartPickers.svelte.d.ts +70 -0
- package/dist/SvGridChartView.svelte +13 -14
- package/dist/SvModal.svelte +7 -1
- package/dist/SvModal.svelte.d.ts +5 -0
- package/dist/ai.d.ts +40 -8
- package/dist/ai.js +135 -24
- package/dist/aria.d.ts +11 -0
- package/dist/build-api.js +108 -22
- package/dist/cdn/GridMenus-7kbpnnBW.js +635 -0
- package/dist/cdn/GridMenus-BL7ZgQvU.js +636 -0
- package/dist/cdn/SvChartMenu-Bl6PBbkT.js +58 -0
- package/dist/cdn/SvChartMenu-FBSMINA6.js +59 -0
- package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-BeU_TkbQ.js} +15 -14
- package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-yX8vzleW.js} +38 -37
- package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-DQwt4UAs.js} +139 -180
- package/dist/cdn/SvDateTimePicker-vNU6bZ-q.js +775 -0
- package/dist/cdn/{SvGridCellEditor-CjEJMJKc.js → SvGridCellEditor-B1p-vCK5.js} +188 -178
- package/dist/cdn/{SvGridCellEditor-BWnTi2N7.js → SvGridCellEditor-D_0q4xAu.js} +98 -88
- package/dist/cdn/SvGridChart-C3EWAZaM.js +3574 -0
- package/dist/cdn/SvGridChart-DzLkSwCH.js +3572 -0
- package/dist/cdn/SvGridChartBuilder-BE2T1ykB.js +783 -0
- package/dist/cdn/SvGridChartBuilder-CfII62sZ.js +784 -0
- package/dist/cdn/SvGridChartPanel-CCX5_Wgd.js +675 -0
- package/dist/cdn/SvGridChartPanel-xf4sWVTo.js +699 -0
- package/dist/cdn/SvGridChartView-CfuXmY5I.js +48 -0
- package/dist/cdn/SvGridChartView-DYwabQWj.js +47 -0
- package/dist/cdn/SvMenuList-CLN8OuIK.js +452 -0
- package/dist/cdn/SvMenuList-DkGhNKjf.js +451 -0
- package/dist/cdn/SvModal-BAE-pjZX.js +396 -0
- package/dist/cdn/SvModal-CIZWCcad.js +397 -0
- package/dist/cdn/chart-Dz7SqMXH.js +4341 -0
- package/dist/cdn/chart-export-BXqueeWJ.js +135 -0
- package/dist/cdn/chart-export-pdf-DONFUnNY.js +110 -0
- package/dist/cdn/chart-panel-messages-CUbf2R4i.js +1052 -0
- package/dist/cdn/chart-panel-messages-CmNrMdsr.js +1053 -0
- package/dist/cdn/chart-summary-BJW_tg_X.js +138 -0
- package/dist/cdn/chart-trend-CaN9mDEV.js +50 -0
- package/dist/cdn/chart-validate-HcOVFAfJ.js +188 -0
- package/dist/cdn/{column-resize-DsfNXMom.js → column-resize-DpmOLfRp.js} +3 -3
- package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CtquV-p3.js} +583 -603
- package/dist/cdn/{date-format-BNii4zeD.js → date-format-D6KFzU_W.js} +347 -367
- package/dist/cdn/dismissable-DAHetSNk.js +44 -0
- package/dist/cdn/editor-contract-LXgQJAgd.js +22 -0
- package/dist/cdn/focus-trap-BBOQYBma.js +65 -0
- package/dist/cdn/{row-resize-BRcimkUT.js → row-resize-niQCp040.js} +44 -36
- package/dist/cdn/{src-DNbz94Ct.js → src-BKhZ6eXd.js} +10227 -9787
- package/dist/cdn/{src-BJ2opd0j.js → src-V1uu8iE9.js} +6568 -6128
- package/dist/cdn/svgrid.js +26 -15
- package/dist/cdn/svgrid.svelte-external.js +26 -15
- package/dist/chart-axes.d.ts +53 -0
- package/dist/chart-axes.js +351 -0
- package/dist/chart-cartesian.d.ts +88 -0
- package/dist/chart-cartesian.js +1862 -0
- package/dist/chart-decimate.d.ts +51 -0
- package/dist/chart-decimate.js +199 -0
- package/dist/chart-export-pdf.d.ts +47 -0
- package/dist/chart-export-pdf.js +187 -0
- package/dist/chart-export.d.ts +2 -1
- package/dist/chart-export.js +18 -3
- package/dist/chart-financial.d.ts +91 -0
- package/dist/chart-financial.js +175 -0
- package/dist/chart-flow.d.ts +10 -0
- package/dist/chart-flow.js +298 -0
- package/dist/chart-format.d.ts +18 -0
- package/dist/chart-format.js +65 -0
- package/dist/chart-grid.d.ts +8 -0
- package/dist/chart-grid.js +190 -0
- package/dist/chart-hierarchy.d.ts +17 -0
- package/dist/chart-hierarchy.js +146 -0
- package/dist/chart-indicators.d.ts +115 -0
- package/dist/chart-indicators.js +389 -0
- package/dist/chart-messages.d.ts +95 -0
- package/dist/chart-messages.js +92 -0
- package/dist/chart-motion.d.ts +16 -0
- package/dist/chart-motion.js +141 -0
- package/dist/chart-panel-messages.d.ts +265 -0
- package/dist/chart-panel-messages.js +285 -0
- package/dist/chart-pivot.d.ts +87 -0
- package/dist/chart-pivot.js +156 -0
- package/dist/chart-polar.d.ts +25 -0
- package/dist/chart-polar.js +700 -0
- package/dist/chart-samples.d.ts +16 -0
- package/dist/chart-samples.js +153 -0
- package/dist/chart-scale.d.ts +113 -0
- package/dist/chart-scale.js +405 -0
- package/dist/chart-stats.d.ts +111 -0
- package/dist/chart-stats.js +419 -0
- package/dist/chart-stream.d.ts +45 -0
- package/dist/chart-stream.js +91 -0
- package/dist/chart-summary.d.ts +27 -0
- package/dist/chart-summary.js +205 -0
- package/dist/chart-sync.svelte.d.ts +32 -0
- package/dist/chart-sync.svelte.js +23 -0
- package/dist/chart-table.d.ts +35 -0
- package/dist/chart-table.js +165 -0
- package/dist/chart-trend.d.ts +19 -0
- package/dist/chart-trend.js +61 -0
- package/dist/chart-types.d.ts +1456 -0
- package/dist/chart-types.js +6 -0
- package/dist/chart-validate.d.ts +43 -0
- package/dist/chart-validate.js +272 -0
- package/dist/chart-zoom.d.ts +40 -0
- package/dist/chart-zoom.js +145 -0
- package/dist/chart.d.ts +186 -865
- package/dist/chart.js +460 -2204
- package/dist/clipboard.d.ts +1 -0
- package/dist/clipboard.js +137 -18
- package/dist/column-resize.d.ts +3 -0
- package/dist/column-resize.js +4 -1
- package/dist/columns.js +3 -0
- package/dist/command-context.d.ts +19 -0
- package/dist/command-context.js +116 -0
- package/dist/core.d.ts +7 -0
- package/dist/createPopoverSelect.svelte.d.ts +2 -2
- package/dist/editing.d.ts +2 -8
- package/dist/editing.js +262 -33
- package/dist/fill-patterns.d.ts +0 -5
- package/dist/fill-patterns.js +56 -2
- package/dist/grid-messages.d.ts +10 -3
- package/dist/grid-messages.js +5 -0
- package/dist/history.d.ts +83 -0
- package/dist/history.js +133 -0
- package/dist/index.d.ts +24 -6
- package/dist/index.js +20 -2
- package/dist/keyboard-handlers.js +88 -31
- package/dist/keyboard.d.ts +55 -0
- package/dist/keyboard.js +132 -0
- package/dist/menus.d.ts +3 -1
- package/dist/menus.js +7 -2
- package/dist/merges.d.ts +97 -0
- package/dist/merges.js +147 -0
- package/dist/row-resize.d.ts +3 -0
- package/dist/row-resize.js +14 -0
- package/dist/scroll-sync.d.ts +1 -0
- package/dist/scroll-sync.js +24 -0
- package/dist/selection.d.ts +16 -0
- package/dist/selection.js +108 -16
- package/dist/shortcut-registry.d.ts +145 -0
- package/dist/shortcut-registry.js +44 -0
- package/dist/svgrid-wrapper.types.d.ts +213 -22
- package/dist/virtualization/virtualizer.js +38 -17
- package/package.json +6 -1
- package/src/GridMenus.svelte +44 -2
- package/src/SvChartMenu.svelte +69 -0
- package/src/SvChartPanes.svelte +113 -0
- package/src/SvChartPanes.test.ts +66 -0
- package/src/SvGrid.controller.svelte.ts +629 -172
- package/src/SvGrid.css +105 -2
- package/src/SvGrid.svelte +386 -514
- package/src/SvGrid.types.ts +188 -8
- package/src/SvGridCellEditor.svelte +17 -0
- package/src/SvGridChart.svelte +2257 -329
- package/src/SvGridChart.test.ts +1419 -1
- package/src/SvGridChart.types.ts +336 -0
- package/src/SvGridChartBuilder.svelte +323 -0
- package/src/SvGridChartPanel.svelte +221 -275
- package/src/SvGridChartPickers.svelte +417 -0
- package/src/SvGridChartView.svelte +13 -14
- package/src/SvModal.svelte +7 -1
- package/src/SvModal.test.ts +9 -0
- package/src/ai.test.ts +57 -2
- package/src/ai.ts +166 -22
- package/src/aria.d.ts +11 -0
- package/src/build-api.coverage.test.ts +184 -0
- package/src/build-api.ts +107 -18
- package/src/chart-axes.test.ts +573 -0
- package/src/chart-axes.ts +342 -0
- package/src/chart-cartesian.ts +1863 -0
- package/src/chart-decimate.test.ts +249 -0
- package/src/chart-decimate.ts +186 -0
- package/src/chart-export-pdf.test.ts +91 -0
- package/src/chart-export-pdf.ts +207 -0
- package/src/chart-export.test.ts +14 -0
- package/src/chart-export.ts +13 -3
- package/src/chart-financial.ts +208 -0
- package/src/chart-flow.ts +286 -0
- package/src/chart-format.ts +67 -0
- package/src/chart-grid.ts +179 -0
- package/src/chart-hierarchy.ts +135 -0
- package/src/chart-indicators.test.ts +443 -0
- package/src/chart-indicators.ts +384 -0
- package/src/chart-messages.test.ts +21 -0
- package/src/chart-messages.ts +184 -0
- package/src/chart-motion.test.ts +71 -0
- package/src/chart-motion.ts +148 -0
- package/src/chart-panel-messages.test.ts +29 -0
- package/src/chart-panel-messages.ts +550 -0
- package/src/chart-pivot.test.ts +54 -0
- package/src/chart-pivot.ts +225 -0
- package/src/chart-polar.ts +674 -0
- package/src/chart-samples.ts +106 -0
- package/src/chart-scale.ts +394 -0
- package/src/chart-series-types.test.ts +714 -0
- package/src/chart-stats.ts +409 -0
- package/src/chart-stream.test.ts +75 -0
- package/src/chart-stream.ts +114 -0
- package/src/chart-summary.test.ts +90 -0
- package/src/chart-summary.ts +197 -0
- package/src/chart-sync.svelte.ts +41 -0
- package/src/chart-table.test.ts +76 -0
- package/src/chart-table.ts +178 -0
- package/src/chart-trend.ts +47 -0
- package/src/chart-types.ts +1309 -0
- package/src/chart-validate.test.ts +100 -0
- package/src/chart-validate.ts +247 -0
- package/src/chart-zoom.test.ts +137 -0
- package/src/chart-zoom.ts +124 -0
- package/src/chart.coverage.test.ts +81 -0
- package/src/chart.test.ts +56 -3
- package/src/chart.ts +557 -2784
- package/src/clipboard.test.ts +151 -0
- package/src/clipboard.ts +145 -16
- package/src/column-resize.ts +7 -1
- package/src/columns.ts +2 -0
- package/src/command-context.test.ts +200 -0
- package/src/command-context.ts +137 -0
- package/src/core.ts +7 -0
- package/src/editing.test.ts +281 -3
- package/src/editing.ts +256 -38
- package/src/fill-patterns.test.ts +35 -0
- package/src/fill-patterns.ts +56 -2
- package/src/grid-messages.ts +16 -3
- package/src/history.test.ts +196 -0
- package/src/history.ts +162 -0
- package/src/icon-seam.test.ts +3 -2
- package/src/index.ts +150 -2
- package/src/keyboard-handlers.coverage.test.ts +126 -6
- package/src/keyboard-handlers.ts +91 -30
- package/src/keyboard-shortcuts.seam.test.ts +200 -0
- package/src/keyboard.test.ts +124 -1
- package/src/keyboard.ts +142 -0
- package/src/menus.test.ts +26 -0
- package/src/menus.ts +10 -3
- package/src/merges.test.ts +112 -0
- package/src/merges.ts +168 -0
- package/src/pivot.grid.test.ts +40 -5
- package/src/row-resize.test.ts +44 -0
- package/src/row-resize.ts +17 -0
- package/src/scroll-sync.test.ts +42 -1
- package/src/scroll-sync.ts +25 -0
- package/src/selection.multi-range.test.ts +34 -0
- package/src/selection.test.ts +30 -2
- package/src/selection.ts +111 -19
- package/src/shortcut-registry.test.ts +97 -0
- package/src/shortcut-registry.ts +181 -0
- package/src/svgrid-wrapper.types.ts +184 -13
- package/src/svgrid.behavior.test.ts +135 -0
- package/src/svgrid.charting.test.ts +513 -3
- package/src/svgrid.context-menu.test.ts +31 -0
- package/src/svgrid.new-features.wrapper.test.ts +10 -3
- package/src/virtualization/virtualizer.ts +39 -17
- package/dist/cdn/GridMenus-DyFA-QxA.js +0 -602
- package/dist/cdn/GridMenus-M2XP9t0_.js +0 -603
- package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
- package/dist/cdn/SvGridChart-DAb9RbRs.js +0 -1480
- package/dist/cdn/SvGridChart-Yl-IXZFO.js +0 -1481
- package/dist/cdn/SvGridChartPanel-DayQpDsR.js +0 -834
- package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +0 -810
- package/dist/cdn/SvGridChartView-CxfakxR0.js +0 -56
- package/dist/cdn/SvGridChartView-Dm_YEx9L.js +0 -55
- 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
|
+
}
|