@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,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thinning long series before layout, and the one place that knows which
|
|
3
|
+
* arrays on a spec run parallel to `categories`.
|
|
4
|
+
*
|
|
5
|
+
* `buildChart` scales fine (100k line points is ~140 ms of maths); what does
|
|
6
|
+
* not scale is asking the DOM to hold a mark per point, and past one point
|
|
7
|
+
* per pixel the extra marks carry no information anyway. So a long line or
|
|
8
|
+
* area series is reduced to about one point per pixel with an algorithm that
|
|
9
|
+
* keeps its visual shape (Largest-Triangle-Three-Buckets) or its extremes
|
|
10
|
+
* (min / max per bucket), and every category-parallel array is picked with the
|
|
11
|
+
* same indices so tooltips, drill-through and error bars stay aligned.
|
|
12
|
+
*/
|
|
13
|
+
import type { ChartDecimateConfig, ChartSpec } from './chart-types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Every array on a {@link ChartSeries} indexed by category. Anything that
|
|
16
|
+
* narrows a spec to a subset of categories (the zoom window, decimation) picks
|
|
17
|
+
* each of these with the same indices; a key missing here is a mark drawn
|
|
18
|
+
* against the wrong category, which is why the list is one constant rather
|
|
19
|
+
* than a convention. `values` is included for completeness.
|
|
20
|
+
*/
|
|
21
|
+
export declare const PER_CATEGORY_SERIES_KEYS: readonly ["values", "rowIds", "upperValues", "lowerValues", "ohlc", "boxes", "errors", "markers", "colors", "lowValues", "targets", "volumes"];
|
|
22
|
+
/** Spec-level arrays indexed by category (see {@link PER_CATEGORY_SERIES_KEYS}). */
|
|
23
|
+
export declare const PER_CATEGORY_SPEC_KEYS: readonly ["waterfallTotals"];
|
|
24
|
+
/**
|
|
25
|
+
* Narrow a spec to the categories at `indices` (ascending), keeping every
|
|
26
|
+
* category-parallel array in step. The zoom window and decimation both go
|
|
27
|
+
* through here, so there is exactly one list of arrays to keep in step.
|
|
28
|
+
*/
|
|
29
|
+
export declare function pickCategories(spec: ChartSpec, indices: ReadonlyArray<number>): ChartSpec;
|
|
30
|
+
/**
|
|
31
|
+
* Largest-Triangle-Three-Buckets: the indices of the `target` points that best
|
|
32
|
+
* preserve the visual shape of `values` plotted against their index. The
|
|
33
|
+
* first and last points are always kept. Non-finite values are skipped as
|
|
34
|
+
* candidates, so a gap cannot be "chosen" as a representative point.
|
|
35
|
+
*/
|
|
36
|
+
export declare function lttb(values: ReadonlyArray<number>, target: number, xs?: ReadonlyArray<number>): number[];
|
|
37
|
+
/**
|
|
38
|
+
* The indices of the minimum and maximum in each of `buckets` equal slices of
|
|
39
|
+
* `values`, plus the first and last point. Keeps every spike, which is what
|
|
40
|
+
* monitoring data wants and what LTTB will sometimes smooth over.
|
|
41
|
+
*/
|
|
42
|
+
export declare function minMaxIndices(values: ReadonlyArray<number>, buckets: number): number[];
|
|
43
|
+
/**
|
|
44
|
+
* Thin a spec's line / area series to about one point per pixel of plot
|
|
45
|
+
* width. Nothing happens below the threshold, for a spec that says
|
|
46
|
+
* `decimate: false`, or when any series draws bars, candles or boxes (those
|
|
47
|
+
* marks ARE the data; thinning them would hide categories). Every series is
|
|
48
|
+
* decimated separately and the union of the kept indices is used, so a spike
|
|
49
|
+
* that only one series has still survives.
|
|
50
|
+
*/
|
|
51
|
+
export declare function decimateSpec(spec: ChartSpec, plotW: number, cfg?: ChartDecimateConfig | undefined): ChartSpec;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every array on a {@link ChartSeries} indexed by category. Anything that
|
|
3
|
+
* narrows a spec to a subset of categories (the zoom window, decimation) picks
|
|
4
|
+
* each of these with the same indices; a key missing here is a mark drawn
|
|
5
|
+
* against the wrong category, which is why the list is one constant rather
|
|
6
|
+
* than a convention. `values` is included for completeness.
|
|
7
|
+
*/
|
|
8
|
+
export const PER_CATEGORY_SERIES_KEYS = [
|
|
9
|
+
'values',
|
|
10
|
+
'rowIds',
|
|
11
|
+
'upperValues',
|
|
12
|
+
'lowerValues',
|
|
13
|
+
'ohlc',
|
|
14
|
+
'boxes',
|
|
15
|
+
'errors',
|
|
16
|
+
'markers',
|
|
17
|
+
'colors',
|
|
18
|
+
'lowValues',
|
|
19
|
+
'targets',
|
|
20
|
+
'volumes',
|
|
21
|
+
];
|
|
22
|
+
/** Spec-level arrays indexed by category (see {@link PER_CATEGORY_SERIES_KEYS}). */
|
|
23
|
+
export const PER_CATEGORY_SPEC_KEYS = ['waterfallTotals'];
|
|
24
|
+
/**
|
|
25
|
+
* Narrow a spec to the categories at `indices` (ascending), keeping every
|
|
26
|
+
* category-parallel array in step. The zoom window and decimation both go
|
|
27
|
+
* through here, so there is exactly one list of arrays to keep in step.
|
|
28
|
+
*/
|
|
29
|
+
export function pickCategories(spec, indices) {
|
|
30
|
+
const pick = (arr) => {
|
|
31
|
+
if (!arr)
|
|
32
|
+
return undefined;
|
|
33
|
+
const out = new Array(indices.length);
|
|
34
|
+
for (let k = 0; k < indices.length; k += 1)
|
|
35
|
+
out[k] = arr[indices[k]];
|
|
36
|
+
return out;
|
|
37
|
+
};
|
|
38
|
+
const series = spec.series.map((s) => {
|
|
39
|
+
const next = { ...s, values: pick(s.values) ?? [] };
|
|
40
|
+
for (const key of PER_CATEGORY_SERIES_KEYS) {
|
|
41
|
+
if (key === 'values')
|
|
42
|
+
continue;
|
|
43
|
+
const arr = s[key];
|
|
44
|
+
if (arr)
|
|
45
|
+
next[key] = pick(arr);
|
|
46
|
+
}
|
|
47
|
+
return next;
|
|
48
|
+
});
|
|
49
|
+
const out = { ...spec, categories: pick(spec.categories) ?? [], series };
|
|
50
|
+
for (const key of PER_CATEGORY_SPEC_KEYS) {
|
|
51
|
+
const arr = spec[key];
|
|
52
|
+
if (arr)
|
|
53
|
+
out[key] = pick(arr);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Largest-Triangle-Three-Buckets: the indices of the `target` points that best
|
|
59
|
+
* preserve the visual shape of `values` plotted against their index. The
|
|
60
|
+
* first and last points are always kept. Non-finite values are skipped as
|
|
61
|
+
* candidates, so a gap cannot be "chosen" as a representative point.
|
|
62
|
+
*/
|
|
63
|
+
export function lttb(values, target, xs) {
|
|
64
|
+
const n = values.length;
|
|
65
|
+
// The x of point j: its index unless the caller positions points by value
|
|
66
|
+
// (a numeric or log axis), where the triangles must use the real spacing.
|
|
67
|
+
const xAt = (j) => (xs && Number.isFinite(xs[j]) ? xs[j] : j);
|
|
68
|
+
if (target >= n || n <= 2)
|
|
69
|
+
return values.map((_, i) => i);
|
|
70
|
+
if (target < 3)
|
|
71
|
+
return [0, n - 1];
|
|
72
|
+
const out = [0];
|
|
73
|
+
const every = (n - 2) / (target - 2);
|
|
74
|
+
let a = 0;
|
|
75
|
+
for (let i = 0; i < target - 2; i += 1) {
|
|
76
|
+
// The next bucket's centroid is the far corner of every candidate triangle.
|
|
77
|
+
const nextStart = Math.floor((i + 1) * every) + 1;
|
|
78
|
+
const nextEnd = Math.min(Math.floor((i + 2) * every) + 1, n);
|
|
79
|
+
let avgX = 0;
|
|
80
|
+
let avgY = 0;
|
|
81
|
+
let cnt = 0;
|
|
82
|
+
for (let j = nextStart; j < nextEnd; j += 1) {
|
|
83
|
+
const v = values[j];
|
|
84
|
+
if (!Number.isFinite(v))
|
|
85
|
+
continue;
|
|
86
|
+
avgX += xAt(j);
|
|
87
|
+
avgY += v;
|
|
88
|
+
cnt += 1;
|
|
89
|
+
}
|
|
90
|
+
if (cnt) {
|
|
91
|
+
avgX /= cnt;
|
|
92
|
+
avgY /= cnt;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
avgX = (xAt(nextStart) + xAt(Math.min(nextEnd, n - 1))) / 2;
|
|
96
|
+
avgY = values[a];
|
|
97
|
+
}
|
|
98
|
+
const start = Math.floor(i * every) + 1;
|
|
99
|
+
const end = Math.min(Math.floor((i + 1) * every) + 1, n);
|
|
100
|
+
const ax = xAt(a);
|
|
101
|
+
const ay = Number.isFinite(values[a]) ? values[a] : 0;
|
|
102
|
+
let best = -1;
|
|
103
|
+
let bestArea = -1;
|
|
104
|
+
for (let j = start; j < end; j += 1) {
|
|
105
|
+
const v = values[j];
|
|
106
|
+
if (!Number.isFinite(v))
|
|
107
|
+
continue;
|
|
108
|
+
const area = Math.abs((ax - avgX) * (v - ay) - (ax - xAt(j)) * (avgY - ay));
|
|
109
|
+
if (area > bestArea) {
|
|
110
|
+
bestArea = area;
|
|
111
|
+
best = j;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (best < 0)
|
|
115
|
+
best = start;
|
|
116
|
+
out.push(best);
|
|
117
|
+
a = best;
|
|
118
|
+
}
|
|
119
|
+
out.push(n - 1);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The indices of the minimum and maximum in each of `buckets` equal slices of
|
|
124
|
+
* `values`, plus the first and last point. Keeps every spike, which is what
|
|
125
|
+
* monitoring data wants and what LTTB will sometimes smooth over.
|
|
126
|
+
*/
|
|
127
|
+
export function minMaxIndices(values, buckets) {
|
|
128
|
+
const n = values.length;
|
|
129
|
+
if (buckets >= n || n <= 2)
|
|
130
|
+
return values.map((_, i) => i);
|
|
131
|
+
const keep = new Set([0, n - 1]);
|
|
132
|
+
const size = n / Math.max(1, buckets);
|
|
133
|
+
for (let b = 0; b < buckets; b += 1) {
|
|
134
|
+
const start = Math.floor(b * size);
|
|
135
|
+
const end = Math.min(n, Math.floor((b + 1) * size));
|
|
136
|
+
let lo = -1;
|
|
137
|
+
let hi = -1;
|
|
138
|
+
for (let j = start; j < end; j += 1) {
|
|
139
|
+
const v = values[j];
|
|
140
|
+
if (!Number.isFinite(v))
|
|
141
|
+
continue;
|
|
142
|
+
if (lo < 0 || v < values[lo])
|
|
143
|
+
lo = j;
|
|
144
|
+
if (hi < 0 || v > values[hi])
|
|
145
|
+
hi = j;
|
|
146
|
+
}
|
|
147
|
+
if (lo >= 0)
|
|
148
|
+
keep.add(lo);
|
|
149
|
+
if (hi >= 0)
|
|
150
|
+
keep.add(hi);
|
|
151
|
+
}
|
|
152
|
+
return [...keep].sort((x, y) => x - y);
|
|
153
|
+
}
|
|
154
|
+
/** The decimation settings a spec asks for, defaults filled in. */
|
|
155
|
+
function resolveDecimate(cfg, plotW) {
|
|
156
|
+
if (cfg === false)
|
|
157
|
+
return null;
|
|
158
|
+
const o = typeof cfg === 'object' ? cfg : {};
|
|
159
|
+
const target = Math.max(8, Math.round(o.target ?? plotW));
|
|
160
|
+
return { method: o.method ?? 'lttb', target, threshold: Math.max(target, Math.round(o.threshold ?? target)) };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Thin a spec's line / area series to about one point per pixel of plot
|
|
164
|
+
* width. Nothing happens below the threshold, for a spec that says
|
|
165
|
+
* `decimate: false`, or when any series draws bars, candles or boxes (those
|
|
166
|
+
* marks ARE the data; thinning them would hide categories). Every series is
|
|
167
|
+
* decimated separately and the union of the kept indices is used, so a spike
|
|
168
|
+
* that only one series has still survives.
|
|
169
|
+
*/
|
|
170
|
+
export function decimateSpec(spec, plotW, cfg = spec.decimate) {
|
|
171
|
+
const opt = resolveDecimate(cfg, plotW);
|
|
172
|
+
if (!opt)
|
|
173
|
+
return spec;
|
|
174
|
+
const n = spec.categories.length;
|
|
175
|
+
if (n <= opt.threshold || !spec.series.length)
|
|
176
|
+
return spec;
|
|
177
|
+
const lineLike = (t) => t === 'line' || t === 'area';
|
|
178
|
+
if (!lineLike(spec.type) && !spec.series.every((s) => lineLike(s.type)))
|
|
179
|
+
return spec;
|
|
180
|
+
if (spec.series.some((s) => (s.type && !lineLike(s.type)) || s.ohlc || s.boxes))
|
|
181
|
+
return spec;
|
|
182
|
+
// On a numeric axis the points sit where their values are, so the
|
|
183
|
+
// triangles are measured in those positions (log10 of them on a log axis).
|
|
184
|
+
const numeric = (spec.xAxis?.type ?? spec.xType) === 'number';
|
|
185
|
+
const isLog = numeric && spec.xAxis?.scale === 'log';
|
|
186
|
+
const xs = numeric
|
|
187
|
+
? spec.categories.map((c) => { const v = Number(c); return isLog ? (v > 0 ? Math.log10(v) : Number.NaN) : v; })
|
|
188
|
+
: undefined;
|
|
189
|
+
const keep = new Set();
|
|
190
|
+
for (const s of spec.series) {
|
|
191
|
+
const idx = opt.method === 'minmax' ? minMaxIndices(s.values, Math.floor(opt.target / 2)) : lttb(s.values, opt.target, xs);
|
|
192
|
+
for (const i of idx)
|
|
193
|
+
keep.add(i);
|
|
194
|
+
}
|
|
195
|
+
const indices = [...keep].sort((a, b) => a - b);
|
|
196
|
+
if (indices.length >= n)
|
|
197
|
+
return spec;
|
|
198
|
+
return pickCategories(spec, indices);
|
|
199
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF and print for a rendered chart, with no PDF library.
|
|
3
|
+
*
|
|
4
|
+
* A one-page PDF is a short text file with one binary blob in it, so the
|
|
5
|
+
* writer here is the whole format we need: a catalog, a page, a Helvetica
|
|
6
|
+
* font, one image XObject and a content stream that places the text and the
|
|
7
|
+
* image. The chart is rasterised through the existing PNG path and re-encoded
|
|
8
|
+
* as JPEG, which the PDF embeds as-is under `/DCTDecode`; nothing needs zlib.
|
|
9
|
+
* JPEG has no alpha, so the chart background is always painted.
|
|
10
|
+
*
|
|
11
|
+
* Print opens a window with the standalone SVG and calls `print()`.
|
|
12
|
+
*/
|
|
13
|
+
import { type ChartExportOptions } from './chart-export.js';
|
|
14
|
+
/** Options for the PDF: page, orientation, the text around the chart. */
|
|
15
|
+
export type ChartPdfOptions = ChartExportOptions & {
|
|
16
|
+
/** Paper size. Default A4. */
|
|
17
|
+
page?: 'A4' | 'Letter';
|
|
18
|
+
/** Default: whichever fits the chart's aspect ratio better. */
|
|
19
|
+
orientation?: 'portrait' | 'landscape';
|
|
20
|
+
title?: string;
|
|
21
|
+
subtitle?: string;
|
|
22
|
+
caption?: string;
|
|
23
|
+
/** JPEG quality, 0..1. Default 0.92. */
|
|
24
|
+
quality?: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Assemble a one-page PDF from a JPEG and the text around it. Exported for
|
|
28
|
+
* tests and for callers that already have a JPEG; `chartToPdfBlob` is the
|
|
29
|
+
* one that starts from a chart element.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildChartPdf(jpeg: {
|
|
32
|
+
bytes: Uint8Array;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
}, opts?: Pick<ChartPdfOptions, 'page' | 'orientation' | 'title' | 'subtitle' | 'caption'>): Uint8Array;
|
|
36
|
+
/** The chart as a one-page PDF `Blob`. */
|
|
37
|
+
export declare function chartToPdfBlob(source: SVGSVGElement | HTMLElement, options?: ChartPdfOptions): Promise<Blob>;
|
|
38
|
+
/** Download the chart as a `.pdf` file. */
|
|
39
|
+
export declare function downloadChartPdf(source: SVGSVGElement | HTMLElement, filename?: string, options?: ChartPdfOptions): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Print the chart: a new window with the standalone SVG (and the title
|
|
42
|
+
* above it), sized to the page, that opens the print dialog once it has
|
|
43
|
+
* drawn. Returns false when the window was blocked.
|
|
44
|
+
*/
|
|
45
|
+
export declare function printChart(source: SVGSVGElement | HTMLElement, options?: ChartExportOptions & {
|
|
46
|
+
title?: string;
|
|
47
|
+
}): boolean;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF and print for a rendered chart, with no PDF library.
|
|
3
|
+
*
|
|
4
|
+
* A one-page PDF is a short text file with one binary blob in it, so the
|
|
5
|
+
* writer here is the whole format we need: a catalog, a page, a Helvetica
|
|
6
|
+
* font, one image XObject and a content stream that places the text and the
|
|
7
|
+
* image. The chart is rasterised through the existing PNG path and re-encoded
|
|
8
|
+
* as JPEG, which the PDF embeds as-is under `/DCTDecode`; nothing needs zlib.
|
|
9
|
+
* JPEG has no alpha, so the chart background is always painted.
|
|
10
|
+
*
|
|
11
|
+
* Print opens a window with the standalone SVG and calls `print()`.
|
|
12
|
+
*/
|
|
13
|
+
import { chartToPngBlob, chartToSvgString } from './chart-export.js';
|
|
14
|
+
const PAGES = { A4: [595.28, 841.89], Letter: [612, 792] };
|
|
15
|
+
/** Latin-1 bytes of a string; anything outside it becomes `?`. */
|
|
16
|
+
function latin1(s) {
|
|
17
|
+
const out = new Uint8Array(s.length);
|
|
18
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
19
|
+
const c = s.charCodeAt(i);
|
|
20
|
+
out[i] = c < 256 ? c : 63;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
/** A PDF string literal: parentheses and backslashes escaped. */
|
|
25
|
+
function pdfText(s) {
|
|
26
|
+
return '(' + s.replace(/[\\()]/g, (m) => '\\' + m).replace(/[\r\n]+/g, ' ') + ')';
|
|
27
|
+
}
|
|
28
|
+
/** Roughly how wide Helvetica text is at a size, for fitting a line. */
|
|
29
|
+
function textWidth(s, size) {
|
|
30
|
+
return s.length * size * 0.5;
|
|
31
|
+
}
|
|
32
|
+
/** Re-encode a PNG blob as JPEG on a painted background. */
|
|
33
|
+
function pngToJpeg(png, background, quality) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const url = URL.createObjectURL(png);
|
|
36
|
+
const img = new Image();
|
|
37
|
+
img.onload = () => {
|
|
38
|
+
const canvas = document.createElement('canvas');
|
|
39
|
+
canvas.width = img.naturalWidth || img.width;
|
|
40
|
+
canvas.height = img.naturalHeight || img.height;
|
|
41
|
+
const ctx = canvas.getContext('2d');
|
|
42
|
+
if (!ctx) {
|
|
43
|
+
URL.revokeObjectURL(url);
|
|
44
|
+
reject(new Error('canvas 2d context unavailable'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
ctx.fillStyle = background;
|
|
48
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
49
|
+
ctx.drawImage(img, 0, 0);
|
|
50
|
+
URL.revokeObjectURL(url);
|
|
51
|
+
canvas.toBlob((blob) => {
|
|
52
|
+
if (!blob) {
|
|
53
|
+
reject(new Error('toBlob failed'));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
blob.arrayBuffer().then((buf) => resolve({ bytes: new Uint8Array(buf), width: canvas.width, height: canvas.height }), reject);
|
|
57
|
+
}, 'image/jpeg', quality);
|
|
58
|
+
};
|
|
59
|
+
img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('failed to decode chart PNG')); };
|
|
60
|
+
img.src = url;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Assemble a one-page PDF from a JPEG and the text around it. Exported for
|
|
65
|
+
* tests and for callers that already have a JPEG; `chartToPdfBlob` is the
|
|
66
|
+
* one that starts from a chart element.
|
|
67
|
+
*/
|
|
68
|
+
export function buildChartPdf(jpeg, opts = {}) {
|
|
69
|
+
const [pw0, ph0] = PAGES[opts.page ?? 'A4'];
|
|
70
|
+
const landscape = opts.orientation ? opts.orientation === 'landscape' : jpeg.width / jpeg.height > pw0 / ph0;
|
|
71
|
+
const pw = landscape ? ph0 : pw0;
|
|
72
|
+
const ph = landscape ? pw0 : ph0;
|
|
73
|
+
const margin = 40;
|
|
74
|
+
const title = (opts.title ?? '').trim();
|
|
75
|
+
const subtitle = (opts.subtitle ?? '').trim();
|
|
76
|
+
const caption = (opts.caption ?? '').trim();
|
|
77
|
+
// Text block at the top, image below it, caption at the foot.
|
|
78
|
+
let y = ph - margin;
|
|
79
|
+
const content = [];
|
|
80
|
+
const line = (text, size, gray) => {
|
|
81
|
+
y -= size + 4;
|
|
82
|
+
content.push(`BT /F1 ${size} Tf ${gray} g ${margin} ${y.toFixed(2)} Td ${pdfText(text)} Tj ET`);
|
|
83
|
+
};
|
|
84
|
+
if (title)
|
|
85
|
+
line(title, 16, 0);
|
|
86
|
+
if (subtitle)
|
|
87
|
+
line(subtitle, 11, 0.4);
|
|
88
|
+
if (title || subtitle)
|
|
89
|
+
y -= 8;
|
|
90
|
+
const footer = caption ? 20 : 0;
|
|
91
|
+
const availW = pw - margin * 2;
|
|
92
|
+
const availH = y - margin - footer;
|
|
93
|
+
const scale = Math.min(availW / jpeg.width, availH / jpeg.height);
|
|
94
|
+
const iw = jpeg.width * scale;
|
|
95
|
+
const ih = jpeg.height * scale;
|
|
96
|
+
const ix = margin + (availW - iw) / 2;
|
|
97
|
+
const iy = y - ih;
|
|
98
|
+
content.push(`q ${iw.toFixed(2)} 0 0 ${ih.toFixed(2)} ${ix.toFixed(2)} ${iy.toFixed(2)} cm /Im1 Do Q`);
|
|
99
|
+
if (caption) {
|
|
100
|
+
const size = 9;
|
|
101
|
+
const text = textWidth(caption, size) > availW ? caption.slice(0, Math.floor(availW / (size * 0.5)) - 3) + '...' : caption;
|
|
102
|
+
content.push(`BT /F1 ${size} Tf 0.4 g ${margin} ${(margin - 4).toFixed(2)} Td ${pdfText(text)} Tj ET`);
|
|
103
|
+
}
|
|
104
|
+
const stream = content.join('\n');
|
|
105
|
+
const objects = [];
|
|
106
|
+
const offsets = [];
|
|
107
|
+
let length = 0;
|
|
108
|
+
const push = (part) => {
|
|
109
|
+
objects.push(part);
|
|
110
|
+
length += typeof part === 'string' ? part.length : part.length;
|
|
111
|
+
};
|
|
112
|
+
push('%PDF-1.4\n%âãÏÓ\n');
|
|
113
|
+
const obj = (n, body, bin) => {
|
|
114
|
+
offsets[n] = length;
|
|
115
|
+
push(`${n} 0 obj\n${body}\n`);
|
|
116
|
+
if (bin) {
|
|
117
|
+
push('stream\n');
|
|
118
|
+
push(bin);
|
|
119
|
+
push('\nendstream\n');
|
|
120
|
+
}
|
|
121
|
+
push('endobj\n');
|
|
122
|
+
};
|
|
123
|
+
obj(1, '<< /Type /Catalog /Pages 2 0 R >>');
|
|
124
|
+
obj(2, '<< /Type /Pages /Kids [3 0 R] /Count 1 >>');
|
|
125
|
+
obj(3, `<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${pw.toFixed(2)} ${ph.toFixed(2)}] /Resources << /XObject << /Im1 4 0 R >> /Font << /F1 5 0 R >> >> /Contents 6 0 R >>`);
|
|
126
|
+
obj(4, `<< /Type /XObject /Subtype /Image /Width ${jpeg.width} /Height ${jpeg.height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${jpeg.bytes.length} >>`, jpeg.bytes);
|
|
127
|
+
obj(5, '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>');
|
|
128
|
+
obj(6, `<< /Length ${stream.length} >>`, latin1(stream));
|
|
129
|
+
const xref = length;
|
|
130
|
+
push(`xref\n0 7\n0000000000 65535 f \n`);
|
|
131
|
+
for (let n = 1; n <= 6; n += 1)
|
|
132
|
+
push(`${String(offsets[n]).padStart(10, '0')} 00000 n \n`);
|
|
133
|
+
push(`trailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`);
|
|
134
|
+
const out = new Uint8Array(new ArrayBuffer(length));
|
|
135
|
+
let at = 0;
|
|
136
|
+
for (const part of objects) {
|
|
137
|
+
const bytes = typeof part === 'string' ? latin1(part) : part;
|
|
138
|
+
out.set(bytes, at);
|
|
139
|
+
at += bytes.length;
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
/** The chart as a one-page PDF `Blob`. */
|
|
144
|
+
export async function chartToPdfBlob(source, options = {}) {
|
|
145
|
+
const svg = source instanceof SVGSVGElement ? source : (source.querySelector('svg.sv-grid-chart-svg') ?? source.querySelector('svg'));
|
|
146
|
+
const background = options.background ?? (svg ? getComputedStyle(svg).getPropertyValue('--sg-bg').trim() || '#ffffff' : '#ffffff');
|
|
147
|
+
const png = await chartToPngBlob(source, { ...options, background, scale: options.scale ?? 2 });
|
|
148
|
+
const jpeg = await pngToJpeg(png, background, options.quality ?? 0.92);
|
|
149
|
+
const bytes = buildChartPdf(jpeg, options);
|
|
150
|
+
return new Blob([bytes.buffer], { type: 'application/pdf' });
|
|
151
|
+
}
|
|
152
|
+
/** Download the chart as a `.pdf` file. */
|
|
153
|
+
export async function downloadChartPdf(source, filename = 'chart.pdf', options = {}) {
|
|
154
|
+
const blob = await chartToPdfBlob(source, options);
|
|
155
|
+
const url = URL.createObjectURL(blob);
|
|
156
|
+
const a = document.createElement('a');
|
|
157
|
+
a.href = url;
|
|
158
|
+
a.download = filename;
|
|
159
|
+
document.body.appendChild(a);
|
|
160
|
+
a.click();
|
|
161
|
+
a.remove();
|
|
162
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Print the chart: a new window with the standalone SVG (and the title
|
|
166
|
+
* above it), sized to the page, that opens the print dialog once it has
|
|
167
|
+
* drawn. Returns false when the window was blocked.
|
|
168
|
+
*/
|
|
169
|
+
export function printChart(source, options = {}) {
|
|
170
|
+
const svg = chartToSvgString(source, options);
|
|
171
|
+
const win = window.open('', '_blank', 'width=900,height=700');
|
|
172
|
+
if (!win)
|
|
173
|
+
return false;
|
|
174
|
+
const esc = (s) => s.replace(/[&<>]/g, (m) => ({ '&': '&', '<': '<', '>': '>' })[m]);
|
|
175
|
+
win.document.write(`<!doctype html><html><head><meta charset="utf-8"><title>${esc(options.title ?? 'Chart')}</title>` +
|
|
176
|
+
`<style>body{margin:24px;font-family:system-ui,sans-serif}h1{font-size:16px;margin:0 0 12px}svg{max-width:100%;height:auto}@page{margin:12mm}</style></head><body>` +
|
|
177
|
+
(options.title ? `<h1>${esc(options.title)}</h1>` : '') +
|
|
178
|
+
svg +
|
|
179
|
+
`</body></html>`);
|
|
180
|
+
win.document.close();
|
|
181
|
+
const go = () => { win.focus(); win.print(); };
|
|
182
|
+
if (win.document.readyState === 'complete')
|
|
183
|
+
setTimeout(go, 50);
|
|
184
|
+
else
|
|
185
|
+
win.addEventListener('load', () => setTimeout(go, 50));
|
|
186
|
+
return true;
|
|
187
|
+
}
|
package/dist/chart-export.d.ts
CHANGED
|
@@ -31,7 +31,8 @@ export declare function downloadChartPng(source: SVGSVGElement | HTMLElement, fi
|
|
|
31
31
|
*
|
|
32
32
|
* A series that carries more than one number per category widens instead of
|
|
33
33
|
* losing them. `ohlc` becomes four columns, `boxes` five plus its outliers,
|
|
34
|
-
* `errors` two
|
|
34
|
+
* `errors` two, `lowValues` a low / high pair, `targets` a value / target
|
|
35
|
+
* pair. This used to write `values` and nothing else, which quietly
|
|
35
36
|
* made "Export CSV" on a candlestick chart hand back the closing prices only -
|
|
36
37
|
* the screen-reader table was given the four real numbers on purpose and the
|
|
37
38
|
* export, which is what people actually take away, was not.
|
package/dist/chart-export.js
CHANGED
|
@@ -123,7 +123,9 @@ export async function downloadChartPng(source, filename = 'chart.png', options =
|
|
|
123
123
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
124
124
|
}
|
|
125
125
|
const csvCell = (v) => {
|
|
126
|
-
|
|
126
|
+
// A gap is an empty cell: `null` already was, and a NaN gap (the way a
|
|
127
|
+
// forecast series marks the months before it starts) wrote the word "NaN".
|
|
128
|
+
const s = v == null || (typeof v === 'number' && !Number.isFinite(v)) ? '' : String(v);
|
|
127
129
|
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
128
130
|
};
|
|
129
131
|
/**
|
|
@@ -132,7 +134,8 @@ const csvCell = (v) => {
|
|
|
132
134
|
*
|
|
133
135
|
* A series that carries more than one number per category widens instead of
|
|
134
136
|
* losing them. `ohlc` becomes four columns, `boxes` five plus its outliers,
|
|
135
|
-
* `errors` two
|
|
137
|
+
* `errors` two, `lowValues` a low / high pair, `targets` a value / target
|
|
138
|
+
* pair. This used to write `values` and nothing else, which quietly
|
|
136
139
|
* made "Export CSV" on a candlestick chart hand back the closing prices only -
|
|
137
140
|
* the screen-reader table was given the four real numbers on purpose and the
|
|
138
141
|
* export, which is what people actually take away, was not.
|
|
@@ -165,11 +168,15 @@ export function chartSpecToCsv(spec) {
|
|
|
165
168
|
const header = ['Category'];
|
|
166
169
|
for (const s of series) {
|
|
167
170
|
if (s.ohlc)
|
|
168
|
-
header.push(`${s.label} Open`, `${s.label} High`, `${s.label} Low`, `${s.label} Close`);
|
|
171
|
+
header.push(`${s.label} Open`, `${s.label} High`, `${s.label} Low`, `${s.label} Close`, ...(s.volumes ? [`${s.label} Volume`] : []));
|
|
169
172
|
else if (s.boxes)
|
|
170
173
|
header.push(`${s.label} Min`, `${s.label} Q1`, `${s.label} Median`, `${s.label} Q3`, `${s.label} Max`, `${s.label} Outliers`);
|
|
171
174
|
else if (s.errors)
|
|
172
175
|
header.push(s.label, `${s.label} Low`, `${s.label} High`);
|
|
176
|
+
else if (s.lowValues)
|
|
177
|
+
header.push(`${s.label} low`, `${s.label} high`);
|
|
178
|
+
else if (s.targets)
|
|
179
|
+
header.push(s.label, `${s.label} target`);
|
|
173
180
|
else
|
|
174
181
|
header.push(s.label);
|
|
175
182
|
}
|
|
@@ -179,6 +186,8 @@ export function chartSpecToCsv(spec) {
|
|
|
179
186
|
if (s.ohlc) {
|
|
180
187
|
const k = s.ohlc[i];
|
|
181
188
|
cells.push(k?.o ?? '', k?.h ?? '', k?.l ?? '', k?.c ?? '');
|
|
189
|
+
if (s.volumes)
|
|
190
|
+
cells.push(s.volumes[i] ?? '');
|
|
182
191
|
}
|
|
183
192
|
else if (s.boxes) {
|
|
184
193
|
const b = s.boxes[i];
|
|
@@ -192,6 +201,12 @@ export function chartSpecToCsv(spec) {
|
|
|
192
201
|
const hi = e == null ? '' : typeof e === 'number' ? (v ?? 0) + Math.abs(e) : Math.max(e.lo, e.hi);
|
|
193
202
|
cells.push(v ?? '', lo, hi);
|
|
194
203
|
}
|
|
204
|
+
else if (s.lowValues) {
|
|
205
|
+
cells.push(s.lowValues[i] ?? '', s.values[i] ?? '');
|
|
206
|
+
}
|
|
207
|
+
else if (s.targets) {
|
|
208
|
+
cells.push(s.values[i] ?? '', s.targets[i] ?? '');
|
|
209
|
+
}
|
|
195
210
|
else {
|
|
196
211
|
cells.push(s.values[i] ?? '');
|
|
197
212
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Price-chart helpers: candle transforms, calendar resampling and the
|
|
3
|
+
* row-to-candlestick adapter. Indicators live in chart-indicators. DOM-free.
|
|
4
|
+
*/
|
|
5
|
+
import type { ChartSpec, OhlcBar, SeriesOverlay } from './chart-types.js';
|
|
6
|
+
import { type ChartTimeBucket } from './chart-stats.js';
|
|
7
|
+
import type { ChartIndicatorSpec } from './chart-indicators.js';
|
|
8
|
+
/**
|
|
9
|
+
* Heikin-Ashi ("average bar") candles from ordinary OHLC bars. Each close is
|
|
10
|
+
* the mean of the four prices, each open the midpoint of the previous
|
|
11
|
+
* Heikin-Ashi body, and the high / low stretch to include both. Runs of one
|
|
12
|
+
* colour then read as trends, at the cost of the true open and close.
|
|
13
|
+
* `null` bars (gaps) stay null and reset the chain.
|
|
14
|
+
*/
|
|
15
|
+
export declare function heikinAshi(bars: ReadonlyArray<OhlcBar | null>): Array<OhlcBar | null>;
|
|
16
|
+
/**
|
|
17
|
+
* Roll daily (or finer) bars up to a coarser calendar bucket: the first
|
|
18
|
+
* open, the highest high, the lowest low, the last close, the summed volume.
|
|
19
|
+
* Categories are ISO dates and come back as the bucket starts (`bucketStart`),
|
|
20
|
+
* sorted; bars whose date does not parse are dropped. Pass `volumes` to get
|
|
21
|
+
* them summed alongside.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resampleOhlc(categories: ReadonlyArray<string>, ohlc: ReadonlyArray<OhlcBar | null>, opts: {
|
|
24
|
+
bucket: ChartTimeBucket;
|
|
25
|
+
volumes?: ReadonlyArray<number>;
|
|
26
|
+
}): {
|
|
27
|
+
categories: string[];
|
|
28
|
+
ohlc: Array<OhlcBar | null>;
|
|
29
|
+
volumes?: number[];
|
|
30
|
+
};
|
|
31
|
+
/** What the grid's chart panel stores for a price chart: the column ids. */
|
|
32
|
+
export type OhlcColumns = {
|
|
33
|
+
open: string | null;
|
|
34
|
+
high: string | null;
|
|
35
|
+
low: string | null;
|
|
36
|
+
close: string | null;
|
|
37
|
+
volume: string | null;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Guess the price columns from their names: the first numeric column called
|
|
41
|
+
* open / high / low / close (or last, price) / volume. What the chart panel
|
|
42
|
+
* shows before the reader picks anything; a picked column always wins.
|
|
43
|
+
*/
|
|
44
|
+
export declare function guessOhlcColumns(measures: ReadonlyArray<{
|
|
45
|
+
id: string;
|
|
46
|
+
field: string;
|
|
47
|
+
label: string;
|
|
48
|
+
}>, picked?: Partial<OhlcColumns> | null): OhlcColumns;
|
|
49
|
+
/**
|
|
50
|
+
* The `rowsToDirectSpec` options for the grid panel's price chart, or null
|
|
51
|
+
* while a price column is still unpicked. `fieldOf` maps a column id to its
|
|
52
|
+
* field name; the date column is the category.
|
|
53
|
+
*/
|
|
54
|
+
export declare function ohlcDirectOptions(fieldOf: (id: string | null) => string | null | undefined, dateId: string | null, cols: OhlcColumns, extra: {
|
|
55
|
+
bucket?: ChartTimeBucket | null;
|
|
56
|
+
candleStyle?: ChartSpec['candleStyle'] | null;
|
|
57
|
+
}): {
|
|
58
|
+
category: string;
|
|
59
|
+
ohlc: NonNullable<Parameters<typeof import('./chart.js').rowsToDirectSpec>[2]['ohlc']>;
|
|
60
|
+
} | null;
|
|
61
|
+
/** The indicator names the grid's chart panel offers. */
|
|
62
|
+
export type ChartPanelIndicator = 'volume' | 'rsi' | 'macd' | 'stochastic' | 'atr' | 'obv' | 'sma' | 'ema' | 'bb' | 'vwap';
|
|
63
|
+
/**
|
|
64
|
+
* Split the panel's indicator list into the panes that go under the price
|
|
65
|
+
* (in order) and the one overlay that rides on it (a series carries a single
|
|
66
|
+
* `overlay`, so the first overlay kind wins).
|
|
67
|
+
*/
|
|
68
|
+
export declare function splitPanelIndicators(kinds: ReadonlyArray<ChartPanelIndicator>): {
|
|
69
|
+
panes: ChartIndicatorSpec[];
|
|
70
|
+
overlay: SeriesOverlay | null;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* A candlestick spec straight from rows: one bar per date (sorted), with the
|
|
74
|
+
* volume alongside when a field is named, optionally rolled up to a calendar
|
|
75
|
+
* bucket first. The x axis is ordinal-time, so weekends and holidays leave no
|
|
76
|
+
* gaps.
|
|
77
|
+
*/
|
|
78
|
+
export declare function rowsToOhlcSpec<T extends Record<string, unknown>>(rows: ReadonlyArray<T>, opts: {
|
|
79
|
+
date: keyof T & string;
|
|
80
|
+
open: keyof T & string;
|
|
81
|
+
high: keyof T & string;
|
|
82
|
+
low: keyof T & string;
|
|
83
|
+
close: keyof T & string;
|
|
84
|
+
volume?: keyof T & string;
|
|
85
|
+
bucket?: ChartTimeBucket;
|
|
86
|
+
/** The series label. Default "Price". */
|
|
87
|
+
label?: string;
|
|
88
|
+
type?: 'candlestick' | 'ohlc';
|
|
89
|
+
candleStyle?: ChartSpec['candleStyle'];
|
|
90
|
+
lastPriceLine?: boolean;
|
|
91
|
+
}): ChartSpec;
|