@classic-homes/charts-svelte 0.1.38 → 0.3.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/dist/lib/components/base/ChartContainer.svelte +175 -9
- package/dist/lib/components/base/ChartContainer.svelte.d.ts +26 -0
- 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/ChartLegend.svelte +59 -0
- package/dist/lib/components/base/ChartLegend.svelte.d.ts +10 -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 +6 -0
- package/dist/lib/components/base/index.js +2 -0
- package/dist/lib/components/core/AreaChart.svelte +204 -92
- package/dist/lib/components/core/AreaChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/BarChart.svelte +202 -74
- package/dist/lib/components/core/BarChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/DonutChart.svelte +189 -100
- package/dist/lib/components/core/DonutChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/LineChart.svelte +195 -97
- package/dist/lib/components/core/LineChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/PieChart.svelte +137 -56
- package/dist/lib/components/core/PieChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/ScatterChart.svelte +167 -95
- package/dist/lib/components/core/ScatterChart.svelte.d.ts +2 -1
- package/dist/lib/components/extended/CandlestickChart.svelte +106 -61
- package/dist/lib/components/extended/FunnelChart.svelte +139 -56
- package/dist/lib/components/extended/FunnelChart.svelte.d.ts +2 -1
- package/dist/lib/components/extended/GaugeChart.svelte +111 -39
- package/dist/lib/components/extended/HeatmapChart.svelte +107 -58
- package/dist/lib/components/extended/RadarChart.svelte +111 -37
- package/dist/lib/components/extended/RadarChart.svelte.d.ts +2 -1
- package/dist/lib/components/extended/SankeyChart.svelte +99 -40
- package/dist/lib/components/extended/TreemapChart.svelte +118 -45
- 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 +2 -1
- package/dist/lib/composables/index.js +2 -1
- package/dist/lib/composables/useChart.svelte.d.ts +34 -0
- package/dist/lib/composables/useChart.svelte.js +121 -9
- package/dist/lib/composables/useChartLegend.svelte.d.ts +37 -0
- package/dist/lib/composables/useChartLegend.svelte.js +54 -0
- 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 -4
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
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';
|
|
3
11
|
import { cn } from '../../utils.js';
|
|
4
12
|
import ChartSkeleton from './ChartSkeleton.svelte';
|
|
5
13
|
import ChartError from './ChartError.svelte';
|
|
6
14
|
import ChartEmpty from './ChartEmpty.svelte';
|
|
15
|
+
import ChartDataTable from './ChartDataTable.svelte';
|
|
7
16
|
|
|
8
17
|
interface Props {
|
|
9
18
|
title: string;
|
|
10
19
|
description?: string;
|
|
11
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
|
+
*/
|
|
12
29
|
loading?: boolean;
|
|
13
30
|
error?: string | null;
|
|
14
31
|
empty?: boolean;
|
|
@@ -16,6 +33,23 @@
|
|
|
16
33
|
onRetry?: () => void;
|
|
17
34
|
class?: string;
|
|
18
35
|
wrapperClass?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Custom legend, rendered beside the chart rather than inside the
|
|
38
|
+
* `role="img"` element so its controls stay accessible
|
|
39
|
+
*/
|
|
40
|
+
legend?: Snippet;
|
|
41
|
+
/** Where the custom legend sits (defaults to 'bottom') */
|
|
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;
|
|
19
53
|
children: Snippet;
|
|
20
54
|
}
|
|
21
55
|
|
|
@@ -23,6 +57,8 @@
|
|
|
23
57
|
title,
|
|
24
58
|
description,
|
|
25
59
|
height = 400,
|
|
60
|
+
minHeight,
|
|
61
|
+
maxHeight,
|
|
26
62
|
loading,
|
|
27
63
|
error,
|
|
28
64
|
empty,
|
|
@@ -30,6 +66,13 @@
|
|
|
30
66
|
onRetry,
|
|
31
67
|
class: className,
|
|
32
68
|
wrapperClass,
|
|
69
|
+
legend,
|
|
70
|
+
legendPosition = 'bottom',
|
|
71
|
+
dataTable,
|
|
72
|
+
showDataTable = 'sr-only',
|
|
73
|
+
format,
|
|
74
|
+
exportable,
|
|
75
|
+
exportImage,
|
|
33
76
|
children,
|
|
34
77
|
}: Props = $props();
|
|
35
78
|
|
|
@@ -38,27 +81,150 @@
|
|
|
38
81
|
|
|
39
82
|
const heightStyle = $derived(typeof height === 'number' ? `${height}px` : height);
|
|
40
83
|
const descriptionId = $derived(description ? `${uid}-desc` : undefined);
|
|
84
|
+
const tableId = `${uid}-table`;
|
|
85
|
+
|
|
86
|
+
// The legend's DOM order follows its visual position
|
|
87
|
+
const side = $derived(legendPosition === 'left' || legendPosition === 'right');
|
|
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';
|
|
41
117
|
</script>
|
|
42
118
|
|
|
43
|
-
{#
|
|
44
|
-
<ChartSkeleton {height} class={wrapperClass} />
|
|
45
|
-
{:else if error}
|
|
46
|
-
<ChartError message={error} {height} {onRetry} class={wrapperClass} />
|
|
47
|
-
{:else if empty}
|
|
48
|
-
<ChartEmpty message={emptyMessage} {height} class={wrapperClass} />
|
|
49
|
-
{:else}
|
|
119
|
+
{#snippet chart(chartClass: string)}
|
|
50
120
|
<div
|
|
51
121
|
role="img"
|
|
52
122
|
aria-label={title}
|
|
53
123
|
aria-describedby={descriptionId}
|
|
124
|
+
aria-busy={loading || undefined}
|
|
54
125
|
data-testid="chart-container"
|
|
55
|
-
class={cn('
|
|
126
|
+
class={cn('relative', chartClass)}
|
|
56
127
|
>
|
|
57
128
|
{#if description}
|
|
58
129
|
<span id={descriptionId} class="sr-only">{description}</span>
|
|
59
130
|
{/if}
|
|
60
|
-
<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
|
+
>
|
|
61
137
|
{@render children()}
|
|
62
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}
|
|
149
|
+
</div>
|
|
150
|
+
{/snippet}
|
|
151
|
+
|
|
152
|
+
{#snippet legendArea(content: Snippet)}
|
|
153
|
+
<div
|
|
154
|
+
data-testid="chart-legend"
|
|
155
|
+
class={cn('flex shrink-0 p-2', side ? 'items-center' : 'justify-center')}
|
|
156
|
+
>
|
|
157
|
+
{@render content()}
|
|
158
|
+
</div>
|
|
159
|
+
{/snippet}
|
|
160
|
+
|
|
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} />
|
|
169
|
+
{:else if error}
|
|
170
|
+
<ChartError message={error} {height} {onRetry} class={wrapperClass} />
|
|
171
|
+
{:else if empty}
|
|
172
|
+
<ChartEmpty message={emptyMessage} {height} class={wrapperClass} />
|
|
173
|
+
{:else if framed}
|
|
174
|
+
<div
|
|
175
|
+
data-testid="chart-frame"
|
|
176
|
+
class={cn('flex flex-col rounded-lg border border-border bg-card', wrapperClass)}
|
|
177
|
+
>
|
|
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()}
|
|
63
226
|
</div>
|
|
227
|
+
{:else}
|
|
228
|
+
{@render chart(cn('rounded-lg border border-border bg-card', wrapperClass))}
|
|
229
|
+
{@render srTable()}
|
|
64
230
|
{/if}
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type ChartDataTable as ChartDataTableData, type ChartExportConfig, type ChartLegendPosition } from '@classic-homes/charts-core';
|
|
2
3
|
interface Props {
|
|
3
4
|
title: string;
|
|
4
5
|
description?: string;
|
|
5
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
|
+
*/
|
|
6
15
|
loading?: boolean;
|
|
7
16
|
error?: string | null;
|
|
8
17
|
empty?: boolean;
|
|
@@ -10,6 +19,23 @@ interface Props {
|
|
|
10
19
|
onRetry?: () => void;
|
|
11
20
|
class?: string;
|
|
12
21
|
wrapperClass?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Custom legend, rendered beside the chart rather than inside the
|
|
24
|
+
* `role="img"` element so its controls stay accessible
|
|
25
|
+
*/
|
|
26
|
+
legend?: Snippet;
|
|
27
|
+
/** Where the custom legend sits (defaults to 'bottom') */
|
|
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;
|
|
13
39
|
children: Snippet;
|
|
14
40
|
}
|
|
15
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;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { LegendRenderProps } from '@classic-homes/charts-core';
|
|
3
|
+
import { cn } from '../../utils.js';
|
|
4
|
+
|
|
5
|
+
interface Props
|
|
6
|
+
extends
|
|
7
|
+
Pick<LegendRenderProps, 'items' | 'toggle'>,
|
|
8
|
+
Partial<Omit<LegendRenderProps, 'items' | 'toggle'>> {
|
|
9
|
+
/** Lay items out in a column (defaults to vertical for left/right positions) */
|
|
10
|
+
orientation?: 'horizontal' | 'vertical';
|
|
11
|
+
/** Additional classes for the list */
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
items,
|
|
17
|
+
toggle,
|
|
18
|
+
position = 'bottom',
|
|
19
|
+
chartTitle,
|
|
20
|
+
orientation,
|
|
21
|
+
class: className,
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
|
|
24
|
+
const vertical = $derived(
|
|
25
|
+
(orientation ?? (position === 'left' || position === 'right' ? 'vertical' : 'horizontal')) ===
|
|
26
|
+
'vertical'
|
|
27
|
+
);
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<!--
|
|
31
|
+
Accessible HTML legend for use with a chart's `renderLegend` snippet. Each
|
|
32
|
+
item is a toggle button that shows or hides its series.
|
|
33
|
+
-->
|
|
34
|
+
<ul
|
|
35
|
+
aria-label={chartTitle ? `${chartTitle} legend` : 'Chart legend'}
|
|
36
|
+
class={cn('flex gap-1 text-sm', vertical ? 'flex-col' : 'flex-wrap justify-center', className)}
|
|
37
|
+
>
|
|
38
|
+
{#each items as item (item.name)}
|
|
39
|
+
<li>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
aria-pressed={item.visible}
|
|
43
|
+
onclick={() => toggle(item.name)}
|
|
44
|
+
class={cn(
|
|
45
|
+
'inline-flex items-center gap-2 rounded-md px-2 py-1 text-left hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
46
|
+
item.visible ? 'text-foreground' : 'text-muted-foreground line-through'
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
<span
|
|
50
|
+
aria-hidden="true"
|
|
51
|
+
class="h-3 w-3 shrink-0 rounded-sm"
|
|
52
|
+
style:background-color={item.color}
|
|
53
|
+
style:opacity={item.visible ? 1 : 0.35}
|
|
54
|
+
></span>
|
|
55
|
+
{item.name}
|
|
56
|
+
</button>
|
|
57
|
+
</li>
|
|
58
|
+
{/each}
|
|
59
|
+
</ul>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LegendRenderProps } from '@classic-homes/charts-core';
|
|
2
|
+
interface Props extends Pick<LegendRenderProps, 'items' | 'toggle'>, Partial<Omit<LegendRenderProps, 'items' | 'toggle'>> {
|
|
3
|
+
/** Lay items out in a column (defaults to vertical for left/right positions) */
|
|
4
|
+
orientation?: 'horizontal' | 'vertical';
|
|
5
|
+
/** Additional classes for the list */
|
|
6
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const ChartLegend: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type ChartLegend = ReturnType<typeof ChartLegend>;
|
|
10
|
+
export default ChartLegend;
|
|
@@ -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
|
|
|
@@ -2,12 +2,18 @@ export { default as ChartContainer } from './ChartContainer.svelte';
|
|
|
2
2
|
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
|
+
export { default as ChartLegend } from './ChartLegend.svelte';
|
|
6
|
+
export { default as ChartDataTable } from './ChartDataTable.svelte';
|
|
5
7
|
import type { ComponentProps } from 'svelte';
|
|
6
8
|
import type ChartContainer from './ChartContainer.svelte';
|
|
7
9
|
import type ChartSkeleton from './ChartSkeleton.svelte';
|
|
8
10
|
import type ChartError from './ChartError.svelte';
|
|
9
11
|
import type ChartEmpty from './ChartEmpty.svelte';
|
|
12
|
+
import type ChartLegend from './ChartLegend.svelte';
|
|
13
|
+
import type ChartDataTable from './ChartDataTable.svelte';
|
|
10
14
|
export type ChartContainerProps = ComponentProps<typeof ChartContainer>;
|
|
11
15
|
export type ChartSkeletonProps = ComponentProps<typeof ChartSkeleton>;
|
|
12
16
|
export type ChartErrorProps = ComponentProps<typeof ChartError>;
|
|
13
17
|
export type ChartEmptyProps = ComponentProps<typeof ChartEmpty>;
|
|
18
|
+
export type ChartLegendProps = ComponentProps<typeof ChartLegend>;
|
|
19
|
+
export type ChartDataTableProps = ComponentProps<typeof ChartDataTable>;
|
|
@@ -2,3 +2,5 @@ export { default as ChartContainer } from './ChartContainer.svelte';
|
|
|
2
2
|
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
|
+
export { default as ChartLegend } from './ChartLegend.svelte';
|
|
6
|
+
export { default as ChartDataTable } from './ChartDataTable.svelte';
|