@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,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;
|