@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,190 @@
|
|
|
1
|
+
import { round, formatChartValue, resolveColorScale, sampleGradient, pickContrastText, thinCategoryTicks } from './chart-scale.js';
|
|
2
|
+
// ---- Calendar heatmap --------------------------------------------
|
|
3
|
+
// GitHub-style year-of-days view: 7 rows (Sun..Sat) x N weeks. Each
|
|
4
|
+
// cell is a small square shaded by `calendarValues[i].value` via the
|
|
5
|
+
// sequential color scale. Days with no value render blank (border only)
|
|
6
|
+
// so missing data is visually obvious.
|
|
7
|
+
/** @internal Lay out the calendar family. Called by buildChart. */
|
|
8
|
+
export function layoutCalendar(ctx) {
|
|
9
|
+
const { spec, width, height, theme, empty, frame } = ctx;
|
|
10
|
+
const values = spec.calendarValues ?? [];
|
|
11
|
+
if (!values.length && !spec.calendarStart)
|
|
12
|
+
return { ...empty };
|
|
13
|
+
// Build a value lookup + figure out the date range.
|
|
14
|
+
const valueByDate = new Map();
|
|
15
|
+
let vMin = Infinity, vMax = -Infinity;
|
|
16
|
+
for (const v of values) {
|
|
17
|
+
valueByDate.set(v.date, v.value);
|
|
18
|
+
if (Number.isFinite(v.value)) {
|
|
19
|
+
if (v.value < vMin)
|
|
20
|
+
vMin = v.value;
|
|
21
|
+
if (v.value > vMax)
|
|
22
|
+
vMax = v.value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (vMin === Infinity) {
|
|
26
|
+
vMin = 0;
|
|
27
|
+
vMax = 1;
|
|
28
|
+
}
|
|
29
|
+
if (vMin === vMax)
|
|
30
|
+
vMax = vMin + 1;
|
|
31
|
+
const stops = resolveColorScale(spec.colorScale, vMin, vMax, theme);
|
|
32
|
+
const colorAt = (v) => sampleGradient(stops, (v - vMin) / (vMax - vMin));
|
|
33
|
+
// Determine date range. If calendarStart/End set, use them, otherwise
|
|
34
|
+
// span the data + round to whole weeks (Sun..Sat).
|
|
35
|
+
const sorted = values.map((v) => v.date).sort();
|
|
36
|
+
const startStr = spec.calendarStart ?? sorted[0] ?? '2026-01-01';
|
|
37
|
+
const endStr = spec.calendarEnd ?? sorted[sorted.length - 1] ?? startStr;
|
|
38
|
+
const start = new Date(startStr + 'T00:00:00Z');
|
|
39
|
+
const end = new Date(endStr + 'T00:00:00Z');
|
|
40
|
+
// Roll start back to its Sunday, end forward to its Saturday.
|
|
41
|
+
start.setUTCDate(start.getUTCDate() - start.getUTCDay());
|
|
42
|
+
end.setUTCDate(end.getUTCDate() + (6 - end.getUTCDay()));
|
|
43
|
+
const totalDays = Math.round((end.getTime() - start.getTime()) / 86400000) + 1;
|
|
44
|
+
const weeks = Math.ceil(totalDays / 7);
|
|
45
|
+
const padL = 36, padR = 80, padT = 26 + frame.top, padB = 16 + frame.bottom;
|
|
46
|
+
const plotW = Math.max(1, width - padL - padR);
|
|
47
|
+
const plotH = Math.max(1, height - padT - padB);
|
|
48
|
+
// Cell size: fit weeks across width, 7 rows down height.
|
|
49
|
+
const cellW = Math.floor(plotW / weeks);
|
|
50
|
+
const cellH = Math.floor(plotH / 7);
|
|
51
|
+
const cellSize = Math.max(6, Math.min(cellW, cellH));
|
|
52
|
+
const plot = { x: padL, y: padT, w: cellSize * weeks, h: cellSize * 7 };
|
|
53
|
+
const cells = [];
|
|
54
|
+
let lastMonth = -1;
|
|
55
|
+
const monthTicks = [];
|
|
56
|
+
for (let i = 0; i < totalDays; i += 1) {
|
|
57
|
+
const day = new Date(start.getTime() + i * 86400000);
|
|
58
|
+
const col = Math.floor(i / 7);
|
|
59
|
+
const row = i % 7;
|
|
60
|
+
const date = day.toISOString().slice(0, 10);
|
|
61
|
+
const has = valueByDate.has(date);
|
|
62
|
+
const v = valueByDate.get(date) ?? 0;
|
|
63
|
+
cells.push({
|
|
64
|
+
x: padL + col * cellSize,
|
|
65
|
+
y: padT + row * cellSize,
|
|
66
|
+
size: cellSize,
|
|
67
|
+
date, value: v,
|
|
68
|
+
defined: has,
|
|
69
|
+
color: has ? colorAt(v) : 'transparent',
|
|
70
|
+
});
|
|
71
|
+
// A month label only for a month inside the range: the end is rolled
|
|
72
|
+
// forward to its Saturday, and a December that ends on a Thursday used
|
|
73
|
+
// to pick up a "Jan" for the two padding days of the next year.
|
|
74
|
+
if (day.getUTCDate() === 1 && day.getUTCMonth() !== lastMonth && date >= startStr && date <= endStr) {
|
|
75
|
+
lastMonth = day.getUTCMonth();
|
|
76
|
+
monthTicks.push({
|
|
77
|
+
label: day.toLocaleDateString(undefined, { month: 'short' }),
|
|
78
|
+
x: padL + col * cellSize,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const legend = Array.from({ length: 5 }, (_, i) => {
|
|
83
|
+
const t = i / 4;
|
|
84
|
+
const value = legendValue(vMin, vMax, t);
|
|
85
|
+
return { value, color: colorAt(value), label: formatChartValue(value, spec.valueFormat, spec) };
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
...empty,
|
|
89
|
+
plot,
|
|
90
|
+
calendarCells: cells,
|
|
91
|
+
calendarMonthTicks: monthTicks,
|
|
92
|
+
calendarLegend: legend,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// ---- Heatmap ------------------------------------------------------
|
|
96
|
+
// Each series is one row, series.values are the cells across categories.
|
|
97
|
+
// Color comes from a sequential/diverging/custom palette mapped to the
|
|
98
|
+
// global value range. Cell text contrasts black/white against the cell.
|
|
99
|
+
/**
|
|
100
|
+
* A legend step between the extremes, rounded to the precision the range
|
|
101
|
+
* earns: whole numbers when the steps are a unit or more apart, so a count
|
|
102
|
+
* legend reads "20, 94, 167" rather than "20, 93.5, 167", and enough
|
|
103
|
+
* decimals to tell the steps apart when they are not.
|
|
104
|
+
*/
|
|
105
|
+
function legendValue(vMin, vMax, t) {
|
|
106
|
+
const raw = vMin + (vMax - vMin) * t;
|
|
107
|
+
const step = Math.abs(vMax - vMin) / 4;
|
|
108
|
+
if (!(step > 0))
|
|
109
|
+
return raw;
|
|
110
|
+
const decimals = step >= 1 ? 0 : Math.min(6, Math.ceil(-Math.log10(step)) + 1);
|
|
111
|
+
return Number(raw.toFixed(decimals));
|
|
112
|
+
}
|
|
113
|
+
/** @internal Lay out the heatmap family. Called by buildChart. */
|
|
114
|
+
export function layoutHeatmap(ctx) {
|
|
115
|
+
const { spec, series, width, height, theme, empty, frame } = ctx;
|
|
116
|
+
if (!series.length || !spec.categories.length)
|
|
117
|
+
return { ...empty, plot: { x: 0, y: 0, w: width, h: height } };
|
|
118
|
+
// Layout: left gutter for row labels, bottom for column labels.
|
|
119
|
+
const maxRowLabel = series.reduce((m, s) => Math.max(m, s.label.length), 0);
|
|
120
|
+
const padL = 12 + Math.min(180, Math.max(60, maxRowLabel * 7));
|
|
121
|
+
const padR = 64; // room for the right-side legend bar
|
|
122
|
+
const padT = 12 + frame.top;
|
|
123
|
+
const padB = 32 + frame.bottom;
|
|
124
|
+
const plotW = Math.max(1, width - padL - padR);
|
|
125
|
+
const plotH = Math.max(1, height - padT - padB);
|
|
126
|
+
const plot = { x: padL, y: padT, w: plotW, h: plotH };
|
|
127
|
+
const cellW = plotW / spec.categories.length;
|
|
128
|
+
const cellH = plotH / series.length;
|
|
129
|
+
// Resolve value range across the whole matrix.
|
|
130
|
+
let vMin = Infinity, vMax = -Infinity;
|
|
131
|
+
for (const s of series)
|
|
132
|
+
for (const v of s.values) {
|
|
133
|
+
if (!Number.isFinite(v))
|
|
134
|
+
continue;
|
|
135
|
+
if (v < vMin)
|
|
136
|
+
vMin = v;
|
|
137
|
+
if (v > vMax)
|
|
138
|
+
vMax = v;
|
|
139
|
+
}
|
|
140
|
+
if (vMin === Infinity) {
|
|
141
|
+
vMin = 0;
|
|
142
|
+
vMax = 1;
|
|
143
|
+
}
|
|
144
|
+
if (vMin === vMax)
|
|
145
|
+
vMax = vMin + 1;
|
|
146
|
+
// Pick the palette stops.
|
|
147
|
+
const stops = resolveColorScale(spec.colorScale, vMin, vMax, theme);
|
|
148
|
+
const colorAt = (v) => sampleGradient(stops, (v - vMin) / (vMax - vMin));
|
|
149
|
+
const heatmapCells = [];
|
|
150
|
+
series.forEach((s, ri) => {
|
|
151
|
+
s.values.forEach((v, ci) => {
|
|
152
|
+
if (!Number.isFinite(v))
|
|
153
|
+
return;
|
|
154
|
+
const color = colorAt(v);
|
|
155
|
+
heatmapCells.push({
|
|
156
|
+
x: round(padL + cellW * ci),
|
|
157
|
+
y: round(padT + cellH * ri),
|
|
158
|
+
w: round(cellW),
|
|
159
|
+
h: round(cellH),
|
|
160
|
+
color,
|
|
161
|
+
textColor: pickContrastText(color),
|
|
162
|
+
value: v,
|
|
163
|
+
rowLabel: s.label,
|
|
164
|
+
colLabel: spec.categories[ci] ?? '',
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
const heatmapRowTicks = series.map((s, i) => ({
|
|
169
|
+
value: i,
|
|
170
|
+
y: round(padT + cellH * i + cellH / 2),
|
|
171
|
+
label: s.label,
|
|
172
|
+
}));
|
|
173
|
+
// Column labels thin out the way a bar chart's do: twenty-four hours at
|
|
174
|
+
// 33px a cell wrote "00:0001:0002:00" across the foot of the matrix.
|
|
175
|
+
const heatmapColTicks = thinCategoryTicks(spec.categories, (i) => round(padL + cellW * i + cellW / 2), cellW, false);
|
|
176
|
+
// Legend: sample 5 stops across the range.
|
|
177
|
+
const heatmapLegend = Array.from({ length: 5 }, (_, i) => {
|
|
178
|
+
const t = i / 4;
|
|
179
|
+
const value = legendValue(vMin, vMax, t);
|
|
180
|
+
return { value, color: colorAt(value), label: formatChartValue(value, spec.valueFormat, spec) };
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
...empty,
|
|
184
|
+
plot,
|
|
185
|
+
heatmapCells,
|
|
186
|
+
heatmapRowTicks,
|
|
187
|
+
heatmapColTicks,
|
|
188
|
+
heatmapLegend,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hierarchy layouts: the squarified tree-map.
|
|
3
|
+
*/
|
|
4
|
+
import type { TreeNode, ChartGeometry, LayoutCtx } from './chart-types.js';
|
|
5
|
+
/** @internal Lay out the treemap family. Called by buildChart. */
|
|
6
|
+
export declare function layoutTreemap(ctx: LayoutCtx): ChartGeometry;
|
|
7
|
+
/**
|
|
8
|
+
* The subtree at `path` (a list of node names from the root's children down),
|
|
9
|
+
* or the tree itself for an empty path. What a drilldown shows after each
|
|
10
|
+
* click: the clicked node becomes the root. Null when the path does not exist.
|
|
11
|
+
*/
|
|
12
|
+
export declare function drillTree(tree: TreeNode, path: ReadonlyArray<string>): TreeNode | null;
|
|
13
|
+
/**
|
|
14
|
+
* The path (node names from the root's children) to the first node called
|
|
15
|
+
* `name`, depth first, or null. What a click on a leaf's label needs.
|
|
16
|
+
*/
|
|
17
|
+
export declare function pathTo(tree: TreeNode, name: string): string[] | null;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { round, pickContrastText, DEFAULT_PALETTE } from './chart-scale.js';
|
|
2
|
+
// ---- Tree-map -----------------------------------------------------
|
|
3
|
+
// Squarified tree-map (Bruls et al. 2000): each level recursively
|
|
4
|
+
// partitions its rectangle in proportion to its children, picking the
|
|
5
|
+
// split orientation that keeps aspect ratios closest to 1.
|
|
6
|
+
/** @internal Lay out the treemap family. Called by buildChart. */
|
|
7
|
+
export function layoutTreemap(ctx) {
|
|
8
|
+
const { spec, width, height, empty, frame } = ctx;
|
|
9
|
+
const root = spec.treemap ?? spec.tree;
|
|
10
|
+
if (!root)
|
|
11
|
+
return { ...empty };
|
|
12
|
+
const padL = 4, padR = 4, padT = 4 + frame.top, padB = 4 + frame.bottom;
|
|
13
|
+
const plotW = Math.max(1, width - padL - padR);
|
|
14
|
+
const plotH = Math.max(1, height - padT - padB);
|
|
15
|
+
const plot = { x: padL, y: padT, w: plotW, h: plotH };
|
|
16
|
+
const palette = spec.palette ?? DEFAULT_PALETTE;
|
|
17
|
+
const cells = [];
|
|
18
|
+
function totalOf(n) {
|
|
19
|
+
if (n.children?.length)
|
|
20
|
+
return n.children.reduce((s, c) => s + totalOf(c), 0);
|
|
21
|
+
return Math.max(0, n.value ?? 0);
|
|
22
|
+
}
|
|
23
|
+
function squarify(items, x, y, w, h, depth) {
|
|
24
|
+
if (!items.length || w <= 0 || h <= 0)
|
|
25
|
+
return;
|
|
26
|
+
const totals = items.map(totalOf);
|
|
27
|
+
const sum = totals.reduce((a, b) => a + b, 0);
|
|
28
|
+
if (sum <= 0)
|
|
29
|
+
return;
|
|
30
|
+
// Process largest-first so big items dominate the first row.
|
|
31
|
+
const ordered = items
|
|
32
|
+
.map((n, i) => ({ node: n, value: totals[i] }))
|
|
33
|
+
.sort((a, b) => b.value - a.value);
|
|
34
|
+
let cx = x, cy = y, cw = w, ch = h, remaining = sum;
|
|
35
|
+
let row = [];
|
|
36
|
+
const worstRatio = (vals, shortSide, rowSum, scale) => {
|
|
37
|
+
if (rowSum <= 0)
|
|
38
|
+
return Infinity;
|
|
39
|
+
const rowArea = rowSum * scale;
|
|
40
|
+
const rowSide = rowArea / shortSide;
|
|
41
|
+
let worst = 0;
|
|
42
|
+
for (const v of vals) {
|
|
43
|
+
const cell = v * scale;
|
|
44
|
+
const long = cell / rowSide;
|
|
45
|
+
const r = Math.max(shortSide / long, long / shortSide);
|
|
46
|
+
if (r > worst)
|
|
47
|
+
worst = r;
|
|
48
|
+
}
|
|
49
|
+
return worst;
|
|
50
|
+
};
|
|
51
|
+
function flushRow() {
|
|
52
|
+
if (!row.length)
|
|
53
|
+
return;
|
|
54
|
+
const rowSum = row.reduce((a, b) => a + b.value, 0);
|
|
55
|
+
const scale = (cw * ch) / remaining;
|
|
56
|
+
const horizontal = cw >= ch;
|
|
57
|
+
const shortSide = horizontal ? ch : cw;
|
|
58
|
+
const rowSide = (rowSum * scale) / shortSide;
|
|
59
|
+
let offset = 0;
|
|
60
|
+
for (const it of row) {
|
|
61
|
+
const cellSize = (it.value * scale) / rowSide;
|
|
62
|
+
const cx2 = horizontal ? cx : cx + offset;
|
|
63
|
+
const cy2 = horizontal ? cy + offset : cy;
|
|
64
|
+
const ww = horizontal ? rowSide : cellSize;
|
|
65
|
+
const hh = horizontal ? cellSize : rowSide;
|
|
66
|
+
const color = it.node.color ?? palette[(depth + cells.length) % palette.length];
|
|
67
|
+
// Leaf: emit a cell. Branch: recurse into the rect minus a label gutter.
|
|
68
|
+
if (it.node.children?.length) {
|
|
69
|
+
cells.push({
|
|
70
|
+
x: round(cx2), y: round(cy2), w: round(ww), h: round(hh),
|
|
71
|
+
color, textColor: pickContrastText(color),
|
|
72
|
+
name: it.node.name, value: it.value, depth,
|
|
73
|
+
});
|
|
74
|
+
const labelH = Math.min(18, hh * 0.25);
|
|
75
|
+
squarify(it.node.children, cx2 + 1, cy2 + labelH, ww - 2, hh - labelH - 1, depth + 1);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
cells.push({
|
|
79
|
+
x: round(cx2), y: round(cy2), w: round(ww), h: round(hh),
|
|
80
|
+
color, textColor: pickContrastText(color),
|
|
81
|
+
name: it.node.name, value: it.value, depth,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
offset += cellSize;
|
|
85
|
+
}
|
|
86
|
+
// Shrink the remaining strip.
|
|
87
|
+
if (horizontal) {
|
|
88
|
+
cx += rowSide;
|
|
89
|
+
cw -= rowSide;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
cy += rowSide;
|
|
93
|
+
ch -= rowSide;
|
|
94
|
+
}
|
|
95
|
+
remaining -= rowSum;
|
|
96
|
+
row = [];
|
|
97
|
+
}
|
|
98
|
+
for (const it of ordered) {
|
|
99
|
+
const scale = (cw * ch) / remaining;
|
|
100
|
+
const shortSide = Math.min(cw, ch);
|
|
101
|
+
const rowSum = row.reduce((a, b) => a + b.value, 0);
|
|
102
|
+
const currWorst = worstRatio(row.map((r) => r.value), shortSide, rowSum, scale);
|
|
103
|
+
const nextWorst = worstRatio([...row.map((r) => r.value), it.value], shortSide, rowSum + it.value, scale);
|
|
104
|
+
if (row.length && nextWorst > currWorst) {
|
|
105
|
+
flushRow();
|
|
106
|
+
}
|
|
107
|
+
row.push(it);
|
|
108
|
+
}
|
|
109
|
+
flushRow();
|
|
110
|
+
}
|
|
111
|
+
const seedItems = root.children ?? [root];
|
|
112
|
+
squarify(seedItems, padL, padT, plotW, plotH, 0);
|
|
113
|
+
return { ...empty, plot, treemapCells: cells };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The subtree at `path` (a list of node names from the root's children down),
|
|
117
|
+
* or the tree itself for an empty path. What a drilldown shows after each
|
|
118
|
+
* click: the clicked node becomes the root. Null when the path does not exist.
|
|
119
|
+
*/
|
|
120
|
+
export function drillTree(tree, path) {
|
|
121
|
+
let node = tree;
|
|
122
|
+
for (const name of path) {
|
|
123
|
+
node = node?.children?.find((c) => c.name === name);
|
|
124
|
+
if (!node)
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return node ?? null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The path (node names from the root's children) to the first node called
|
|
131
|
+
* `name`, depth first, or null. What a click on a leaf's label needs.
|
|
132
|
+
*/
|
|
133
|
+
export function pathTo(tree, name) {
|
|
134
|
+
const walk = (n, acc) => {
|
|
135
|
+
for (const c of n.children ?? []) {
|
|
136
|
+
const next = [...acc, c.name];
|
|
137
|
+
if (c.name === name)
|
|
138
|
+
return next;
|
|
139
|
+
const deeper = walk(c, next);
|
|
140
|
+
if (deeper)
|
|
141
|
+
return deeper;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
};
|
|
145
|
+
return walk(tree, []);
|
|
146
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
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 { ChartSpec, OhlcBar, SeriesOverlay } from './chart-types.js';
|
|
13
|
+
import { type RegressionFit } from './chart-stats.js';
|
|
14
|
+
/** Which indicator a pane shows, and with what parameters. */
|
|
15
|
+
export type ChartIndicatorSpec = {
|
|
16
|
+
kind: 'rsi' | 'macd' | 'stochastic' | 'atr' | 'obv' | 'volume';
|
|
17
|
+
/** The series (by label) the indicator reads. Defaults to the first
|
|
18
|
+
* series with `ohlc`, else the first series. */
|
|
19
|
+
source?: string;
|
|
20
|
+
/** Periods: `period` (RSI / ATR / stochastic %K), `fast` / `slow` /
|
|
21
|
+
* `signal` (MACD), `smooth` (stochastic %D). Each has the usual default. */
|
|
22
|
+
params?: {
|
|
23
|
+
period?: number;
|
|
24
|
+
fast?: number;
|
|
25
|
+
slow?: number;
|
|
26
|
+
signal?: number;
|
|
27
|
+
smooth?: number;
|
|
28
|
+
};
|
|
29
|
+
/** Pane height in px; the default is a third of the main chart. */
|
|
30
|
+
height?: number;
|
|
31
|
+
};
|
|
32
|
+
/** Weighted moving average: the latest value weighs `period`, the oldest 1. */
|
|
33
|
+
export declare function wma(values: ReadonlyArray<number>, period: number): number[];
|
|
34
|
+
/**
|
|
35
|
+
* Bollinger bands: a simple moving average with a band `k` standard
|
|
36
|
+
* deviations either side (population deviation, as the terminals compute it).
|
|
37
|
+
*/
|
|
38
|
+
export declare function bollingerBands(values: ReadonlyArray<number>, period?: number, k?: number): {
|
|
39
|
+
middle: number[];
|
|
40
|
+
upper: number[];
|
|
41
|
+
lower: number[];
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Relative strength index over `period` bars, 0 to 100. The first value lands
|
|
45
|
+
* at index `period`; before it there is nothing to average.
|
|
46
|
+
*/
|
|
47
|
+
export declare function rsi(values: ReadonlyArray<number>, period?: number): number[];
|
|
48
|
+
/**
|
|
49
|
+
* Moving average convergence / divergence: the fast EMA minus the slow one,
|
|
50
|
+
* its signal EMA, and the histogram between them.
|
|
51
|
+
*/
|
|
52
|
+
export declare function macd(values: ReadonlyArray<number>, fast?: number, slow?: number, signal?: number): {
|
|
53
|
+
macd: number[];
|
|
54
|
+
signal: number[];
|
|
55
|
+
histogram: number[];
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Volume-weighted average price, cumulative from the first bar. Reset it per
|
|
59
|
+
* session by resampling or slicing before calling.
|
|
60
|
+
*/
|
|
61
|
+
export declare function vwap(ohlc: ReadonlyArray<OhlcBar | null | undefined>, volumes: ReadonlyArray<number>): number[];
|
|
62
|
+
/** Average true range with Wilder's smoothing. */
|
|
63
|
+
export declare function atr(ohlc: ReadonlyArray<OhlcBar | null | undefined>, period?: number): number[];
|
|
64
|
+
/**
|
|
65
|
+
* Stochastic oscillator: %K is where the close sits in the last `period`
|
|
66
|
+
* bars' range (0 to 100), %D its `smooth`-bar simple average.
|
|
67
|
+
*/
|
|
68
|
+
export declare function stochastic(ohlc: ReadonlyArray<OhlcBar | null | undefined>, period?: number, smooth?: number): {
|
|
69
|
+
k: number[];
|
|
70
|
+
d: number[];
|
|
71
|
+
};
|
|
72
|
+
/** On-balance volume: a running total that adds volume on up closes and
|
|
73
|
+
* subtracts it on down closes. */
|
|
74
|
+
export declare function obv(closes: ReadonlyArray<number>, volumes: ReadonlyArray<number>): number[];
|
|
75
|
+
/**
|
|
76
|
+
* Compute overlay values for a series spec like 'sma:7' / 'ema:14' / 'wma:9' /
|
|
77
|
+
* 'linear'. `'bb:N:K'` returns the band's middle line (the engine draws the
|
|
78
|
+
* band from `bollingerBands` itself) and `'vwap'` needs bars and volumes, so
|
|
79
|
+
* it comes back as gaps here; pass `extra` for those two.
|
|
80
|
+
*/
|
|
81
|
+
export declare function computeOverlay(values: number[], spec: SeriesOverlay, extra?: {
|
|
82
|
+
ohlc?: ReadonlyArray<OhlcBar | null>;
|
|
83
|
+
volumes?: ReadonlyArray<number>;
|
|
84
|
+
}): number[];
|
|
85
|
+
/**
|
|
86
|
+
* {@link computeOverlay} plus what a regression knows about itself: its
|
|
87
|
+
* R-squared and equation. The moving averages, the bands and VWAP carry
|
|
88
|
+
* neither.
|
|
89
|
+
*/
|
|
90
|
+
export declare function computeOverlayFit(values: number[], spec: SeriesOverlay, extra?: {
|
|
91
|
+
ohlc?: ReadonlyArray<OhlcBar | null>;
|
|
92
|
+
volumes?: ReadonlyArray<number>;
|
|
93
|
+
}): {
|
|
94
|
+
values: number[];
|
|
95
|
+
r2?: number;
|
|
96
|
+
equation?: string;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* The regression an overlay string names (`linear`, `poly:N`, `exp`, `log`,
|
|
100
|
+
* `power`), fitted to `values` on their index or on `xs`; `null` for the
|
|
101
|
+
* moving averages and bands, which are not regressions. A scatter passes its
|
|
102
|
+
* points' x and y so the curve is y on x, and reads `predict` to draw it.
|
|
103
|
+
*/
|
|
104
|
+
export declare function regressionFit(values: ReadonlyArray<number>, spec: SeriesOverlay, xs?: ReadonlyArray<number>): RegressionFit | null;
|
|
105
|
+
/** The name a tooltip or legend gives an overlay: `sma:20` reads "SMA 20",
|
|
106
|
+
* `poly:3` "poly 3", `bb:20:2` "BB 20", the rest as written. */
|
|
107
|
+
export declare function overlayName(spec: SeriesOverlay): string;
|
|
108
|
+
/**
|
|
109
|
+
* Build the spec of an indicator pane from a price spec: the same categories
|
|
110
|
+
* and x axis, the indicator's lines or bars, and the reference lines the
|
|
111
|
+
* indicator is read against (30 / 70 for RSI, 20 / 80 for the stochastic, a
|
|
112
|
+
* zero line for MACD). `SvChartPanes` stacks these under the price chart and
|
|
113
|
+
* syncs them; a pane is also a complete spec for a standalone `SvChart`.
|
|
114
|
+
*/
|
|
115
|
+
export declare function indicatorPane(spec: ChartSpec, ind: ChartIndicatorSpec): ChartSpec;
|