@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,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart spec diagnostics. A `ChartSpec` is plain data, and plain data gets
|
|
3
|
+
* typos: a `serie` key, a `values` array one short of the categories, a log
|
|
4
|
+
* axis pinned at zero. The engine draws what it can and stays quiet, which
|
|
5
|
+
* is right for production and wrong for the developer who wonders why a bar
|
|
6
|
+
* is missing. `validateChartSpec` says what is off, with the path of the
|
|
7
|
+
* field and a severity: `error` for what will not draw as intended,
|
|
8
|
+
* `warning` for what is ignored or looks like a slip.
|
|
9
|
+
*
|
|
10
|
+
* Pure and dependency-free (types only), so the same rules run in `SvChart`
|
|
11
|
+
* in development, in the MCP server's code check and in a test. The key and
|
|
12
|
+
* type lists are exported for the JSON schema's drift guard.
|
|
13
|
+
*/
|
|
14
|
+
import type { ChartSeries, ChartSpec, ChartType } from './chart-types.js';
|
|
15
|
+
/** One finding of {@link validateChartSpec}. */
|
|
16
|
+
export type ChartDiagnostic = {
|
|
17
|
+
/** A stable rule id, `chart/<rule>`. */
|
|
18
|
+
code: string;
|
|
19
|
+
/** The field's path in the spec, like `series[1].values` or `yAxis.min`. */
|
|
20
|
+
path: string;
|
|
21
|
+
message: string;
|
|
22
|
+
/** `error`: will not draw as intended. `warning`: ignored, or a likely slip. */
|
|
23
|
+
severity: 'error' | 'warning';
|
|
24
|
+
};
|
|
25
|
+
/** Every `ChartType` the engine draws. */
|
|
26
|
+
export declare const CHART_TYPES: ReadonlyArray<ChartType>;
|
|
27
|
+
/** Every top-level key a `ChartSpec` may carry. */
|
|
28
|
+
export declare const KNOWN_SPEC_KEYS: ReadonlyArray<keyof ChartSpec>;
|
|
29
|
+
/** Every key a `ChartSeries` may carry. */
|
|
30
|
+
export declare const KNOWN_SERIES_KEYS: ReadonlyArray<keyof ChartSeries>;
|
|
31
|
+
/** The known key closest to `key`, when it is close enough to be a typo. */
|
|
32
|
+
export declare function nearestKey(key: string, known: ReadonlyArray<string>): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Log every finding of a spec once per spec object, the way `SvChart` does in
|
|
35
|
+
* development, and return them. A spec already reported (by any chart) is
|
|
36
|
+
* skipped, so a spec shared by ten charts warns once.
|
|
37
|
+
*/
|
|
38
|
+
export declare function warnChartSpec(spec: unknown, log?: (line: string) => void): ChartDiagnostic[];
|
|
39
|
+
/**
|
|
40
|
+
* Check a spec and return every finding, an empty array for a clean one. Never
|
|
41
|
+
* throws: a spec that is not even an object comes back as one error.
|
|
42
|
+
*/
|
|
43
|
+
export declare function validateChartSpec(spec: unknown): ChartDiagnostic[];
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/** Every `ChartType` the engine draws. */
|
|
2
|
+
export const CHART_TYPES = [
|
|
3
|
+
'bar', 'line', 'area', 'pie', 'scatter', 'heatmap', 'waterfall', 'funnel', 'radar', 'calendar', 'gauge',
|
|
4
|
+
'treemap', 'sankey', 'candlestick', 'ohlc', 'boxplot', 'histogram', 'range-bar', 'range-area', 'lollipop',
|
|
5
|
+
'dumbbell', 'pareto', 'stream', 'sunburst', 'radial-bar', 'radial-column', 'nightingale', 'chord', 'bullet',
|
|
6
|
+
];
|
|
7
|
+
/** Every top-level key a `ChartSpec` may carry. */
|
|
8
|
+
export const KNOWN_SPEC_KEYS = [
|
|
9
|
+
'type', 'categories', 'series', 'width', 'height', 'palette', 'categoryColors', 'valueFormat', 'locale',
|
|
10
|
+
'currency', 'categoryGroups', 'stacked', 'stacked100', 'orientation', 'innerRadius', 'referenceLines',
|
|
11
|
+
'xType', 'yAxisTitle', 'y2AxisTitle', 'xAxisTitle', 'yScale', 'y2Scale', 'xAxis', 'yAxis', 'y2Axis',
|
|
12
|
+
'title', 'subtitle', 'caption', 'referenceBands', 'nullAs', 'decimate', 'dataLabels', 'annotations',
|
|
13
|
+
'seriesLabels', 'responsive', 'style', 'drawings', 'lastPriceLine', 'patternFallback', 'calendarValues',
|
|
14
|
+
'calendarStart', 'calendarEnd', 'gaugeValue', 'gaugeMin', 'gaugeMax', 'gaugeTarget', 'gaugeRanges',
|
|
15
|
+
'gaugeUnit', 'treemap', 'sankeyNodes', 'sankeyLinks', 'waterfallTotals', 'waterfallColors', 'candleColors',
|
|
16
|
+
'candleStyle', 'funnelShape', 'stackOffset', 'binEdges', 'tree', 'bulletRanges', 'colorScale',
|
|
17
|
+
];
|
|
18
|
+
/** Every key a `ChartSeries` may carry. */
|
|
19
|
+
export const KNOWN_SERIES_KEYS = [
|
|
20
|
+
'label', 'values', 'color', 'type', 'ohlc', 'volumes', 'boxes', 'errors', 'axis', 'points', 'rowIds',
|
|
21
|
+
'overlay', 'overlayColor', 'pattern', 'smooth', 'upperValues', 'lowerValues', 'marker', 'markers', 'colors',
|
|
22
|
+
'stack', 'strokeWidth', 'dash', 'opacity', 'visible', 'gradient', 'step', 'connectNulls', 'nullAs',
|
|
23
|
+
'lowValues', 'targets',
|
|
24
|
+
];
|
|
25
|
+
const SERIES_TYPES = new Set(['bar', 'line', 'area', 'scatter', 'candlestick', 'ohlc', 'boxplot', 'range-bar', 'range-area', 'lollipop', 'dumbbell']);
|
|
26
|
+
/** The series arrays that must run parallel to `categories`. */
|
|
27
|
+
const PER_CATEGORY = ['values', 'rowIds', 'upperValues', 'lowerValues', 'ohlc', 'boxes', 'errors', 'markers', 'colors', 'lowValues', 'targets', 'volumes'];
|
|
28
|
+
/** Families whose `series[].values` are not indexed by `categories`. */
|
|
29
|
+
const FREE_LENGTH_TYPES = new Set(['scatter', 'gauge', 'treemap', 'sankey', 'calendar', 'sunburst', 'chord', 'histogram']);
|
|
30
|
+
const OVERLAY_RE = /^(linear|exp|log|power|vwap|sma:\d+|ema:\d+|wma:\d+|bb:\d+:\d+(?:\.\d+)?|poly:\d+)$/;
|
|
31
|
+
/** Levenshtein distance, for a "did you mean" on a misspelt key. */
|
|
32
|
+
function distance(a, b) {
|
|
33
|
+
const dp = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
34
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
35
|
+
let prev = dp[0];
|
|
36
|
+
dp[0] = i;
|
|
37
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
38
|
+
const tmp = dp[j];
|
|
39
|
+
dp[j] = Math.min(dp[j] + 1, dp[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
40
|
+
prev = tmp;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return dp[b.length];
|
|
44
|
+
}
|
|
45
|
+
/** The known key closest to `key`, when it is close enough to be a typo. */
|
|
46
|
+
export function nearestKey(key, known) {
|
|
47
|
+
let best = null;
|
|
48
|
+
let bestD = Infinity;
|
|
49
|
+
for (const k of known) {
|
|
50
|
+
const d = distance(key.toLowerCase(), k.toLowerCase());
|
|
51
|
+
if (d < bestD) {
|
|
52
|
+
bestD = d;
|
|
53
|
+
best = k;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return best !== null && bestD <= Math.max(1, Math.floor(key.length / 3)) ? best : null;
|
|
57
|
+
}
|
|
58
|
+
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
59
|
+
const isNum = (v) => typeof v === 'number';
|
|
60
|
+
const warnedSpecs = new WeakSet();
|
|
61
|
+
/**
|
|
62
|
+
* Log every finding of a spec once per spec object, the way `SvChart` does in
|
|
63
|
+
* development, and return them. A spec already reported (by any chart) is
|
|
64
|
+
* skipped, so a spec shared by ten charts warns once.
|
|
65
|
+
*/
|
|
66
|
+
export function warnChartSpec(spec, log = (line) => console.warn(line)) {
|
|
67
|
+
if (typeof spec !== 'object' || spec === null || warnedSpecs.has(spec))
|
|
68
|
+
return [];
|
|
69
|
+
warnedSpecs.add(spec);
|
|
70
|
+
const out = validateChartSpec(spec);
|
|
71
|
+
for (const d of out)
|
|
72
|
+
log(`[SvChart] ${d.path ? d.path + ': ' : ''}${d.message}`);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Check a spec and return every finding, an empty array for a clean one. Never
|
|
77
|
+
* throws: a spec that is not even an object comes back as one error.
|
|
78
|
+
*/
|
|
79
|
+
export function validateChartSpec(spec) {
|
|
80
|
+
const out = [];
|
|
81
|
+
const err = (code, path, message) => out.push({ code: `chart/${code}`, path, message, severity: 'error' });
|
|
82
|
+
const warn = (code, path, message) => out.push({ code: `chart/${code}`, path, message, severity: 'warning' });
|
|
83
|
+
if (!isObj(spec)) {
|
|
84
|
+
err('not-an-object', '', 'a ChartSpec is an object with type, categories and series');
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
const s = spec;
|
|
88
|
+
// ---- Keys and type -------------------------------------------------------
|
|
89
|
+
for (const k of Object.keys(s)) {
|
|
90
|
+
if (KNOWN_SPEC_KEYS.includes(k))
|
|
91
|
+
continue;
|
|
92
|
+
const near = nearestKey(k, KNOWN_SPEC_KEYS);
|
|
93
|
+
warn('unknown-key', k, near ? `unknown field "${k}"; did you mean "${near}"?` : `unknown field "${k}" is ignored`);
|
|
94
|
+
}
|
|
95
|
+
const type = s.type;
|
|
96
|
+
if (typeof type !== 'string' || !CHART_TYPES.includes(type)) {
|
|
97
|
+
const near = typeof type === 'string' ? nearestKey(type, CHART_TYPES) : null;
|
|
98
|
+
err('unknown-type', 'type', near ? `unknown chart type "${String(type)}"; did you mean "${near}"?` : `unknown chart type "${String(type)}"`);
|
|
99
|
+
}
|
|
100
|
+
const categories = Array.isArray(s.categories) ? s.categories : null;
|
|
101
|
+
if (!categories)
|
|
102
|
+
err('categories', 'categories', 'categories must be an array of labels');
|
|
103
|
+
const n = categories?.length ?? 0;
|
|
104
|
+
const series = Array.isArray(s.series) ? s.series : null;
|
|
105
|
+
if (!series)
|
|
106
|
+
err('series', 'series', 'series must be an array');
|
|
107
|
+
// ---- Series ----------------------------------------------------------------
|
|
108
|
+
const fixedLength = typeof type === 'string' && !FREE_LENGTH_TYPES.has(type);
|
|
109
|
+
series?.forEach((sr, i) => {
|
|
110
|
+
const p = `series[${i}]`;
|
|
111
|
+
if (!isObj(sr)) {
|
|
112
|
+
err('series-shape', p, 'a series is an object with label and values');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
for (const k of Object.keys(sr)) {
|
|
116
|
+
if (KNOWN_SERIES_KEYS.includes(k))
|
|
117
|
+
continue;
|
|
118
|
+
const near = nearestKey(k, KNOWN_SERIES_KEYS);
|
|
119
|
+
warn('unknown-series-key', `${p}.${k}`, near ? `unknown series field "${k}"; did you mean "${near}"?` : `unknown series field "${k}" is ignored`);
|
|
120
|
+
}
|
|
121
|
+
if (typeof sr.label !== 'string')
|
|
122
|
+
warn('series-label', `${p}.label`, 'a series wants a string label; the legend and tooltip name it');
|
|
123
|
+
if (!Array.isArray(sr.values)) {
|
|
124
|
+
err('series-values', `${p}.values`, 'values must be an array of numbers');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const values = sr.values;
|
|
128
|
+
if (fixedLength && categories && values.length !== n && !(sr.points && values.length === 0)) {
|
|
129
|
+
err('series-length', `${p}.values`, `${values.length} values for ${n} categories; they run in parallel`);
|
|
130
|
+
}
|
|
131
|
+
for (const key of PER_CATEGORY) {
|
|
132
|
+
if (key === 'values')
|
|
133
|
+
continue;
|
|
134
|
+
const arr = sr[key];
|
|
135
|
+
if (arr === undefined)
|
|
136
|
+
continue;
|
|
137
|
+
if (!Array.isArray(arr)) {
|
|
138
|
+
err('series-array', `${p}.${key}`, `${key} must be an array parallel to values`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (fixedLength && arr.length !== values.length)
|
|
142
|
+
err('series-length', `${p}.${key}`, `${key} has ${arr.length} entries for ${values.length} values; they run in parallel`);
|
|
143
|
+
}
|
|
144
|
+
if (sr.type !== undefined && (typeof sr.type !== 'string' || !SERIES_TYPES.has(sr.type)))
|
|
145
|
+
err('series-type', `${p}.type`, `unknown series type "${String(sr.type)}"`);
|
|
146
|
+
if (sr.axis !== undefined && sr.axis !== 'left' && sr.axis !== 'right')
|
|
147
|
+
err('series-axis', `${p}.axis`, `axis is 'left' or 'right', not "${String(sr.axis)}"`);
|
|
148
|
+
if (sr.stack !== undefined) {
|
|
149
|
+
const kind = sr.type ?? type;
|
|
150
|
+
if (kind !== 'bar' && kind !== 'area')
|
|
151
|
+
warn('stack-kind', `${p}.stack`, 'stack groups apply to bar and area series; ignored here');
|
|
152
|
+
}
|
|
153
|
+
if (sr.overlay !== undefined && (typeof sr.overlay !== 'string' || !OVERLAY_RE.test(sr.overlay)))
|
|
154
|
+
err('overlay', `${p}.overlay`, `unknown overlay "${String(sr.overlay)}"; expected linear, poly:N, exp, log, power, sma:N, ema:N, wma:N, bb:N:K or vwap`);
|
|
155
|
+
if (Array.isArray(sr.ohlc)) {
|
|
156
|
+
;
|
|
157
|
+
sr.ohlc.forEach((b, j) => {
|
|
158
|
+
if (b === null || b === undefined)
|
|
159
|
+
return;
|
|
160
|
+
if (!isObj(b) || !isNum(b.o) || !isNum(b.h) || !isNum(b.l) || !isNum(b.c))
|
|
161
|
+
err('ohlc-shape', `${p}.ohlc[${j}]`, 'an OHLC bar is { o, h, l, c } or null');
|
|
162
|
+
else if (b.l > b.h)
|
|
163
|
+
err('ohlc-range', `${p}.ohlc[${j}]`, `low ${b.l} is above high ${b.h}`);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (Array.isArray(sr.boxes)) {
|
|
167
|
+
;
|
|
168
|
+
sr.boxes.forEach((b, j) => {
|
|
169
|
+
if (b === null || b === undefined)
|
|
170
|
+
return;
|
|
171
|
+
if (!isObj(b) || !isNum(b.min) || !isNum(b.q1) || !isNum(b.median) || !isNum(b.q3) || !isNum(b.max))
|
|
172
|
+
err('box-shape', `${p}.boxes[${j}]`, 'a box is { min, q1, median, q3, max } or null');
|
|
173
|
+
else if (b.q1 > b.median || b.median > b.q3)
|
|
174
|
+
err('box-order', `${p}.boxes[${j}]`, 'a box wants q1 <= median <= q3');
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (Array.isArray(sr.points)) {
|
|
178
|
+
;
|
|
179
|
+
sr.points.forEach((pt, j) => {
|
|
180
|
+
if (!isObj(pt) || !isNum(pt.x) || !isNum(pt.y))
|
|
181
|
+
err('point-shape', `${p}.points[${j}]`, 'a scatter point is { x, y, r?, label? }');
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
// ---- Axes ----------------------------------------------------------------
|
|
186
|
+
for (const key of ['xAxis', 'yAxis', 'y2Axis']) {
|
|
187
|
+
const ax = s[key];
|
|
188
|
+
if (ax === undefined)
|
|
189
|
+
continue;
|
|
190
|
+
if (!isObj(ax)) {
|
|
191
|
+
err('axis-shape', key, `${key} must be an object`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (isNum(ax.min) && isNum(ax.max) && ax.min >= ax.max)
|
|
195
|
+
err('axis-range', `${key}.min`, `min ${ax.min} is not below max ${ax.max}`);
|
|
196
|
+
if (ax.scale === 'log' && isNum(ax.min) && ax.min <= 0)
|
|
197
|
+
err('axis-log-min', `${key}.min`, 'a log axis cannot start at or below zero');
|
|
198
|
+
if (ax.scale === 'log' && key === 'xAxis' && ax.type !== undefined && ax.type !== 'number')
|
|
199
|
+
warn('axis-log-x', `${key}.scale`, "a log x axis needs type: 'number'");
|
|
200
|
+
if (ax.tickCount !== undefined && (!isNum(ax.tickCount) || ax.tickCount <= 0))
|
|
201
|
+
err('axis-ticks', `${key}.tickCount`, 'tickCount must be a positive number');
|
|
202
|
+
if (ax.labelRotation !== undefined && ax.labelRotation !== 'auto' && !isNum(ax.labelRotation))
|
|
203
|
+
err('axis-rotation', `${key}.labelRotation`, "labelRotation is a number of degrees or 'auto'");
|
|
204
|
+
if (ax.scale !== undefined && ax.scale !== 'linear' && ax.scale !== 'log')
|
|
205
|
+
err('axis-scale', `${key}.scale`, `scale is 'linear' or 'log', not "${String(ax.scale)}"`);
|
|
206
|
+
}
|
|
207
|
+
// ---- Spec-level shapes -----------------------------------------------------
|
|
208
|
+
if (s.stacked100 && typeof type === 'string' && type !== 'bar' && type !== 'area')
|
|
209
|
+
warn('stacked100-kind', 'stacked100', 'stacked100 applies to bar and area charts; ignored here');
|
|
210
|
+
if (s.innerRadius !== undefined && (!isNum(s.innerRadius) || s.innerRadius < 0 || s.innerRadius > 1))
|
|
211
|
+
err('inner-radius', 'innerRadius', 'innerRadius is a fraction 0..1 of the pie radius');
|
|
212
|
+
if (Array.isArray(s.categoryGroups) && categories) {
|
|
213
|
+
const span = s.categoryGroups.reduce((a, g) => a + (isObj(g) && isNum(g.span) ? g.span : 0), 0);
|
|
214
|
+
if (span !== n)
|
|
215
|
+
err('category-groups', 'categoryGroups', `the group spans add up to ${span}, not the ${n} categories`);
|
|
216
|
+
}
|
|
217
|
+
if (Array.isArray(s.binEdges) && categories && s.binEdges.length !== n + 1)
|
|
218
|
+
err('bin-edges', 'binEdges', `binEdges wants ${n + 1} edges for ${n} bins`);
|
|
219
|
+
if (Array.isArray(s.waterfallTotals) && categories && s.waterfallTotals.length !== n)
|
|
220
|
+
err('waterfall-totals', 'waterfallTotals', `waterfallTotals has ${s.waterfallTotals.length} flags for ${n} categories`);
|
|
221
|
+
if (Array.isArray(s.responsive)) {
|
|
222
|
+
;
|
|
223
|
+
s.responsive.forEach((r, i) => {
|
|
224
|
+
const p = `responsive[${i}]`;
|
|
225
|
+
if (!isObj(r)) {
|
|
226
|
+
err('responsive-shape', p, 'a rule is { maxWidth?, minWidth?, maxHeight?, spec?, legend? }');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (!isNum(r.maxWidth) && !isNum(r.minWidth) && !isNum(r.maxHeight))
|
|
230
|
+
warn('responsive-size', p, 'a rule with no maxWidth, minWidth or maxHeight always applies');
|
|
231
|
+
if (isObj(r.spec)) {
|
|
232
|
+
for (const k of ['width', 'height', 'responsive'])
|
|
233
|
+
if (k in r.spec)
|
|
234
|
+
warn('responsive-size-key', `${p}.spec.${k}`, `${k} in a responsive rule is ignored; a rule cannot change the size`);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// Anchors on the category axis that name no category.
|
|
239
|
+
if (categories && typeof type === 'string' && !FREE_LENGTH_TYPES.has(type)) {
|
|
240
|
+
const known = new Set(categories.map(String));
|
|
241
|
+
const isTimeOrNumber = (s.xType ?? (isObj(s.xAxis) ? s.xAxis.type : undefined));
|
|
242
|
+
const anchored = isTimeOrNumber === 'time' || isTimeOrNumber === 'number';
|
|
243
|
+
const check = (list, path, valueOf) => {
|
|
244
|
+
if (!Array.isArray(list) || anchored)
|
|
245
|
+
return;
|
|
246
|
+
list.forEach((item, i) => {
|
|
247
|
+
if (!isObj(item))
|
|
248
|
+
return;
|
|
249
|
+
const v = valueOf(item);
|
|
250
|
+
if (typeof v === 'string' && !known.has(v))
|
|
251
|
+
warn('unknown-category', `${path}[${i}]`, `"${v}" is not one of the categories, so it has no place on the axis`);
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
check(s.annotations, 'annotations', (a) => (isObj(a.at) ? a.at.category : undefined));
|
|
255
|
+
check(Array.isArray(s.referenceLines) ? s.referenceLines.filter((r) => isObj(r) && r.axis === 'x') : [], 'referenceLines', (r) => r.value);
|
|
256
|
+
// On a time or number axis a drawing's x is a timestamp or a number the
|
|
257
|
+
// axis places itself; the tools write one for every pointer position, so
|
|
258
|
+
// matching those against the category list warned on every drawing.
|
|
259
|
+
if (Array.isArray(s.drawings) && !anchored) {
|
|
260
|
+
;
|
|
261
|
+
s.drawings.forEach((d, i) => {
|
|
262
|
+
if (!isObj(d) || !Array.isArray(d.points))
|
|
263
|
+
return;
|
|
264
|
+
d.points.forEach((pt, j) => {
|
|
265
|
+
if (isObj(pt) && typeof pt.x === 'string' && !known.has(pt.x))
|
|
266
|
+
warn('unknown-category', `drawings[${i}].points[${j}].x`, `"${pt.x}" is not one of the categories`);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zoom-window arithmetic for the chart: the category window a wheel, a pinch,
|
|
3
|
+
* a pan or a range preset produces. Pure functions over indices and dates, so
|
|
4
|
+
* the renderer's gestures are thin and the maths is unit-tested.
|
|
5
|
+
*/
|
|
6
|
+
import type { ChartZoomWindow } from './chart-types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Zoom the window in or out around an anchor category, keeping the anchor at
|
|
9
|
+
* the same relative position. `factor < 1` zooms in, `> 1` out; a null window
|
|
10
|
+
* means the whole axis. Never narrower than 2 categories.
|
|
11
|
+
*/
|
|
12
|
+
export declare function wheelWindow(win: ChartZoomWindow | null, n: number, anchor: number, factor: number): ChartZoomWindow | null;
|
|
13
|
+
/** Slide the window by `delta` categories, clamped to the axis. */
|
|
14
|
+
export declare function panWindow(win: ChartZoomWindow | null, n: number, delta: number): ChartZoomWindow | null;
|
|
15
|
+
/**
|
|
16
|
+
* Resize the window by a pinch: `scale > 1` spreads the fingers (zoom in),
|
|
17
|
+
* around the category under the pinch midpoint.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pinchWindow(win: ChartZoomWindow | null, n: number, midpoint: number, scale: number): ChartZoomWindow | null;
|
|
20
|
+
/** The range presets a time axis offers. */
|
|
21
|
+
export type ChartRangePreset = '1W' | '1M' | '3M' | '6M' | 'YTD' | '1Y' | 'All' | {
|
|
22
|
+
label: string;
|
|
23
|
+
days?: number;
|
|
24
|
+
from?: string;
|
|
25
|
+
to?: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The window a preset selects on a time / ordinal-time axis whose categories
|
|
29
|
+
* parse as dates: the last N days up to the latest category, the year to
|
|
30
|
+
* date, or an explicit from / to. `'All'` (and a preset nothing matches)
|
|
31
|
+
* returns null, the whole axis. `now` defaults to the last category, which is
|
|
32
|
+
* what "1M" means on a chart that stops last quarter.
|
|
33
|
+
*/
|
|
34
|
+
export declare function presetWindow(categories: ReadonlyArray<string>, preset: ChartRangePreset, now?: number): ChartZoomWindow | null;
|
|
35
|
+
/**
|
|
36
|
+
* The category whose date is nearest to `t`, for syncing a hover between two
|
|
37
|
+
* charts whose categories differ. Categories that do not parse are skipped;
|
|
38
|
+
* returns -1 when none do.
|
|
39
|
+
*/
|
|
40
|
+
export declare function nearestIndexByTime(categories: ReadonlyArray<string>, t: number): number;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** Clamp a window to [0, n-1] with at least one category in it. */
|
|
2
|
+
function clampWindow(i0, i1, n) {
|
|
3
|
+
if (n <= 0)
|
|
4
|
+
return null;
|
|
5
|
+
let a = Math.max(0, Math.min(n - 1, Math.round(i0)));
|
|
6
|
+
let b = Math.max(0, Math.min(n - 1, Math.round(i1)));
|
|
7
|
+
if (b < a)
|
|
8
|
+
[a, b] = [b, a];
|
|
9
|
+
if (a === 0 && b === n - 1)
|
|
10
|
+
return null;
|
|
11
|
+
return { i0: a, i1: b };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Zoom the window in or out around an anchor category, keeping the anchor at
|
|
15
|
+
* the same relative position. `factor < 1` zooms in, `> 1` out; a null window
|
|
16
|
+
* means the whole axis. Never narrower than 2 categories.
|
|
17
|
+
*/
|
|
18
|
+
export function wheelWindow(win, n, anchor, factor) {
|
|
19
|
+
if (n <= 2)
|
|
20
|
+
return null;
|
|
21
|
+
const i0 = win ? win.i0 : 0;
|
|
22
|
+
const i1 = win ? win.i1 : n - 1;
|
|
23
|
+
const span = i1 - i0 + 1;
|
|
24
|
+
const next = Math.max(2, Math.min(n, Math.round(span * factor)));
|
|
25
|
+
if (next === span)
|
|
26
|
+
return win;
|
|
27
|
+
// Where the anchor sits inside the window, 0..1, so it stays put.
|
|
28
|
+
const rel = span > 1 ? (Math.max(i0, Math.min(i1, anchor)) - i0) / (span - 1) : 0.5;
|
|
29
|
+
let a = Math.round(anchor - rel * (next - 1));
|
|
30
|
+
a = Math.max(0, Math.min(n - next, a));
|
|
31
|
+
return clampWindow(a, a + next - 1, n);
|
|
32
|
+
}
|
|
33
|
+
/** Slide the window by `delta` categories, clamped to the axis. */
|
|
34
|
+
export function panWindow(win, n, delta) {
|
|
35
|
+
if (!win || !delta)
|
|
36
|
+
return win;
|
|
37
|
+
const span = win.i1 - win.i0;
|
|
38
|
+
let a = win.i0 + Math.round(delta);
|
|
39
|
+
a = Math.max(0, Math.min(n - 1 - span, a));
|
|
40
|
+
return clampWindow(a, a + span, n);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resize the window by a pinch: `scale > 1` spreads the fingers (zoom in),
|
|
44
|
+
* around the category under the pinch midpoint.
|
|
45
|
+
*/
|
|
46
|
+
export function pinchWindow(win, n, midpoint, scale) {
|
|
47
|
+
if (!Number.isFinite(scale) || scale <= 0)
|
|
48
|
+
return win;
|
|
49
|
+
return wheelWindow(win, n, midpoint, 1 / scale);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The window a preset selects on a time / ordinal-time axis whose categories
|
|
53
|
+
* parse as dates: the last N days up to the latest category, the year to
|
|
54
|
+
* date, or an explicit from / to. `'All'` (and a preset nothing matches)
|
|
55
|
+
* returns null, the whole axis. `now` defaults to the last category, which is
|
|
56
|
+
* what "1M" means on a chart that stops last quarter.
|
|
57
|
+
*/
|
|
58
|
+
export function presetWindow(categories, preset, now) {
|
|
59
|
+
const n = categories.length;
|
|
60
|
+
if (!n)
|
|
61
|
+
return null;
|
|
62
|
+
const times = categories.map((c) => Date.parse(c));
|
|
63
|
+
const last = [...times].reverse().find(Number.isFinite);
|
|
64
|
+
if (last == null)
|
|
65
|
+
return null;
|
|
66
|
+
const end = now ?? last;
|
|
67
|
+
const DAY = 86400000;
|
|
68
|
+
let from;
|
|
69
|
+
let to = end;
|
|
70
|
+
if (typeof preset === 'object') {
|
|
71
|
+
if (preset.from)
|
|
72
|
+
from = Date.parse(preset.from);
|
|
73
|
+
else if (preset.days)
|
|
74
|
+
from = end - preset.days * DAY;
|
|
75
|
+
else
|
|
76
|
+
return null;
|
|
77
|
+
if (preset.to)
|
|
78
|
+
to = Date.parse(preset.to);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
switch (preset) {
|
|
82
|
+
case '1W':
|
|
83
|
+
from = end - 7 * DAY;
|
|
84
|
+
break;
|
|
85
|
+
case '1M':
|
|
86
|
+
from = shiftMonths(end, -1);
|
|
87
|
+
break;
|
|
88
|
+
case '3M':
|
|
89
|
+
from = shiftMonths(end, -3);
|
|
90
|
+
break;
|
|
91
|
+
case '6M':
|
|
92
|
+
from = shiftMonths(end, -6);
|
|
93
|
+
break;
|
|
94
|
+
case '1Y':
|
|
95
|
+
from = shiftMonths(end, -12);
|
|
96
|
+
break;
|
|
97
|
+
case 'YTD': {
|
|
98
|
+
const d = new Date(end);
|
|
99
|
+
from = Date.UTC(d.getUTCFullYear(), 0, 1);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
default: return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!Number.isFinite(from) || !Number.isFinite(to))
|
|
106
|
+
return null;
|
|
107
|
+
let i0 = -1;
|
|
108
|
+
let i1 = -1;
|
|
109
|
+
for (let i = 0; i < n; i += 1) {
|
|
110
|
+
const t = times[i];
|
|
111
|
+
if (!Number.isFinite(t))
|
|
112
|
+
continue;
|
|
113
|
+
if (t >= from && i0 < 0)
|
|
114
|
+
i0 = i;
|
|
115
|
+
if (t <= to)
|
|
116
|
+
i1 = i;
|
|
117
|
+
}
|
|
118
|
+
if (i0 < 0 || i1 < 0 || i1 < i0)
|
|
119
|
+
return null;
|
|
120
|
+
return clampWindow(i0, i1, n);
|
|
121
|
+
}
|
|
122
|
+
function shiftMonths(t, months) {
|
|
123
|
+
const d = new Date(t);
|
|
124
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months, d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes());
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The category whose date is nearest to `t`, for syncing a hover between two
|
|
128
|
+
* charts whose categories differ. Categories that do not parse are skipped;
|
|
129
|
+
* returns -1 when none do.
|
|
130
|
+
*/
|
|
131
|
+
export function nearestIndexByTime(categories, t) {
|
|
132
|
+
let best = -1;
|
|
133
|
+
let bestD = Infinity;
|
|
134
|
+
for (let i = 0; i < categories.length; i += 1) {
|
|
135
|
+
const ct = Date.parse(categories[i]);
|
|
136
|
+
if (!Number.isFinite(ct))
|
|
137
|
+
continue;
|
|
138
|
+
const d = Math.abs(ct - t);
|
|
139
|
+
if (d < bestD) {
|
|
140
|
+
bestD = d;
|
|
141
|
+
best = i;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return best;
|
|
145
|
+
}
|