@classic-homes/charts-svelte 0.1.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/dist/lib/components/base/ChartContainer.svelte +63 -0
- package/dist/lib/components/base/ChartContainer.svelte.d.ts +17 -0
- package/dist/lib/components/base/ChartEmpty.svelte +39 -0
- package/dist/lib/components/base/ChartEmpty.svelte.d.ts +8 -0
- package/dist/lib/components/base/ChartError.svelte +49 -0
- package/dist/lib/components/base/ChartError.svelte.d.ts +9 -0
- package/dist/lib/components/base/ChartSkeleton.svelte +37 -0
- package/dist/lib/components/base/ChartSkeleton.svelte.d.ts +7 -0
- package/dist/lib/components/base/index.d.ts +4 -0
- package/dist/lib/components/base/index.js +4 -0
- package/dist/lib/components/core/AreaChart.svelte +198 -0
- package/dist/lib/components/core/AreaChart.svelte.d.ts +7 -0
- package/dist/lib/components/core/BarChart.svelte +186 -0
- package/dist/lib/components/core/BarChart.svelte.d.ts +7 -0
- package/dist/lib/components/core/DonutChart.svelte +207 -0
- package/dist/lib/components/core/DonutChart.svelte.d.ts +7 -0
- package/dist/lib/components/core/LineChart.svelte +203 -0
- package/dist/lib/components/core/LineChart.svelte.d.ts +7 -0
- package/dist/lib/components/core/PieChart.svelte +156 -0
- package/dist/lib/components/core/PieChart.svelte.d.ts +7 -0
- package/dist/lib/components/core/ScatterChart.svelte +224 -0
- package/dist/lib/components/core/ScatterChart.svelte.d.ts +7 -0
- package/dist/lib/components/core/index.d.ts +6 -0
- package/dist/lib/components/core/index.js +6 -0
- package/dist/lib/components/extended/CandlestickChart.svelte +200 -0
- package/dist/lib/components/extended/CandlestickChart.svelte.d.ts +7 -0
- package/dist/lib/components/extended/FunnelChart.svelte +142 -0
- package/dist/lib/components/extended/FunnelChart.svelte.d.ts +7 -0
- package/dist/lib/components/extended/GaugeChart.svelte +113 -0
- package/dist/lib/components/extended/GaugeChart.svelte.d.ts +7 -0
- package/dist/lib/components/extended/HeatmapChart.svelte +159 -0
- package/dist/lib/components/extended/HeatmapChart.svelte.d.ts +7 -0
- package/dist/lib/components/extended/RadarChart.svelte +131 -0
- package/dist/lib/components/extended/RadarChart.svelte.d.ts +7 -0
- package/dist/lib/components/extended/SankeyChart.svelte +129 -0
- package/dist/lib/components/extended/SankeyChart.svelte.d.ts +7 -0
- package/dist/lib/components/extended/TreemapChart.svelte +133 -0
- package/dist/lib/components/extended/TreemapChart.svelte.d.ts +7 -0
- package/dist/lib/components/extended/index.d.ts +7 -0
- package/dist/lib/components/extended/index.js +7 -0
- package/dist/lib/components/index.d.ts +3 -0
- package/dist/lib/components/index.js +6 -0
- package/dist/lib/composables/index.d.ts +2 -0
- package/dist/lib/composables/index.js +2 -0
- package/dist/lib/composables/useChartTheme.svelte.d.ts +11 -0
- package/dist/lib/composables/useChartTheme.svelte.js +66 -0
- package/dist/lib/composables/useReducedMotion.svelte.d.ts +6 -0
- package/dist/lib/composables/useReducedMotion.svelte.js +26 -0
- package/dist/lib/index.d.ts +9 -0
- package/dist/lib/index.js +17 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/package.json +45 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../../utils.js';
|
|
4
|
+
import ChartSkeleton from './ChartSkeleton.svelte';
|
|
5
|
+
import ChartError from './ChartError.svelte';
|
|
6
|
+
import ChartEmpty from './ChartEmpty.svelte';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
title: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
height?: number | string;
|
|
12
|
+
loading?: boolean;
|
|
13
|
+
error?: string | null;
|
|
14
|
+
empty?: boolean;
|
|
15
|
+
emptyMessage?: string;
|
|
16
|
+
onRetry?: () => void;
|
|
17
|
+
class?: string;
|
|
18
|
+
wrapperClass?: string;
|
|
19
|
+
children: Snippet;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
title,
|
|
24
|
+
description,
|
|
25
|
+
height = 400,
|
|
26
|
+
loading,
|
|
27
|
+
error,
|
|
28
|
+
empty,
|
|
29
|
+
emptyMessage,
|
|
30
|
+
onRetry,
|
|
31
|
+
class: className,
|
|
32
|
+
wrapperClass,
|
|
33
|
+
children,
|
|
34
|
+
}: Props = $props();
|
|
35
|
+
|
|
36
|
+
const heightStyle = $derived(typeof height === 'number' ? `${height}px` : height);
|
|
37
|
+
const descriptionId = $derived(
|
|
38
|
+
description ? `${title.replace(/\s+/g, '-').toLowerCase()}-desc` : undefined
|
|
39
|
+
);
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
{#if loading}
|
|
43
|
+
<ChartSkeleton {height} class={wrapperClass} />
|
|
44
|
+
{:else if error}
|
|
45
|
+
<ChartError message={error} {height} {onRetry} class={wrapperClass} />
|
|
46
|
+
{:else if empty}
|
|
47
|
+
<ChartEmpty message={emptyMessage} {height} class={wrapperClass} />
|
|
48
|
+
{:else}
|
|
49
|
+
<div
|
|
50
|
+
role="img"
|
|
51
|
+
aria-label={title}
|
|
52
|
+
aria-describedby={descriptionId}
|
|
53
|
+
data-testid="chart-container"
|
|
54
|
+
class={cn('rounded-lg border border-border bg-card', wrapperClass)}
|
|
55
|
+
>
|
|
56
|
+
{#if description}
|
|
57
|
+
<span id={descriptionId} class="sr-only">{description}</span>
|
|
58
|
+
{/if}
|
|
59
|
+
<div class={cn('h-full w-full', className)} style:height={heightStyle}>
|
|
60
|
+
{@render children()}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
{/if}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
height?: number | string;
|
|
6
|
+
loading?: boolean;
|
|
7
|
+
error?: string | null;
|
|
8
|
+
empty?: boolean;
|
|
9
|
+
emptyMessage?: string;
|
|
10
|
+
onRetry?: () => void;
|
|
11
|
+
class?: string;
|
|
12
|
+
wrapperClass?: string;
|
|
13
|
+
children: Snippet;
|
|
14
|
+
}
|
|
15
|
+
declare const ChartContainer: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type ChartContainer = ReturnType<typeof ChartContainer>;
|
|
17
|
+
export default ChartContainer;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
message?: string;
|
|
6
|
+
height?: number | string;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { message = 'No data available', height = 400, class: className }: Props = $props();
|
|
11
|
+
|
|
12
|
+
const heightStyle = $derived(typeof height === 'number' ? `${height}px` : height);
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
data-testid="chart-empty"
|
|
17
|
+
class={cn(
|
|
18
|
+
'flex flex-col items-center justify-center rounded-lg border border-border bg-muted/50',
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
style:height={heightStyle}
|
|
22
|
+
>
|
|
23
|
+
<svg
|
|
24
|
+
class="mb-3 h-10 w-10 text-muted-foreground"
|
|
25
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
26
|
+
fill="none"
|
|
27
|
+
viewBox="0 0 24 24"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
>
|
|
31
|
+
<path
|
|
32
|
+
stroke-linecap="round"
|
|
33
|
+
stroke-linejoin="round"
|
|
34
|
+
stroke-width="1.5"
|
|
35
|
+
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
|
36
|
+
/>
|
|
37
|
+
</svg>
|
|
38
|
+
<p class="text-sm text-muted-foreground">{message}</p>
|
|
39
|
+
</div>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
message: string;
|
|
6
|
+
height?: number | string;
|
|
7
|
+
onRetry?: () => void;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { message, height = 400, onRetry, class: className }: Props = $props();
|
|
12
|
+
|
|
13
|
+
const heightStyle = $derived(typeof height === 'number' ? `${height}px` : height);
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div
|
|
17
|
+
data-testid="chart-error"
|
|
18
|
+
role="alert"
|
|
19
|
+
class={cn(
|
|
20
|
+
'flex flex-col items-center justify-center rounded-lg border border-destructive/50 bg-destructive/10',
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
style:height={heightStyle}
|
|
24
|
+
>
|
|
25
|
+
<svg
|
|
26
|
+
class="mb-3 h-10 w-10 text-destructive"
|
|
27
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
28
|
+
fill="none"
|
|
29
|
+
viewBox="0 0 24 24"
|
|
30
|
+
stroke="currentColor"
|
|
31
|
+
aria-hidden="true"
|
|
32
|
+
>
|
|
33
|
+
<path
|
|
34
|
+
stroke-linecap="round"
|
|
35
|
+
stroke-linejoin="round"
|
|
36
|
+
stroke-width="2"
|
|
37
|
+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
38
|
+
/>
|
|
39
|
+
</svg>
|
|
40
|
+
<p class="mb-2 text-sm font-medium text-destructive">{message}</p>
|
|
41
|
+
{#if onRetry}
|
|
42
|
+
<button
|
|
43
|
+
onclick={onRetry}
|
|
44
|
+
class="rounded-md bg-destructive px-3 py-1.5 text-xs font-medium text-destructive-foreground transition-colors hover:bg-destructive/90"
|
|
45
|
+
>
|
|
46
|
+
Retry
|
|
47
|
+
</button>
|
|
48
|
+
{/if}
|
|
49
|
+
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
height?: number | string;
|
|
6
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { height = 400, class: className }: Props = $props();
|
|
10
|
+
|
|
11
|
+
const heightStyle = $derived(typeof height === 'number' ? `${height}px` : height);
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<div
|
|
15
|
+
data-testid="chart-skeleton"
|
|
16
|
+
class={cn('animate-pulse rounded-lg border border-border bg-muted', className)}
|
|
17
|
+
style:height={heightStyle}
|
|
18
|
+
>
|
|
19
|
+
<div class="flex h-full flex-col p-4">
|
|
20
|
+
<!-- Title skeleton -->
|
|
21
|
+
<div class="mb-4 h-5 w-1/3 rounded bg-muted-foreground/20"></div>
|
|
22
|
+
|
|
23
|
+
<!-- Chart area skeleton -->
|
|
24
|
+
<div class="flex flex-1 items-end gap-2 px-4 pb-8">
|
|
25
|
+
{#each [40, 65, 45, 80, 55, 70, 50] as barHeight}
|
|
26
|
+
<div class="flex-1 rounded-t bg-muted-foreground/20" style:height="{barHeight}%"></div>
|
|
27
|
+
{/each}
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- X-axis skeleton -->
|
|
31
|
+
<div class="flex justify-between px-4">
|
|
32
|
+
{#each [1, 2, 3, 4, 5, 6, 7] as _}
|
|
33
|
+
<div class="h-3 w-8 rounded bg-muted-foreground/20"></div>
|
|
34
|
+
{/each}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as echarts from 'echarts/core';
|
|
3
|
+
import { LineChart as EChartsLineChart } from 'echarts/charts';
|
|
4
|
+
import {
|
|
5
|
+
GridComponent,
|
|
6
|
+
TooltipComponent,
|
|
7
|
+
LegendComponent,
|
|
8
|
+
TitleComponent,
|
|
9
|
+
} from 'echarts/components';
|
|
10
|
+
import { CanvasRenderer } from 'echarts/renderers';
|
|
11
|
+
import type { EChartsOption } from 'echarts';
|
|
12
|
+
|
|
13
|
+
import type { AreaChartProps, DataPointEventParams } from '@classic-homes/charts-core';
|
|
14
|
+
|
|
15
|
+
import { cn } from '../../utils.js';
|
|
16
|
+
import { useChartTheme } from '../../composables/useChartTheme.svelte.js';
|
|
17
|
+
import { useReducedMotion } from '../../composables/useReducedMotion.svelte.js';
|
|
18
|
+
import ChartContainer from '../base/ChartContainer.svelte';
|
|
19
|
+
|
|
20
|
+
// Register required ECharts modules
|
|
21
|
+
echarts.use([
|
|
22
|
+
EChartsLineChart,
|
|
23
|
+
GridComponent,
|
|
24
|
+
TooltipComponent,
|
|
25
|
+
LegendComponent,
|
|
26
|
+
TitleComponent,
|
|
27
|
+
CanvasRenderer,
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
interface Props extends Omit<AreaChartProps, 'class'> {
|
|
31
|
+
class?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let {
|
|
35
|
+
title,
|
|
36
|
+
description,
|
|
37
|
+
data,
|
|
38
|
+
height = 400,
|
|
39
|
+
loading,
|
|
40
|
+
error,
|
|
41
|
+
emptyMessage,
|
|
42
|
+
theme = 'auto',
|
|
43
|
+
animation = true,
|
|
44
|
+
showLegend = true,
|
|
45
|
+
showTooltip = true,
|
|
46
|
+
showGrid = true,
|
|
47
|
+
smooth = false,
|
|
48
|
+
stacked = false,
|
|
49
|
+
gradient = true,
|
|
50
|
+
onClick,
|
|
51
|
+
class: className,
|
|
52
|
+
}: Props = $props();
|
|
53
|
+
|
|
54
|
+
let containerEl: HTMLDivElement | null = $state(null);
|
|
55
|
+
let chart: echarts.ECharts | null = null;
|
|
56
|
+
|
|
57
|
+
const chartTheme = useChartTheme(() => theme);
|
|
58
|
+
const reducedMotion = useReducedMotion();
|
|
59
|
+
|
|
60
|
+
const isEmpty = $derived(
|
|
61
|
+
!data?.categories?.length || !data?.series?.length || data.series.every((s) => !s.data?.length)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const option: EChartsOption = $derived.by(() => {
|
|
65
|
+
if (isEmpty) return {};
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
grid: {
|
|
69
|
+
left: '3%',
|
|
70
|
+
right: '4%',
|
|
71
|
+
bottom: '3%',
|
|
72
|
+
containLabel: true,
|
|
73
|
+
},
|
|
74
|
+
tooltip: showTooltip
|
|
75
|
+
? {
|
|
76
|
+
trigger: 'axis',
|
|
77
|
+
axisPointer: {
|
|
78
|
+
type: 'cross',
|
|
79
|
+
label: {
|
|
80
|
+
backgroundColor: '#6a7985',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
: undefined,
|
|
85
|
+
legend: showLegend
|
|
86
|
+
? {
|
|
87
|
+
data: data.series.map((s) => s.name),
|
|
88
|
+
bottom: 0,
|
|
89
|
+
}
|
|
90
|
+
: undefined,
|
|
91
|
+
xAxis: {
|
|
92
|
+
type: 'category',
|
|
93
|
+
boundaryGap: false,
|
|
94
|
+
data: data.categories,
|
|
95
|
+
},
|
|
96
|
+
yAxis: {
|
|
97
|
+
type: 'value',
|
|
98
|
+
splitLine: {
|
|
99
|
+
show: showGrid,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
series: data.series.map((series, index) => ({
|
|
103
|
+
name: series.name,
|
|
104
|
+
type: 'line',
|
|
105
|
+
data: series.data,
|
|
106
|
+
smooth,
|
|
107
|
+
stack: stacked ? 'Total' : undefined,
|
|
108
|
+
areaStyle: gradient
|
|
109
|
+
? {
|
|
110
|
+
opacity: 0.8,
|
|
111
|
+
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
112
|
+
{
|
|
113
|
+
offset: 0,
|
|
114
|
+
color:
|
|
115
|
+
series.color ||
|
|
116
|
+
`rgba(${60 + index * 40}, ${160 - index * 20}, ${180 - index * 30}, 0.8)`,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
offset: 1,
|
|
120
|
+
color:
|
|
121
|
+
series.color ||
|
|
122
|
+
`rgba(${60 + index * 40}, ${160 - index * 20}, ${180 - index * 30}, 0.1)`,
|
|
123
|
+
},
|
|
124
|
+
]),
|
|
125
|
+
}
|
|
126
|
+
: { opacity: 0.5 },
|
|
127
|
+
itemStyle: series.color ? { color: series.color } : undefined,
|
|
128
|
+
emphasis: {
|
|
129
|
+
focus: 'series',
|
|
130
|
+
},
|
|
131
|
+
showSymbol: false,
|
|
132
|
+
})),
|
|
133
|
+
animation: animation && !reducedMotion.value,
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const accessibilityDescription = $derived(
|
|
138
|
+
description ||
|
|
139
|
+
(!isEmpty
|
|
140
|
+
? `${title}. Area chart with ${data.categories.length} data points across ${data.series.length} series.`
|
|
141
|
+
: undefined)
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Initialize chart instance (only depends on container and theme)
|
|
145
|
+
$effect(() => {
|
|
146
|
+
if (!containerEl) return;
|
|
147
|
+
|
|
148
|
+
if (chart) {
|
|
149
|
+
chart.dispose();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
chart = echarts.init(containerEl, chartTheme.theme);
|
|
153
|
+
|
|
154
|
+
if (onClick) {
|
|
155
|
+
chart.on('click', (params) => {
|
|
156
|
+
onClick({
|
|
157
|
+
type: params.type || 'click',
|
|
158
|
+
componentType: params.componentType || 'series',
|
|
159
|
+
seriesIndex: params.seriesIndex,
|
|
160
|
+
seriesName: params.seriesName || '',
|
|
161
|
+
dataIndex: params.dataIndex || 0,
|
|
162
|
+
name: params.name || '',
|
|
163
|
+
value: params.value as number,
|
|
164
|
+
color: params.color as string,
|
|
165
|
+
} as DataPointEventParams);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const handleResize = () => chart?.resize();
|
|
170
|
+
window.addEventListener('resize', handleResize);
|
|
171
|
+
|
|
172
|
+
return () => {
|
|
173
|
+
window.removeEventListener('resize', handleResize);
|
|
174
|
+
chart?.dispose();
|
|
175
|
+
chart = null;
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Update options when they change (handles both initial and subsequent updates)
|
|
180
|
+
$effect(() => {
|
|
181
|
+
if (chart && !isEmpty) {
|
|
182
|
+
chart.setOption(option, { notMerge: true, lazyUpdate: true });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
</script>
|
|
186
|
+
|
|
187
|
+
<ChartContainer
|
|
188
|
+
{title}
|
|
189
|
+
description={accessibilityDescription}
|
|
190
|
+
{height}
|
|
191
|
+
{loading}
|
|
192
|
+
{error}
|
|
193
|
+
empty={isEmpty}
|
|
194
|
+
{emptyMessage}
|
|
195
|
+
class={cn(className)}
|
|
196
|
+
>
|
|
197
|
+
<div bind:this={containerEl} class="h-full w-full"></div>
|
|
198
|
+
</ChartContainer>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AreaChartProps } from '@classic-homes/charts-core';
|
|
2
|
+
interface Props extends Omit<AreaChartProps, 'class'> {
|
|
3
|
+
class?: string;
|
|
4
|
+
}
|
|
5
|
+
declare const AreaChart: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type AreaChart = ReturnType<typeof AreaChart>;
|
|
7
|
+
export default AreaChart;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as echarts from 'echarts/core';
|
|
3
|
+
import { BarChart as EChartsBarChart } from 'echarts/charts';
|
|
4
|
+
import {
|
|
5
|
+
GridComponent,
|
|
6
|
+
TooltipComponent,
|
|
7
|
+
LegendComponent,
|
|
8
|
+
TitleComponent,
|
|
9
|
+
} from 'echarts/components';
|
|
10
|
+
import { CanvasRenderer } from 'echarts/renderers';
|
|
11
|
+
import type { EChartsOption } from 'echarts';
|
|
12
|
+
|
|
13
|
+
import type { BarChartProps, DataPointEventParams } from '@classic-homes/charts-core';
|
|
14
|
+
import { generateBarChartDescription } from '@classic-homes/charts-core';
|
|
15
|
+
|
|
16
|
+
import { cn } from '../../utils.js';
|
|
17
|
+
import { useChartTheme } from '../../composables/useChartTheme.svelte.js';
|
|
18
|
+
import { useReducedMotion } from '../../composables/useReducedMotion.svelte.js';
|
|
19
|
+
import ChartContainer from '../base/ChartContainer.svelte';
|
|
20
|
+
|
|
21
|
+
// Register required ECharts modules
|
|
22
|
+
echarts.use([
|
|
23
|
+
EChartsBarChart,
|
|
24
|
+
GridComponent,
|
|
25
|
+
TooltipComponent,
|
|
26
|
+
LegendComponent,
|
|
27
|
+
TitleComponent,
|
|
28
|
+
CanvasRenderer,
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
interface Props extends Omit<BarChartProps, 'class'> {
|
|
32
|
+
class?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let {
|
|
36
|
+
title,
|
|
37
|
+
description,
|
|
38
|
+
data,
|
|
39
|
+
height = 400,
|
|
40
|
+
loading,
|
|
41
|
+
error,
|
|
42
|
+
emptyMessage,
|
|
43
|
+
theme = 'auto',
|
|
44
|
+
animation = true,
|
|
45
|
+
showLegend = true,
|
|
46
|
+
showTooltip = true,
|
|
47
|
+
showGrid = true,
|
|
48
|
+
orientation = 'vertical',
|
|
49
|
+
stacked = false,
|
|
50
|
+
showValues = false,
|
|
51
|
+
onClick,
|
|
52
|
+
class: className,
|
|
53
|
+
}: Props = $props();
|
|
54
|
+
|
|
55
|
+
let containerEl: HTMLDivElement | null = $state(null);
|
|
56
|
+
let chart: echarts.ECharts | null = null;
|
|
57
|
+
|
|
58
|
+
const chartTheme = useChartTheme(() => theme);
|
|
59
|
+
const reducedMotion = useReducedMotion();
|
|
60
|
+
|
|
61
|
+
const isEmpty = $derived(
|
|
62
|
+
!data?.categories?.length || !data?.series?.length || data.series.every((s) => !s.data?.length)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const isHorizontal = $derived(orientation === 'horizontal');
|
|
66
|
+
|
|
67
|
+
const option: EChartsOption = $derived.by(() => {
|
|
68
|
+
if (isEmpty) return {};
|
|
69
|
+
|
|
70
|
+
const categoryAxis = {
|
|
71
|
+
type: 'category' as const,
|
|
72
|
+
data: data.categories,
|
|
73
|
+
axisTick: {
|
|
74
|
+
alignWithLabel: true,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const valueAxis = {
|
|
79
|
+
type: 'value' as const,
|
|
80
|
+
splitLine: {
|
|
81
|
+
show: showGrid,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
grid: {
|
|
87
|
+
left: '3%',
|
|
88
|
+
right: '4%',
|
|
89
|
+
bottom: '3%',
|
|
90
|
+
containLabel: true,
|
|
91
|
+
},
|
|
92
|
+
tooltip: showTooltip
|
|
93
|
+
? {
|
|
94
|
+
trigger: 'axis',
|
|
95
|
+
axisPointer: {
|
|
96
|
+
type: 'shadow',
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
: undefined,
|
|
100
|
+
legend: showLegend
|
|
101
|
+
? {
|
|
102
|
+
data: data.series.map((s) => s.name),
|
|
103
|
+
bottom: 0,
|
|
104
|
+
}
|
|
105
|
+
: undefined,
|
|
106
|
+
xAxis: isHorizontal ? valueAxis : categoryAxis,
|
|
107
|
+
yAxis: isHorizontal ? categoryAxis : valueAxis,
|
|
108
|
+
series: data.series.map((series) => ({
|
|
109
|
+
name: series.name,
|
|
110
|
+
type: 'bar',
|
|
111
|
+
data: series.data,
|
|
112
|
+
stack: stacked ? 'Total' : undefined,
|
|
113
|
+
itemStyle: series.color ? { color: series.color } : undefined,
|
|
114
|
+
label: showValues
|
|
115
|
+
? {
|
|
116
|
+
show: true,
|
|
117
|
+
position: isHorizontal ? 'right' : 'top',
|
|
118
|
+
}
|
|
119
|
+
: undefined,
|
|
120
|
+
emphasis: {
|
|
121
|
+
focus: 'series',
|
|
122
|
+
},
|
|
123
|
+
})),
|
|
124
|
+
animation: animation && !reducedMotion.value,
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const accessibilityDescription = $derived(
|
|
129
|
+
description || (!isEmpty ? generateBarChartDescription(title, data) : undefined)
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Initialize chart instance (only depends on container and theme)
|
|
133
|
+
$effect(() => {
|
|
134
|
+
if (!containerEl) return;
|
|
135
|
+
|
|
136
|
+
if (chart) {
|
|
137
|
+
chart.dispose();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
chart = echarts.init(containerEl, chartTheme.theme);
|
|
141
|
+
|
|
142
|
+
if (onClick) {
|
|
143
|
+
chart.on('click', (params) => {
|
|
144
|
+
onClick({
|
|
145
|
+
type: params.type || 'click',
|
|
146
|
+
componentType: params.componentType || 'series',
|
|
147
|
+
seriesIndex: params.seriesIndex,
|
|
148
|
+
seriesName: params.seriesName || '',
|
|
149
|
+
dataIndex: params.dataIndex || 0,
|
|
150
|
+
name: params.name || '',
|
|
151
|
+
value: params.value as number,
|
|
152
|
+
color: params.color as string,
|
|
153
|
+
} as DataPointEventParams);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const handleResize = () => chart?.resize();
|
|
158
|
+
window.addEventListener('resize', handleResize);
|
|
159
|
+
|
|
160
|
+
return () => {
|
|
161
|
+
window.removeEventListener('resize', handleResize);
|
|
162
|
+
chart?.dispose();
|
|
163
|
+
chart = null;
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Update options when they change (handles both initial and subsequent updates)
|
|
168
|
+
$effect(() => {
|
|
169
|
+
if (chart && !isEmpty) {
|
|
170
|
+
chart.setOption(option, { notMerge: true, lazyUpdate: true });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
</script>
|
|
174
|
+
|
|
175
|
+
<ChartContainer
|
|
176
|
+
{title}
|
|
177
|
+
description={accessibilityDescription}
|
|
178
|
+
{height}
|
|
179
|
+
{loading}
|
|
180
|
+
{error}
|
|
181
|
+
empty={isEmpty}
|
|
182
|
+
{emptyMessage}
|
|
183
|
+
class={cn(className)}
|
|
184
|
+
>
|
|
185
|
+
<div bind:this={containerEl} class="h-full w-full"></div>
|
|
186
|
+
</ChartContainer>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BarChartProps } from '@classic-homes/charts-core';
|
|
2
|
+
interface Props extends Omit<BarChartProps, 'class'> {
|
|
3
|
+
class?: string;
|
|
4
|
+
}
|
|
5
|
+
declare const BarChart: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type BarChart = ReturnType<typeof BarChart>;
|
|
7
|
+
export default BarChart;
|