@classic-homes/charts-svelte 0.2.0 → 0.3.1
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 +50 -0
- package/dist/lib/components/base/ChartContainer.svelte +141 -14
- package/dist/lib/components/base/ChartContainer.svelte.d.ts +19 -1
- package/dist/lib/components/base/ChartDataTable.svelte +59 -0
- package/dist/lib/components/base/ChartDataTable.svelte.d.ts +15 -0
- package/dist/lib/components/base/ChartEmpty.svelte +1 -0
- package/dist/lib/components/base/ChartSkeleton.svelte +7 -2
- package/dist/lib/components/base/ChartSkeleton.svelte.d.ts +2 -0
- package/dist/lib/components/base/index.d.ts +3 -0
- package/dist/lib/components/base/index.js +1 -0
- package/dist/lib/components/core/AreaChart.svelte +115 -70
- package/dist/lib/components/core/BarChart.svelte +134 -62
- package/dist/lib/components/core/DonutChart.svelte +41 -22
- package/dist/lib/components/core/LineChart.svelte +104 -73
- package/dist/lib/components/core/PieChart.svelte +41 -22
- package/dist/lib/components/core/ScatterChart.svelte +72 -68
- package/dist/lib/components/extended/CandlestickChart.svelte +67 -44
- package/dist/lib/components/extended/FunnelChart.svelte +41 -27
- package/dist/lib/components/extended/GaugeChart.svelte +61 -10
- package/dist/lib/components/extended/HeatmapChart.svelte +62 -33
- package/dist/lib/components/extended/RadarChart.svelte +39 -19
- package/dist/lib/components/extended/SankeyChart.svelte +50 -20
- package/dist/lib/components/extended/TreemapChart.svelte +53 -21
- package/dist/lib/components/index.d.ts +2 -1
- package/dist/lib/components/index.js +3 -1
- package/dist/lib/components/report/BoxPlotChart.svelte +253 -0
- package/dist/lib/components/report/BoxPlotChart.svelte.d.ts +7 -0
- package/dist/lib/components/report/BulletChart.svelte +231 -0
- package/dist/lib/components/report/BulletChart.svelte.d.ts +7 -0
- package/dist/lib/components/report/ComboChart.svelte +327 -0
- package/dist/lib/components/report/ComboChart.svelte.d.ts +8 -0
- package/dist/lib/components/report/DotPlotChart.svelte +325 -0
- package/dist/lib/components/report/DotPlotChart.svelte.d.ts +8 -0
- package/dist/lib/components/report/HistogramChart.svelte +226 -0
- package/dist/lib/components/report/HistogramChart.svelte.d.ts +7 -0
- package/dist/lib/components/report/Sparkline.svelte +181 -0
- package/dist/lib/components/report/Sparkline.svelte.d.ts +4 -0
- package/dist/lib/components/report/StatCard.svelte +146 -0
- package/dist/lib/components/report/StatCard.svelte.d.ts +4 -0
- package/dist/lib/components/report/StatGroup.svelte +35 -0
- package/dist/lib/components/report/StatGroup.svelte.d.ts +12 -0
- package/dist/lib/components/report/WaterfallChart.svelte +271 -0
- package/dist/lib/components/report/WaterfallChart.svelte.d.ts +7 -0
- package/dist/lib/components/report/index.d.ts +12 -0
- package/dist/lib/components/report/index.js +9 -0
- package/dist/lib/composables/index.d.ts +1 -1
- package/dist/lib/composables/index.js +1 -1
- package/dist/lib/composables/useChart.svelte.d.ts +34 -0
- package/dist/lib/composables/useChart.svelte.js +121 -9
- package/dist/lib/composables/useChartTheme.svelte.js +14 -11
- package/dist/lib/events.d.ts +15 -0
- package/dist/lib/events.js +56 -0
- package/dist/lib/index.d.ts +4 -3
- package/dist/lib/index.js +7 -3
- package/package.json +10 -5
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @classic-homes/charts-svelte
|
|
2
|
+
|
|
3
|
+
Svelte ECharts components for the Classic Theme design system. Charts pick up the Classic light and dark themes from `@classic-homes/charts-core`.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @classic-homes/charts-svelte echarts svelte
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependencies: `echarts@^5.5.0`, `svelte@^5.20.0`.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```svelte
|
|
16
|
+
<script lang="ts">
|
|
17
|
+
import { LineChart } from '@classic-homes/charts-svelte';
|
|
18
|
+
|
|
19
|
+
const data = {
|
|
20
|
+
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
|
|
21
|
+
series: [
|
|
22
|
+
{ name: '2024', data: [120, 132, 101, 134, 90, 230] },
|
|
23
|
+
{ name: '2025', data: [220, 182, 191, 234, 290, 330] },
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<LineChart title="Monthly Revenue" {data} smooth height={400} />
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Exports
|
|
32
|
+
|
|
33
|
+
- **Charts**: `LineChart`, `BarChart`, `PieChart`, `AreaChart`, `ScatterChart`, `DonutChart`, `RadarChart`, `FunnelChart`, `GaugeChart`, `HeatmapChart`, `TreemapChart`, `CandlestickChart`, `SankeyChart`, `ComboChart`, `DotPlotChart`, `BulletChart`, `HistogramChart`, `BoxPlotChart`, `WaterfallChart`
|
|
34
|
+
- **Stat helpers**: `Sparkline`, `StatCard`, `StatGroup`
|
|
35
|
+
- **Base components**: `ChartContainer`, `ChartSkeleton`, `ChartError`, `ChartEmpty`, `ChartLegend`, `ChartDataTable`
|
|
36
|
+
- **Composables**: `useChart`, `useChartTheme`, `useChartResize`, `useChartLegend`, `useReducedMotion`, `registerClassicThemes`, `registerEchartsModules`, `exportChartImage`
|
|
37
|
+
- **Data helpers** (from `@classic-homes/charts-core`): `fromRecords`, `toSliceData`, `binValues`, `boxplotStats`, `waterfallLayout`, `formatValue`, `computeDelta`, `tableToCsv`, `downloadCsv`
|
|
38
|
+
|
|
39
|
+
Chart data and option types are exported from the package root, and also from `@classic-homes/charts-core/types`.
|
|
40
|
+
|
|
41
|
+
```svelte
|
|
42
|
+
<script lang="ts">
|
|
43
|
+
import { ChartSkeleton, DonutChart, type PieSliceData } from '@classic-homes/charts-svelte';
|
|
44
|
+
</script>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Related Packages
|
|
48
|
+
|
|
49
|
+
- `@classic-homes/charts-core`: shared themes, types, and utilities
|
|
50
|
+
- `@classic-homes/charts-react`: React equivalents for the core and extended charts
|
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
chartFilename,
|
|
5
|
+
downloadCsv,
|
|
6
|
+
downloadFile,
|
|
7
|
+
type ChartDataTable as ChartDataTableData,
|
|
8
|
+
type ChartExportConfig,
|
|
9
|
+
type ChartLegendPosition,
|
|
10
|
+
} from '@classic-homes/charts-core';
|
|
4
11
|
import { cn } from '../../utils.js';
|
|
5
12
|
import ChartSkeleton from './ChartSkeleton.svelte';
|
|
6
13
|
import ChartError from './ChartError.svelte';
|
|
7
14
|
import ChartEmpty from './ChartEmpty.svelte';
|
|
15
|
+
import ChartDataTable from './ChartDataTable.svelte';
|
|
8
16
|
|
|
9
17
|
interface Props {
|
|
10
18
|
title: string;
|
|
11
19
|
description?: string;
|
|
12
20
|
height?: number | string;
|
|
21
|
+
/** Minimum height in pixels */
|
|
22
|
+
minHeight?: number;
|
|
23
|
+
/** Maximum height in pixels */
|
|
24
|
+
maxHeight?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Loading. With no data yet this shows a skeleton; over an existing chart
|
|
27
|
+
* it dims the chart instead, keeping its zoom and legend state.
|
|
28
|
+
*/
|
|
13
29
|
loading?: boolean;
|
|
14
30
|
error?: string | null;
|
|
15
31
|
empty?: boolean;
|
|
@@ -24,6 +40,16 @@
|
|
|
24
40
|
legend?: Snippet;
|
|
25
41
|
/** Where the custom legend sits (defaults to 'bottom') */
|
|
26
42
|
legendPosition?: ChartLegendPosition;
|
|
43
|
+
/** The chart's data as a table, for screen readers, "Show data" and CSV export */
|
|
44
|
+
dataTable?: ChartDataTableData;
|
|
45
|
+
/** 'sr-only' renders the table for screen readers, true adds a visible toggle */
|
|
46
|
+
showDataTable?: boolean | 'sr-only';
|
|
47
|
+
/** Formats values in the data table */
|
|
48
|
+
format?: (value: number) => string;
|
|
49
|
+
/** Show download buttons */
|
|
50
|
+
exportable?: boolean | ChartExportConfig;
|
|
51
|
+
/** Renders the chart to an image data URL, for the PNG download */
|
|
52
|
+
exportImage?: () => string | null;
|
|
27
53
|
children: Snippet;
|
|
28
54
|
}
|
|
29
55
|
|
|
@@ -31,6 +57,8 @@
|
|
|
31
57
|
title,
|
|
32
58
|
description,
|
|
33
59
|
height = 400,
|
|
60
|
+
minHeight,
|
|
61
|
+
maxHeight,
|
|
34
62
|
loading,
|
|
35
63
|
error,
|
|
36
64
|
empty,
|
|
@@ -40,6 +68,11 @@
|
|
|
40
68
|
wrapperClass,
|
|
41
69
|
legend,
|
|
42
70
|
legendPosition = 'bottom',
|
|
71
|
+
dataTable,
|
|
72
|
+
showDataTable = 'sr-only',
|
|
73
|
+
format,
|
|
74
|
+
exportable,
|
|
75
|
+
exportImage,
|
|
43
76
|
children,
|
|
44
77
|
}: Props = $props();
|
|
45
78
|
|
|
@@ -48,10 +81,39 @@
|
|
|
48
81
|
|
|
49
82
|
const heightStyle = $derived(typeof height === 'number' ? `${height}px` : height);
|
|
50
83
|
const descriptionId = $derived(description ? `${uid}-desc` : undefined);
|
|
84
|
+
const tableId = `${uid}-table`;
|
|
51
85
|
|
|
52
86
|
// The legend's DOM order follows its visual position
|
|
53
87
|
const side = $derived(legendPosition === 'left' || legendPosition === 'right');
|
|
54
88
|
const leading = $derived(legendPosition === 'top' || legendPosition === 'left');
|
|
89
|
+
|
|
90
|
+
const exportConfig = $derived<ChartExportConfig | undefined>(
|
|
91
|
+
exportable ? (exportable === true ? {} : exportable) : undefined
|
|
92
|
+
);
|
|
93
|
+
const canExportImage = $derived(!!exportConfig && exportConfig.image !== false && !!exportImage);
|
|
94
|
+
const canExportCsv = $derived(!!exportConfig && exportConfig.csv !== false && !!dataTable);
|
|
95
|
+
const filename = $derived(exportConfig?.filename ?? chartFilename(title));
|
|
96
|
+
|
|
97
|
+
// A hidden table of thousands of rows (a dense scatter or heatmap) costs
|
|
98
|
+
// more than it helps; "Show data" and CSV export still offer the data
|
|
99
|
+
const SR_TABLE_MAX_ROWS = 500;
|
|
100
|
+
|
|
101
|
+
let tableOpen = $state(false);
|
|
102
|
+
const toggleTable = $derived(showDataTable === true && !!dataTable);
|
|
103
|
+
const toolbar = $derived(canExportImage || canExportCsv || toggleTable);
|
|
104
|
+
const framed = $derived(!!legend || toolbar);
|
|
105
|
+
|
|
106
|
+
function downloadImage() {
|
|
107
|
+
const url = exportImage?.();
|
|
108
|
+
if (url) downloadFile(url, `${filename}.png`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function downloadData() {
|
|
112
|
+
if (dataTable) downloadCsv(dataTable, filename);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const buttonClass =
|
|
116
|
+
'rounded-md px-2 py-1 text-xs font-medium text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring';
|
|
55
117
|
</script>
|
|
56
118
|
|
|
57
119
|
{#snippet chart(chartClass: string)}
|
|
@@ -59,15 +121,31 @@
|
|
|
59
121
|
role="img"
|
|
60
122
|
aria-label={title}
|
|
61
123
|
aria-describedby={descriptionId}
|
|
124
|
+
aria-busy={loading || undefined}
|
|
62
125
|
data-testid="chart-container"
|
|
63
|
-
class={chartClass}
|
|
126
|
+
class={cn('relative', chartClass)}
|
|
64
127
|
>
|
|
65
128
|
{#if description}
|
|
66
129
|
<span id={descriptionId} class="sr-only">{description}</span>
|
|
67
130
|
{/if}
|
|
68
|
-
<div
|
|
131
|
+
<div
|
|
132
|
+
class={cn('h-full w-full', className)}
|
|
133
|
+
style:height={heightStyle}
|
|
134
|
+
style:min-height={minHeight !== undefined ? `${minHeight}px` : undefined}
|
|
135
|
+
style:max-height={maxHeight !== undefined ? `${maxHeight}px` : undefined}
|
|
136
|
+
>
|
|
69
137
|
{@render children()}
|
|
70
138
|
</div>
|
|
139
|
+
{#if loading}
|
|
140
|
+
<div
|
|
141
|
+
data-testid="chart-loading-overlay"
|
|
142
|
+
class="absolute inset-0 flex items-center justify-center rounded-lg bg-card/60"
|
|
143
|
+
>
|
|
144
|
+
<span
|
|
145
|
+
class="h-6 w-6 animate-spin rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground motion-reduce:animate-none"
|
|
146
|
+
></span>
|
|
147
|
+
</div>
|
|
148
|
+
{/if}
|
|
71
149
|
</div>
|
|
72
150
|
{/snippet}
|
|
73
151
|
|
|
@@ -80,24 +158,73 @@
|
|
|
80
158
|
</div>
|
|
81
159
|
{/snippet}
|
|
82
160
|
|
|
83
|
-
{#
|
|
84
|
-
|
|
161
|
+
{#snippet srTable()}
|
|
162
|
+
{#if dataTable && showDataTable === 'sr-only' && dataTable.rows.length <= SR_TABLE_MAX_ROWS}
|
|
163
|
+
<ChartDataTable table={dataTable} caption={`${title} data`} {format} srOnly />
|
|
164
|
+
{/if}
|
|
165
|
+
{/snippet}
|
|
166
|
+
|
|
167
|
+
{#if loading && empty}
|
|
168
|
+
<ChartSkeleton {height} label={`Loading ${title}`} class={wrapperClass} />
|
|
85
169
|
{:else if error}
|
|
86
170
|
<ChartError message={error} {height} {onRetry} class={wrapperClass} />
|
|
87
171
|
{:else if empty}
|
|
88
172
|
<ChartEmpty message={emptyMessage} {height} class={wrapperClass} />
|
|
89
|
-
{:else if
|
|
173
|
+
{:else if framed}
|
|
90
174
|
<div
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
side ? 'flex-row' : 'flex-col',
|
|
94
|
-
wrapperClass
|
|
95
|
-
)}
|
|
175
|
+
data-testid="chart-frame"
|
|
176
|
+
class={cn('flex flex-col rounded-lg border border-border bg-card', wrapperClass)}
|
|
96
177
|
>
|
|
97
|
-
{#if
|
|
98
|
-
|
|
99
|
-
|
|
178
|
+
{#if toolbar}
|
|
179
|
+
<div data-testid="chart-toolbar" class="flex justify-end gap-1 px-2 pt-2 print:hidden">
|
|
180
|
+
{#if toggleTable}
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
class={buttonClass}
|
|
184
|
+
aria-expanded={tableOpen}
|
|
185
|
+
aria-controls={tableId}
|
|
186
|
+
onclick={() => (tableOpen = !tableOpen)}
|
|
187
|
+
>
|
|
188
|
+
{tableOpen ? 'Hide data' : 'Show data'}
|
|
189
|
+
</button>
|
|
190
|
+
{/if}
|
|
191
|
+
{#if canExportImage}
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
class={buttonClass}
|
|
195
|
+
aria-label={`Download ${title} as PNG`}
|
|
196
|
+
onclick={downloadImage}
|
|
197
|
+
>
|
|
198
|
+
PNG
|
|
199
|
+
</button>
|
|
200
|
+
{/if}
|
|
201
|
+
{#if canExportCsv}
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
class={buttonClass}
|
|
205
|
+
aria-label={`Download ${title} data as CSV`}
|
|
206
|
+
onclick={downloadData}
|
|
207
|
+
>
|
|
208
|
+
CSV
|
|
209
|
+
</button>
|
|
210
|
+
{/if}
|
|
211
|
+
</div>
|
|
212
|
+
{/if}
|
|
213
|
+
<div class={cn('flex min-h-0 flex-1', side ? 'flex-row' : 'flex-col')}>
|
|
214
|
+
{#if legend && leading}{@render legendArea(legend)}{/if}
|
|
215
|
+
{@render chart('min-h-0 min-w-0 flex-1')}
|
|
216
|
+
{#if legend && !leading}{@render legendArea(legend)}{/if}
|
|
217
|
+
</div>
|
|
218
|
+
{#if toggleTable && dataTable}
|
|
219
|
+
<div id={tableId} hidden={!tableOpen} class="border-t border-border p-2">
|
|
220
|
+
{#if tableOpen}
|
|
221
|
+
<ChartDataTable table={dataTable} caption={`${title} data`} {format} />
|
|
222
|
+
{/if}
|
|
223
|
+
</div>
|
|
224
|
+
{/if}
|
|
225
|
+
{@render srTable()}
|
|
100
226
|
</div>
|
|
101
227
|
{:else}
|
|
102
228
|
{@render chart(cn('rounded-lg border border-border bg-card', wrapperClass))}
|
|
229
|
+
{@render srTable()}
|
|
103
230
|
{/if}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
import type
|
|
2
|
+
import { type ChartDataTable as ChartDataTableData, type ChartExportConfig, type ChartLegendPosition } from '@classic-homes/charts-core';
|
|
3
3
|
interface Props {
|
|
4
4
|
title: string;
|
|
5
5
|
description?: string;
|
|
6
6
|
height?: number | string;
|
|
7
|
+
/** Minimum height in pixels */
|
|
8
|
+
minHeight?: number;
|
|
9
|
+
/** Maximum height in pixels */
|
|
10
|
+
maxHeight?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Loading. With no data yet this shows a skeleton; over an existing chart
|
|
13
|
+
* it dims the chart instead, keeping its zoom and legend state.
|
|
14
|
+
*/
|
|
7
15
|
loading?: boolean;
|
|
8
16
|
error?: string | null;
|
|
9
17
|
empty?: boolean;
|
|
@@ -18,6 +26,16 @@ interface Props {
|
|
|
18
26
|
legend?: Snippet;
|
|
19
27
|
/** Where the custom legend sits (defaults to 'bottom') */
|
|
20
28
|
legendPosition?: ChartLegendPosition;
|
|
29
|
+
/** The chart's data as a table, for screen readers, "Show data" and CSV export */
|
|
30
|
+
dataTable?: ChartDataTableData;
|
|
31
|
+
/** 'sr-only' renders the table for screen readers, true adds a visible toggle */
|
|
32
|
+
showDataTable?: boolean | 'sr-only';
|
|
33
|
+
/** Formats values in the data table */
|
|
34
|
+
format?: (value: number) => string;
|
|
35
|
+
/** Show download buttons */
|
|
36
|
+
exportable?: boolean | ChartExportConfig;
|
|
37
|
+
/** Renders the chart to an image data URL, for the PNG download */
|
|
38
|
+
exportImage?: () => string | null;
|
|
21
39
|
children: Snippet;
|
|
22
40
|
}
|
|
23
41
|
declare const ChartContainer: import("svelte").Component<Props, {}, "">;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { formatTableCell, type ChartDataTable } from '@classic-homes/charts-core';
|
|
3
|
+
import { cn } from '../../utils.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Table contents */
|
|
7
|
+
table: ChartDataTable;
|
|
8
|
+
/** Caption naming the chart the data belongs to */
|
|
9
|
+
caption: string;
|
|
10
|
+
/** Formats 'value' columns */
|
|
11
|
+
format?: (value: number) => string;
|
|
12
|
+
/** Hide visually, leaving the table to screen readers */
|
|
13
|
+
srOnly?: boolean;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let { table, caption, format, srOnly = false, class: className }: Props = $props();
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<div
|
|
21
|
+
data-testid="chart-data-table"
|
|
22
|
+
class={cn(srOnly ? 'sr-only' : 'max-h-80 overflow-auto', className)}
|
|
23
|
+
>
|
|
24
|
+
<table class="w-full border-collapse text-sm">
|
|
25
|
+
<caption class={srOnly ? undefined : 'sr-only'}>{caption}</caption>
|
|
26
|
+
<thead>
|
|
27
|
+
<tr>
|
|
28
|
+
{#each table.headers as header, column (column)}
|
|
29
|
+
<th
|
|
30
|
+
scope="col"
|
|
31
|
+
class={cn(
|
|
32
|
+
'sticky top-0 border-b border-border bg-card px-3 py-2 font-medium text-muted-foreground',
|
|
33
|
+
column === 0 ? 'text-left' : 'text-right'
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
{header}
|
|
37
|
+
</th>
|
|
38
|
+
{/each}
|
|
39
|
+
</tr>
|
|
40
|
+
</thead>
|
|
41
|
+
<tbody>
|
|
42
|
+
{#each table.rows as row, rowIndex (rowIndex)}
|
|
43
|
+
<tr class="border-b border-border last:border-0">
|
|
44
|
+
{#each row as cell, column (column)}
|
|
45
|
+
{#if column === 0}
|
|
46
|
+
<th scope="row" class="px-3 py-1.5 text-left font-normal text-foreground">
|
|
47
|
+
{formatTableCell(table, cell, column, format)}
|
|
48
|
+
</th>
|
|
49
|
+
{:else}
|
|
50
|
+
<td class="px-3 py-1.5 text-right tabular-nums text-foreground">
|
|
51
|
+
{formatTableCell(table, cell, column, format)}
|
|
52
|
+
</td>
|
|
53
|
+
{/if}
|
|
54
|
+
{/each}
|
|
55
|
+
</tr>
|
|
56
|
+
{/each}
|
|
57
|
+
</tbody>
|
|
58
|
+
</table>
|
|
59
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ChartDataTable } from '@classic-homes/charts-core';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Table contents */
|
|
4
|
+
table: ChartDataTable;
|
|
5
|
+
/** Caption naming the chart the data belongs to */
|
|
6
|
+
caption: string;
|
|
7
|
+
/** Formats 'value' columns */
|
|
8
|
+
format?: (value: number) => string;
|
|
9
|
+
/** Hide visually, leaving the table to screen readers */
|
|
10
|
+
srOnly?: boolean;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
declare const ChartDataTable: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type ChartDataTable = ReturnType<typeof ChartDataTable>;
|
|
15
|
+
export default ChartDataTable;
|
|
@@ -3,20 +3,25 @@
|
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
5
|
height?: number | string;
|
|
6
|
+
/** Accessible name announced while loading (defaults to 'Loading chart') */
|
|
7
|
+
label?: string;
|
|
6
8
|
class?: string;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
let { height = 400, class: className }: Props = $props();
|
|
11
|
+
let { height = 400, label = 'Loading chart', class: className }: Props = $props();
|
|
10
12
|
|
|
11
13
|
const heightStyle = $derived(typeof height === 'number' ? `${height}px` : height);
|
|
12
14
|
</script>
|
|
13
15
|
|
|
14
16
|
<div
|
|
15
17
|
data-testid="chart-skeleton"
|
|
18
|
+
role="status"
|
|
19
|
+
aria-busy="true"
|
|
20
|
+
aria-label={label}
|
|
16
21
|
class={cn('animate-pulse rounded-lg border border-border bg-muted', className)}
|
|
17
22
|
style:height={heightStyle}
|
|
18
23
|
>
|
|
19
|
-
<div class="flex h-full flex-col p-4">
|
|
24
|
+
<div class="flex h-full flex-col p-4" aria-hidden="true">
|
|
20
25
|
<!-- Title skeleton -->
|
|
21
26
|
<div class="mb-4 h-5 w-1/3 rounded bg-muted-foreground/20"></div>
|
|
22
27
|
|
|
@@ -3,14 +3,17 @@ export { default as ChartSkeleton } from './ChartSkeleton.svelte';
|
|
|
3
3
|
export { default as ChartError } from './ChartError.svelte';
|
|
4
4
|
export { default as ChartEmpty } from './ChartEmpty.svelte';
|
|
5
5
|
export { default as ChartLegend } from './ChartLegend.svelte';
|
|
6
|
+
export { default as ChartDataTable } from './ChartDataTable.svelte';
|
|
6
7
|
import type { ComponentProps } from 'svelte';
|
|
7
8
|
import type ChartContainer from './ChartContainer.svelte';
|
|
8
9
|
import type ChartSkeleton from './ChartSkeleton.svelte';
|
|
9
10
|
import type ChartError from './ChartError.svelte';
|
|
10
11
|
import type ChartEmpty from './ChartEmpty.svelte';
|
|
11
12
|
import type ChartLegend from './ChartLegend.svelte';
|
|
13
|
+
import type ChartDataTable from './ChartDataTable.svelte';
|
|
12
14
|
export type ChartContainerProps = ComponentProps<typeof ChartContainer>;
|
|
13
15
|
export type ChartSkeletonProps = ComponentProps<typeof ChartSkeleton>;
|
|
14
16
|
export type ChartErrorProps = ComponentProps<typeof ChartError>;
|
|
15
17
|
export type ChartEmptyProps = ComponentProps<typeof ChartEmpty>;
|
|
16
18
|
export type ChartLegendProps = ComponentProps<typeof ChartLegend>;
|
|
19
|
+
export type ChartDataTableProps = ComponentProps<typeof ChartDataTable>;
|
|
@@ -3,3 +3,4 @@ export { default as ChartSkeleton } from './ChartSkeleton.svelte';
|
|
|
3
3
|
export { default as ChartError } from './ChartError.svelte';
|
|
4
4
|
export { default as ChartEmpty } from './ChartEmpty.svelte';
|
|
5
5
|
export { default as ChartLegend } from './ChartLegend.svelte';
|
|
6
|
+
export { default as ChartDataTable } from './ChartDataTable.svelte';
|