@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,419 @@
|
|
|
1
|
+
// The two trend primitives live in their own leaf (see chart-trend.ts for
|
|
2
|
+
// why) and are re-exported here so every importer of chart-stats keeps working.
|
|
3
|
+
export { linearTrend, pearson } from './chart-trend.js';
|
|
4
|
+
/**
|
|
5
|
+
* Five-number summary of a raw sample, with the 1.5 IQR whisker rule.
|
|
6
|
+
*
|
|
7
|
+
* Whiskers stop at the last observation INSIDE the fence rather than at the
|
|
8
|
+
* fence itself, which is what makes them read as real data; anything past them
|
|
9
|
+
* comes back in `outliers`. Quartiles use linear interpolation between the two
|
|
10
|
+
* neighbouring order statistics.
|
|
11
|
+
*
|
|
12
|
+
* Returns `null` for an empty sample, so a category with no observations is a
|
|
13
|
+
* gap rather than a box drawn at zero.
|
|
14
|
+
*/
|
|
15
|
+
export function boxStats(sample, whisker = 1.5) {
|
|
16
|
+
const v = sample.filter((n) => Number.isFinite(n)).slice().sort((a, b) => a - b);
|
|
17
|
+
if (!v.length)
|
|
18
|
+
return null;
|
|
19
|
+
const q = (p) => {
|
|
20
|
+
const pos = (v.length - 1) * p;
|
|
21
|
+
const lo = Math.floor(pos);
|
|
22
|
+
const hi = Math.ceil(pos);
|
|
23
|
+
return lo === hi ? v[lo] : v[lo] + (v[hi] - v[lo]) * (pos - lo);
|
|
24
|
+
};
|
|
25
|
+
const q1 = q(0.25);
|
|
26
|
+
const median = q(0.5);
|
|
27
|
+
const q3 = q(0.75);
|
|
28
|
+
const fenceLo = q1 - whisker * (q3 - q1);
|
|
29
|
+
const fenceHi = q3 + whisker * (q3 - q1);
|
|
30
|
+
const inside = v.filter((n) => n >= fenceLo && n <= fenceHi);
|
|
31
|
+
const outliers = v.filter((n) => n < fenceLo || n > fenceHi);
|
|
32
|
+
return {
|
|
33
|
+
// `inside` can only be empty if every point is an outlier, which the fence
|
|
34
|
+
// rule makes impossible (q1 and q3 are always within it) - but a degenerate
|
|
35
|
+
// sample should still produce a box rather than `undefined` coordinates.
|
|
36
|
+
min: inside.length ? inside[0] : v[0],
|
|
37
|
+
q1,
|
|
38
|
+
median,
|
|
39
|
+
q3,
|
|
40
|
+
max: inside.length ? inside[inside.length - 1] : v[v.length - 1],
|
|
41
|
+
...(outliers.length ? { outliers } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const NO_FIT = (values) => ({ fitted: values.map(() => NaN), r2: 0, equation: '', predict: () => NaN });
|
|
45
|
+
/** The x of each value: the caller's, else the index. */
|
|
46
|
+
const xsOf = (values, xs) => (xs ? values.map((_, i) => xs[i] ?? NaN) : values.map((_, i) => i));
|
|
47
|
+
/** R-squared of a fit: 1 minus residual over total variance across the
|
|
48
|
+
* indices where both the value and the fit are finite. 0 when there is no
|
|
49
|
+
* variance to explain. */
|
|
50
|
+
export function rSquared(values, fitted) {
|
|
51
|
+
let n = 0;
|
|
52
|
+
let mean = 0;
|
|
53
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
54
|
+
if (Number.isFinite(values[i]) && Number.isFinite(fitted[i])) {
|
|
55
|
+
n += 1;
|
|
56
|
+
mean += values[i];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (n < 2)
|
|
60
|
+
return 0;
|
|
61
|
+
mean /= n;
|
|
62
|
+
let ssRes = 0;
|
|
63
|
+
let ssTot = 0;
|
|
64
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
65
|
+
if (!Number.isFinite(values[i]) || !Number.isFinite(fitted[i]))
|
|
66
|
+
continue;
|
|
67
|
+
ssRes += (values[i] - fitted[i]) ** 2;
|
|
68
|
+
ssTot += (values[i] - mean) ** 2;
|
|
69
|
+
}
|
|
70
|
+
if (ssTot === 0)
|
|
71
|
+
return ssRes === 0 ? 1 : 0;
|
|
72
|
+
return Math.max(0, Math.min(1, 1 - ssRes / ssTot));
|
|
73
|
+
}
|
|
74
|
+
const fmtCoef = (c) => (Math.abs(c) >= 1000 || (Math.abs(c) < 0.001 && c !== 0) ? c.toExponential(2) : String(Math.round(c * 1000) / 1000));
|
|
75
|
+
/** Solve a small linear system by Gaussian elimination with partial pivoting;
|
|
76
|
+
* null when singular. */
|
|
77
|
+
function solve(a, b) {
|
|
78
|
+
const n = b.length;
|
|
79
|
+
const m = a.map((row, i) => [...row, b[i]]);
|
|
80
|
+
for (let c = 0; c < n; c += 1) {
|
|
81
|
+
let p = c;
|
|
82
|
+
for (let r = c + 1; r < n; r += 1)
|
|
83
|
+
if (Math.abs(m[r][c]) > Math.abs(m[p][c]))
|
|
84
|
+
p = r;
|
|
85
|
+
if (Math.abs(m[p][c]) < 1e-12)
|
|
86
|
+
return null;
|
|
87
|
+
[m[c], m[p]] = [m[p], m[c]];
|
|
88
|
+
for (let r = 0; r < n; r += 1) {
|
|
89
|
+
if (r === c)
|
|
90
|
+
continue;
|
|
91
|
+
const f = m[r][c] / m[c][c];
|
|
92
|
+
for (let k = c; k <= n; k += 1)
|
|
93
|
+
m[r][k] -= f * m[c][k];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return m.map((row, i) => row[n] / row[i]);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Least-squares polynomial of `degree` (clamped to 1..6) through
|
|
100
|
+
* (x, values[i]), x the index or the caller's `xs`. The x values are
|
|
101
|
+
* normalised to [-1, 1] before the normal equations are solved, so a
|
|
102
|
+
* degree-6 fit over a thousand points does not lose the low coefficients to
|
|
103
|
+
* rounding. Non-finite values are skipped and fitted as NaN.
|
|
104
|
+
*/
|
|
105
|
+
export function polynomialFit(values, degree, xs) {
|
|
106
|
+
const d = Math.max(1, Math.min(6, Math.round(degree)));
|
|
107
|
+
const n = values.length;
|
|
108
|
+
const nan = NO_FIT(values);
|
|
109
|
+
const X = xsOf(values, xs);
|
|
110
|
+
const idx = [];
|
|
111
|
+
for (let i = 0; i < n; i += 1)
|
|
112
|
+
if (Number.isFinite(values[i]) && Number.isFinite(X[i]))
|
|
113
|
+
idx.push(i);
|
|
114
|
+
if (idx.length <= d)
|
|
115
|
+
return nan;
|
|
116
|
+
let lo = Infinity;
|
|
117
|
+
let hi = -Infinity;
|
|
118
|
+
for (const i of idx) {
|
|
119
|
+
lo = Math.min(lo, X[i]);
|
|
120
|
+
hi = Math.max(hi, X[i]);
|
|
121
|
+
}
|
|
122
|
+
if (!xs) {
|
|
123
|
+
lo = 0;
|
|
124
|
+
hi = Math.max(1, n - 1);
|
|
125
|
+
}
|
|
126
|
+
const scale = Math.max(hi - lo, 1e-12);
|
|
127
|
+
const nx = (x) => (2 * (x - lo)) / scale - 1;
|
|
128
|
+
// Normal equations: sum x^(j+k) c_k = sum x^j y.
|
|
129
|
+
const a = Array.from({ length: d + 1 }, () => new Array(d + 1).fill(0));
|
|
130
|
+
const b = new Array(d + 1).fill(0);
|
|
131
|
+
for (const i of idx) {
|
|
132
|
+
const x = nx(X[i]);
|
|
133
|
+
const y = values[i];
|
|
134
|
+
const pow = [1];
|
|
135
|
+
for (let k = 1; k <= 2 * d; k += 1)
|
|
136
|
+
pow.push(pow[k - 1] * x);
|
|
137
|
+
for (let j = 0; j <= d; j += 1) {
|
|
138
|
+
b[j] += pow[j] * y;
|
|
139
|
+
for (let k = 0; k <= d; k += 1)
|
|
140
|
+
a[j][k] += pow[j + k];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const c = solve(a, b);
|
|
144
|
+
if (!c)
|
|
145
|
+
return nan;
|
|
146
|
+
const predict = (x) => {
|
|
147
|
+
const u = nx(x);
|
|
148
|
+
let y = 0;
|
|
149
|
+
let p = 1;
|
|
150
|
+
for (let k = 0; k <= d; k += 1) {
|
|
151
|
+
y += c[k] * p;
|
|
152
|
+
p *= u;
|
|
153
|
+
}
|
|
154
|
+
return y;
|
|
155
|
+
};
|
|
156
|
+
const fitted = values.map((v, i) => (Number.isFinite(v) && Number.isFinite(X[i]) ? predict(X[i]) : NaN));
|
|
157
|
+
const terms = c.map((ck, k) => (k === 0 ? fmtCoef(ck) : `${fmtCoef(ck)} u^${k}`)).reverse();
|
|
158
|
+
const u = xs ? `u = 2(x - ${fmtCoef(lo)})/${fmtCoef(scale)} - 1` : `u = 2x/${scale} - 1`;
|
|
159
|
+
return { fitted, r2: rSquared(values, fitted), equation: `y = ${terms.join(' + ')} (${u})`, predict };
|
|
160
|
+
}
|
|
161
|
+
/** Ordinary least squares of values on their index, or on `xs`, as a {@link RegressionFit}. */
|
|
162
|
+
export function linearFit(values, xs) {
|
|
163
|
+
const X = xsOf(values, xs);
|
|
164
|
+
const { slope, intercept, ok } = lineThrough(X, values);
|
|
165
|
+
if (!ok)
|
|
166
|
+
return NO_FIT(values);
|
|
167
|
+
const predict = (x) => slope * x + intercept;
|
|
168
|
+
const fitted = values.map((v, i) => (Number.isFinite(v) && Number.isFinite(X[i]) ? predict(X[i]) : NaN));
|
|
169
|
+
return { fitted, r2: rSquared(values, fitted), equation: `y = ${fmtCoef(slope)} x + ${fmtCoef(intercept)}`, predict };
|
|
170
|
+
}
|
|
171
|
+
/** Fit y = a * e^(b x) by least squares on ln y, x the index or `xs`;
|
|
172
|
+
* values at or below zero cannot be logged and are skipped (fitted as NaN). */
|
|
173
|
+
export function exponentialFit(values, xs) {
|
|
174
|
+
const X = xsOf(values, xs);
|
|
175
|
+
const logs = values.map((v) => (Number.isFinite(v) && v > 0 ? Math.log(v) : NaN));
|
|
176
|
+
const { slope: b, intercept, ok } = lineThrough(X, logs);
|
|
177
|
+
if (!ok)
|
|
178
|
+
return NO_FIT(values);
|
|
179
|
+
const a = Math.exp(intercept);
|
|
180
|
+
const predict = (x) => a * Math.exp(b * x);
|
|
181
|
+
const fitted = values.map((v, i) => (Number.isFinite(v) && v > 0 && Number.isFinite(X[i]) ? predict(X[i]) : NaN));
|
|
182
|
+
return { fitted, r2: rSquared(values, fitted), equation: `y = ${fmtCoef(a)} e^(${fmtCoef(b)} x)`, predict };
|
|
183
|
+
}
|
|
184
|
+
/** Fit y = a + b ln(x + 1) by least squares (x the index, shifted by one so
|
|
185
|
+
* the first point has a logarithm), or y = a + b ln(x) on `xs`, where
|
|
186
|
+
* points with x at or below zero are skipped. */
|
|
187
|
+
export function logarithmicFit(values, xs) {
|
|
188
|
+
const shift = xs ? 0 : 1;
|
|
189
|
+
const lx = xsOf(values, xs).map((x) => (x + shift > 0 ? Math.log(x + shift) : NaN));
|
|
190
|
+
const { slope, intercept, ok } = lineThrough(lx, values);
|
|
191
|
+
if (!ok)
|
|
192
|
+
return NO_FIT(values);
|
|
193
|
+
const predict = (x) => (x + shift > 0 ? intercept + slope * Math.log(x + shift) : NaN);
|
|
194
|
+
const fitted = values.map((v, i) => (Number.isFinite(v) && Number.isFinite(lx[i]) ? intercept + slope * lx[i] : NaN));
|
|
195
|
+
const arg = xs ? 'x' : 'x + 1';
|
|
196
|
+
return { fitted, r2: rSquared(values, fitted), equation: `y = ${fmtCoef(intercept)} + ${fmtCoef(slope)} ln(${arg})`, predict };
|
|
197
|
+
}
|
|
198
|
+
/** Fit y = a (x + 1)^b by least squares on ln y against ln(x + 1), or
|
|
199
|
+
* y = a x^b on `xs`; values (and x on `xs`) at or below zero are skipped
|
|
200
|
+
* (fitted as NaN). */
|
|
201
|
+
export function powerFit(values, xs) {
|
|
202
|
+
const shift = xs ? 0 : 1;
|
|
203
|
+
const lx = xsOf(values, xs).map((x) => (x + shift > 0 ? Math.log(x + shift) : NaN));
|
|
204
|
+
const logs = values.map((v) => (Number.isFinite(v) && v > 0 ? Math.log(v) : NaN));
|
|
205
|
+
const { slope, intercept, ok } = lineThrough(lx, logs);
|
|
206
|
+
if (!ok)
|
|
207
|
+
return NO_FIT(values);
|
|
208
|
+
const a = Math.exp(intercept);
|
|
209
|
+
const predict = (x) => (x + shift > 0 ? a * Math.pow(x + shift, slope) : NaN);
|
|
210
|
+
const fitted = values.map((v, i) => (Number.isFinite(v) && v > 0 && Number.isFinite(lx[i]) ? a * Math.exp(slope * lx[i]) : NaN));
|
|
211
|
+
const arg = xs ? 'x' : '(x + 1)';
|
|
212
|
+
return { fitted, r2: rSquared(values, fitted), equation: `y = ${fmtCoef(a)} ${arg}^${fmtCoef(slope)}`, predict };
|
|
213
|
+
}
|
|
214
|
+
/** Least squares of y on arbitrary x, skipping non-finite pairs. */
|
|
215
|
+
function lineThrough(xs, ys) {
|
|
216
|
+
let n = 0, sx = 0, sy = 0, sxx = 0, sxy = 0;
|
|
217
|
+
for (let i = 0; i < ys.length; i += 1) {
|
|
218
|
+
const x = xs[i];
|
|
219
|
+
const y = ys[i];
|
|
220
|
+
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
221
|
+
continue;
|
|
222
|
+
n += 1;
|
|
223
|
+
sx += x;
|
|
224
|
+
sy += y;
|
|
225
|
+
sxx += x * x;
|
|
226
|
+
sxy += x * y;
|
|
227
|
+
}
|
|
228
|
+
if (n < 2)
|
|
229
|
+
return { slope: 0, intercept: 0, ok: false };
|
|
230
|
+
const denom = n * sxx - sx * sx;
|
|
231
|
+
const slope = denom === 0 ? 0 : (n * sxy - sx * sy) / denom;
|
|
232
|
+
return { slope, intercept: (sy - slope * sx) / n, ok: true };
|
|
233
|
+
}
|
|
234
|
+
/** Simple moving average over a window of `period` values. Window centres
|
|
235
|
+
* trail to the right (typical for time-series). NaN for points before the
|
|
236
|
+
* window is full. */
|
|
237
|
+
export function simpleMovingAverage(values, period) {
|
|
238
|
+
if (period < 1)
|
|
239
|
+
return values.slice();
|
|
240
|
+
const out = new Array(values.length).fill(NaN);
|
|
241
|
+
let sum = 0, count = 0;
|
|
242
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
243
|
+
const v = values[i];
|
|
244
|
+
if (Number.isFinite(v)) {
|
|
245
|
+
sum += v;
|
|
246
|
+
count += 1;
|
|
247
|
+
}
|
|
248
|
+
if (i >= period) {
|
|
249
|
+
const drop = values[i - period];
|
|
250
|
+
if (Number.isFinite(drop)) {
|
|
251
|
+
sum -= drop;
|
|
252
|
+
count -= 1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (i >= period - 1 && count > 0)
|
|
256
|
+
out[i] = sum / count;
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
/** Exponential moving average. Smoothing factor alpha = 2 / (period + 1). */
|
|
261
|
+
export function exponentialMovingAverage(values, period) {
|
|
262
|
+
const alpha = 2 / (Math.max(1, period) + 1);
|
|
263
|
+
const out = new Array(values.length).fill(NaN);
|
|
264
|
+
let prev = null;
|
|
265
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
266
|
+
const v = values[i];
|
|
267
|
+
if (!Number.isFinite(v)) {
|
|
268
|
+
out[i] = prev ?? NaN;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
prev = prev == null ? v : alpha * v + (1 - alpha) * prev;
|
|
272
|
+
out[i] = prev;
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
/** The reducers that need every observation, not a running sum and count. */
|
|
277
|
+
export function reducerNeedsSamples(reducer) {
|
|
278
|
+
return !(reducer === 'sum' || reducer === 'avg' || reducer === 'count');
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Collapse a group's observations to one number. `sum` / `avg` / `count` are
|
|
282
|
+
* what `rowsToChartSpec` always did; the rest are the ones a pivot table
|
|
283
|
+
* offers and a chart panel was missing: `min`, `max`, `median`, `first`,
|
|
284
|
+
* `last`, `countDistinct`, and `pN` for the Nth percentile (`'p90'`). An
|
|
285
|
+
* empty group reduces to 0 for `sum` / `count` and to NaN otherwise, so a
|
|
286
|
+
* missing bucket draws as a gap rather than as a fake zero.
|
|
287
|
+
*/
|
|
288
|
+
export function reduceValues(samples, reducer) {
|
|
289
|
+
const n = samples.length;
|
|
290
|
+
switch (reducer) {
|
|
291
|
+
case 'count':
|
|
292
|
+
return n;
|
|
293
|
+
case 'sum': {
|
|
294
|
+
let s = 0;
|
|
295
|
+
for (const v of samples)
|
|
296
|
+
s += v;
|
|
297
|
+
return s;
|
|
298
|
+
}
|
|
299
|
+
case 'avg': {
|
|
300
|
+
if (!n)
|
|
301
|
+
return 0;
|
|
302
|
+
let s = 0;
|
|
303
|
+
for (const v of samples)
|
|
304
|
+
s += v;
|
|
305
|
+
return s / n;
|
|
306
|
+
}
|
|
307
|
+
case 'min':
|
|
308
|
+
return n ? Math.min(...samples) : Number.NaN;
|
|
309
|
+
case 'max':
|
|
310
|
+
return n ? Math.max(...samples) : Number.NaN;
|
|
311
|
+
case 'first':
|
|
312
|
+
return n ? samples[0] : Number.NaN;
|
|
313
|
+
case 'last':
|
|
314
|
+
return n ? samples[n - 1] : Number.NaN;
|
|
315
|
+
case 'countDistinct':
|
|
316
|
+
return new Set(samples).size;
|
|
317
|
+
case 'median':
|
|
318
|
+
return percentile(samples, 50);
|
|
319
|
+
default: {
|
|
320
|
+
const m = /^p(\d+(?:\.\d+)?)$/.exec(reducer);
|
|
321
|
+
return m ? percentile(samples, Number(m[1])) : Number.NaN;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/** The p-th percentile (0..100) by linear interpolation between order statistics. */
|
|
326
|
+
export function percentile(samples, p) {
|
|
327
|
+
const v = samples.filter((n) => Number.isFinite(n)).slice().sort((a, b) => a - b);
|
|
328
|
+
if (!v.length)
|
|
329
|
+
return Number.NaN;
|
|
330
|
+
const pos = (v.length - 1) * Math.max(0, Math.min(100, p)) / 100;
|
|
331
|
+
const lo = Math.floor(pos);
|
|
332
|
+
const hi = Math.ceil(pos);
|
|
333
|
+
return lo === hi ? v[lo] : v[lo] + (v[hi] - v[lo]) * (pos - lo);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The ISO date (`YYYY-MM-DD`, UTC) that starts the bucket containing `date`:
|
|
337
|
+
* the day itself, the Monday of its week, the first of its month, quarter or
|
|
338
|
+
* year. Returns `''` for a value that is not a date, so the caller can drop
|
|
339
|
+
* the row rather than file it under 1970.
|
|
340
|
+
*/
|
|
341
|
+
export function bucketStart(date, bucket) {
|
|
342
|
+
const t = date instanceof Date ? date.getTime() : typeof date === 'number' ? date : Date.parse(date);
|
|
343
|
+
if (!Number.isFinite(t))
|
|
344
|
+
return '';
|
|
345
|
+
const d = new Date(t);
|
|
346
|
+
const y = d.getUTCFullYear();
|
|
347
|
+
const m = d.getUTCMonth();
|
|
348
|
+
let out;
|
|
349
|
+
switch (bucket) {
|
|
350
|
+
case 'day':
|
|
351
|
+
out = new Date(Date.UTC(y, m, d.getUTCDate()));
|
|
352
|
+
break;
|
|
353
|
+
case 'week': {
|
|
354
|
+
// Monday-based, which is what a business calendar reads.
|
|
355
|
+
const day = (d.getUTCDay() + 6) % 7;
|
|
356
|
+
out = new Date(Date.UTC(y, m, d.getUTCDate() - day));
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
case 'month':
|
|
360
|
+
out = new Date(Date.UTC(y, m, 1));
|
|
361
|
+
break;
|
|
362
|
+
case 'quarter':
|
|
363
|
+
out = new Date(Date.UTC(y, m - (m % 3), 1));
|
|
364
|
+
break;
|
|
365
|
+
default:
|
|
366
|
+
out = new Date(Date.UTC(y, 0, 1));
|
|
367
|
+
}
|
|
368
|
+
return out.toISOString().slice(0, 10);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Bin a raw sample for a histogram. The bin count comes from `bins`, from a
|
|
372
|
+
* `binWidth`, or from a rule: `'sturges'` (default, log2 n + 1), `'fd'`
|
|
373
|
+
* (Freedman-Diaconis, 2 IQR / cbrt n) or `'sqrt'`. The range is the data's
|
|
374
|
+
* extent unless `min` / `max` pin it; the top edge is inclusive. Edges are
|
|
375
|
+
* rounded to a sensible precision for the width, so labels read as `10, 20,
|
|
376
|
+
* 30` rather than `10.000000001`.
|
|
377
|
+
*/
|
|
378
|
+
export function binValues(values, opts = {}) {
|
|
379
|
+
const v = values.filter((n) => Number.isFinite(n));
|
|
380
|
+
if (!v.length)
|
|
381
|
+
return { categories: [], counts: [], edges: [], width: 0 };
|
|
382
|
+
const lo = opts.min ?? Math.min(...v);
|
|
383
|
+
let hi = opts.max ?? Math.max(...v);
|
|
384
|
+
if (hi <= lo)
|
|
385
|
+
hi = lo + 1;
|
|
386
|
+
let count;
|
|
387
|
+
if (opts.bins && opts.bins > 0)
|
|
388
|
+
count = Math.round(opts.bins);
|
|
389
|
+
else if (opts.binWidth && opts.binWidth > 0)
|
|
390
|
+
count = Math.max(1, Math.ceil((hi - lo) / opts.binWidth));
|
|
391
|
+
else if (opts.method === 'sqrt')
|
|
392
|
+
count = Math.ceil(Math.sqrt(v.length));
|
|
393
|
+
else if (opts.method === 'fd') {
|
|
394
|
+
const iqr = percentile(v, 75) - percentile(v, 25);
|
|
395
|
+
const w = iqr > 0 ? (2 * iqr) / Math.cbrt(v.length) : 0;
|
|
396
|
+
count = w > 0 ? Math.ceil((hi - lo) / w) : Math.ceil(Math.log2(v.length) + 1);
|
|
397
|
+
}
|
|
398
|
+
else
|
|
399
|
+
count = Math.ceil(Math.log2(v.length) + 1);
|
|
400
|
+
count = Math.max(1, Math.min(1000, count));
|
|
401
|
+
const width = opts.binWidth && opts.binWidth > 0 ? opts.binWidth : (hi - lo) / count;
|
|
402
|
+
// Precision from the width: a 0.25 width keeps two decimals, a 10 width none.
|
|
403
|
+
const decimals = width >= 1 ? 0 : Math.min(6, Math.ceil(-Math.log10(width)) + 1);
|
|
404
|
+
const fix = (n) => Number(n.toFixed(decimals));
|
|
405
|
+
const edges = [];
|
|
406
|
+
for (let i = 0; i <= count; i += 1)
|
|
407
|
+
edges.push(fix(lo + i * width));
|
|
408
|
+
const counts = new Array(count).fill(0);
|
|
409
|
+
for (const n of v) {
|
|
410
|
+
let i = Math.floor((n - lo) / width);
|
|
411
|
+
if (i >= count)
|
|
412
|
+
i = count - 1;
|
|
413
|
+
if (i < 0)
|
|
414
|
+
i = 0;
|
|
415
|
+
counts[i] = (counts[i] ?? 0) + 1;
|
|
416
|
+
}
|
|
417
|
+
const categories = counts.map((_, i) => String(fix(lo + (i + 0.5) * width)));
|
|
418
|
+
return { categories, counts, edges, width };
|
|
419
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live data. A feed hands a chart one new point every few hundred
|
|
3
|
+
* milliseconds; the spec is immutable state, so each tick wants a new spec
|
|
4
|
+
* with the point appended and the oldest dropped, with every per-category
|
|
5
|
+
* array kept in step. Demo 159 rebuilt the whole spec from a rows array on
|
|
6
|
+
* every tick; `appendPoints` does the append in one call and keeps the
|
|
7
|
+
* confidence bands, row ids, candles and volumes parallel by construction,
|
|
8
|
+
* because it walks the same key list the zoom window and decimation use.
|
|
9
|
+
*/
|
|
10
|
+
import type { ChartSpec, OhlcBar } from './chart-types.js';
|
|
11
|
+
/** One new category with its values, for {@link appendPoints}. */
|
|
12
|
+
export type ChartPoint = {
|
|
13
|
+
/** The category label (a date string on a time axis). */
|
|
14
|
+
category: string;
|
|
15
|
+
/** One value per series in spec order, or values keyed by series label;
|
|
16
|
+
* a series not named gets a gap (NaN). */
|
|
17
|
+
values: ReadonlyArray<number> | Record<string, number>;
|
|
18
|
+
/** The bar for a candlestick series (the first series with `ohlc`). */
|
|
19
|
+
ohlc?: OhlcBar | null;
|
|
20
|
+
/** The volume for that series. */
|
|
21
|
+
volume?: number;
|
|
22
|
+
/** Row ids behind the point, one array per series in spec order. */
|
|
23
|
+
rowIds?: ReadonlyArray<ReadonlyArray<string | number>>;
|
|
24
|
+
/** Confidence band ends, one per series in spec order. */
|
|
25
|
+
upper?: ReadonlyArray<number>;
|
|
26
|
+
lower?: ReadonlyArray<number>;
|
|
27
|
+
/** Range low / bullet target, one per series in spec order. */
|
|
28
|
+
low?: ReadonlyArray<number>;
|
|
29
|
+
target?: ReadonlyArray<number>;
|
|
30
|
+
};
|
|
31
|
+
/** What {@link appendPoints} takes besides the points. */
|
|
32
|
+
export type AppendPointsOptions = {
|
|
33
|
+
/** Keep only the last `window` categories after appending. */
|
|
34
|
+
window?: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Append one or more points to a spec and return the new spec, trimmed to
|
|
38
|
+
* the trailing `window` categories when one is given. Every array a series
|
|
39
|
+
* carries per category (values, row ids, bands, candles, volumes, colours,
|
|
40
|
+
* markers, errors, lows, targets) grows by the same count, with a gap where
|
|
41
|
+
* the point says nothing; so do the spec-level ones (waterfall totals). The
|
|
42
|
+
* input is never mutated: only the arrays that grow are copied, which keeps
|
|
43
|
+
* Svelte's state diffing cheap at 4 Hz.
|
|
44
|
+
*/
|
|
45
|
+
export declare function appendPoints(spec: ChartSpec, points: ChartPoint | ReadonlyArray<ChartPoint>, opts?: AppendPointsOptions): ChartSpec;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { PER_CATEGORY_SERIES_KEYS, PER_CATEGORY_SPEC_KEYS, pickCategories } from './chart-decimate.js';
|
|
2
|
+
/** The gap value each per-category array takes when a point does not set it. */
|
|
3
|
+
function gapFor(key) {
|
|
4
|
+
switch (key) {
|
|
5
|
+
case 'rowIds': return [];
|
|
6
|
+
case 'ohlc':
|
|
7
|
+
case 'boxes':
|
|
8
|
+
case 'markers':
|
|
9
|
+
case 'colors':
|
|
10
|
+
case 'errors': return null;
|
|
11
|
+
default: return Number.NaN;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Append one or more points to a spec and return the new spec, trimmed to
|
|
16
|
+
* the trailing `window` categories when one is given. Every array a series
|
|
17
|
+
* carries per category (values, row ids, bands, candles, volumes, colours,
|
|
18
|
+
* markers, errors, lows, targets) grows by the same count, with a gap where
|
|
19
|
+
* the point says nothing; so do the spec-level ones (waterfall totals). The
|
|
20
|
+
* input is never mutated: only the arrays that grow are copied, which keeps
|
|
21
|
+
* Svelte's state diffing cheap at 4 Hz.
|
|
22
|
+
*/
|
|
23
|
+
export function appendPoints(spec, points, opts = {}) {
|
|
24
|
+
const list = Array.isArray(points) ? points : [points];
|
|
25
|
+
if (!list.length)
|
|
26
|
+
return opts.window ? trim(spec, opts.window) : spec;
|
|
27
|
+
const categories = [...spec.categories, ...list.map((p) => p.category)];
|
|
28
|
+
const ohlcOwner = spec.series.findIndex((s) => s.ohlc);
|
|
29
|
+
const series = spec.series.map((s, si) => {
|
|
30
|
+
const next = { ...s, values: [...s.values] };
|
|
31
|
+
for (const p of list) {
|
|
32
|
+
const v = Array.isArray(p.values) ? p.values[si] : p.values[s.label];
|
|
33
|
+
next.values.push(typeof v === 'number' ? v : Number.NaN);
|
|
34
|
+
}
|
|
35
|
+
for (const key of PER_CATEGORY_SERIES_KEYS) {
|
|
36
|
+
if (key === 'values')
|
|
37
|
+
continue;
|
|
38
|
+
const has = s[key] !== undefined;
|
|
39
|
+
// A key the series does not carry is only created when a point brings
|
|
40
|
+
// it (a candle, a volume), so a plain line stays a plain line.
|
|
41
|
+
const brings = list.some((p) => (key === 'ohlc' && p.ohlc !== undefined && si === (ohlcOwner < 0 ? 0 : ohlcOwner)) ||
|
|
42
|
+
(key === 'volumes' && p.volume !== undefined && si === (ohlcOwner < 0 ? 0 : ohlcOwner)) ||
|
|
43
|
+
(key === 'rowIds' && p.rowIds?.[si] !== undefined) ||
|
|
44
|
+
(key === 'upperValues' && p.upper?.[si] !== undefined) ||
|
|
45
|
+
(key === 'lowerValues' && p.lower?.[si] !== undefined) ||
|
|
46
|
+
(key === 'lowValues' && p.low?.[si] !== undefined) ||
|
|
47
|
+
(key === 'targets' && p.target?.[si] !== undefined));
|
|
48
|
+
if (!has && !brings)
|
|
49
|
+
continue;
|
|
50
|
+
const base = has ? [...s[key]] : new Array(s.values.length).fill(gapFor(key));
|
|
51
|
+
for (const p of list) {
|
|
52
|
+
let v = undefined;
|
|
53
|
+
if (key === 'ohlc' && si === (ohlcOwner < 0 ? 0 : ohlcOwner))
|
|
54
|
+
v = p.ohlc;
|
|
55
|
+
else if (key === 'volumes' && si === (ohlcOwner < 0 ? 0 : ohlcOwner))
|
|
56
|
+
v = p.volume;
|
|
57
|
+
else if (key === 'rowIds')
|
|
58
|
+
v = p.rowIds?.[si];
|
|
59
|
+
else if (key === 'upperValues')
|
|
60
|
+
v = p.upper?.[si];
|
|
61
|
+
else if (key === 'lowerValues')
|
|
62
|
+
v = p.lower?.[si];
|
|
63
|
+
else if (key === 'lowValues')
|
|
64
|
+
v = p.low?.[si];
|
|
65
|
+
else if (key === 'targets')
|
|
66
|
+
v = p.target?.[si];
|
|
67
|
+
base.push(v === undefined ? gapFor(key) : v);
|
|
68
|
+
}
|
|
69
|
+
;
|
|
70
|
+
next[key] = base;
|
|
71
|
+
}
|
|
72
|
+
return next;
|
|
73
|
+
});
|
|
74
|
+
let out = { ...spec, categories, series };
|
|
75
|
+
for (const key of PER_CATEGORY_SPEC_KEYS) {
|
|
76
|
+
const arr = spec[key];
|
|
77
|
+
if (arr)
|
|
78
|
+
out[key] = [...arr, ...list.map(() => false)];
|
|
79
|
+
}
|
|
80
|
+
if (opts.window)
|
|
81
|
+
out = trim(out, opts.window);
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/** The last `window` categories of a spec (the spec itself when it fits). */
|
|
85
|
+
function trim(spec, window) {
|
|
86
|
+
const n = spec.categories.length;
|
|
87
|
+
if (window <= 0 || n <= window)
|
|
88
|
+
return spec;
|
|
89
|
+
const indices = Array.from({ length: window }, (_, k) => n - window + k);
|
|
90
|
+
return pickCategories(spec, indices);
|
|
91
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A chart in a sentence or two. `chartSummary` reads a spec the way a
|
|
3
|
+
* colleague would read the picture: which way each series goes and by how
|
|
4
|
+
* much, where it peaks, what the biggest slices are, whether two measures
|
|
5
|
+
* move together, how the last session closed. `SvChart` puts the result in
|
|
6
|
+
* the SVG's `aria-description`, the context menu copies it, and the AI
|
|
7
|
+
* "explain this chart" prompt is grounded on it, so the words a screen
|
|
8
|
+
* reader hears and the words a model reasons from are the same words.
|
|
9
|
+
*
|
|
10
|
+
* Pure and dependency-light (two small leaves: the number formatter and the
|
|
11
|
+
* trend primitives, kept apart from the engine's modules so the chunk a
|
|
12
|
+
* bundler shares between this lazy module and the base stays tiny), so it
|
|
13
|
+
* runs on the server and in a test, and the renderer loads it lazily.
|
|
14
|
+
*/
|
|
15
|
+
import type { ChartSpec } from './chart-types.js';
|
|
16
|
+
/** What {@link chartSummary} takes. */
|
|
17
|
+
export type ChartSummaryOptions = {
|
|
18
|
+
/** How numbers are written. Default: the spec's `valueFormat` / `locale` / `currency`. */
|
|
19
|
+
formatValue?: (v: number) => string;
|
|
20
|
+
/** How many series a cartesian chart describes before it says "and N more". Default 4. */
|
|
21
|
+
maxSeries?: number;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Describe a spec in plain words. Never throws: an empty spec of any type
|
|
25
|
+
* comes back as "No data." so a live region always has something to say.
|
|
26
|
+
*/
|
|
27
|
+
export declare function chartSummary(spec: ChartSpec, opts?: ChartSummaryOptions): string;
|