@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
package/src/GridMenus.svelte
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import SvListBox from "./SvListBox.svelte";
|
|
15
15
|
import type {
|
|
16
16
|
FilterOperator,
|
|
17
|
+
MenuPosition,
|
|
17
18
|
} from "./SvGrid.types";
|
|
18
19
|
import {
|
|
19
20
|
getEditorInputType,
|
|
@@ -175,6 +176,29 @@
|
|
|
175
176
|
import("./SvDateRangeInput.svelte").then((m) => (MenuRangePicker = m.default));
|
|
176
177
|
});
|
|
177
178
|
|
|
179
|
+
/**
|
|
180
|
+
* The context menu is placed on a guessed height (280px, flipped above the
|
|
181
|
+
* click when that would not fit) before it exists. A consumer's menu can be
|
|
182
|
+
* far taller - the spreadsheet shell's has thirteen entries - and then the
|
|
183
|
+
* guess leaves its tail below the viewport. Once the menu is in the DOM its
|
|
184
|
+
* real box is measured and the position pulled back inside: up when it runs
|
|
185
|
+
* off the bottom, left when it runs off the right. The style reads
|
|
186
|
+
* `contextMenuPos`, so writing it moves the menu; the action's `update`
|
|
187
|
+
* runs on every position change and settles in one pass.
|
|
188
|
+
*/
|
|
189
|
+
function keepInViewport(node: HTMLElement, _pos: MenuPosition) {
|
|
190
|
+
const fit = () => {
|
|
191
|
+
const pos = ctrl.contextMenuPos;
|
|
192
|
+
const box = node.getBoundingClientRect();
|
|
193
|
+
if (!box.width && !box.height) return;
|
|
194
|
+
const x = Math.max(8, Math.min(pos.x, window.innerWidth - box.width - 8));
|
|
195
|
+
const y = Math.max(8, Math.min(pos.y, window.innerHeight - box.height - 8));
|
|
196
|
+
if (x !== pos.x || y !== pos.y) ctrl.contextMenuPos = { x, y };
|
|
197
|
+
};
|
|
198
|
+
fit();
|
|
199
|
+
return { update: fit };
|
|
200
|
+
}
|
|
201
|
+
|
|
178
202
|
/**
|
|
179
203
|
* Opening the filter popover used to leave focus on the funnel button
|
|
180
204
|
* outside it, so a keyboard user landed nowhere and had to tab in past the
|
|
@@ -832,6 +856,8 @@
|
|
|
832
856
|
{/if}
|
|
833
857
|
|
|
834
858
|
{#if contextMenuFor}
|
|
859
|
+
{@const items = contextMenuItems()}
|
|
860
|
+
{@const gutter = items.some((it) => it.icon)}
|
|
835
861
|
<div
|
|
836
862
|
class="sv-grid-menu-backdrop"
|
|
837
863
|
role="presentation"
|
|
@@ -843,8 +869,9 @@
|
|
|
843
869
|
data-svgrid-menu={ctrl.gridDomId}
|
|
844
870
|
role="menu"
|
|
845
871
|
style={`left: ${contextMenuPos.x}px; top: ${contextMenuPos.y}px;`}
|
|
872
|
+
use:keepInViewport={contextMenuPos}
|
|
846
873
|
>
|
|
847
|
-
{#each
|
|
874
|
+
{#each items as item (item.key)}
|
|
848
875
|
{#if item.separator}
|
|
849
876
|
<div class="sv-grid-menu-sep"></div>
|
|
850
877
|
{:else}
|
|
@@ -854,7 +881,22 @@
|
|
|
854
881
|
role="menuitem"
|
|
855
882
|
disabled={item.disabled}
|
|
856
883
|
onclick={() => { item.run?.(); closeContextMenu(); }}
|
|
857
|
-
>
|
|
884
|
+
>
|
|
885
|
+
<!-- Every item keeps the gutter once one has an icon, so the labels
|
|
886
|
+
line up. The label is its own span with no whitespace text
|
|
887
|
+
around it, so the button reads exactly its label. -->
|
|
888
|
+
{#if gutter}<span class="sv-grid-menu-icon" aria-hidden="true">
|
|
889
|
+
{#if typeof item.icon === "string"}
|
|
890
|
+
{@render icon(item.icon)}
|
|
891
|
+
{:else if item.icon}
|
|
892
|
+
<svg class="sv-grid-menu-icon-own" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
|
893
|
+
{#each item.icon.paths as path, i (i)}
|
|
894
|
+
<path d={path.d} fill={path.fill ? "currentColor" : "none"} stroke-width={path.width} stroke-dasharray={path.dash} />
|
|
895
|
+
{/each}
|
|
896
|
+
</svg>
|
|
897
|
+
{/if}
|
|
898
|
+
</span>{/if}<span class="sv-grid-menu-label">{item.label}</span>
|
|
899
|
+
</button>
|
|
858
900
|
{/if}
|
|
859
901
|
{/each}
|
|
860
902
|
</div>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The chart's right-click menu: the same portalled, focus-trapped,
|
|
4
|
+
* dismissable panel `SvContextMenu` opens, without the zone `<div>` that
|
|
5
|
+
* component wraps its children in. The chart owns its own `contextmenu`
|
|
6
|
+
* handler (the SVG is the zone) and hands this the items and a position.
|
|
7
|
+
*
|
|
8
|
+
* Lazy-loaded by `SvGridChart` on the first right-click, so a chart that is
|
|
9
|
+
* never right-clicked never pays for the menu machinery.
|
|
10
|
+
*/
|
|
11
|
+
import { portalToBody, popIn } from './popover'
|
|
12
|
+
import { createDismissableLayer, onScrollOutside } from './a11y/dismissable'
|
|
13
|
+
import { createFocusTrap } from './a11y/focus-trap'
|
|
14
|
+
import SvMenuList, { type MenuItem } from './SvMenuList.svelte'
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
items: ReadonlyArray<MenuItem>
|
|
18
|
+
x: number
|
|
19
|
+
y: number
|
|
20
|
+
onClose: () => void
|
|
21
|
+
ariaLabel?: string
|
|
22
|
+
}
|
|
23
|
+
let { items, x, y, onClose, ariaLabel = 'Chart menu' }: Props = $props()
|
|
24
|
+
|
|
25
|
+
let panelEl = $state<HTMLDivElement | null>(null)
|
|
26
|
+
const MENU_W = 220
|
|
27
|
+
const pos = $derived.by(() => {
|
|
28
|
+
if (typeof window === 'undefined') return { x, y }
|
|
29
|
+
const estH = Math.min(items.length, 12) * 34 + 8
|
|
30
|
+
return {
|
|
31
|
+
x: Math.max(6, Math.min(x, window.innerWidth - MENU_W - 6)),
|
|
32
|
+
y: Math.max(6, Math.min(y, window.innerHeight - estH - 6)),
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
$effect(() => {
|
|
37
|
+
if (!panelEl) return
|
|
38
|
+
const trap = createFocusTrap(panelEl, {
|
|
39
|
+
initialFocus: () => panelEl?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])') ?? null,
|
|
40
|
+
})
|
|
41
|
+
trap.activate()
|
|
42
|
+
const layer = createDismissableLayer({ element: () => panelEl, onDismiss: onClose })
|
|
43
|
+
layer.activate()
|
|
44
|
+
const offScroll = onScrollOutside(() => panelEl, onClose)
|
|
45
|
+
return () => { layer.release(); trap.release(); offScroll() }
|
|
46
|
+
})
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<div
|
|
50
|
+
bind:this={panelEl}
|
|
51
|
+
class="sv-ctx sv-grid-chart-menu"
|
|
52
|
+
use:portalToBody
|
|
53
|
+
use:popIn={{}}
|
|
54
|
+
style:position="fixed"
|
|
55
|
+
style:top={`${pos.y}px`}
|
|
56
|
+
style:left={`${pos.x}px`}
|
|
57
|
+
aria-label={ariaLabel}
|
|
58
|
+
>
|
|
59
|
+
<SvMenuList {items} onclose={onClose} onselect={() => onClose()} />
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
:global(.sv-grid-chart-menu) {
|
|
64
|
+
z-index: 2147483646; min-width: 220px;
|
|
65
|
+
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
66
|
+
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
|
|
67
|
+
box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
|
|
68
|
+
}
|
|
69
|
+
</style>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvChartPanes - a price chart with indicator panes stacked under it, the
|
|
4
|
+
* way a trading terminal lays them out: one shared x axis (labelled on the
|
|
5
|
+
* last pane only), one value-axis gutter width so the plots line up, and
|
|
6
|
+
* one crosshair / zoom window across all of them.
|
|
7
|
+
*
|
|
8
|
+
* Composition, not a new layout: every pane is a full `SvChart` in a sync
|
|
9
|
+
* group of its own, so tooltips, keyboard navigation, the screen-reader
|
|
10
|
+
* table, export and the context menu all come for free, and the geometry
|
|
11
|
+
* stays single-plot. The main chart takes every `SvChart` prop.
|
|
12
|
+
*/
|
|
13
|
+
import SvGridChart from './SvGridChart.svelte'
|
|
14
|
+
import { indicatorPane, type ChartIndicatorSpec } from './chart-indicators'
|
|
15
|
+
import type { ChartSpec, ChartZoomWindow } from './chart-types'
|
|
16
|
+
import type { SvChartProps } from './SvGridChart.types'
|
|
17
|
+
|
|
18
|
+
type Props = Omit<SvChartProps, 'spec' | 'syncGroup' | 'zoom'> & {
|
|
19
|
+
/** The price spec: a candlestick / OHLC / line chart with the series the
|
|
20
|
+
* indicators read (`ohlc` and `volumes` on it for the volume-based ones). */
|
|
21
|
+
spec: ChartSpec
|
|
22
|
+
/** The panes under the price, top to bottom. */
|
|
23
|
+
indicators?: ChartIndicatorSpec[]
|
|
24
|
+
/** A sync group to share with charts outside the stack. Default: private. */
|
|
25
|
+
syncGroup?: string
|
|
26
|
+
/** Width of the value-axis gutter every pane uses, so the plots line up.
|
|
27
|
+
* Default 56. */
|
|
28
|
+
axisWidth?: number
|
|
29
|
+
/** The shared zoom window, bindable. */
|
|
30
|
+
zoom?: ChartZoomWindow | null
|
|
31
|
+
}
|
|
32
|
+
let {
|
|
33
|
+
spec,
|
|
34
|
+
indicators = [],
|
|
35
|
+
syncGroup,
|
|
36
|
+
axisWidth = 56,
|
|
37
|
+
zoom = $bindable(null),
|
|
38
|
+
onZoom,
|
|
39
|
+
zoomable,
|
|
40
|
+
legend,
|
|
41
|
+
toolbar,
|
|
42
|
+
...rest
|
|
43
|
+
}: Props = $props()
|
|
44
|
+
|
|
45
|
+
const uid = `svcp-${Math.random().toString(36).slice(2, 8)}`
|
|
46
|
+
const group = $derived(syncGroup ?? uid)
|
|
47
|
+
// Every pane is laid out at the stack's pixel width, so the panes line up
|
|
48
|
+
// and none of them is a 520px viewBox stretched across a 900px column.
|
|
49
|
+
// Heights stay what the specs say; only the width follows the container.
|
|
50
|
+
let hostW = $state(0)
|
|
51
|
+
const widthOf = (s: ChartSpec) => (hostW > 0 && !s.width ? { width: Math.round(hostW) } : {})
|
|
52
|
+
/** The price chart hides its x labels when a pane below will show them. */
|
|
53
|
+
const mainSpec = $derived<ChartSpec>({
|
|
54
|
+
...spec,
|
|
55
|
+
...widthOf(spec),
|
|
56
|
+
xAxis: { ...spec.xAxis, ...(indicators.length ? { labels: false } : {}) },
|
|
57
|
+
yAxis: { ...spec.yAxis, width: axisWidth },
|
|
58
|
+
...(spec.y2Axis || spec.series.some((s) => s.axis === 'right') ? { y2Axis: { ...spec.y2Axis, width: axisWidth } } : {}),
|
|
59
|
+
})
|
|
60
|
+
// The pane's title is drawn by this component as a small corner label
|
|
61
|
+
// rather than by the chart frame, which would take a third of a 100px
|
|
62
|
+
// pane for it.
|
|
63
|
+
const panes = $derived(
|
|
64
|
+
indicators.map((ind, i) => {
|
|
65
|
+
const { title, ...pane } = indicatorPane(spec, ind)
|
|
66
|
+
return {
|
|
67
|
+
title: title ?? ind.kind,
|
|
68
|
+
spec: {
|
|
69
|
+
...pane,
|
|
70
|
+
...widthOf(pane),
|
|
71
|
+
xAxis: { ...pane.xAxis, labels: i === indicators.length - 1 },
|
|
72
|
+
yAxis: { ...pane.yAxis, width: axisWidth },
|
|
73
|
+
} satisfies ChartSpec,
|
|
74
|
+
}
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<div class="sv-chart-panes" bind:clientWidth={hostW}>
|
|
80
|
+
<div class="sv-chart-pane is-main">
|
|
81
|
+
<SvGridChart spec={mainSpec} syncGroup={group} bind:zoom {onZoom} {zoomable} {legend} {toolbar} {...rest} />
|
|
82
|
+
</div>
|
|
83
|
+
{#each panes as pane, i (`${indicators[i]?.kind}-${i}`)}
|
|
84
|
+
<div class="sv-chart-pane is-indicator" data-indicator={indicators[i]?.kind}>
|
|
85
|
+
<span class="sv-chart-pane-title" style:left={`${axisWidth + 6}px`}>{pane.title}</span>
|
|
86
|
+
<SvGridChart spec={pane.spec} syncGroup={group} zoomable={zoomable ? { drag: false, wheel: typeof zoomable === 'object' ? zoomable.wheel : false, pan: typeof zoomable === 'object' ? zoomable.pan : false } : false} legend={false} toolbar={false} interactive localeText={rest.localeText} />
|
|
87
|
+
</div>
|
|
88
|
+
{/each}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<style>
|
|
92
|
+
.sv-chart-panes {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: 2px;
|
|
96
|
+
min-width: 0;
|
|
97
|
+
}
|
|
98
|
+
.sv-chart-pane {
|
|
99
|
+
position: relative;
|
|
100
|
+
min-width: 0;
|
|
101
|
+
}
|
|
102
|
+
/* The title of a pane sits in its top-left corner, small, like a terminal. */
|
|
103
|
+
/* Placed just right of the value-axis gutter, clear of the tick labels. */
|
|
104
|
+
.sv-chart-pane-title {
|
|
105
|
+
position: absolute;
|
|
106
|
+
top: 2px;
|
|
107
|
+
z-index: 1;
|
|
108
|
+
font-size: 10px;
|
|
109
|
+
font-weight: 600;
|
|
110
|
+
color: var(--sg-muted, #64748b);
|
|
111
|
+
pointer-events: none;
|
|
112
|
+
}
|
|
113
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { flushSync, mount, unmount } from 'svelte'
|
|
3
|
+
import SvChartPanes from './SvChartPanes.svelte'
|
|
4
|
+
import type { ChartSpec, OhlcBar } from './chart-types'
|
|
5
|
+
|
|
6
|
+
let app: ReturnType<typeof mount> | null = null
|
|
7
|
+
let target: HTMLElement | null = null
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
if (app) unmount(app)
|
|
10
|
+
app = null
|
|
11
|
+
target?.remove()
|
|
12
|
+
target = null
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const closes = [44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00, 46.03, 46.41, 46.22, 45.64]
|
|
16
|
+
const bars: OhlcBar[] = closes.map((c, i) => ({ o: i ? closes[i - 1]! : c, h: c + 1, l: c - 1, c }))
|
|
17
|
+
const price: ChartSpec = {
|
|
18
|
+
type: 'candlestick',
|
|
19
|
+
categories: closes.map((_, i) => `2026-01-${String(i + 1).padStart(2, '0')}`),
|
|
20
|
+
series: [{ label: 'ACME', values: closes, ohlc: bars, volumes: closes.map((_, i) => 100 + i) }],
|
|
21
|
+
xType: 'ordinal-time',
|
|
22
|
+
width: 600,
|
|
23
|
+
height: 240,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function render(props: Record<string, unknown>) {
|
|
27
|
+
target = document.createElement('div')
|
|
28
|
+
document.body.appendChild(target)
|
|
29
|
+
app = mount(SvChartPanes, { target, props: props as never })
|
|
30
|
+
flushSync()
|
|
31
|
+
return target
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('SvChartPanes', () => {
|
|
35
|
+
it('stacks the price chart over one pane per indicator, x labels on the last pane only', () => {
|
|
36
|
+
const el = render({ spec: price, indicators: [{ kind: 'volume' }, { kind: 'rsi', params: { period: 5 } }] })
|
|
37
|
+
expect(el.querySelectorAll('.sv-grid-chart-svg')).toHaveLength(3)
|
|
38
|
+
const panes = [...el.querySelectorAll('.sv-chart-pane.is-indicator')]
|
|
39
|
+
expect(panes.map((p) => p.getAttribute('data-indicator'))).toEqual(['volume', 'rsi'])
|
|
40
|
+
expect(panes.map((p) => p.querySelector('.sv-chart-pane-title')!.textContent)).toEqual(['Volume', 'RSI 5'])
|
|
41
|
+
// Category (x) labels: none on the main chart and the volume pane, some on the RSI pane.
|
|
42
|
+
const xLabels = (root: Element) => root.querySelectorAll('text.sv-grid-chart-axis.is-x, .sv-grid-chart-xlabel').length
|
|
43
|
+
const [main, vol, rsi] = [el.querySelector('.sv-chart-pane.is-main')!, panes[0]!, panes[1]!]
|
|
44
|
+
expect(xLabels(rsi)).toBeGreaterThan(0)
|
|
45
|
+
expect(xLabels(main)).toBe(0)
|
|
46
|
+
expect(xLabels(vol)).toBe(0)
|
|
47
|
+
// The volume pane draws bars, the RSI pane a line with its 30 / 70 lines.
|
|
48
|
+
expect(vol.querySelectorAll('.sv-grid-chart-bar').length).toBe(20)
|
|
49
|
+
expect(rsi.querySelectorAll('.sv-grid-chart-refline').length).toBe(2)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('shares the crosshair across the stack through a private sync group', () => {
|
|
53
|
+
const el = render({ spec: price, indicators: [{ kind: 'volume' }] })
|
|
54
|
+
const main = el.querySelector('.sv-chart-pane.is-main')!
|
|
55
|
+
;(main.querySelectorAll('.sv-grid-chart-cat-hit')[3] as SVGElement).dispatchEvent(new FocusEvent('focus'))
|
|
56
|
+
flushSync()
|
|
57
|
+
expect(main.querySelector('.sv-grid-chart-crosshair')).toBeTruthy()
|
|
58
|
+
expect(el.querySelector('.sv-chart-pane.is-indicator .sv-grid-chart-crosshair')).toBeTruthy()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('with no indicators it is one chart with its own x labels', () => {
|
|
62
|
+
const el = render({ spec: price })
|
|
63
|
+
expect(el.querySelectorAll('.sv-grid-chart-svg')).toHaveLength(1)
|
|
64
|
+
expect(el.querySelectorAll('text.sv-grid-chart-axis.is-x, .sv-grid-chart-xlabel').length).toBeGreaterThan(0)
|
|
65
|
+
})
|
|
66
|
+
})
|