@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,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Technical indicators for financial charts: the overlays that sit on the
|
|
3
|
+
* price (Bollinger bands, VWAP, weighted moving average) and the oscillators
|
|
4
|
+
* that get a pane of their own (RSI, MACD, stochastic, ATR, OBV, volume).
|
|
5
|
+
*
|
|
6
|
+
* Every function is pure, takes plain arrays and returns arrays of the same
|
|
7
|
+
* length, NaN-padded where the window is not full yet, so the result lines up
|
|
8
|
+
* with `categories` and draws as a gap rather than as a fake zero. Wilder's
|
|
9
|
+
* smoothing (RSI, ATR) seeds with a simple average over the first period and
|
|
10
|
+
* then smooths recursively, which is what every charting terminal shows.
|
|
11
|
+
*/
|
|
12
|
+
import type { ChartReferenceLine, ChartSeries, ChartSpec, OhlcBar, SeriesOverlay } from './chart-types'
|
|
13
|
+
import { exponentialFit, exponentialMovingAverage, linearFit, logarithmicFit, polynomialFit, powerFit, simpleMovingAverage, type RegressionFit } from './chart-stats'
|
|
14
|
+
|
|
15
|
+
/** Which indicator a pane shows, and with what parameters. */
|
|
16
|
+
export type ChartIndicatorSpec = {
|
|
17
|
+
kind: 'rsi' | 'macd' | 'stochastic' | 'atr' | 'obv' | 'volume'
|
|
18
|
+
/** The series (by label) the indicator reads. Defaults to the first
|
|
19
|
+
* series with `ohlc`, else the first series. */
|
|
20
|
+
source?: string
|
|
21
|
+
/** Periods: `period` (RSI / ATR / stochastic %K), `fast` / `slow` /
|
|
22
|
+
* `signal` (MACD), `smooth` (stochastic %D). Each has the usual default. */
|
|
23
|
+
params?: { period?: number; fast?: number; slow?: number; signal?: number; smooth?: number }
|
|
24
|
+
/** Pane height in px; the default is a third of the main chart. */
|
|
25
|
+
height?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const nanArray = (n: number): number[] => new Array(n).fill(Number.NaN)
|
|
29
|
+
|
|
30
|
+
/** Weighted moving average: the latest value weighs `period`, the oldest 1. */
|
|
31
|
+
export function wma(values: ReadonlyArray<number>, period: number): number[] {
|
|
32
|
+
const n = values.length
|
|
33
|
+
const p = Math.max(1, Math.floor(period))
|
|
34
|
+
const out = nanArray(n)
|
|
35
|
+
const denom = (p * (p + 1)) / 2
|
|
36
|
+
for (let i = p - 1; i < n; i += 1) {
|
|
37
|
+
let sum = 0
|
|
38
|
+
let ok = true
|
|
39
|
+
for (let k = 0; k < p; k += 1) {
|
|
40
|
+
const v = values[i - k]!
|
|
41
|
+
if (!Number.isFinite(v)) { ok = false; break }
|
|
42
|
+
sum += v * (p - k)
|
|
43
|
+
}
|
|
44
|
+
if (ok) out[i] = sum / denom
|
|
45
|
+
}
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Bollinger bands: a simple moving average with a band `k` standard
|
|
51
|
+
* deviations either side (population deviation, as the terminals compute it).
|
|
52
|
+
*/
|
|
53
|
+
export function bollingerBands(values: ReadonlyArray<number>, period = 20, k = 2): { middle: number[]; upper: number[]; lower: number[] } {
|
|
54
|
+
const n = values.length
|
|
55
|
+
const p = Math.max(1, Math.floor(period))
|
|
56
|
+
const middle = simpleMovingAverage(values as number[], p)
|
|
57
|
+
const upper = nanArray(n)
|
|
58
|
+
const lower = nanArray(n)
|
|
59
|
+
for (let i = p - 1; i < n; i += 1) {
|
|
60
|
+
const m = middle[i]!
|
|
61
|
+
if (!Number.isFinite(m)) continue
|
|
62
|
+
let ss = 0
|
|
63
|
+
let count = 0
|
|
64
|
+
for (let j = i - p + 1; j <= i; j += 1) {
|
|
65
|
+
const v = values[j]!
|
|
66
|
+
if (!Number.isFinite(v)) continue
|
|
67
|
+
ss += (v - m) * (v - m)
|
|
68
|
+
count += 1
|
|
69
|
+
}
|
|
70
|
+
const sd = count ? Math.sqrt(ss / count) : 0
|
|
71
|
+
upper[i] = m + k * sd
|
|
72
|
+
lower[i] = m - k * sd
|
|
73
|
+
}
|
|
74
|
+
return { middle, upper, lower }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Relative strength index over `period` bars, 0 to 100. The first value lands
|
|
79
|
+
* at index `period`; before it there is nothing to average.
|
|
80
|
+
*/
|
|
81
|
+
export function rsi(values: ReadonlyArray<number>, period = 14): number[] {
|
|
82
|
+
const n = values.length
|
|
83
|
+
const p = Math.max(1, Math.floor(period))
|
|
84
|
+
const out = nanArray(n)
|
|
85
|
+
if (n <= p) return out
|
|
86
|
+
let gain = 0
|
|
87
|
+
let loss = 0
|
|
88
|
+
for (let i = 1; i <= p; i += 1) {
|
|
89
|
+
const d = values[i]! - values[i - 1]!
|
|
90
|
+
if (d > 0) gain += d
|
|
91
|
+
else loss -= d
|
|
92
|
+
}
|
|
93
|
+
let avgGain = gain / p
|
|
94
|
+
let avgLoss = loss / p
|
|
95
|
+
const rs = (g: number, l: number) => (l === 0 ? 100 : 100 - 100 / (1 + g / l))
|
|
96
|
+
out[p] = rs(avgGain, avgLoss)
|
|
97
|
+
for (let i = p + 1; i < n; i += 1) {
|
|
98
|
+
const d = values[i]! - values[i - 1]!
|
|
99
|
+
avgGain = (avgGain * (p - 1) + (d > 0 ? d : 0)) / p
|
|
100
|
+
avgLoss = (avgLoss * (p - 1) + (d < 0 ? -d : 0)) / p
|
|
101
|
+
out[i] = rs(avgGain, avgLoss)
|
|
102
|
+
}
|
|
103
|
+
return out
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Moving average convergence / divergence: the fast EMA minus the slow one,
|
|
108
|
+
* its signal EMA, and the histogram between them.
|
|
109
|
+
*/
|
|
110
|
+
export function macd(values: ReadonlyArray<number>, fast = 12, slow = 26, signal = 9): { macd: number[]; signal: number[]; histogram: number[] } {
|
|
111
|
+
const n = values.length
|
|
112
|
+
const f = exponentialMovingAverage(values as number[], fast)
|
|
113
|
+
const s = exponentialMovingAverage(values as number[], slow)
|
|
114
|
+
const line = nanArray(n)
|
|
115
|
+
// The slow EMA is only meaningful once it has seen `slow` values; before
|
|
116
|
+
// that both averages are still warming up and the difference is noise.
|
|
117
|
+
for (let i = slow - 1; i < n; i += 1) line[i] = f[i]! - s[i]!
|
|
118
|
+
const sig = nanArray(n)
|
|
119
|
+
const hist = nanArray(n)
|
|
120
|
+
const alpha = 2 / (Math.max(1, signal) + 1)
|
|
121
|
+
let prev: number | null = null
|
|
122
|
+
let seen = 0
|
|
123
|
+
for (let i = 0; i < n; i += 1) {
|
|
124
|
+
const v = line[i]!
|
|
125
|
+
if (!Number.isFinite(v)) continue
|
|
126
|
+
seen += 1
|
|
127
|
+
prev = prev == null ? v : alpha * v + (1 - alpha) * prev
|
|
128
|
+
if (seen >= signal) {
|
|
129
|
+
sig[i] = prev
|
|
130
|
+
hist[i] = v - prev
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { macd: line, signal: sig, histogram: hist }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Typical price of a bar: the mean of its high, low and close. */
|
|
137
|
+
const typical = (b: OhlcBar) => (b.h + b.l + b.c) / 3
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Volume-weighted average price, cumulative from the first bar. Reset it per
|
|
141
|
+
* session by resampling or slicing before calling.
|
|
142
|
+
*/
|
|
143
|
+
export function vwap(ohlc: ReadonlyArray<OhlcBar | null | undefined>, volumes: ReadonlyArray<number>): number[] {
|
|
144
|
+
const n = ohlc.length
|
|
145
|
+
const out = nanArray(n)
|
|
146
|
+
let pv = 0
|
|
147
|
+
let vol = 0
|
|
148
|
+
for (let i = 0; i < n; i += 1) {
|
|
149
|
+
const b = ohlc[i]
|
|
150
|
+
const v = volumes[i]
|
|
151
|
+
if (!b || !Number.isFinite(v)) { out[i] = vol > 0 ? pv / vol : Number.NaN; continue }
|
|
152
|
+
pv += typical(b) * v!
|
|
153
|
+
vol += v!
|
|
154
|
+
out[i] = vol > 0 ? pv / vol : Number.NaN
|
|
155
|
+
}
|
|
156
|
+
return out
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** True range of bar i: the widest of high-low, high-prevClose, low-prevClose. */
|
|
160
|
+
function trueRange(ohlc: ReadonlyArray<OhlcBar | null | undefined>, i: number): number {
|
|
161
|
+
const b = ohlc[i]
|
|
162
|
+
if (!b) return Number.NaN
|
|
163
|
+
const prev = i > 0 ? ohlc[i - 1] : null
|
|
164
|
+
if (!prev) return b.h - b.l
|
|
165
|
+
return Math.max(b.h - b.l, Math.abs(b.h - prev.c), Math.abs(b.l - prev.c))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Average true range with Wilder's smoothing. */
|
|
169
|
+
export function atr(ohlc: ReadonlyArray<OhlcBar | null | undefined>, period = 14): number[] {
|
|
170
|
+
const n = ohlc.length
|
|
171
|
+
const p = Math.max(1, Math.floor(period))
|
|
172
|
+
const out = nanArray(n)
|
|
173
|
+
if (n < p) return out
|
|
174
|
+
let sum = 0
|
|
175
|
+
for (let i = 0; i < p; i += 1) {
|
|
176
|
+
const tr = trueRange(ohlc, i)
|
|
177
|
+
if (!Number.isFinite(tr)) return out
|
|
178
|
+
sum += tr
|
|
179
|
+
}
|
|
180
|
+
let prev = sum / p
|
|
181
|
+
out[p - 1] = prev
|
|
182
|
+
for (let i = p; i < n; i += 1) {
|
|
183
|
+
const tr = trueRange(ohlc, i)
|
|
184
|
+
if (!Number.isFinite(tr)) { out[i] = Number.NaN; continue }
|
|
185
|
+
prev = (prev * (p - 1) + tr) / p
|
|
186
|
+
out[i] = prev
|
|
187
|
+
}
|
|
188
|
+
return out
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Stochastic oscillator: %K is where the close sits in the last `period`
|
|
193
|
+
* bars' range (0 to 100), %D its `smooth`-bar simple average.
|
|
194
|
+
*/
|
|
195
|
+
export function stochastic(ohlc: ReadonlyArray<OhlcBar | null | undefined>, period = 14, smooth = 3): { k: number[]; d: number[] } {
|
|
196
|
+
const n = ohlc.length
|
|
197
|
+
const p = Math.max(1, Math.floor(period))
|
|
198
|
+
const k = nanArray(n)
|
|
199
|
+
for (let i = p - 1; i < n; i += 1) {
|
|
200
|
+
let hi = -Infinity
|
|
201
|
+
let lo = Infinity
|
|
202
|
+
let ok = true
|
|
203
|
+
for (let j = i - p + 1; j <= i; j += 1) {
|
|
204
|
+
const b = ohlc[j]
|
|
205
|
+
if (!b) { ok = false; break }
|
|
206
|
+
if (b.h > hi) hi = b.h
|
|
207
|
+
if (b.l < lo) lo = b.l
|
|
208
|
+
}
|
|
209
|
+
const c = ohlc[i]?.c
|
|
210
|
+
if (!ok || c == null) continue
|
|
211
|
+
k[i] = hi === lo ? 50 : ((c - lo) / (hi - lo)) * 100
|
|
212
|
+
}
|
|
213
|
+
// %D is a strict average: it starts once `smooth` %K values exist, rather
|
|
214
|
+
// than averaging whatever is there (which is what the tolerant SMA does).
|
|
215
|
+
const sm = Math.max(1, Math.floor(smooth))
|
|
216
|
+
const d = nanArray(n)
|
|
217
|
+
for (let i = sm - 1; i < n; i += 1) {
|
|
218
|
+
let sum = 0
|
|
219
|
+
let ok = true
|
|
220
|
+
for (let j = i - sm + 1; j <= i; j += 1) {
|
|
221
|
+
if (!Number.isFinite(k[j])) { ok = false; break }
|
|
222
|
+
sum += k[j]!
|
|
223
|
+
}
|
|
224
|
+
if (ok) d[i] = sum / sm
|
|
225
|
+
}
|
|
226
|
+
return { k, d }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** On-balance volume: a running total that adds volume on up closes and
|
|
230
|
+
* subtracts it on down closes. */
|
|
231
|
+
export function obv(closes: ReadonlyArray<number>, volumes: ReadonlyArray<number>): number[] {
|
|
232
|
+
const n = closes.length
|
|
233
|
+
const out = nanArray(n)
|
|
234
|
+
let total = 0
|
|
235
|
+
for (let i = 0; i < n; i += 1) {
|
|
236
|
+
const v = volumes[i]
|
|
237
|
+
if (!Number.isFinite(closes[i]) || !Number.isFinite(v)) { out[i] = total; continue }
|
|
238
|
+
if (i > 0 && Number.isFinite(closes[i - 1])) {
|
|
239
|
+
if (closes[i]! > closes[i - 1]!) total += v!
|
|
240
|
+
else if (closes[i]! < closes[i - 1]!) total -= v!
|
|
241
|
+
}
|
|
242
|
+
out[i] = total
|
|
243
|
+
}
|
|
244
|
+
return out
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Compute overlay values for a series spec like 'sma:7' / 'ema:14' / 'wma:9' /
|
|
249
|
+
* 'linear'. `'bb:N:K'` returns the band's middle line (the engine draws the
|
|
250
|
+
* band from `bollingerBands` itself) and `'vwap'` needs bars and volumes, so
|
|
251
|
+
* it comes back as gaps here; pass `extra` for those two.
|
|
252
|
+
*/
|
|
253
|
+
export function computeOverlay(values: number[], spec: SeriesOverlay, extra?: { ohlc?: ReadonlyArray<OhlcBar | null>; volumes?: ReadonlyArray<number> }): number[] {
|
|
254
|
+
return computeOverlayFit(values, spec, extra).values
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* {@link computeOverlay} plus what a regression knows about itself: its
|
|
259
|
+
* R-squared and equation. The moving averages, the bands and VWAP carry
|
|
260
|
+
* neither.
|
|
261
|
+
*/
|
|
262
|
+
export function computeOverlayFit(values: number[], spec: SeriesOverlay, extra?: { ohlc?: ReadonlyArray<OhlcBar | null>; volumes?: ReadonlyArray<number> }): { values: number[]; r2?: number; equation?: string } {
|
|
263
|
+
const fit = (f: { fitted: number[]; r2: number; equation: string }) => (f.equation ? { values: f.fitted, r2: f.r2, equation: f.equation } : { values: f.fitted })
|
|
264
|
+
const reg = regressionFit(values, spec)
|
|
265
|
+
if (reg) return fit(reg)
|
|
266
|
+
return { values: computeOverlayPlain(values, spec, extra) }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The regression an overlay string names (`linear`, `poly:N`, `exp`, `log`,
|
|
271
|
+
* `power`), fitted to `values` on their index or on `xs`; `null` for the
|
|
272
|
+
* moving averages and bands, which are not regressions. A scatter passes its
|
|
273
|
+
* points' x and y so the curve is y on x, and reads `predict` to draw it.
|
|
274
|
+
*/
|
|
275
|
+
export function regressionFit(values: ReadonlyArray<number>, spec: SeriesOverlay, xs?: ReadonlyArray<number>): RegressionFit | null {
|
|
276
|
+
if (spec === 'linear') return linearFit(values, xs)
|
|
277
|
+
if (spec === 'exp') return exponentialFit(values, xs)
|
|
278
|
+
if (spec === 'log') return logarithmicFit(values, xs)
|
|
279
|
+
if (spec === 'power') return powerFit(values, xs)
|
|
280
|
+
const poly = /^poly:(\d+)$/.exec(spec)
|
|
281
|
+
if (poly) return polynomialFit(values, Number(poly[1]), xs)
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** The name a tooltip or legend gives an overlay: `sma:20` reads "SMA 20",
|
|
286
|
+
* `poly:3` "poly 3", `bb:20:2` "BB 20", the rest as written. */
|
|
287
|
+
export function overlayName(spec: SeriesOverlay): string {
|
|
288
|
+
const m = /^(sma|ema|wma|bb|poly):(\d+)/.exec(spec)
|
|
289
|
+
if (!m) return spec === 'linear' ? 'trend' : spec
|
|
290
|
+
return `${m[1] === 'poly' ? 'poly' : m[1]!.toUpperCase()} ${m[2]}`
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function computeOverlayPlain(values: number[], spec: SeriesOverlay, extra?: { ohlc?: ReadonlyArray<OhlcBar | null>; volumes?: ReadonlyArray<number> }): number[] {
|
|
294
|
+
if (spec === 'vwap') return extra?.ohlc && extra.volumes ? vwap(extra.ohlc, extra.volumes) : values.map(() => NaN)
|
|
295
|
+
const bb = /^bb:(\d+):(\d+(?:\.\d+)?)$/.exec(spec)
|
|
296
|
+
if (bb) return bollingerBands(values, Number(bb[1]), Number(bb[2])).middle
|
|
297
|
+
const m = /^(sma|ema|wma):(\d+)$/.exec(spec)
|
|
298
|
+
if (!m) return values.map(() => NaN)
|
|
299
|
+
const period = Number(m[2])
|
|
300
|
+
if (m[1] === 'wma') return wma(values, period)
|
|
301
|
+
return m[1] === 'ema' ? exponentialMovingAverage(values, period) : simpleMovingAverage(values, period)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The series an indicator reads: by label, else the first with `ohlc`, else the first. */
|
|
305
|
+
function sourceSeries(spec: ChartSpec, ind: ChartIndicatorSpec): ChartSeries | null {
|
|
306
|
+
if (ind.source) return spec.series.find((s) => s.label === ind.source) ?? null
|
|
307
|
+
return spec.series.find((s) => s.ohlc) ?? spec.series[0] ?? null
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Closes of a series: its `ohlc` closes when it has them, else its values. */
|
|
311
|
+
function closesOf(s: ChartSeries): number[] {
|
|
312
|
+
if (s.ohlc) return s.ohlc.map((b) => (b ? b.c : Number.NaN))
|
|
313
|
+
return s.values.slice()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Build the spec of an indicator pane from a price spec: the same categories
|
|
318
|
+
* and x axis, the indicator's lines or bars, and the reference lines the
|
|
319
|
+
* indicator is read against (30 / 70 for RSI, 20 / 80 for the stochastic, a
|
|
320
|
+
* zero line for MACD). `SvChartPanes` stacks these under the price chart and
|
|
321
|
+
* syncs them; a pane is also a complete spec for a standalone `SvChart`.
|
|
322
|
+
*/
|
|
323
|
+
export function indicatorPane(spec: ChartSpec, ind: ChartIndicatorSpec): ChartSpec {
|
|
324
|
+
const src = sourceSeries(spec, ind)
|
|
325
|
+
const n = spec.categories.length
|
|
326
|
+
const closes = src ? closesOf(src) : nanArray(n)
|
|
327
|
+
const ohlc = src?.ohlc ?? closes.map((c) => (Number.isFinite(c) ? { o: c, h: c, l: c, c } : null))
|
|
328
|
+
const volumes = src?.volumes ?? nanArray(n)
|
|
329
|
+
const params = ind.params ?? {}
|
|
330
|
+
const base: Pick<ChartSpec, 'categories' | 'xType' | 'xAxis' | 'height' | 'palette' | 'valueFormat' | 'locale'> = {
|
|
331
|
+
categories: spec.categories,
|
|
332
|
+
xType: spec.xType,
|
|
333
|
+
xAxis: spec.xAxis,
|
|
334
|
+
height: ind.height ?? Math.max(90, Math.round((spec.height ?? 300) / 3)),
|
|
335
|
+
palette: spec.palette,
|
|
336
|
+
valueFormat: 'number',
|
|
337
|
+
locale: spec.locale,
|
|
338
|
+
}
|
|
339
|
+
const refs = (lines: Array<[number, string]>): ChartReferenceLine[] =>
|
|
340
|
+
lines.map(([value, label]) => ({ value, label, dashed: true }))
|
|
341
|
+
switch (ind.kind) {
|
|
342
|
+
case 'rsi': {
|
|
343
|
+
const p = params.period ?? 14
|
|
344
|
+
return { ...base, type: 'line', title: `RSI ${p}`, series: [{ label: `RSI ${p}`, values: rsi(closes, p), marker: 'none' }],
|
|
345
|
+
yAxis: { min: 0, max: 100, tickInterval: 50, nice: false }, referenceLines: refs([[30, '30'], [70, '70']]) }
|
|
346
|
+
}
|
|
347
|
+
case 'stochastic': {
|
|
348
|
+
const p = params.period ?? 14
|
|
349
|
+
const sm = params.smooth ?? 3
|
|
350
|
+
const { k, d } = stochastic(ohlc, p, sm)
|
|
351
|
+
return { ...base, type: 'line', title: `Stochastic ${p}, ${sm}`, series: [
|
|
352
|
+
{ label: '%K', values: k, marker: 'none' }, { label: '%D', values: d, marker: 'none', dash: [4, 3] },
|
|
353
|
+
], yAxis: { min: 0, max: 100, tickInterval: 50, nice: false }, referenceLines: refs([[20, '20'], [80, '80']]) }
|
|
354
|
+
}
|
|
355
|
+
case 'macd': {
|
|
356
|
+
const fast = params.fast ?? 12
|
|
357
|
+
const slow = params.slow ?? 26
|
|
358
|
+
const signal = params.signal ?? 9
|
|
359
|
+
const m = macd(closes, fast, slow, signal)
|
|
360
|
+
return { ...base, type: 'bar', title: `MACD ${fast}, ${slow}, ${signal}`, series: [
|
|
361
|
+
{ label: 'Histogram', values: m.histogram, colors: m.histogram.map((v) => (v < 0 ? '#ef4444' : '#16a34a')) },
|
|
362
|
+
{ label: 'MACD', values: m.macd, type: 'line', marker: 'none' },
|
|
363
|
+
{ label: 'Signal', values: m.signal, type: 'line', marker: 'none', dash: [4, 3] },
|
|
364
|
+
], referenceLines: refs([[0, '']]) }
|
|
365
|
+
}
|
|
366
|
+
case 'atr': {
|
|
367
|
+
const p = params.period ?? 14
|
|
368
|
+
return { ...base, type: 'line', title: `ATR ${p}`, series: [{ label: `ATR ${p}`, values: atr(ohlc, p), marker: 'none' }], yAxis: { min: 0 } }
|
|
369
|
+
}
|
|
370
|
+
case 'obv':
|
|
371
|
+
return { ...base, type: 'line', title: 'OBV', series: [{ label: 'OBV', values: obv(closes, volumes), marker: 'none' }], valueFormat: 'compact' }
|
|
372
|
+
case 'volume':
|
|
373
|
+
default: {
|
|
374
|
+
// Up bars in the up colour, down bars in the down colour, like the candles.
|
|
375
|
+
const colors = ohlc.map((b, i) => {
|
|
376
|
+
if (!b) return null
|
|
377
|
+
const prev = i > 0 ? ohlc[i - 1] : null
|
|
378
|
+
const up = prev ? b.c >= prev.c : b.c >= b.o
|
|
379
|
+
return up ? '#16a34a' : '#ef4444'
|
|
380
|
+
})
|
|
381
|
+
return { ...base, type: 'bar', title: 'Volume', series: [{ label: 'Volume', values: volumes.slice(), colors }], yAxis: { min: 0 }, valueFormat: 'compact' }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { chartMessage, defaultChartMessages, resolveChartMessages } from './chart-messages'
|
|
3
|
+
|
|
4
|
+
describe('chart messages', () => {
|
|
5
|
+
it('resolves a partial map over the English defaults and ignores empty values', () => {
|
|
6
|
+
const m = resolveChartMessages({ png: 'Bild', noData: '' })
|
|
7
|
+
expect(m.png).toBe('Bild')
|
|
8
|
+
expect(m.noData).toBe(defaultChartMessages.noData)
|
|
9
|
+
expect(resolveChartMessages()).toBe(defaultChartMessages)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('fills placeholders and leaves unknown ones alone', () => {
|
|
13
|
+
expect(chartMessage('{type} chart', { type: 'bar' })).toBe('bar chart')
|
|
14
|
+
expect(chartMessage('{series}: {value} at {category}', { series: 'S', value: 3, category: 'Q1' })).toBe('S: 3 at Q1')
|
|
15
|
+
expect(chartMessage('+{n} more {x}', { n: 4 })).toBe('+4 more {x}')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('every default has a value', () => {
|
|
19
|
+
for (const [k, v] of Object.entries(defaultChartMessages)) expect(v, k).not.toBe('')
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart chrome localization. Every string `SvChart` renders on its own -
|
|
3
|
+
* toolbar buttons and their titles, the context menu, the legend's overflow
|
|
4
|
+
* and hint text, the empty state, and everything it says to assistive
|
|
5
|
+
* technology (the image label, the data table caption, the live region, the
|
|
6
|
+
* marks' labels) - lives here as a flat map with English defaults. Pass
|
|
7
|
+
* `localeText` on `<SvChart>` to override any subset; unset keys fall back
|
|
8
|
+
* to English, so omitting it is a no-op. The same `resolveMessages` idiom as
|
|
9
|
+
* the grid's `localeText` (grid-messages.ts) and the editors'.
|
|
10
|
+
*
|
|
11
|
+
* Placeholders in braces (`{type}`, `{n}`) are filled by {@link chartMessage}.
|
|
12
|
+
*/
|
|
13
|
+
import { resolveMessages } from './editor-contract'
|
|
14
|
+
|
|
15
|
+
/** Every localizable chart string, grouped by area in the comments. */
|
|
16
|
+
export type ChartMessages = {
|
|
17
|
+
// The image and its data table
|
|
18
|
+
/** The SVG's accessible name. `{type}` is the chart type. */
|
|
19
|
+
chartLabel: string
|
|
20
|
+
/** The hidden data table's caption. `{type}` is the chart type. */
|
|
21
|
+
dataTable: string
|
|
22
|
+
/** Appended to the caption when the table is capped. `{shown}` / `{total}`. */
|
|
23
|
+
dataTableCapped: string
|
|
24
|
+
noData: string
|
|
25
|
+
/** A point with no value, in labels and the live region. */
|
|
26
|
+
noValue: string
|
|
27
|
+
// What the live region and the marks say
|
|
28
|
+
/** The live region while stepping through series: `{series}`, `{value}`, `{category}`. */
|
|
29
|
+
seriesAtCategory: string
|
|
30
|
+
/** A dense chart's single hit zone: `{count}`, `{from}`, `{to}`. */
|
|
31
|
+
densePoints: string
|
|
32
|
+
/** Appended to a slice or cell label that drills further. */
|
|
33
|
+
drillIn: string
|
|
34
|
+
/** A sankey node's label: `{label}`, `{in}`, `{out}`. */
|
|
35
|
+
sankeyNode: string
|
|
36
|
+
/** The donut's centre caption under the total. */
|
|
37
|
+
total: string
|
|
38
|
+
/** A regression overlay's goodness of fit in the tooltip. `{value}` is 0..1. */
|
|
39
|
+
rSquared: string
|
|
40
|
+
/** The context menu item that copies the chart's plain-language summary. */
|
|
41
|
+
describeChart: string
|
|
42
|
+
/** The live region after the summary was copied. */
|
|
43
|
+
describeCopied: string
|
|
44
|
+
// Toolbar
|
|
45
|
+
drillPath: string
|
|
46
|
+
drillRoot: string
|
|
47
|
+
dateRange: string
|
|
48
|
+
/** A preset button's title. `{preset}` is its label. */
|
|
49
|
+
showLast: string
|
|
50
|
+
pan: string
|
|
51
|
+
panTitle: string
|
|
52
|
+
resetZoom: string
|
|
53
|
+
resetZoomTitle: string
|
|
54
|
+
annotate: string
|
|
55
|
+
annotateOnTitle: string
|
|
56
|
+
annotateOffTitle: string
|
|
57
|
+
drawingTools: string
|
|
58
|
+
/** A drawing tool button's title. `{tool}` is the tool's label. */
|
|
59
|
+
draw: string
|
|
60
|
+
toolTrend: string
|
|
61
|
+
toolRay: string
|
|
62
|
+
toolFib: string
|
|
63
|
+
toolRect: string
|
|
64
|
+
toolArrow: string
|
|
65
|
+
toolText: string
|
|
66
|
+
/** A drawing's accessible name. `{tool}` is the tool's label. */
|
|
67
|
+
drawing: string
|
|
68
|
+
deleteDrawing: string
|
|
69
|
+
deleteDrawingTitle: string
|
|
70
|
+
clearDrawings: string
|
|
71
|
+
clearDrawingsTitle: string
|
|
72
|
+
noteText: string
|
|
73
|
+
/** A note marker's name while annotating. `{label}` is the note. */
|
|
74
|
+
removeNote: string
|
|
75
|
+
png: string
|
|
76
|
+
downloadPng: string
|
|
77
|
+
svg: string
|
|
78
|
+
downloadSvg: string
|
|
79
|
+
pdf: string
|
|
80
|
+
downloadPdf: string
|
|
81
|
+
downloadCsv: string
|
|
82
|
+
print: string
|
|
83
|
+
printTitle: string
|
|
84
|
+
copy: string
|
|
85
|
+
copied: string
|
|
86
|
+
copyTitle: string
|
|
87
|
+
copyAsImage: string
|
|
88
|
+
// Context menu
|
|
89
|
+
range: string
|
|
90
|
+
series: string
|
|
91
|
+
// Legend
|
|
92
|
+
showSeries: string
|
|
93
|
+
hideSeries: string
|
|
94
|
+
isolateHint: string
|
|
95
|
+
/** The overflow toggle. `{n}` is how many chips are folded away. */
|
|
96
|
+
showMore: string
|
|
97
|
+
showLess: string
|
|
98
|
+
// Brush
|
|
99
|
+
rangeBrush: string
|
|
100
|
+
/** The brush's value text. `{from}` / `{to}` are the window's ends. */
|
|
101
|
+
brushRange: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The English defaults. */
|
|
105
|
+
export const defaultChartMessages: ChartMessages = {
|
|
106
|
+
chartLabel: '{type} chart',
|
|
107
|
+
dataTable: '{type} chart data',
|
|
108
|
+
dataTableCapped: ', first {shown} of {total} rows',
|
|
109
|
+
noData: 'No data',
|
|
110
|
+
noValue: 'no value',
|
|
111
|
+
seriesAtCategory: '{series}: {value} at {category}',
|
|
112
|
+
densePoints: '{count} points, {from} to {to}. Arrow keys step through values.',
|
|
113
|
+
drillIn: ', drill in',
|
|
114
|
+
sankeyNode: '{label}: in {in}, out {out}',
|
|
115
|
+
total: 'Total',
|
|
116
|
+
rSquared: 'R\u00b2 {value}',
|
|
117
|
+
describeChart: 'Describe chart',
|
|
118
|
+
describeCopied: 'Description copied',
|
|
119
|
+
drillPath: 'Drilldown path',
|
|
120
|
+
drillRoot: 'All',
|
|
121
|
+
dateRange: 'Date range',
|
|
122
|
+
showLast: 'Show the last {preset}',
|
|
123
|
+
pan: 'Pan',
|
|
124
|
+
panTitle: 'Drag the plot to pan the zoomed window',
|
|
125
|
+
resetZoom: 'Reset zoom',
|
|
126
|
+
resetZoomTitle: 'Reset zoom (or double-click the plot)',
|
|
127
|
+
annotate: 'Annotate',
|
|
128
|
+
annotateOnTitle: 'Click the plot to pin a note, or a marker to remove it',
|
|
129
|
+
annotateOffTitle: 'Pin notes on the chart',
|
|
130
|
+
drawingTools: 'Drawing tools',
|
|
131
|
+
draw: 'Draw: {tool}',
|
|
132
|
+
toolTrend: 'Trend',
|
|
133
|
+
toolRay: 'Ray',
|
|
134
|
+
toolFib: 'Fib',
|
|
135
|
+
toolRect: 'Rect',
|
|
136
|
+
toolArrow: 'Arrow',
|
|
137
|
+
toolText: 'Text',
|
|
138
|
+
drawing: '{tool} drawing',
|
|
139
|
+
deleteDrawing: 'Delete',
|
|
140
|
+
deleteDrawingTitle: 'Remove the selected drawing (Delete)',
|
|
141
|
+
clearDrawings: 'Clear',
|
|
142
|
+
clearDrawingsTitle: 'Remove every drawing',
|
|
143
|
+
noteText: 'Note text',
|
|
144
|
+
removeNote: 'Remove note: {label}',
|
|
145
|
+
png: 'PNG',
|
|
146
|
+
downloadPng: 'Download PNG',
|
|
147
|
+
svg: 'SVG',
|
|
148
|
+
downloadSvg: 'Download SVG',
|
|
149
|
+
pdf: 'PDF',
|
|
150
|
+
downloadPdf: 'Download PDF',
|
|
151
|
+
downloadCsv: 'Download CSV',
|
|
152
|
+
print: 'Print',
|
|
153
|
+
printTitle: 'Print the chart',
|
|
154
|
+
copy: 'Copy',
|
|
155
|
+
copied: '✓ Copied',
|
|
156
|
+
copyTitle: 'Copy chart as image',
|
|
157
|
+
copyAsImage: 'Copy as image',
|
|
158
|
+
range: 'Range',
|
|
159
|
+
series: 'Series',
|
|
160
|
+
showSeries: 'Show',
|
|
161
|
+
hideSeries: 'Hide',
|
|
162
|
+
isolateHint: 'double-click to isolate',
|
|
163
|
+
showMore: '+{n} more',
|
|
164
|
+
showLess: 'Show less',
|
|
165
|
+
rangeBrush: 'Range brush',
|
|
166
|
+
brushRange: '{from} to {to}',
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Merge a consumer's `localeText` over the English defaults. Undefined or
|
|
171
|
+
* empty values fall back to the default, so a partial map only replaces the
|
|
172
|
+
* keys it sets.
|
|
173
|
+
*/
|
|
174
|
+
export function resolveChartMessages(overrides?: Partial<ChartMessages> | null): ChartMessages {
|
|
175
|
+
return resolveMessages(defaultChartMessages, overrides)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Fill a message's `{name}` placeholders. Unknown placeholders are left as
|
|
180
|
+
* they are, so a translation that drops one loses nothing.
|
|
181
|
+
*/
|
|
182
|
+
export function chartMessage(template: string, vars: Record<string, string | number>): string {
|
|
183
|
+
return template.replace(/\{(\w+)\}/g, (m, k: string) => (k in vars ? String(vars[k]) : m))
|
|
184
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildChart } from './chart'
|
|
3
|
+
import { interpolateGeometry } from './chart-motion'
|
|
4
|
+
import type { ChartSpec } from './chart-types'
|
|
5
|
+
|
|
6
|
+
const bars = (values: number[], categories = ['a', 'b', 'c']): ChartSpec => ({
|
|
7
|
+
type: 'bar', categories, series: [{ label: 's', values }], width: 400, height: 200,
|
|
8
|
+
})
|
|
9
|
+
const lines = (values: number[]): ChartSpec => ({
|
|
10
|
+
type: 'line', categories: ['a', 'b', 'c'], series: [{ label: 's', values }], width: 400, height: 200,
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('interpolateGeometry', () => {
|
|
14
|
+
const a = buildChart(bars([1, 2, 3]), 'light')
|
|
15
|
+
const b = buildChart(bars([3, 2, 1]), 'light')
|
|
16
|
+
|
|
17
|
+
it('is the start at t = 0 and the end at t = 1, without copying', () => {
|
|
18
|
+
expect(interpolateGeometry(a, b, 0)).toBe(a)
|
|
19
|
+
expect(interpolateGeometry(a, b, 1)).toBe(b)
|
|
20
|
+
expect(interpolateGeometry(a, b, 1.5)).toBe(b)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('moves bars halfway at t = 0.5, keyed by series and label', () => {
|
|
24
|
+
const mid = interpolateGeometry(a, b, 0.5)
|
|
25
|
+
expect(mid.bars).toHaveLength(3)
|
|
26
|
+
for (let i = 0; i < 3; i += 1) {
|
|
27
|
+
expect(mid.bars[i]!.y).toBeCloseTo((a.bars[i]!.y + b.bars[i]!.y) / 2, 1)
|
|
28
|
+
expect(mid.bars[i]!.h).toBeCloseTo((a.bars[i]!.h + b.bars[i]!.h) / 2, 1)
|
|
29
|
+
}
|
|
30
|
+
// Everything that is not a mark comes from the destination.
|
|
31
|
+
expect(mid.plot).toEqual(b.plot)
|
|
32
|
+
expect(mid.xTicks).toEqual(b.xTicks)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('grows a new bar from the baseline and shrinks a removed one into it', () => {
|
|
36
|
+
const c = buildChart(bars([1, 2, 3, 4], ['a', 'b', 'c', 'd']), 'light')
|
|
37
|
+
const grow = interpolateGeometry(a, c, 0.5)
|
|
38
|
+
const fresh = grow.bars.find((x) => x.label === 'd')!
|
|
39
|
+
expect(fresh.h).toBeCloseTo(c.bars[3]!.h / 2, 1)
|
|
40
|
+
expect(fresh.y + fresh.h).toBeCloseTo(c.plot.y + c.plot.h, 1)
|
|
41
|
+
const shrink = interpolateGeometry(c, a, 0.5)
|
|
42
|
+
const gone = shrink.bars.find((x) => x.label === 'd')!
|
|
43
|
+
expect(gone).toBeTruthy()
|
|
44
|
+
expect(gone.h).toBeCloseTo(c.bars[3]!.h / 2, 1)
|
|
45
|
+
expect(shrink.bars).toHaveLength(4)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('slides line points and rebuilds the path; a changed point count snaps', () => {
|
|
49
|
+
const l0 = buildChart(lines([1, 2, 3]), 'light')
|
|
50
|
+
const l1 = buildChart(lines([3, 2, 1]), 'light')
|
|
51
|
+
const mid = interpolateGeometry(l0, l1, 0.5)
|
|
52
|
+
expect(mid.lines[0]!.points[0]!.y).toBeCloseTo((l0.lines[0]!.points[0]!.y + l1.lines[0]!.points[0]!.y) / 2, 1)
|
|
53
|
+
expect(mid.lines[0]!.path).not.toBe(l1.lines[0]!.path)
|
|
54
|
+
const l2 = buildChart({ ...lines([1, 2]), categories: ['a', 'b'] }, 'light')
|
|
55
|
+
expect(interpolateGeometry(l0, l2, 0.5).lines[0]).toBe(l2.lines[0])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('a type change is not tweened', () => {
|
|
59
|
+
const pie = buildChart({ ...bars([1, 2, 3]), type: 'pie' }, 'light')
|
|
60
|
+
expect(interpolateGeometry(a, pie, 0.5)).toBe(pie)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('pie slices and sunburst arcs interpolate their angles', () => {
|
|
64
|
+
const p0 = buildChart({ ...bars([1, 1, 2]), type: 'pie' }, 'light')
|
|
65
|
+
const p1 = buildChart({ ...bars([2, 1, 1]), type: 'pie' }, 'light')
|
|
66
|
+
const mid = interpolateGeometry(p0, p1, 0.5)
|
|
67
|
+
expect(mid.slices).toHaveLength(3)
|
|
68
|
+
expect(mid.slices[0]!.arc!.a1).toBeCloseTo((p0.slices[0]!.arc!.a1 + p1.slices[0]!.arc!.a1) / 2, 6)
|
|
69
|
+
expect(mid.slices[0]!.path).not.toBe(p1.slices[0]!.path)
|
|
70
|
+
})
|
|
71
|
+
})
|