@svgrid/grid 1.0.0
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/LICENSE +33 -0
- package/README.md +39 -0
- package/dist/FlexRender.svelte +96 -0
- package/dist/FlexRender.svelte.d.ts +49 -0
- package/dist/SvGrid.svelte +8742 -0
- package/dist/SvGrid.svelte.d.ts +381 -0
- package/dist/SvGridChart.svelte +653 -0
- package/dist/SvGridChart.svelte.d.ts +25 -0
- package/dist/SvGridChart.test.d.ts +1 -0
- package/dist/SvGridChart.test.js +62 -0
- package/dist/SvGridDropdown.svelte +675 -0
- package/dist/SvGridDropdown.svelte.d.ts +40 -0
- package/dist/a11y.contract.test.d.ts +1 -0
- package/dist/a11y.contract.test.js +39 -0
- package/dist/a11y.d.ts +38 -0
- package/dist/a11y.js +37 -0
- package/dist/a11y.test.d.ts +1 -0
- package/dist/a11y.test.js +44 -0
- package/dist/cell-formatting.d.ts +17 -0
- package/dist/cell-formatting.js +141 -0
- package/dist/cell-formatting.test.d.ts +1 -0
- package/dist/cell-formatting.test.js +234 -0
- package/dist/chart-export.d.ts +14 -0
- package/dist/chart-export.js +112 -0
- package/dist/chart.d.ts +226 -0
- package/dist/chart.js +732 -0
- package/dist/chart.test.d.ts +1 -0
- package/dist/chart.test.js +289 -0
- package/dist/collaboration.d.ts +74 -0
- package/dist/collaboration.js +98 -0
- package/dist/collaboration.test.d.ts +1 -0
- package/dist/collaboration.test.js +65 -0
- package/dist/conditional-formatting.d.ts +91 -0
- package/dist/conditional-formatting.js +170 -0
- package/dist/conditional-formatting.test.d.ts +1 -0
- package/dist/conditional-formatting.test.js +87 -0
- package/dist/core.coverage.test.d.ts +1 -0
- package/dist/core.coverage.test.js +186 -0
- package/dist/core.d.ts +370 -0
- package/dist/core.js +568 -0
- package/dist/core.performance.test.d.ts +1 -0
- package/dist/core.performance.test.js +29 -0
- package/dist/createGrid.svelte.d.ts +6 -0
- package/dist/createGrid.svelte.js +17 -0
- package/dist/createGrid.test.d.ts +1 -0
- package/dist/createGrid.test.js +9 -0
- package/dist/createGridState.svelte.d.ts +3 -0
- package/dist/createGridState.svelte.js +13 -0
- package/dist/editors/cell-editors.d.ts +23 -0
- package/dist/editors/cell-editors.js +97 -0
- package/dist/editors/cell-editors.test.d.ts +1 -0
- package/dist/editors/cell-editors.test.js +75 -0
- package/dist/fill-patterns.d.ts +30 -0
- package/dist/fill-patterns.js +207 -0
- package/dist/fill-patterns.test.d.ts +1 -0
- package/dist/fill-patterns.test.js +81 -0
- package/dist/filtering/excel-filters.d.ts +26 -0
- package/dist/filtering/excel-filters.js +64 -0
- package/dist/filtering/excel-filters.test.d.ts +1 -0
- package/dist/filtering/excel-filters.test.js +116 -0
- package/dist/filtering/locale-filter.test.d.ts +13 -0
- package/dist/filtering/locale-filter.test.js +189 -0
- package/dist/flex-render.d.ts +1 -0
- package/dist/flex-render.js +2 -0
- package/dist/flex-render.test.d.ts +1 -0
- package/dist/flex-render.test.js +142 -0
- package/dist/group-aggregate.test.d.ts +1 -0
- package/dist/group-aggregate.test.js +32 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +27 -0
- package/dist/keyboard.d.ts +8 -0
- package/dist/keyboard.js +91 -0
- package/dist/keyboard.test.d.ts +1 -0
- package/dist/keyboard.test.js +27 -0
- package/dist/merge-objects.d.ts +9 -0
- package/dist/merge-objects.js +28 -0
- package/dist/named-views.d.ts +42 -0
- package/dist/named-views.js +80 -0
- package/dist/named-views.test.d.ts +1 -0
- package/dist/named-views.test.js +57 -0
- package/dist/new-features.test.d.ts +1 -0
- package/dist/new-features.test.js +183 -0
- package/dist/render-component.d.ts +13 -0
- package/dist/render-component.js +14 -0
- package/dist/render-component.test.d.ts +1 -0
- package/dist/render-component.test.js +41 -0
- package/dist/server-data-source.d.ts +76 -0
- package/dist/server-data-source.js +83 -0
- package/dist/server-data-source.test.d.ts +1 -0
- package/dist/server-data-source.test.js +81 -0
- package/dist/sparkline.d.ts +56 -0
- package/dist/sparkline.js +97 -0
- package/dist/sparkline.test.d.ts +1 -0
- package/dist/sparkline.test.js +52 -0
- package/dist/static-functions.d.ts +1 -0
- package/dist/static-functions.js +1 -0
- package/dist/subscribe.d.ts +9 -0
- package/dist/subscribe.js +31 -0
- package/dist/subscribe.test.d.ts +1 -0
- package/dist/subscribe.test.js +93 -0
- package/dist/sv-grid-scrollbar.d.ts +1 -0
- package/dist/sv-grid-scrollbar.js +310 -0
- package/dist/svgrid-wrapper.types.d.ts +341 -0
- package/dist/svgrid-wrapper.types.js +1 -0
- package/dist/svgrid.api-extensions.test.d.ts +14 -0
- package/dist/svgrid.api-extensions.test.js +334 -0
- package/dist/svgrid.api.test.d.ts +15 -0
- package/dist/svgrid.api.test.js +169 -0
- package/dist/svgrid.behavior.test.d.ts +15 -0
- package/dist/svgrid.behavior.test.js +605 -0
- package/dist/svgrid.column-reorder.test.d.ts +15 -0
- package/dist/svgrid.column-reorder.test.js +224 -0
- package/dist/svgrid.features.test.d.ts +1 -0
- package/dist/svgrid.features.test.js +129 -0
- package/dist/svgrid.interaction.test.d.ts +9 -0
- package/dist/svgrid.interaction.test.js +318 -0
- package/dist/svgrid.locale-filtering.test.d.ts +15 -0
- package/dist/svgrid.locale-filtering.test.js +261 -0
- package/dist/svgrid.new-features.wrapper.test.d.ts +1 -0
- package/dist/svgrid.new-features.wrapper.test.js +136 -0
- package/dist/svgrid.row-pinning.test.d.ts +14 -0
- package/dist/svgrid.row-pinning.test.js +250 -0
- package/dist/svgrid.set-filter.test.d.ts +17 -0
- package/dist/svgrid.set-filter.test.js +268 -0
- package/dist/svgrid.wrapper.test.d.ts +1 -0
- package/dist/svgrid.wrapper.test.js +33 -0
- package/dist/test-setup.d.ts +1 -0
- package/dist/test-setup.js +29 -0
- package/dist/transaction.test.d.ts +1 -0
- package/dist/transaction.test.js +89 -0
- package/dist/virtualization/column-virtualizer.d.ts +20 -0
- package/dist/virtualization/column-virtualizer.js +19 -0
- package/dist/virtualization/column-virtualizer.test.d.ts +1 -0
- package/dist/virtualization/column-virtualizer.test.js +25 -0
- package/dist/virtualization/svelte-virtualizer.svelte.d.ts +11 -0
- package/dist/virtualization/svelte-virtualizer.svelte.js +20 -0
- package/dist/virtualization/types.d.ts +28 -0
- package/dist/virtualization/types.js +1 -0
- package/dist/virtualization/virtualizer.d.ts +13 -0
- package/dist/virtualization/virtualizer.js +232 -0
- package/dist/virtualization/virtualizer.test.d.ts +1 -0
- package/dist/virtualization/virtualizer.test.js +41 -0
- package/package.json +69 -0
- package/src/FlexRender.svelte +96 -0
- package/src/SvGrid.svelte +8742 -0
- package/src/SvGridChart.svelte +653 -0
- package/src/SvGridChart.test.ts +69 -0
- package/src/SvGridDropdown.svelte +675 -0
- package/src/a11y.contract.test.ts +49 -0
- package/src/a11y.test.ts +59 -0
- package/src/a11y.ts +59 -0
- package/src/cell-formatting.test.ts +273 -0
- package/src/cell-formatting.ts +169 -0
- package/src/chart-export.ts +144 -0
- package/src/chart.test.ts +311 -0
- package/src/chart.ts +934 -0
- package/src/collaboration.test.ts +74 -0
- package/src/collaboration.ts +161 -0
- package/src/conditional-formatting.test.ts +107 -0
- package/src/conditional-formatting.ts +239 -0
- package/src/core.coverage.test.ts +230 -0
- package/src/core.performance.test.ts +30 -0
- package/src/core.ts +991 -0
- package/src/createGrid.svelte.ts +42 -0
- package/src/createGrid.test.ts +10 -0
- package/src/createGridState.svelte.ts +17 -0
- package/src/editors/cell-editors.test.ts +80 -0
- package/src/editors/cell-editors.ts +127 -0
- package/src/fill-patterns.test.ts +93 -0
- package/src/fill-patterns.ts +229 -0
- package/src/filtering/excel-filters.test.ts +131 -0
- package/src/filtering/excel-filters.ts +101 -0
- package/src/filtering/locale-filter.test.ts +220 -0
- package/src/flex-render.test.ts +143 -0
- package/src/flex-render.ts +3 -0
- package/src/group-aggregate.test.ts +48 -0
- package/src/index.ts +159 -0
- package/src/keyboard.test.ts +59 -0
- package/src/keyboard.ts +97 -0
- package/src/merge-objects.ts +48 -0
- package/src/named-views.test.ts +66 -0
- package/src/named-views.ts +120 -0
- package/src/new-features.test.ts +217 -0
- package/src/render-component.test.ts +51 -0
- package/src/render-component.ts +28 -0
- package/src/server-data-source.test.ts +88 -0
- package/src/server-data-source.ts +163 -0
- package/src/sparkline.test.ts +59 -0
- package/src/sparkline.ts +164 -0
- package/src/static-functions.ts +11 -0
- package/src/subscribe.test.ts +103 -0
- package/src/subscribe.ts +38 -0
- package/src/sv-grid-scrollbar.ts +347 -0
- package/src/svgrid-wrapper.types.ts +382 -0
- package/src/svgrid.api-extensions.test.ts +362 -0
- package/src/svgrid.api.test.ts +192 -0
- package/src/svgrid.behavior.test.ts +657 -0
- package/src/svgrid.column-reorder.test.ts +234 -0
- package/src/svgrid.features.test.ts +157 -0
- package/src/svgrid.interaction.test.ts +355 -0
- package/src/svgrid.locale-filtering.test.ts +259 -0
- package/src/svgrid.new-features.wrapper.test.ts +164 -0
- package/src/svgrid.row-pinning.test.ts +266 -0
- package/src/svgrid.set-filter.test.ts +270 -0
- package/src/svgrid.wrapper.test.ts +35 -0
- package/src/test-setup.ts +37 -0
- package/src/transaction.test.ts +100 -0
- package/src/virtualization/column-virtualizer.test.ts +27 -0
- package/src/virtualization/column-virtualizer.ts +30 -0
- package/src/virtualization/svelte-virtualizer.svelte.ts +24 -0
- package/src/virtualization/types.ts +30 -0
- package/src/virtualization/virtualizer.test.ts +47 -0
- package/src/virtualization/virtualizer.ts +270 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvGridChart - renders a `ChartSpec` as inline SVG. Supports grouped +
|
|
4
|
+
* stacked bars, line, area, pie / donut, combo charts (per-series type),
|
|
5
|
+
* a secondary Y axis, signed Y domains, and axis titles. Interactive: a
|
|
6
|
+
* unified crosshair tooltip (hover a category -> all series at once),
|
|
7
|
+
* focus tooltips, a clickable legend that toggles series, optional data
|
|
8
|
+
* labels, and an `onSelect` drill hook. No external charting dependency.
|
|
9
|
+
*/
|
|
10
|
+
import { buildChart, DEFAULT_PALETTE, type ChartSpec, type ChartSelection } from './chart'
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
spec: ChartSpec
|
|
14
|
+
/** Show the (clickable) legend. Default true. */
|
|
15
|
+
legend?: boolean
|
|
16
|
+
/** Enable tooltips + crosshair + legend toggling. Default true. */
|
|
17
|
+
interactive?: boolean
|
|
18
|
+
/** Draw the value on each bar / point / slice. Default false. */
|
|
19
|
+
dataLabels?: boolean
|
|
20
|
+
/** Format a value for tooltips, data labels, AND Y-axis ticks. */
|
|
21
|
+
formatValue?: (value: number) => string
|
|
22
|
+
/** Fired when a category / slice is clicked (drill into the grid). */
|
|
23
|
+
onSelect?: (selection: ChartSelection) => void
|
|
24
|
+
}
|
|
25
|
+
let {
|
|
26
|
+
spec,
|
|
27
|
+
legend = true,
|
|
28
|
+
interactive = true,
|
|
29
|
+
dataLabels = false,
|
|
30
|
+
formatValue,
|
|
31
|
+
onSelect,
|
|
32
|
+
}: Props = $props()
|
|
33
|
+
|
|
34
|
+
const fmt = (v: number) =>
|
|
35
|
+
formatValue
|
|
36
|
+
? formatValue(v)
|
|
37
|
+
: Number.isFinite(v)
|
|
38
|
+
? v.toLocaleString(undefined, { maximumFractionDigits: 2 })
|
|
39
|
+
: String(v)
|
|
40
|
+
|
|
41
|
+
let hidden = $state(new Set<string>())
|
|
42
|
+
// Isolate: double-clicking a legend chip shows ONLY that series/slice.
|
|
43
|
+
let isolated = $state<string | null>(null)
|
|
44
|
+
// Hover-dim: hovering a legend chip dims the other series (visual only).
|
|
45
|
+
let dimmed = $state<string | null>(null)
|
|
46
|
+
const palette = $derived(spec.palette ?? DEFAULT_PALETTE)
|
|
47
|
+
const uid = `svgc-${Math.random().toString(36).slice(2, 8)}`
|
|
48
|
+
|
|
49
|
+
const coloredSeries = $derived(
|
|
50
|
+
spec.series.map((s, i) => ({ ...s, color: s.color ?? palette[i % palette.length]! })),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// Effective hidden set = manual toggles, unless a series is isolated (then
|
|
54
|
+
// everything else is hidden).
|
|
55
|
+
const effectiveHidden = $derived.by(() => {
|
|
56
|
+
if (isolated == null) return hidden
|
|
57
|
+
const labels = spec.type === 'pie' ? spec.categories : spec.series.map((s) => s.label)
|
|
58
|
+
return new Set(labels.filter((l) => l !== isolated))
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const visibleSpec = $derived.by<ChartSpec>(() => {
|
|
62
|
+
if (spec.type === 'pie') {
|
|
63
|
+
const s = coloredSeries[0]
|
|
64
|
+
if (!s) return spec
|
|
65
|
+
const values = s.values.map((v, i) => (effectiveHidden.has(spec.categories[i] ?? String(i)) ? 0 : v))
|
|
66
|
+
return { ...spec, series: [{ ...s, values }] }
|
|
67
|
+
}
|
|
68
|
+
return { ...spec, series: coloredSeries.filter((s) => !effectiveHidden.has(s.label)) }
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const geo = $derived(buildChart(visibleSpec))
|
|
72
|
+
const isCartesian = $derived(spec.type !== 'pie')
|
|
73
|
+
const isScatter = $derived(spec.type === 'scatter')
|
|
74
|
+
const isHorizontal = $derived(geo.orientation === 'horizontal')
|
|
75
|
+
// Per-category band size along the main (category) axis: width when vertical,
|
|
76
|
+
// height when horizontal.
|
|
77
|
+
const slot = $derived(
|
|
78
|
+
(isHorizontal ? geo.plot.h : geo.plot.w) / Math.max(1, spec.categories.length),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// Opacity for a series when another legend chip is being hovered.
|
|
82
|
+
const dimOf = (seriesLabel: string) => (dimmed && dimmed !== seriesLabel ? 0.18 : 1)
|
|
83
|
+
|
|
84
|
+
const legendItems = $derived(
|
|
85
|
+
spec.type === 'pie'
|
|
86
|
+
? spec.categories.map((label, i) => ({ label, color: palette[i % palette.length]!, off: hidden.has(label) }))
|
|
87
|
+
: coloredSeries.map((s) => ({ label: s.label, color: s.color, off: hidden.has(s.label) })),
|
|
88
|
+
)
|
|
89
|
+
// Legend overflow: collapse to the first N chips with a "+N more" toggle so a
|
|
90
|
+
// wide pivot (many series) doesn't flood the chart with legend rows.
|
|
91
|
+
const LEGEND_MAX = 10
|
|
92
|
+
let legendExpanded = $state(false)
|
|
93
|
+
const legendOverflow = $derived(legendItems.length > LEGEND_MAX)
|
|
94
|
+
const shownLegend = $derived(
|
|
95
|
+
legendOverflow && !legendExpanded ? legendItems.slice(0, LEGEND_MAX) : legendItems,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const isEmpty = $derived.by(() => {
|
|
99
|
+
if (spec.type === 'pie') return geo.slices.every((s) => s.value <= 0)
|
|
100
|
+
if (spec.type === 'scatter') return geo.scatterPoints.length === 0
|
|
101
|
+
return geo.bars.length === 0 && geo.lines.every((l) => l.points.every((p) => !p.defined))
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
function toggle(label: string) {
|
|
105
|
+
if (!interactive) return
|
|
106
|
+
if (isolated != null) { isolated = null; return }
|
|
107
|
+
const next = new Set(hidden)
|
|
108
|
+
if (next.has(label)) next.delete(label)
|
|
109
|
+
else next.add(label)
|
|
110
|
+
hidden = next
|
|
111
|
+
}
|
|
112
|
+
// Double-click a legend chip to isolate it (show only that one); double-click
|
|
113
|
+
// again (or the same chip) to clear the isolation.
|
|
114
|
+
function isolate(label: string) {
|
|
115
|
+
if (!interactive) return
|
|
116
|
+
isolated = isolated === label ? null : label
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---- Screen-reader data table -----------------------------------------
|
|
120
|
+
// A visually-hidden table that conveys the same data to assistive tech.
|
|
121
|
+
const srTable = $derived.by(() => {
|
|
122
|
+
if (spec.type === 'pie') {
|
|
123
|
+
const s = coloredSeries[0]
|
|
124
|
+
return {
|
|
125
|
+
cols: ['Category', 'Value'],
|
|
126
|
+
rows: spec.categories.map((c, i) => [c, fmt(s?.values[i] ?? 0)]),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (isScatter) {
|
|
130
|
+
const rows: string[][] = []
|
|
131
|
+
for (const s of coloredSeries) for (const p of s.points ?? []) rows.push([s.label, fmt(p.x), fmt(p.y), p.r != null ? fmt(p.r) : ''])
|
|
132
|
+
return { cols: ['Series', 'X', 'Y', 'Size'], rows }
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
cols: ['Category', ...coloredSeries.map((s) => s.label)],
|
|
136
|
+
rows: spec.categories.map((c, i) => [c, ...coloredSeries.map((s) => fmt(s.values[i] ?? 0))]),
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
function truncate(s: string, n = 12): string {
|
|
140
|
+
return s.length > n ? s.slice(0, n - 1) + '…' : s
|
|
141
|
+
}
|
|
142
|
+
const yTickLabel = (value: number, fallback: string) => (formatValue ? formatValue(value) : fallback)
|
|
143
|
+
|
|
144
|
+
// ---- Tooltip + crosshair ----------------------------------------------
|
|
145
|
+
let chartEl: HTMLElement | null = $state(null)
|
|
146
|
+
type TipRow = { label?: string; color?: string; value: string }
|
|
147
|
+
let tip = $state<{ left: number; top: number; below: boolean; title: string; rows: TipRow[] } | null>(null)
|
|
148
|
+
let activeCat = $state<number | null>(null)
|
|
149
|
+
|
|
150
|
+
function showTip(clientX: number, clientY: number, title: string, rows: TipRow[]) {
|
|
151
|
+
if (!interactive || !chartEl || rows.length === 0) return
|
|
152
|
+
const r = chartEl.getBoundingClientRect()
|
|
153
|
+
const left = Math.max(70, Math.min(r.width - 70, clientX - r.left))
|
|
154
|
+
const top = clientY - r.top
|
|
155
|
+
tip = { left, top, below: top < 44, title, rows }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Unified tooltip: every visible series' value at one category.
|
|
159
|
+
function catRows(i: number): TipRow[] {
|
|
160
|
+
return visibleSpec.series
|
|
161
|
+
.map((s) => ({ label: s.label, color: (s as { color?: string }).color, value: s.values[i] }))
|
|
162
|
+
.filter((r) => Number.isFinite(r.value))
|
|
163
|
+
.map((r) => ({ label: r.label, color: r.color, value: fmt(r.value as number) }))
|
|
164
|
+
}
|
|
165
|
+
function hoverCat(clientX: number, clientY: number, i: number) {
|
|
166
|
+
activeCat = i
|
|
167
|
+
showTip(clientX, clientY, spec.categories[i] ?? '', catRows(i))
|
|
168
|
+
}
|
|
169
|
+
function focusCat(el: Element, i: number) {
|
|
170
|
+
const b = el.getBoundingClientRect()
|
|
171
|
+
hoverCat(b.left + b.width / 2, b.top + 12, i)
|
|
172
|
+
}
|
|
173
|
+
function clearActive() {
|
|
174
|
+
activeCat = null
|
|
175
|
+
tip = null
|
|
176
|
+
}
|
|
177
|
+
function hoverSlice(clientX: number, clientY: number, label: string, value: number, pct: number) {
|
|
178
|
+
showTip(clientX, clientY, label, [{ value: `${fmt(value)} · ${pct.toFixed(1)}%` }])
|
|
179
|
+
}
|
|
180
|
+
function hoverDot(clientX: number, clientY: number, d: (typeof geo.scatterPoints)[number]) {
|
|
181
|
+
const rows: TipRow[] = [
|
|
182
|
+
{ label: 'x', value: fmt(d.x) },
|
|
183
|
+
{ label: 'y', value: fmt(d.y) },
|
|
184
|
+
]
|
|
185
|
+
showTip(clientX, clientY, d.label || d.series, rows)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function select(category: string, series: string, value: number) {
|
|
189
|
+
onSelect?.({ category, series, value })
|
|
190
|
+
}
|
|
191
|
+
function onCatKey(e: KeyboardEvent, i: number) {
|
|
192
|
+
if (!onSelect) return
|
|
193
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
194
|
+
e.preventDefault()
|
|
195
|
+
select(spec.categories[i] ?? '', '', 0)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function onSliceKey(e: KeyboardEvent, label: string, value: number) {
|
|
199
|
+
if (!onSelect) return
|
|
200
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
201
|
+
e.preventDefault()
|
|
202
|
+
select(label, label, value)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
</script>
|
|
206
|
+
|
|
207
|
+
<div class="sv-grid-chart" bind:this={chartEl}>
|
|
208
|
+
<svg
|
|
209
|
+
class="sv-grid-chart-svg"
|
|
210
|
+
class:is-interactive={interactive}
|
|
211
|
+
class:is-clickable={!!onSelect}
|
|
212
|
+
viewBox={`0 0 ${geo.width} ${geo.height}`}
|
|
213
|
+
width="100%"
|
|
214
|
+
role="img"
|
|
215
|
+
aria-label={`${spec.type} chart`}
|
|
216
|
+
aria-describedby={`${uid}-table`}
|
|
217
|
+
>
|
|
218
|
+
{#if isHorizontal}
|
|
219
|
+
<!-- Value axis (vertical gridlines + bottom labels) -->
|
|
220
|
+
{#each geo.valueTicks as t (t.label + t.x)}
|
|
221
|
+
<line class="sv-grid-chart-gridline" x1={t.x} y1={geo.plot.y} x2={t.x} y2={geo.plot.y + geo.plot.h} />
|
|
222
|
+
<text class="sv-grid-chart-axis" x={t.x} y={geo.plot.y + geo.plot.h + 14} text-anchor="middle">{t.label}</text>
|
|
223
|
+
{/each}
|
|
224
|
+
<!-- Category labels down the left -->
|
|
225
|
+
{#each geo.catTicks as t (t.value)}
|
|
226
|
+
<text class="sv-grid-chart-axis" x={geo.plot.x - 6} y={t.y + 3} text-anchor="end">{truncate(t.label, 18)}</text>
|
|
227
|
+
{/each}
|
|
228
|
+
{#if spec.yAxisTitle}
|
|
229
|
+
<text class="sv-grid-chart-axis-title" x={13} y={geo.plot.y + geo.plot.h / 2} text-anchor="middle" transform={`rotate(-90 13 ${geo.plot.y + geo.plot.h / 2})`}>{spec.yAxisTitle}</text>
|
|
230
|
+
{/if}
|
|
231
|
+
{#if spec.xAxisTitle}
|
|
232
|
+
<text class="sv-grid-chart-axis-title" x={geo.plot.x + geo.plot.w / 2} y={geo.height - 3} text-anchor="middle">{spec.xAxisTitle}</text>
|
|
233
|
+
{/if}
|
|
234
|
+
{:else if isCartesian}
|
|
235
|
+
{#each geo.yTicks as t (t.value)}
|
|
236
|
+
<line class="sv-grid-chart-gridline" class:is-zero={t.value === 0} x1={geo.plot.x} y1={t.y} x2={geo.plot.x + geo.plot.w} y2={t.y} />
|
|
237
|
+
<text class="sv-grid-chart-axis" x={geo.plot.x - 6} y={t.y + 3} text-anchor="end">{yTickLabel(t.value, t.label)}</text>
|
|
238
|
+
{/each}
|
|
239
|
+
{#if geo.hasRightAxis}
|
|
240
|
+
{#each geo.y2Ticks as t (t.value)}
|
|
241
|
+
<text class="sv-grid-chart-axis" x={geo.plot.x + geo.plot.w + 6} y={t.y + 3} text-anchor="start">{yTickLabel(t.value, t.label)}</text>
|
|
242
|
+
{/each}
|
|
243
|
+
{/if}
|
|
244
|
+
{#each geo.xTicks as t (t.label + t.x)}
|
|
245
|
+
{#if geo.xLabelRotated}
|
|
246
|
+
<text class="sv-grid-chart-axis" x={t.x} y={geo.plot.y + geo.plot.h + 12} text-anchor="end" transform={`rotate(-40 ${t.x} ${geo.plot.y + geo.plot.h + 12})`}>{truncate(t.label)}</text>
|
|
247
|
+
{:else}
|
|
248
|
+
<text class="sv-grid-chart-axis" x={t.x} y={geo.plot.y + geo.plot.h + 16} text-anchor="middle">{truncate(t.label, 16)}</text>
|
|
249
|
+
{/if}
|
|
250
|
+
{/each}
|
|
251
|
+
{#if spec.yAxisTitle}
|
|
252
|
+
<text class="sv-grid-chart-axis-title" x={13} y={geo.plot.y + geo.plot.h / 2} text-anchor="middle" transform={`rotate(-90 13 ${geo.plot.y + geo.plot.h / 2})`}>{spec.yAxisTitle}</text>
|
|
253
|
+
{/if}
|
|
254
|
+
{#if spec.y2AxisTitle}
|
|
255
|
+
<text class="sv-grid-chart-axis-title" x={geo.width - 5} y={geo.plot.y + geo.plot.h / 2} text-anchor="middle" transform={`rotate(90 ${geo.width - 5} ${geo.plot.y + geo.plot.h / 2})`}>{spec.y2AxisTitle}</text>
|
|
256
|
+
{/if}
|
|
257
|
+
{#if spec.xAxisTitle}
|
|
258
|
+
<text class="sv-grid-chart-axis-title" x={geo.plot.x + geo.plot.w / 2} y={geo.height - 3} text-anchor="middle">{spec.xAxisTitle}</text>
|
|
259
|
+
{/if}
|
|
260
|
+
{/if}
|
|
261
|
+
|
|
262
|
+
{#each geo.lines as line, li (line.label + li)}
|
|
263
|
+
<g style={`opacity:${dimOf(line.label)}`}>
|
|
264
|
+
{#if line.areaPath}
|
|
265
|
+
<path class="sv-grid-chart-area" d={line.areaPath} fill={line.color} fill-opacity="0.15" stroke="none" />
|
|
266
|
+
{/if}
|
|
267
|
+
<path class="sv-grid-chart-linepath" d={line.path} fill="none" stroke={line.color} stroke-width="2" stroke-linejoin="round" stroke-linecap="round" />
|
|
268
|
+
{#each line.points as pt, pi (pi)}
|
|
269
|
+
{#if pt.defined}
|
|
270
|
+
<circle class="sv-grid-chart-dot" class:is-active={activeCat === pi} cx={pt.x} cy={pt.y} r={activeCat === pi ? 4 : 3} fill={line.color} />
|
|
271
|
+
{#if dataLabels}
|
|
272
|
+
<text class="sv-grid-chart-datalabel" x={pt.x} y={pt.y - 7} text-anchor="middle">{fmt(pt.value)}</text>
|
|
273
|
+
{/if}
|
|
274
|
+
{/if}
|
|
275
|
+
{/each}
|
|
276
|
+
</g>
|
|
277
|
+
{/each}
|
|
278
|
+
|
|
279
|
+
{#each geo.bars as bar, bi (bi)}
|
|
280
|
+
<rect class="sv-grid-chart-bar" x={bar.x} y={bar.y} width={bar.w} height={bar.h} rx="1" fill={bar.color} style={`opacity:${dimOf(bar.series)}`} />
|
|
281
|
+
{#if dataLabels && isHorizontal && bar.w > 18}
|
|
282
|
+
<text class="sv-grid-chart-datalabel" class:on-bar={spec.stacked || spec.stacked100} x={spec.stacked || spec.stacked100 ? bar.x + bar.w / 2 : bar.value >= 0 ? bar.x + bar.w + 3 : bar.x - 3} y={bar.y + bar.h / 2 + 3} text-anchor={spec.stacked || spec.stacked100 ? 'middle' : bar.value >= 0 ? 'start' : 'end'}>{fmt(bar.value)}</text>
|
|
283
|
+
{:else if dataLabels && !isHorizontal && bar.h > 13}
|
|
284
|
+
<text class="sv-grid-chart-datalabel" class:on-bar={spec.stacked || spec.stacked100} x={bar.x + bar.w / 2} y={spec.stacked || spec.stacked100 ? bar.y + bar.h / 2 + 3 : bar.value >= 0 ? bar.y - 3 : bar.y + bar.h + 11} text-anchor="middle">{fmt(bar.value)}</text>
|
|
285
|
+
{/if}
|
|
286
|
+
{/each}
|
|
287
|
+
|
|
288
|
+
{#each geo.scatterPoints as dot, di (di)}
|
|
289
|
+
<!-- svelte-ignore a11y_no_static_element_interactions a11y_mouse_events_have_key_events -->
|
|
290
|
+
<circle
|
|
291
|
+
class="sv-grid-chart-scatter"
|
|
292
|
+
cx={dot.cx}
|
|
293
|
+
cy={dot.cy}
|
|
294
|
+
r={dot.r}
|
|
295
|
+
fill={dot.color}
|
|
296
|
+
fill-opacity="0.7"
|
|
297
|
+
stroke={dot.color}
|
|
298
|
+
style={`opacity:${dimOf(dot.series)}`}
|
|
299
|
+
onmousemove={(e) => hoverDot(e.clientX, e.clientY, dot)}
|
|
300
|
+
onmouseleave={clearActive}
|
|
301
|
+
/>
|
|
302
|
+
{/each}
|
|
303
|
+
|
|
304
|
+
<!-- Reference / target lines paint on top so they read over the bars/areas. -->
|
|
305
|
+
{#each geo.referenceLines as ref, ri (ri)}
|
|
306
|
+
<line class="sv-grid-chart-refline" x1={geo.plot.x} y1={ref.y} x2={geo.plot.x + geo.plot.w} y2={ref.y} stroke={ref.color} stroke-dasharray={ref.dashed ? '5 4' : undefined} />
|
|
307
|
+
<text class="sv-grid-chart-reflabel" x={geo.plot.x + geo.plot.w - 3} y={ref.y - 3} text-anchor="end" fill={ref.color}>{ref.label}</text>
|
|
308
|
+
{/each}
|
|
309
|
+
{#each geo.referenceLinesV as ref, ri (ri)}
|
|
310
|
+
<line class="sv-grid-chart-refline" x1={ref.x} y1={geo.plot.y} x2={ref.x} y2={geo.plot.y + geo.plot.h} stroke={ref.color} stroke-dasharray={ref.dashed ? '5 4' : undefined} />
|
|
311
|
+
<text class="sv-grid-chart-reflabel" x={ref.x + 3} y={geo.plot.y + 9} text-anchor="start" fill={ref.color}>{ref.label}</text>
|
|
312
|
+
{/each}
|
|
313
|
+
|
|
314
|
+
{#if isCartesian && activeCat !== null}
|
|
315
|
+
{#if isHorizontal}
|
|
316
|
+
{@const cy = geo.plot.y + slot * activeCat + slot / 2}
|
|
317
|
+
<line class="sv-grid-chart-crosshair" x1={geo.plot.x} y1={cy} x2={geo.plot.x + geo.plot.w} y2={cy} />
|
|
318
|
+
{:else}
|
|
319
|
+
{@const cx = geo.plot.x + slot * activeCat + slot / 2}
|
|
320
|
+
<line class="sv-grid-chart-crosshair" x1={cx} y1={geo.plot.y} x2={cx} y2={geo.plot.y + geo.plot.h} />
|
|
321
|
+
{/if}
|
|
322
|
+
{/if}
|
|
323
|
+
|
|
324
|
+
{#each geo.slices as slice, si (si)}
|
|
325
|
+
<!-- svelte-ignore a11y_no_static_element_interactions a11y_mouse_events_have_key_events a11y_no_noninteractive_tabindex -->
|
|
326
|
+
<path
|
|
327
|
+
class="sv-grid-chart-slice"
|
|
328
|
+
d={slice.path}
|
|
329
|
+
fill={slice.color}
|
|
330
|
+
stroke="var(--sg-bg, #fff)"
|
|
331
|
+
stroke-width="1"
|
|
332
|
+
role={onSelect ? 'button' : 'presentation'}
|
|
333
|
+
tabindex={onSelect ? 0 : undefined}
|
|
334
|
+
aria-label={`${slice.label}: ${fmt(slice.value)}, ${slice.percent.toFixed(1)}%`}
|
|
335
|
+
onmousemove={(e) => hoverSlice(e.clientX, e.clientY, slice.label, slice.value, slice.percent)}
|
|
336
|
+
onmouseleave={clearActive}
|
|
337
|
+
onfocus={(e) => { const b = e.currentTarget.getBoundingClientRect(); hoverSlice(b.left + b.width / 2, b.top, slice.label, slice.value, slice.percent) }}
|
|
338
|
+
onblur={clearActive}
|
|
339
|
+
onclick={() => select(slice.label, slice.label, slice.value)}
|
|
340
|
+
onkeydown={(e) => onSliceKey(e, slice.label, slice.value)}
|
|
341
|
+
/>
|
|
342
|
+
{#if dataLabels && slice.percent >= 6}
|
|
343
|
+
<text class="sv-grid-chart-datalabel on-bar" x={slice.cx} y={slice.cy} text-anchor="middle" dominant-baseline="middle">{slice.percent.toFixed(0)}%</text>
|
|
344
|
+
{/if}
|
|
345
|
+
{/each}
|
|
346
|
+
|
|
347
|
+
{#if geo.donut}
|
|
348
|
+
<text class="sv-grid-chart-donut-total" x={geo.donut.cx} y={geo.donut.cy - 4} text-anchor="middle">{fmt(geo.donut.total)}</text>
|
|
349
|
+
<text class="sv-grid-chart-donut-label" x={geo.donut.cx} y={geo.donut.cy + 11} text-anchor="middle">Total</text>
|
|
350
|
+
{/if}
|
|
351
|
+
|
|
352
|
+
{#if isCartesian && !isScatter}
|
|
353
|
+
<!-- Per-category hover/focus zones drive the unified crosshair tooltip. -->
|
|
354
|
+
{#each spec.categories as cat, i (cat + i)}
|
|
355
|
+
<!-- svelte-ignore a11y_no_static_element_interactions a11y_mouse_events_have_key_events a11y_no_noninteractive_tabindex -->
|
|
356
|
+
<rect
|
|
357
|
+
class="sv-grid-chart-cat-hit"
|
|
358
|
+
x={isHorizontal ? geo.plot.x : geo.plot.x + slot * i}
|
|
359
|
+
y={isHorizontal ? geo.plot.y + slot * i : geo.plot.y}
|
|
360
|
+
width={isHorizontal ? geo.plot.w : slot}
|
|
361
|
+
height={isHorizontal ? slot : geo.plot.h}
|
|
362
|
+
role={onSelect ? 'button' : 'presentation'}
|
|
363
|
+
tabindex={onSelect ? 0 : undefined}
|
|
364
|
+
aria-label={`${cat}: ${catRows(i).map((r) => `${r.label} ${r.value}`).join(', ')}`}
|
|
365
|
+
onmousemove={(e) => hoverCat(e.clientX, e.clientY, i)}
|
|
366
|
+
onmouseleave={clearActive}
|
|
367
|
+
onfocus={(e) => focusCat(e.currentTarget, i)}
|
|
368
|
+
onblur={clearActive}
|
|
369
|
+
onclick={() => select(cat, '', 0)}
|
|
370
|
+
onkeydown={(e) => onCatKey(e, i)}
|
|
371
|
+
/>
|
|
372
|
+
{/each}
|
|
373
|
+
{/if}
|
|
374
|
+
</svg>
|
|
375
|
+
|
|
376
|
+
{#if isEmpty}
|
|
377
|
+
<div class="sv-grid-chart-empty">No data</div>
|
|
378
|
+
{/if}
|
|
379
|
+
|
|
380
|
+
{#if tip}
|
|
381
|
+
<div class="sv-grid-chart-tooltip" class:is-below={tip.below} style={`left:${tip.left}px; top:${tip.top}px;`}>
|
|
382
|
+
<span class="sv-grid-chart-tooltip-title">{tip.title}</span>
|
|
383
|
+
{#each tip.rows as r, ri (ri)}
|
|
384
|
+
<span class="sv-grid-chart-tooltip-row">
|
|
385
|
+
{#if r.color}<span class="sv-grid-chart-tooltip-dot" style={`background:${r.color}`}></span>{/if}
|
|
386
|
+
{#if r.label}<span class="sv-grid-chart-tooltip-row-label">{r.label}</span>{/if}
|
|
387
|
+
<span class="sv-grid-chart-tooltip-row-value">{r.value}</span>
|
|
388
|
+
</span>
|
|
389
|
+
{/each}
|
|
390
|
+
</div>
|
|
391
|
+
{/if}
|
|
392
|
+
|
|
393
|
+
{#if legend && legendItems.length}
|
|
394
|
+
<div class="sv-grid-chart-legend">
|
|
395
|
+
{#each shownLegend as item (item.label)}
|
|
396
|
+
<button
|
|
397
|
+
type="button"
|
|
398
|
+
class="sv-grid-chart-legend-item"
|
|
399
|
+
class:is-off={item.off}
|
|
400
|
+
class:is-isolated={isolated === item.label}
|
|
401
|
+
disabled={!interactive}
|
|
402
|
+
aria-pressed={!item.off}
|
|
403
|
+
onclick={() => toggle(item.label)}
|
|
404
|
+
ondblclick={() => isolate(item.label)}
|
|
405
|
+
onpointerenter={() => interactive && (dimmed = item.label)}
|
|
406
|
+
onpointerleave={() => (dimmed = null)}
|
|
407
|
+
title={interactive ? `${item.off ? 'Show' : 'Hide'} ${item.label} · double-click to isolate` : item.label}
|
|
408
|
+
>
|
|
409
|
+
<span class="sv-grid-chart-swatch" style={`background:${item.off ? 'transparent' : item.color}; border-color:${item.color}`}></span>
|
|
410
|
+
{item.label}
|
|
411
|
+
</button>
|
|
412
|
+
{/each}
|
|
413
|
+
{#if legendOverflow}
|
|
414
|
+
<button type="button" class="sv-grid-chart-legend-more" onclick={() => (legendExpanded = !legendExpanded)}>
|
|
415
|
+
{legendExpanded ? 'Show less' : `+${legendItems.length - LEGEND_MAX} more`}
|
|
416
|
+
</button>
|
|
417
|
+
{/if}
|
|
418
|
+
</div>
|
|
419
|
+
{/if}
|
|
420
|
+
|
|
421
|
+
<!-- Visually-hidden data table: the same data for assistive technology. -->
|
|
422
|
+
<table id={`${uid}-table`} class="sv-grid-chart-sr-only">
|
|
423
|
+
<caption>{spec.type} chart data</caption>
|
|
424
|
+
<thead>
|
|
425
|
+
<tr>{#each srTable.cols as c (c)}<th>{c}</th>{/each}</tr>
|
|
426
|
+
</thead>
|
|
427
|
+
<tbody>
|
|
428
|
+
{#each srTable.rows as row, ri (ri)}
|
|
429
|
+
<tr>{#each row as cell, ci (ci)}<td>{cell}</td>{/each}</tr>
|
|
430
|
+
{/each}
|
|
431
|
+
</tbody>
|
|
432
|
+
</table>
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
<style>
|
|
436
|
+
.sv-grid-chart {
|
|
437
|
+
position: relative;
|
|
438
|
+
display: flex;
|
|
439
|
+
flex-direction: column;
|
|
440
|
+
gap: 8px;
|
|
441
|
+
width: 100%;
|
|
442
|
+
}
|
|
443
|
+
.sv-grid-chart-svg {
|
|
444
|
+
display: block;
|
|
445
|
+
width: 100%;
|
|
446
|
+
height: auto;
|
|
447
|
+
}
|
|
448
|
+
.sv-grid-chart-gridline {
|
|
449
|
+
stroke: var(--sg-border, #e2e8f0);
|
|
450
|
+
stroke-width: 1;
|
|
451
|
+
opacity: 0.6;
|
|
452
|
+
}
|
|
453
|
+
.sv-grid-chart-gridline.is-zero {
|
|
454
|
+
stroke: var(--sg-muted, #94a3b8);
|
|
455
|
+
opacity: 0.9;
|
|
456
|
+
}
|
|
457
|
+
.sv-grid-chart-axis {
|
|
458
|
+
fill: var(--sg-muted, #64748b);
|
|
459
|
+
font-size: 10px;
|
|
460
|
+
font-family: inherit;
|
|
461
|
+
}
|
|
462
|
+
.sv-grid-chart-axis-title {
|
|
463
|
+
fill: var(--sg-muted, #64748b);
|
|
464
|
+
font-size: 11px;
|
|
465
|
+
font-weight: 600;
|
|
466
|
+
font-family: inherit;
|
|
467
|
+
}
|
|
468
|
+
.sv-grid-chart-crosshair {
|
|
469
|
+
stroke: var(--sg-accent, #2563eb);
|
|
470
|
+
stroke-width: 1;
|
|
471
|
+
stroke-dasharray: 3 3;
|
|
472
|
+
opacity: 0.65;
|
|
473
|
+
pointer-events: none;
|
|
474
|
+
}
|
|
475
|
+
.sv-grid-chart-refline {
|
|
476
|
+
stroke-width: 1.5;
|
|
477
|
+
opacity: 0.9;
|
|
478
|
+
pointer-events: none;
|
|
479
|
+
}
|
|
480
|
+
.sv-grid-chart-reflabel {
|
|
481
|
+
font-size: 9.5px;
|
|
482
|
+
font-weight: 700;
|
|
483
|
+
font-family: inherit;
|
|
484
|
+
pointer-events: none;
|
|
485
|
+
}
|
|
486
|
+
.sv-grid-chart-scatter {
|
|
487
|
+
transition: opacity 0.2s ease;
|
|
488
|
+
stroke-width: 1;
|
|
489
|
+
}
|
|
490
|
+
.is-clickable .sv-grid-chart-scatter,
|
|
491
|
+
.sv-grid-chart-scatter:hover {
|
|
492
|
+
cursor: pointer;
|
|
493
|
+
}
|
|
494
|
+
.sv-grid-chart-cat-hit {
|
|
495
|
+
fill: transparent;
|
|
496
|
+
}
|
|
497
|
+
.sv-grid-chart-cat-hit:focus-visible,
|
|
498
|
+
.sv-grid-chart-slice:focus-visible {
|
|
499
|
+
outline: 2px solid var(--sg-accent, #2563eb);
|
|
500
|
+
outline-offset: 1px;
|
|
501
|
+
}
|
|
502
|
+
.sv-grid-chart-datalabel {
|
|
503
|
+
fill: var(--sg-fg, #0f172a);
|
|
504
|
+
font-size: 9.5px;
|
|
505
|
+
font-weight: 600;
|
|
506
|
+
font-variant-numeric: tabular-nums;
|
|
507
|
+
pointer-events: none;
|
|
508
|
+
}
|
|
509
|
+
.sv-grid-chart-datalabel.on-bar {
|
|
510
|
+
fill: #fff;
|
|
511
|
+
}
|
|
512
|
+
.sv-grid-chart-donut-total {
|
|
513
|
+
fill: var(--sg-fg, #0f172a);
|
|
514
|
+
font-size: 16px;
|
|
515
|
+
font-weight: 800;
|
|
516
|
+
font-variant-numeric: tabular-nums;
|
|
517
|
+
}
|
|
518
|
+
.sv-grid-chart-donut-label {
|
|
519
|
+
fill: var(--sg-muted, #64748b);
|
|
520
|
+
font-size: 10px;
|
|
521
|
+
}
|
|
522
|
+
.sv-grid-chart-bar {
|
|
523
|
+
transition: x 0.3s ease, y 0.3s ease, width 0.3s ease, height 0.3s ease;
|
|
524
|
+
}
|
|
525
|
+
.sv-grid-chart-dot {
|
|
526
|
+
transition: cx 0.3s ease, cy 0.3s ease;
|
|
527
|
+
}
|
|
528
|
+
.is-clickable .sv-grid-chart-cat-hit,
|
|
529
|
+
.is-clickable .sv-grid-chart-slice {
|
|
530
|
+
cursor: pointer;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.sv-grid-chart-empty {
|
|
534
|
+
position: absolute;
|
|
535
|
+
inset: 0;
|
|
536
|
+
display: flex;
|
|
537
|
+
align-items: center;
|
|
538
|
+
justify-content: center;
|
|
539
|
+
color: var(--sg-muted, #94a3b8);
|
|
540
|
+
font-size: 13px;
|
|
541
|
+
pointer-events: none;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.sv-grid-chart-tooltip {
|
|
545
|
+
position: absolute;
|
|
546
|
+
z-index: 10;
|
|
547
|
+
transform: translate(-50%, calc(-100% - 12px));
|
|
548
|
+
pointer-events: none;
|
|
549
|
+
white-space: nowrap;
|
|
550
|
+
padding: 6px 9px;
|
|
551
|
+
border-radius: 6px;
|
|
552
|
+
background: var(--sg-fg, #0f172a);
|
|
553
|
+
color: var(--sg-bg, #fff);
|
|
554
|
+
font-size: 11.5px;
|
|
555
|
+
line-height: 1.4;
|
|
556
|
+
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28);
|
|
557
|
+
display: flex;
|
|
558
|
+
flex-direction: column;
|
|
559
|
+
gap: 1px;
|
|
560
|
+
}
|
|
561
|
+
.sv-grid-chart-tooltip.is-below {
|
|
562
|
+
transform: translate(-50%, 16px);
|
|
563
|
+
}
|
|
564
|
+
.sv-grid-chart-tooltip-title {
|
|
565
|
+
opacity: 0.75;
|
|
566
|
+
font-size: 10.5px;
|
|
567
|
+
margin-bottom: 1px;
|
|
568
|
+
}
|
|
569
|
+
.sv-grid-chart-tooltip-row {
|
|
570
|
+
display: flex;
|
|
571
|
+
align-items: center;
|
|
572
|
+
gap: 6px;
|
|
573
|
+
}
|
|
574
|
+
.sv-grid-chart-tooltip-dot {
|
|
575
|
+
width: 8px;
|
|
576
|
+
height: 8px;
|
|
577
|
+
border-radius: 2px;
|
|
578
|
+
flex-shrink: 0;
|
|
579
|
+
}
|
|
580
|
+
.sv-grid-chart-tooltip-row-label {
|
|
581
|
+
opacity: 0.85;
|
|
582
|
+
}
|
|
583
|
+
.sv-grid-chart-tooltip-row-value {
|
|
584
|
+
margin-left: auto;
|
|
585
|
+
padding-left: 10px;
|
|
586
|
+
font-weight: 700;
|
|
587
|
+
font-variant-numeric: tabular-nums;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.sv-grid-chart-legend {
|
|
591
|
+
display: flex;
|
|
592
|
+
flex-wrap: wrap;
|
|
593
|
+
gap: 6px 12px;
|
|
594
|
+
font-size: 12px;
|
|
595
|
+
color: var(--sg-fg, #0f172a);
|
|
596
|
+
}
|
|
597
|
+
.sv-grid-chart-legend-item {
|
|
598
|
+
display: inline-flex;
|
|
599
|
+
align-items: center;
|
|
600
|
+
gap: 6px;
|
|
601
|
+
border: 0;
|
|
602
|
+
background: transparent;
|
|
603
|
+
color: inherit;
|
|
604
|
+
font: inherit;
|
|
605
|
+
padding: 1px 2px;
|
|
606
|
+
border-radius: 4px;
|
|
607
|
+
cursor: pointer;
|
|
608
|
+
}
|
|
609
|
+
.sv-grid-chart-legend-item:disabled {
|
|
610
|
+
cursor: default;
|
|
611
|
+
}
|
|
612
|
+
.sv-grid-chart-legend-item.is-off {
|
|
613
|
+
color: var(--sg-muted, #94a3b8);
|
|
614
|
+
text-decoration: line-through;
|
|
615
|
+
}
|
|
616
|
+
.sv-grid-chart-legend-item.is-isolated {
|
|
617
|
+
background: var(--sg-header-bg, #f1f5f9);
|
|
618
|
+
font-weight: 700;
|
|
619
|
+
}
|
|
620
|
+
.sv-grid-chart-legend-more {
|
|
621
|
+
border: 1px dashed var(--sg-border, #cbd5e1);
|
|
622
|
+
background: transparent;
|
|
623
|
+
color: var(--sg-muted, #64748b);
|
|
624
|
+
font: inherit;
|
|
625
|
+
font-size: 11px;
|
|
626
|
+
padding: 1px 7px;
|
|
627
|
+
border-radius: 999px;
|
|
628
|
+
cursor: pointer;
|
|
629
|
+
}
|
|
630
|
+
.sv-grid-chart-legend-more:hover {
|
|
631
|
+
color: var(--sg-fg, #0f172a);
|
|
632
|
+
border-color: var(--sg-accent, #2563eb);
|
|
633
|
+
}
|
|
634
|
+
.sv-grid-chart-sr-only {
|
|
635
|
+
position: absolute;
|
|
636
|
+
width: 1px;
|
|
637
|
+
height: 1px;
|
|
638
|
+
padding: 0;
|
|
639
|
+
margin: -1px;
|
|
640
|
+
overflow: hidden;
|
|
641
|
+
clip: rect(0 0 0 0);
|
|
642
|
+
white-space: nowrap;
|
|
643
|
+
border: 0;
|
|
644
|
+
}
|
|
645
|
+
.sv-grid-chart-swatch {
|
|
646
|
+
width: 12px;
|
|
647
|
+
height: 12px;
|
|
648
|
+
border-radius: 3px;
|
|
649
|
+
border: 1.5px solid transparent;
|
|
650
|
+
display: inline-block;
|
|
651
|
+
box-sizing: border-box;
|
|
652
|
+
}
|
|
653
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvGridChart - renders a `ChartSpec` as inline SVG. Supports grouped +
|
|
3
|
+
* stacked bars, line, area, pie / donut, combo charts (per-series type),
|
|
4
|
+
* a secondary Y axis, signed Y domains, and axis titles. Interactive: a
|
|
5
|
+
* unified crosshair tooltip (hover a category -> all series at once),
|
|
6
|
+
* focus tooltips, a clickable legend that toggles series, optional data
|
|
7
|
+
* labels, and an `onSelect` drill hook. No external charting dependency.
|
|
8
|
+
*/
|
|
9
|
+
import { type ChartSpec, type ChartSelection } from './chart';
|
|
10
|
+
type Props = {
|
|
11
|
+
spec: ChartSpec;
|
|
12
|
+
/** Show the (clickable) legend. Default true. */
|
|
13
|
+
legend?: boolean;
|
|
14
|
+
/** Enable tooltips + crosshair + legend toggling. Default true. */
|
|
15
|
+
interactive?: boolean;
|
|
16
|
+
/** Draw the value on each bar / point / slice. Default false. */
|
|
17
|
+
dataLabels?: boolean;
|
|
18
|
+
/** Format a value for tooltips, data labels, AND Y-axis ticks. */
|
|
19
|
+
formatValue?: (value: number) => string;
|
|
20
|
+
/** Fired when a category / slice is clicked (drill into the grid). */
|
|
21
|
+
onSelect?: (selection: ChartSelection) => void;
|
|
22
|
+
};
|
|
23
|
+
declare const SvGridChart: import("svelte").Component<Props, {}, "">;
|
|
24
|
+
type SvGridChart = ReturnType<typeof SvGridChart>;
|
|
25
|
+
export default SvGridChart;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|