@svgrid/grid 2.2.27 → 2.2.28
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 +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +179 -179
- package/dist/GridMenus.svelte +734 -734
- package/dist/SvAutoComplete.svelte +165 -165
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +101 -101
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +275 -275
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +160 -160
- package/dist/SvDrawer.svelte +254 -250
- package/dist/SvDropDownList.svelte +374 -374
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGrid.css +2723 -2723
- package/dist/SvGrid.svelte +3333 -3333
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +696 -696
- package/dist/SvGridSelect.svelte +259 -259
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvListBox.svelte +287 -287
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +281 -281
- package/dist/SvNumberInput.svelte +168 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -131
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +200 -200
- package/dist/SvSwitchButton.svelte +107 -107
- package/dist/SvTagsInput.svelte +112 -112
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +204 -204
- package/dist/SvTree.svelte +437 -437
- package/dist/SvTreeSelect.svelte +233 -233
- package/dist/cdn/{GridMenus-BzxEYNtW.js → GridMenus-BbzEvTYO.js} +2 -2
- package/dist/cdn/{GridMenus-Dr-nGEXn.js → GridMenus-E220X2kM.js} +2 -2
- package/dist/cdn/{SvDateTimePicker-B1sDc9Pz.js → SvDateTimePicker-B8GopjhC.js} +2 -2
- package/dist/cdn/{SvDateTimePicker-Bh0FTs1E.js → SvDateTimePicker-DB0aIEk8.js} +2 -2
- package/dist/cdn/{SvGridChart-Bgk4hDs8.js → SvGridChart-BcLCYvw9.js} +1 -1
- package/dist/cdn/{SvGridChart-BKQsO5fZ.js → SvGridChart-BhnFdLd0.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DkCc2Z7b.js → SvGridChartPanel-Cmx9DHAS.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DSkHIQDd.js → SvGridChartPanel-DvjwxLvK.js} +1 -1
- package/dist/cdn/{SvGridChartView-BrbVaELm.js → SvGridChartView-B40DTJSX.js} +1 -1
- package/dist/cdn/{SvGridChartView-i9rWvSKg.js → SvGridChartView-PwqI4s02.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D08sVmAy.js → SvGridDropdown-DjALc_4f.js} +1 -1
- package/dist/cdn/{SvGridDropdown-CGxxopQ4.js → SvGridDropdown-hpnHvOGt.js} +1 -1
- package/dist/cdn/{src-cgs1wcUS.js → src-BxNEslEo.js} +25 -25
- package/dist/cdn/{src-Cw8d9U8j.js → src-C4yKQZ5t.js} +26 -26
- package/dist/cdn/svgrid.js +5 -5
- package/dist/cdn/svgrid.svelte-external.js +5 -5
- package/dist/chart-export.js +8 -8
- package/package.json +10 -10
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +179 -179
- package/src/GridMenus.svelte +734 -734
- package/src/SvAutoComplete.svelte +165 -165
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +101 -101
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +275 -275
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +160 -160
- package/src/SvDrawer.svelte +254 -250
- package/src/SvDrawer.test.ts +18 -1
- package/src/SvDropDownList.svelte +374 -374
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.controller.svelte.ts +3239 -3239
- package/src/SvGrid.css +2723 -2723
- package/src/SvGrid.svelte +3333 -3333
- package/src/SvGrid.types.ts +1613 -1613
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +696 -696
- package/src/SvGridSelect.svelte +259 -259
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvListBox.svelte +287 -287
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +281 -281
- package/src/SvNavPane.test.ts +15 -4
- package/src/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -131
- package/src/SvPopover.svelte +197 -197
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +200 -200
- package/src/SvSwitchButton.svelte +107 -107
- package/src/SvTagsInput.svelte +112 -112
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +204 -204
- package/src/SvTree.svelte +437 -437
- package/src/SvTreeSelect.svelte +233 -233
- package/src/a11y/dismissable.test.ts +119 -119
- package/src/a11y/dismissable.ts +114 -114
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1391 -1391
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +895 -895
- package/src/builtin-editors.grid.test.ts +85 -85
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1111 -1111
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createMenu.svelte.ts +225 -225
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +319 -319
- package/src/editing.test.ts +859 -859
- package/src/editing.ts +675 -675
- package/src/editor-contract.ts +157 -157
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/filter-operators.ts +157 -157
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/index.ts +713 -713
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +172 -172
- package/src/menus.test.ts +700 -700
- package/src/menus.ts +652 -652
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +872 -872
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +524 -524
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +215 -215
- package/src/toast-store.svelte.ts +246 -246
- package/src/toast-store.test.ts +147 -147
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
- package/themes/ember.css +71 -71
package/src/chart-export.ts
CHANGED
|
@@ -1,201 +1,201 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Export a rendered `SvGridChart` to a standalone SVG file or a PNG image.
|
|
3
|
-
*
|
|
4
|
-
* The on-screen chart styles its axes / labels / gridlines with CSS classes
|
|
5
|
-
* and `--sg-*` theme variables, which don't survive a raw serialization. So
|
|
6
|
-
* we clone the SVG, read the live computed colors, and inline a small `<style>`
|
|
7
|
-
* block with concrete values - the export looks like what's on screen.
|
|
8
|
-
*
|
|
9
|
-
* Pass the chart's `<svg class="sv-grid-chart-svg">` element (or any ancestor
|
|
10
|
-
* that contains it).
|
|
11
|
-
*/
|
|
12
|
-
import type { ChartSpec } from './chart'
|
|
13
|
-
|
|
14
|
-
function resolveSvg(el: SVGSVGElement | HTMLElement): SVGSVGElement | null {
|
|
15
|
-
if (el instanceof SVGSVGElement) return el
|
|
16
|
-
return el.querySelector('svg.sv-grid-chart-svg') ?? el.querySelector('svg')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function cssVar(el: Element, name: string, fallback: string): string {
|
|
20
|
-
const v = getComputedStyle(el).getPropertyValue(name).trim()
|
|
21
|
-
return v || fallback
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type ChartExportOptions = {
|
|
25
|
-
/** Background color of the exported image. Default: the grid background. */
|
|
26
|
-
background?: string
|
|
27
|
-
/** PNG pixel scale (1 = viewBox size). Default 2 (retina). */
|
|
28
|
-
scale?: number
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Serialize the chart SVG to a standalone, self-styled SVG string. */
|
|
32
|
-
export function chartToSvgString(
|
|
33
|
-
source: SVGSVGElement | HTMLElement,
|
|
34
|
-
options: ChartExportOptions = {},
|
|
35
|
-
): string {
|
|
36
|
-
const svg = resolveSvg(source)
|
|
37
|
-
if (!svg) throw new Error('chartToSvgString: no <svg> found')
|
|
38
|
-
|
|
39
|
-
const fg = cssVar(svg, '--sg-fg', '#0f172a')
|
|
40
|
-
const muted = cssVar(svg, '--sg-muted', '#64748b')
|
|
41
|
-
const border = cssVar(svg, '--sg-border', '#e2e8f0')
|
|
42
|
-
const bg = options.background ?? cssVar(svg, '--sg-bg', '#ffffff')
|
|
43
|
-
|
|
44
|
-
const vb = svg.viewBox.baseVal
|
|
45
|
-
const w = vb && vb.width ? vb.width : svg.clientWidth || 520
|
|
46
|
-
const h = vb && vb.height ? vb.height : svg.clientHeight || 300
|
|
47
|
-
|
|
48
|
-
const clone = svg.cloneNode(true) as SVGSVGElement
|
|
49
|
-
clone.setAttribute('width', String(w))
|
|
50
|
-
clone.setAttribute('height', String(h))
|
|
51
|
-
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
|
|
52
|
-
|
|
53
|
-
// Inline computed paint/text styles so the export is what's on screen -
|
|
54
|
-
// dynamic SVG presentation attributes (a line's `stroke={color}`) and scoped
|
|
55
|
-
// component CSS don't survive a raw clone + XMLSerializer. Walk the live tree
|
|
56
|
-
// and its clone (1:1 here, before we prune hit layers) and copy the resolved
|
|
57
|
-
// style, so line paths etc. export with their real stroke instead of nothing.
|
|
58
|
-
const PAINT_PROPS = [
|
|
59
|
-
'fill', 'fill-opacity', 'stroke', 'stroke-width', 'stroke-opacity',
|
|
60
|
-
'stroke-dasharray', 'stroke-linecap', 'stroke-linejoin', 'opacity',
|
|
61
|
-
'font-size', 'font-family', 'font-weight', 'text-anchor',
|
|
62
|
-
]
|
|
63
|
-
const liveNodes = svg.querySelectorAll('*')
|
|
64
|
-
const cloneNodes = clone.querySelectorAll('*')
|
|
65
|
-
for (let i = 0; i < liveNodes.length; i += 1) {
|
|
66
|
-
const el = cloneNodes[i] as (SVGElement & { style: CSSStyleDeclaration }) | undefined
|
|
67
|
-
if (!el || !el.style) continue
|
|
68
|
-
const cs = getComputedStyle(liveNodes[i]!)
|
|
69
|
-
for (const prop of PAINT_PROPS) {
|
|
70
|
-
const v = cs.getPropertyValue(prop)
|
|
71
|
-
if (v && v !== 'normal') el.style.setProperty(prop, v)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Drop interaction-only hit layers.
|
|
76
|
-
clone.querySelectorAll('.sv-grid-chart-hit').forEach((n) => n.remove())
|
|
77
|
-
|
|
78
|
-
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
|
|
79
|
-
style.textContent = `
|
|
80
|
-
.sv-grid-chart-axis { fill: ${muted}; font-size: 10px; font-family: sans-serif; }
|
|
81
|
-
.sv-grid-chart-gridline { stroke: ${border}; stroke-width: 1; opacity: 0.6; }
|
|
82
|
-
.sv-grid-chart-gridline.is-zero { stroke: ${muted}; opacity: 0.9; }
|
|
83
|
-
.sv-grid-chart-datalabel { fill: ${fg}; font-size: 9.5px; font-weight: 600; font-family: sans-serif; }
|
|
84
|
-
.sv-grid-chart-datalabel.on-bar { fill: #fff; }
|
|
85
|
-
.sv-grid-chart-donut-total { fill: ${fg}; font-size: 16px; font-weight: 800; font-family: sans-serif; }
|
|
86
|
-
.sv-grid-chart-donut-label { fill: ${muted}; font-size: 10px; font-family: sans-serif; }
|
|
87
|
-
`
|
|
88
|
-
clone.insertBefore(style, clone.firstChild)
|
|
89
|
-
|
|
90
|
-
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
|
91
|
-
rect.setAttribute('x', '0')
|
|
92
|
-
rect.setAttribute('y', '0')
|
|
93
|
-
rect.setAttribute('width', String(w))
|
|
94
|
-
rect.setAttribute('height', String(h))
|
|
95
|
-
rect.setAttribute('fill', bg)
|
|
96
|
-
clone.insertBefore(rect, style.nextSibling)
|
|
97
|
-
|
|
98
|
-
return new XMLSerializer().serializeToString(clone)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function triggerDownload(blobUrl: string, filename: string) {
|
|
102
|
-
const a = document.createElement('a')
|
|
103
|
-
a.href = blobUrl
|
|
104
|
-
a.download = filename
|
|
105
|
-
document.body.appendChild(a)
|
|
106
|
-
a.click()
|
|
107
|
-
a.remove()
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Download the chart as an `.svg` file. */
|
|
111
|
-
export function downloadChartSvg(
|
|
112
|
-
source: SVGSVGElement | HTMLElement,
|
|
113
|
-
filename = 'chart.svg',
|
|
114
|
-
options: ChartExportOptions = {},
|
|
115
|
-
): void {
|
|
116
|
-
const svg = chartToSvgString(source, options)
|
|
117
|
-
const url = URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' }))
|
|
118
|
-
triggerDownload(url, filename)
|
|
119
|
-
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Rasterize the chart to a PNG `Blob`. */
|
|
123
|
-
export function chartToPngBlob(
|
|
124
|
-
source: SVGSVGElement | HTMLElement,
|
|
125
|
-
options: ChartExportOptions = {},
|
|
126
|
-
): Promise<Blob> {
|
|
127
|
-
const svgStr = chartToSvgString(source, options)
|
|
128
|
-
const scale = options.scale ?? 2
|
|
129
|
-
const svgEl = resolveSvg(source)!
|
|
130
|
-
const vb = svgEl.viewBox.baseVal
|
|
131
|
-
const w = (vb && vb.width ? vb.width : svgEl.clientWidth || 520) * scale
|
|
132
|
-
const h = (vb && vb.height ? vb.height : svgEl.clientHeight || 300) * scale
|
|
133
|
-
|
|
134
|
-
return new Promise((resolve, reject) => {
|
|
135
|
-
const url = URL.createObjectURL(new Blob([svgStr], { type: 'image/svg+xml' }))
|
|
136
|
-
const img = new Image()
|
|
137
|
-
img.onload = () => {
|
|
138
|
-
const canvas = document.createElement('canvas')
|
|
139
|
-
canvas.width = Math.round(w)
|
|
140
|
-
canvas.height = Math.round(h)
|
|
141
|
-
const ctx = canvas.getContext('2d')
|
|
142
|
-
if (!ctx) {
|
|
143
|
-
URL.revokeObjectURL(url)
|
|
144
|
-
reject(new Error('canvas 2d context unavailable'))
|
|
145
|
-
return
|
|
146
|
-
}
|
|
147
|
-
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
|
148
|
-
URL.revokeObjectURL(url)
|
|
149
|
-
canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error('toBlob failed'))), 'image/png')
|
|
150
|
-
}
|
|
151
|
-
img.onerror = () => {
|
|
152
|
-
URL.revokeObjectURL(url)
|
|
153
|
-
reject(new Error('failed to rasterize chart SVG'))
|
|
154
|
-
}
|
|
155
|
-
img.src = url
|
|
156
|
-
})
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Download the chart as a `.png` file. */
|
|
160
|
-
export async function downloadChartPng(
|
|
161
|
-
source: SVGSVGElement | HTMLElement,
|
|
162
|
-
filename = 'chart.png',
|
|
163
|
-
options: ChartExportOptions = {},
|
|
164
|
-
): Promise<void> {
|
|
165
|
-
const blob = await chartToPngBlob(source, options)
|
|
166
|
-
const url = URL.createObjectURL(blob)
|
|
167
|
-
triggerDownload(url, filename)
|
|
168
|
-
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const csvCell = (v: unknown): string => {
|
|
172
|
-
const s = v == null ? '' : String(v)
|
|
173
|
-
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Serialize a chart's data to CSV: one row per category, one column per series
|
|
178
|
-
* (`Category, <series 1>, ...`). Empty for exotic specs (sankey/treemap/gauge/
|
|
179
|
-
* calendar) that don't carry a rectangular categories x series grid.
|
|
180
|
-
*/
|
|
181
|
-
export function chartSpecToCsv(spec: ChartSpec): string {
|
|
182
|
-
const cats = spec.categories ?? []
|
|
183
|
-
const series = spec.series ?? []
|
|
184
|
-
if (!cats.length || !series.length) return ''
|
|
185
|
-
const header = ['Category', ...series.map((s) => s.label)].map(csvCell).join(',')
|
|
186
|
-
const rows = cats.map((cat, i) =>
|
|
187
|
-
[cat, ...series.map((s) => s.values[i] ?? '')].map(csvCell).join(','),
|
|
188
|
-
)
|
|
189
|
-
return [header, ...rows].join('\n')
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** Download the chart's data as a `.csv` file. Returns false if there's no
|
|
193
|
-
* rectangular category/series data to export. */
|
|
194
|
-
export function downloadChartCsv(spec: ChartSpec, filename = 'chart.csv'): boolean {
|
|
195
|
-
const csv = chartSpecToCsv(spec)
|
|
196
|
-
if (!csv) return false
|
|
197
|
-
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }))
|
|
198
|
-
triggerDownload(url, filename)
|
|
199
|
-
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
200
|
-
return true
|
|
201
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Export a rendered `SvGridChart` to a standalone SVG file or a PNG image.
|
|
3
|
+
*
|
|
4
|
+
* The on-screen chart styles its axes / labels / gridlines with CSS classes
|
|
5
|
+
* and `--sg-*` theme variables, which don't survive a raw serialization. So
|
|
6
|
+
* we clone the SVG, read the live computed colors, and inline a small `<style>`
|
|
7
|
+
* block with concrete values - the export looks like what's on screen.
|
|
8
|
+
*
|
|
9
|
+
* Pass the chart's `<svg class="sv-grid-chart-svg">` element (or any ancestor
|
|
10
|
+
* that contains it).
|
|
11
|
+
*/
|
|
12
|
+
import type { ChartSpec } from './chart'
|
|
13
|
+
|
|
14
|
+
function resolveSvg(el: SVGSVGElement | HTMLElement): SVGSVGElement | null {
|
|
15
|
+
if (el instanceof SVGSVGElement) return el
|
|
16
|
+
return el.querySelector('svg.sv-grid-chart-svg') ?? el.querySelector('svg')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function cssVar(el: Element, name: string, fallback: string): string {
|
|
20
|
+
const v = getComputedStyle(el).getPropertyValue(name).trim()
|
|
21
|
+
return v || fallback
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ChartExportOptions = {
|
|
25
|
+
/** Background color of the exported image. Default: the grid background. */
|
|
26
|
+
background?: string
|
|
27
|
+
/** PNG pixel scale (1 = viewBox size). Default 2 (retina). */
|
|
28
|
+
scale?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Serialize the chart SVG to a standalone, self-styled SVG string. */
|
|
32
|
+
export function chartToSvgString(
|
|
33
|
+
source: SVGSVGElement | HTMLElement,
|
|
34
|
+
options: ChartExportOptions = {},
|
|
35
|
+
): string {
|
|
36
|
+
const svg = resolveSvg(source)
|
|
37
|
+
if (!svg) throw new Error('chartToSvgString: no <svg> found')
|
|
38
|
+
|
|
39
|
+
const fg = cssVar(svg, '--sg-fg', '#0f172a')
|
|
40
|
+
const muted = cssVar(svg, '--sg-muted', '#64748b')
|
|
41
|
+
const border = cssVar(svg, '--sg-border', '#e2e8f0')
|
|
42
|
+
const bg = options.background ?? cssVar(svg, '--sg-bg', '#ffffff')
|
|
43
|
+
|
|
44
|
+
const vb = svg.viewBox.baseVal
|
|
45
|
+
const w = vb && vb.width ? vb.width : svg.clientWidth || 520
|
|
46
|
+
const h = vb && vb.height ? vb.height : svg.clientHeight || 300
|
|
47
|
+
|
|
48
|
+
const clone = svg.cloneNode(true) as SVGSVGElement
|
|
49
|
+
clone.setAttribute('width', String(w))
|
|
50
|
+
clone.setAttribute('height', String(h))
|
|
51
|
+
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
|
|
52
|
+
|
|
53
|
+
// Inline computed paint/text styles so the export is what's on screen -
|
|
54
|
+
// dynamic SVG presentation attributes (a line's `stroke={color}`) and scoped
|
|
55
|
+
// component CSS don't survive a raw clone + XMLSerializer. Walk the live tree
|
|
56
|
+
// and its clone (1:1 here, before we prune hit layers) and copy the resolved
|
|
57
|
+
// style, so line paths etc. export with their real stroke instead of nothing.
|
|
58
|
+
const PAINT_PROPS = [
|
|
59
|
+
'fill', 'fill-opacity', 'stroke', 'stroke-width', 'stroke-opacity',
|
|
60
|
+
'stroke-dasharray', 'stroke-linecap', 'stroke-linejoin', 'opacity',
|
|
61
|
+
'font-size', 'font-family', 'font-weight', 'text-anchor',
|
|
62
|
+
]
|
|
63
|
+
const liveNodes = svg.querySelectorAll('*')
|
|
64
|
+
const cloneNodes = clone.querySelectorAll('*')
|
|
65
|
+
for (let i = 0; i < liveNodes.length; i += 1) {
|
|
66
|
+
const el = cloneNodes[i] as (SVGElement & { style: CSSStyleDeclaration }) | undefined
|
|
67
|
+
if (!el || !el.style) continue
|
|
68
|
+
const cs = getComputedStyle(liveNodes[i]!)
|
|
69
|
+
for (const prop of PAINT_PROPS) {
|
|
70
|
+
const v = cs.getPropertyValue(prop)
|
|
71
|
+
if (v && v !== 'normal') el.style.setProperty(prop, v)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Drop interaction-only hit layers.
|
|
76
|
+
clone.querySelectorAll('.sv-grid-chart-hit').forEach((n) => n.remove())
|
|
77
|
+
|
|
78
|
+
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
|
|
79
|
+
style.textContent = `
|
|
80
|
+
.sv-grid-chart-axis { fill: ${muted}; font-size: 10px; font-family: sans-serif; }
|
|
81
|
+
.sv-grid-chart-gridline { stroke: ${border}; stroke-width: 1; opacity: 0.6; }
|
|
82
|
+
.sv-grid-chart-gridline.is-zero { stroke: ${muted}; opacity: 0.9; }
|
|
83
|
+
.sv-grid-chart-datalabel { fill: ${fg}; font-size: 9.5px; font-weight: 600; font-family: sans-serif; }
|
|
84
|
+
.sv-grid-chart-datalabel.on-bar { fill: #fff; }
|
|
85
|
+
.sv-grid-chart-donut-total { fill: ${fg}; font-size: 16px; font-weight: 800; font-family: sans-serif; }
|
|
86
|
+
.sv-grid-chart-donut-label { fill: ${muted}; font-size: 10px; font-family: sans-serif; }
|
|
87
|
+
`
|
|
88
|
+
clone.insertBefore(style, clone.firstChild)
|
|
89
|
+
|
|
90
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
|
91
|
+
rect.setAttribute('x', '0')
|
|
92
|
+
rect.setAttribute('y', '0')
|
|
93
|
+
rect.setAttribute('width', String(w))
|
|
94
|
+
rect.setAttribute('height', String(h))
|
|
95
|
+
rect.setAttribute('fill', bg)
|
|
96
|
+
clone.insertBefore(rect, style.nextSibling)
|
|
97
|
+
|
|
98
|
+
return new XMLSerializer().serializeToString(clone)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function triggerDownload(blobUrl: string, filename: string) {
|
|
102
|
+
const a = document.createElement('a')
|
|
103
|
+
a.href = blobUrl
|
|
104
|
+
a.download = filename
|
|
105
|
+
document.body.appendChild(a)
|
|
106
|
+
a.click()
|
|
107
|
+
a.remove()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Download the chart as an `.svg` file. */
|
|
111
|
+
export function downloadChartSvg(
|
|
112
|
+
source: SVGSVGElement | HTMLElement,
|
|
113
|
+
filename = 'chart.svg',
|
|
114
|
+
options: ChartExportOptions = {},
|
|
115
|
+
): void {
|
|
116
|
+
const svg = chartToSvgString(source, options)
|
|
117
|
+
const url = URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' }))
|
|
118
|
+
triggerDownload(url, filename)
|
|
119
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Rasterize the chart to a PNG `Blob`. */
|
|
123
|
+
export function chartToPngBlob(
|
|
124
|
+
source: SVGSVGElement | HTMLElement,
|
|
125
|
+
options: ChartExportOptions = {},
|
|
126
|
+
): Promise<Blob> {
|
|
127
|
+
const svgStr = chartToSvgString(source, options)
|
|
128
|
+
const scale = options.scale ?? 2
|
|
129
|
+
const svgEl = resolveSvg(source)!
|
|
130
|
+
const vb = svgEl.viewBox.baseVal
|
|
131
|
+
const w = (vb && vb.width ? vb.width : svgEl.clientWidth || 520) * scale
|
|
132
|
+
const h = (vb && vb.height ? vb.height : svgEl.clientHeight || 300) * scale
|
|
133
|
+
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const url = URL.createObjectURL(new Blob([svgStr], { type: 'image/svg+xml' }))
|
|
136
|
+
const img = new Image()
|
|
137
|
+
img.onload = () => {
|
|
138
|
+
const canvas = document.createElement('canvas')
|
|
139
|
+
canvas.width = Math.round(w)
|
|
140
|
+
canvas.height = Math.round(h)
|
|
141
|
+
const ctx = canvas.getContext('2d')
|
|
142
|
+
if (!ctx) {
|
|
143
|
+
URL.revokeObjectURL(url)
|
|
144
|
+
reject(new Error('canvas 2d context unavailable'))
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
|
148
|
+
URL.revokeObjectURL(url)
|
|
149
|
+
canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error('toBlob failed'))), 'image/png')
|
|
150
|
+
}
|
|
151
|
+
img.onerror = () => {
|
|
152
|
+
URL.revokeObjectURL(url)
|
|
153
|
+
reject(new Error('failed to rasterize chart SVG'))
|
|
154
|
+
}
|
|
155
|
+
img.src = url
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Download the chart as a `.png` file. */
|
|
160
|
+
export async function downloadChartPng(
|
|
161
|
+
source: SVGSVGElement | HTMLElement,
|
|
162
|
+
filename = 'chart.png',
|
|
163
|
+
options: ChartExportOptions = {},
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
const blob = await chartToPngBlob(source, options)
|
|
166
|
+
const url = URL.createObjectURL(blob)
|
|
167
|
+
triggerDownload(url, filename)
|
|
168
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const csvCell = (v: unknown): string => {
|
|
172
|
+
const s = v == null ? '' : String(v)
|
|
173
|
+
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Serialize a chart's data to CSV: one row per category, one column per series
|
|
178
|
+
* (`Category, <series 1>, ...`). Empty for exotic specs (sankey/treemap/gauge/
|
|
179
|
+
* calendar) that don't carry a rectangular categories x series grid.
|
|
180
|
+
*/
|
|
181
|
+
export function chartSpecToCsv(spec: ChartSpec): string {
|
|
182
|
+
const cats = spec.categories ?? []
|
|
183
|
+
const series = spec.series ?? []
|
|
184
|
+
if (!cats.length || !series.length) return ''
|
|
185
|
+
const header = ['Category', ...series.map((s) => s.label)].map(csvCell).join(',')
|
|
186
|
+
const rows = cats.map((cat, i) =>
|
|
187
|
+
[cat, ...series.map((s) => s.values[i] ?? '')].map(csvCell).join(','),
|
|
188
|
+
)
|
|
189
|
+
return [header, ...rows].join('\n')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Download the chart's data as a `.csv` file. Returns false if there's no
|
|
193
|
+
* rectangular category/series data to export. */
|
|
194
|
+
export function downloadChartCsv(spec: ChartSpec, filename = 'chart.csv'): boolean {
|
|
195
|
+
const csv = chartSpecToCsv(spec)
|
|
196
|
+
if (!csv) return false
|
|
197
|
+
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }))
|
|
198
|
+
triggerDownload(url, filename)
|
|
199
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
200
|
+
return true
|
|
201
|
+
}
|
package/src/chart-view.svelte.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* chart-view registry - the injection seam for the *chart view of the grid*.
|
|
3
|
-
* Setting the `chart` prop on `<SvGrid>` swaps the table for a chart driven by
|
|
4
|
-
* the grid's filtered + sorted rows. Mirrors the `board-view` / `scheduler-view`
|
|
5
|
-
* seam, with one difference: the chart renderer is NOT a paid feature. The chart
|
|
6
|
-
* primitive (`SvChart` / `SvGridChart`) already ships in the free `@svgrid/grid`,
|
|
7
|
-
* so the grid lazy-loads a built-in default renderer (`SvGridChartView`) out of
|
|
8
|
-
* the box. This seam exists only so a host (or enterprise) can *override* that
|
|
9
|
-
* default with a richer renderer - it is not required for the view to work.
|
|
10
|
-
*
|
|
11
|
-
* ```ts
|
|
12
|
-
* // optional: replace the built-in chart view with your own renderer
|
|
13
|
-
* import { registerChartView } from '@svgrid/grid'
|
|
14
|
-
* import MyChartView from './MyChartView.svelte'
|
|
15
|
-
* registerChartView(MyChartView)
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
18
|
-
import type { Component } from 'svelte'
|
|
19
|
-
|
|
20
|
-
let renderer = $state<Component<any> | null>(null)
|
|
21
|
-
|
|
22
|
-
/** Override the built-in chart-view renderer. Optional - the grid ships a
|
|
23
|
-
* free default, so this is only for hosts that want a custom renderer. */
|
|
24
|
-
export function registerChartView(component: Component<any>): void {
|
|
25
|
-
renderer = component
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** The registered chart-view override, or null to use the built-in default. */
|
|
29
|
-
export function getChartView(): Component<any> | null {
|
|
30
|
-
return renderer
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Whether a chart-view override has been registered. */
|
|
34
|
-
export function hasChartView(): boolean {
|
|
35
|
-
return renderer != null
|
|
36
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* chart-view registry - the injection seam for the *chart view of the grid*.
|
|
3
|
+
* Setting the `chart` prop on `<SvGrid>` swaps the table for a chart driven by
|
|
4
|
+
* the grid's filtered + sorted rows. Mirrors the `board-view` / `scheduler-view`
|
|
5
|
+
* seam, with one difference: the chart renderer is NOT a paid feature. The chart
|
|
6
|
+
* primitive (`SvChart` / `SvGridChart`) already ships in the free `@svgrid/grid`,
|
|
7
|
+
* so the grid lazy-loads a built-in default renderer (`SvGridChartView`) out of
|
|
8
|
+
* the box. This seam exists only so a host (or enterprise) can *override* that
|
|
9
|
+
* default with a richer renderer - it is not required for the view to work.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* // optional: replace the built-in chart view with your own renderer
|
|
13
|
+
* import { registerChartView } from '@svgrid/grid'
|
|
14
|
+
* import MyChartView from './MyChartView.svelte'
|
|
15
|
+
* registerChartView(MyChartView)
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import type { Component } from 'svelte'
|
|
19
|
+
|
|
20
|
+
let renderer = $state<Component<any> | null>(null)
|
|
21
|
+
|
|
22
|
+
/** Override the built-in chart-view renderer. Optional - the grid ships a
|
|
23
|
+
* free default, so this is only for hosts that want a custom renderer. */
|
|
24
|
+
export function registerChartView(component: Component<any>): void {
|
|
25
|
+
renderer = component
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The registered chart-view override, or null to use the built-in default. */
|
|
29
|
+
export function getChartView(): Component<any> | null {
|
|
30
|
+
return renderer
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Whether a chart-view override has been registered. */
|
|
34
|
+
export function hasChartView(): boolean {
|
|
35
|
+
return renderer != null
|
|
36
|
+
}
|