@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,207 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as echarts from 'echarts/core';
|
|
3
|
+
import { PieChart as EChartsPieChart } from 'echarts/charts';
|
|
4
|
+
import { TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components';
|
|
5
|
+
import { CanvasRenderer } from 'echarts/renderers';
|
|
6
|
+
import type { EChartsOption } from 'echarts';
|
|
7
|
+
|
|
8
|
+
import type { DonutChartProps, SliceEventParams } from '@classic-homes/charts-core';
|
|
9
|
+
import { generatePieChartDescription } from '@classic-homes/charts-core';
|
|
10
|
+
|
|
11
|
+
import { cn } from '../../utils.js';
|
|
12
|
+
import { useChartTheme } from '../../composables/useChartTheme.svelte.js';
|
|
13
|
+
import { useReducedMotion } from '../../composables/useReducedMotion.svelte.js';
|
|
14
|
+
import ChartContainer from '../base/ChartContainer.svelte';
|
|
15
|
+
|
|
16
|
+
// Register required ECharts modules
|
|
17
|
+
echarts.use([EChartsPieChart, TooltipComponent, LegendComponent, TitleComponent, CanvasRenderer]);
|
|
18
|
+
|
|
19
|
+
interface Props extends Omit<DonutChartProps, 'class'> {
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
title,
|
|
25
|
+
description,
|
|
26
|
+
data,
|
|
27
|
+
height = 400,
|
|
28
|
+
loading,
|
|
29
|
+
error,
|
|
30
|
+
emptyMessage,
|
|
31
|
+
theme = 'auto',
|
|
32
|
+
animation = true,
|
|
33
|
+
showLegend = true,
|
|
34
|
+
showTooltip = true,
|
|
35
|
+
showLabels = true,
|
|
36
|
+
labelPosition = 'outside',
|
|
37
|
+
innerRadius = '50%',
|
|
38
|
+
centerLabel,
|
|
39
|
+
centerValue,
|
|
40
|
+
onClick,
|
|
41
|
+
class: className,
|
|
42
|
+
}: Props = $props();
|
|
43
|
+
|
|
44
|
+
let containerEl: HTMLDivElement | null = $state(null);
|
|
45
|
+
let chart: echarts.ECharts | null = null;
|
|
46
|
+
|
|
47
|
+
const chartTheme = useChartTheme(() => theme);
|
|
48
|
+
const reducedMotion = useReducedMotion();
|
|
49
|
+
|
|
50
|
+
const isEmpty = $derived(!data?.length);
|
|
51
|
+
|
|
52
|
+
const option: EChartsOption = $derived.by(() => {
|
|
53
|
+
if (isEmpty) return {};
|
|
54
|
+
|
|
55
|
+
const graphicElements: unknown[] = [];
|
|
56
|
+
|
|
57
|
+
if (centerLabel || centerValue) {
|
|
58
|
+
graphicElements.push({
|
|
59
|
+
type: 'group',
|
|
60
|
+
left: 'center',
|
|
61
|
+
top: '40%',
|
|
62
|
+
children: [
|
|
63
|
+
...(centerValue !== undefined
|
|
64
|
+
? [
|
|
65
|
+
{
|
|
66
|
+
type: 'text',
|
|
67
|
+
style: {
|
|
68
|
+
text: String(centerValue),
|
|
69
|
+
fontSize: 28,
|
|
70
|
+
fontWeight: 'bold',
|
|
71
|
+
fill: 'currentColor',
|
|
72
|
+
align: 'center',
|
|
73
|
+
},
|
|
74
|
+
top: 0,
|
|
75
|
+
left: 'center',
|
|
76
|
+
},
|
|
77
|
+
]
|
|
78
|
+
: []),
|
|
79
|
+
...(centerLabel
|
|
80
|
+
? [
|
|
81
|
+
{
|
|
82
|
+
type: 'text',
|
|
83
|
+
style: {
|
|
84
|
+
text: centerLabel,
|
|
85
|
+
fontSize: 14,
|
|
86
|
+
fill: '#888',
|
|
87
|
+
align: 'center',
|
|
88
|
+
},
|
|
89
|
+
top: centerValue !== undefined ? 35 : 0,
|
|
90
|
+
left: 'center',
|
|
91
|
+
},
|
|
92
|
+
]
|
|
93
|
+
: []),
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
tooltip: showTooltip
|
|
100
|
+
? {
|
|
101
|
+
trigger: 'item',
|
|
102
|
+
formatter: '{a} <br/>{b}: {c} ({d}%)',
|
|
103
|
+
}
|
|
104
|
+
: undefined,
|
|
105
|
+
legend: showLegend
|
|
106
|
+
? {
|
|
107
|
+
orient: 'horizontal',
|
|
108
|
+
bottom: 0,
|
|
109
|
+
data: data.map((d) => d.name),
|
|
110
|
+
}
|
|
111
|
+
: undefined,
|
|
112
|
+
graphic:
|
|
113
|
+
graphicElements.length > 0 ? (graphicElements as EChartsOption['graphic']) : undefined,
|
|
114
|
+
series: [
|
|
115
|
+
{
|
|
116
|
+
name: title,
|
|
117
|
+
type: 'pie',
|
|
118
|
+
radius: [innerRadius, '70%'],
|
|
119
|
+
center: ['50%', '45%'],
|
|
120
|
+
avoidLabelOverlap: true,
|
|
121
|
+
data: data.map((item) => ({
|
|
122
|
+
name: item.name,
|
|
123
|
+
value: item.value,
|
|
124
|
+
itemStyle: item.color ? { color: item.color } : undefined,
|
|
125
|
+
})),
|
|
126
|
+
label: showLabels
|
|
127
|
+
? {
|
|
128
|
+
show: true,
|
|
129
|
+
position: labelPosition,
|
|
130
|
+
formatter: labelPosition === 'inside' ? '{d}%' : '{b}: {d}%',
|
|
131
|
+
}
|
|
132
|
+
: {
|
|
133
|
+
show: false,
|
|
134
|
+
},
|
|
135
|
+
labelLine: {
|
|
136
|
+
show: showLabels && labelPosition === 'outside',
|
|
137
|
+
},
|
|
138
|
+
emphasis: {
|
|
139
|
+
itemStyle: {
|
|
140
|
+
shadowBlur: 10,
|
|
141
|
+
shadowOffsetX: 0,
|
|
142
|
+
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
animation: animation && !reducedMotion.value,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const accessibilityDescription = $derived(
|
|
152
|
+
description || (!isEmpty ? generatePieChartDescription(title, data, true) : undefined)
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Initialize chart instance (only depends on container and theme)
|
|
156
|
+
$effect(() => {
|
|
157
|
+
if (!containerEl) return;
|
|
158
|
+
|
|
159
|
+
if (chart) {
|
|
160
|
+
chart.dispose();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
chart = echarts.init(containerEl, chartTheme.theme);
|
|
164
|
+
|
|
165
|
+
if (onClick) {
|
|
166
|
+
chart.on('click', (params) => {
|
|
167
|
+
onClick({
|
|
168
|
+
type: params.type || 'click',
|
|
169
|
+
componentType: params.componentType || 'series',
|
|
170
|
+
name: params.name || '',
|
|
171
|
+
value: params.value as number,
|
|
172
|
+
percent: params.percent,
|
|
173
|
+
color: params.color as string,
|
|
174
|
+
} as SliceEventParams);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const handleResize = () => chart?.resize();
|
|
179
|
+
window.addEventListener('resize', handleResize);
|
|
180
|
+
|
|
181
|
+
return () => {
|
|
182
|
+
window.removeEventListener('resize', handleResize);
|
|
183
|
+
chart?.dispose();
|
|
184
|
+
chart = null;
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Update options when they change (handles both initial and subsequent updates)
|
|
189
|
+
$effect(() => {
|
|
190
|
+
if (chart && !isEmpty) {
|
|
191
|
+
chart.setOption(option, { notMerge: true, lazyUpdate: true });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
</script>
|
|
195
|
+
|
|
196
|
+
<ChartContainer
|
|
197
|
+
{title}
|
|
198
|
+
description={accessibilityDescription}
|
|
199
|
+
{height}
|
|
200
|
+
{loading}
|
|
201
|
+
{error}
|
|
202
|
+
empty={isEmpty}
|
|
203
|
+
{emptyMessage}
|
|
204
|
+
class={cn(className)}
|
|
205
|
+
>
|
|
206
|
+
<div bind:this={containerEl} class="h-full w-full"></div>
|
|
207
|
+
</ChartContainer>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DonutChartProps } from '@classic-homes/charts-core';
|
|
2
|
+
interface Props extends Omit<DonutChartProps, 'class'> {
|
|
3
|
+
class?: string;
|
|
4
|
+
}
|
|
5
|
+
declare const DonutChart: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type DonutChart = ReturnType<typeof DonutChart>;
|
|
7
|
+
export default DonutChart;
|
|
@@ -0,0 +1,203 @@
|
|
|
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 { LineChartProps, DataPointEventParams } from '@classic-homes/charts-core';
|
|
14
|
+
import { generateLineChartDescription } 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
|
+
EChartsLineChart,
|
|
24
|
+
GridComponent,
|
|
25
|
+
TooltipComponent,
|
|
26
|
+
LegendComponent,
|
|
27
|
+
TitleComponent,
|
|
28
|
+
CanvasRenderer,
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
interface Props extends Omit<LineChartProps, '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
|
+
smooth = false,
|
|
49
|
+
areaFilled = false,
|
|
50
|
+
showDataPoints = false,
|
|
51
|
+
stacked = false,
|
|
52
|
+
onClick,
|
|
53
|
+
onHover,
|
|
54
|
+
class: className,
|
|
55
|
+
}: Props = $props();
|
|
56
|
+
|
|
57
|
+
let containerEl: HTMLDivElement | null = $state(null);
|
|
58
|
+
let chart: echarts.ECharts | null = null;
|
|
59
|
+
|
|
60
|
+
const chartTheme = useChartTheme(() => theme);
|
|
61
|
+
const reducedMotion = useReducedMotion();
|
|
62
|
+
|
|
63
|
+
const isEmpty = $derived(
|
|
64
|
+
!data?.categories?.length || !data?.series?.length || data.series.every((s) => !s.data?.length)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const option: EChartsOption = $derived.by(() => {
|
|
68
|
+
if (isEmpty) return {};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
grid: {
|
|
72
|
+
left: '3%',
|
|
73
|
+
right: '4%',
|
|
74
|
+
bottom: '3%',
|
|
75
|
+
containLabel: true,
|
|
76
|
+
},
|
|
77
|
+
tooltip: showTooltip
|
|
78
|
+
? {
|
|
79
|
+
trigger: 'axis',
|
|
80
|
+
axisPointer: {
|
|
81
|
+
type: 'cross',
|
|
82
|
+
label: {
|
|
83
|
+
backgroundColor: '#6a7985',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
: undefined,
|
|
88
|
+
legend: showLegend
|
|
89
|
+
? {
|
|
90
|
+
data: data.series.map((s) => s.name),
|
|
91
|
+
bottom: 0,
|
|
92
|
+
}
|
|
93
|
+
: undefined,
|
|
94
|
+
xAxis: {
|
|
95
|
+
type: 'category',
|
|
96
|
+
boundaryGap: false,
|
|
97
|
+
data: data.categories,
|
|
98
|
+
splitLine: {
|
|
99
|
+
show: showGrid,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
yAxis: {
|
|
103
|
+
type: 'value',
|
|
104
|
+
splitLine: {
|
|
105
|
+
show: showGrid,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
series: data.series.map((series) => ({
|
|
109
|
+
name: series.name,
|
|
110
|
+
type: 'line',
|
|
111
|
+
data: series.data,
|
|
112
|
+
smooth,
|
|
113
|
+
showSymbol: showDataPoints,
|
|
114
|
+
symbolSize: showDataPoints ? 6 : 0,
|
|
115
|
+
stack: stacked ? 'Total' : undefined,
|
|
116
|
+
areaStyle: areaFilled ? { opacity: 0.3 } : undefined,
|
|
117
|
+
itemStyle: series.color ? { color: series.color } : undefined,
|
|
118
|
+
emphasis: {
|
|
119
|
+
focus: 'series',
|
|
120
|
+
},
|
|
121
|
+
})),
|
|
122
|
+
animation: animation && !reducedMotion.value,
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const accessibilityDescription = $derived(
|
|
127
|
+
description || (!isEmpty ? generateLineChartDescription(title, data) : undefined)
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Initialize chart instance (only depends on container and theme)
|
|
131
|
+
$effect(() => {
|
|
132
|
+
if (!containerEl) return;
|
|
133
|
+
|
|
134
|
+
// Dispose existing chart if any
|
|
135
|
+
if (chart) {
|
|
136
|
+
chart.dispose();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Create new chart instance
|
|
140
|
+
chart = echarts.init(containerEl, chartTheme.theme);
|
|
141
|
+
|
|
142
|
+
// Setup event handlers
|
|
143
|
+
if (onClick) {
|
|
144
|
+
chart.on('click', (params) => {
|
|
145
|
+
onClick({
|
|
146
|
+
type: params.type || 'click',
|
|
147
|
+
componentType: params.componentType || 'series',
|
|
148
|
+
seriesIndex: params.seriesIndex,
|
|
149
|
+
seriesName: params.seriesName || '',
|
|
150
|
+
dataIndex: params.dataIndex || 0,
|
|
151
|
+
name: params.name || '',
|
|
152
|
+
value: params.value as number,
|
|
153
|
+
color: params.color as string,
|
|
154
|
+
} as DataPointEventParams);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (onHover) {
|
|
159
|
+
chart.on('mouseover', (params) => {
|
|
160
|
+
onHover({
|
|
161
|
+
type: params.type || 'mouseover',
|
|
162
|
+
componentType: params.componentType || 'series',
|
|
163
|
+
seriesIndex: params.seriesIndex,
|
|
164
|
+
seriesName: params.seriesName || '',
|
|
165
|
+
dataIndex: params.dataIndex || 0,
|
|
166
|
+
name: params.name || '',
|
|
167
|
+
value: params.value as number,
|
|
168
|
+
color: params.color as string,
|
|
169
|
+
} as DataPointEventParams);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Resize handler
|
|
174
|
+
const handleResize = () => chart?.resize();
|
|
175
|
+
window.addEventListener('resize', handleResize);
|
|
176
|
+
|
|
177
|
+
return () => {
|
|
178
|
+
window.removeEventListener('resize', handleResize);
|
|
179
|
+
chart?.dispose();
|
|
180
|
+
chart = null;
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Update options when they change (handles both initial and subsequent updates)
|
|
185
|
+
$effect(() => {
|
|
186
|
+
if (chart && !isEmpty) {
|
|
187
|
+
chart.setOption(option, { notMerge: true, lazyUpdate: true });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
</script>
|
|
191
|
+
|
|
192
|
+
<ChartContainer
|
|
193
|
+
{title}
|
|
194
|
+
description={accessibilityDescription}
|
|
195
|
+
{height}
|
|
196
|
+
{loading}
|
|
197
|
+
{error}
|
|
198
|
+
empty={isEmpty}
|
|
199
|
+
{emptyMessage}
|
|
200
|
+
class={cn(className)}
|
|
201
|
+
>
|
|
202
|
+
<div bind:this={containerEl} class="h-full w-full"></div>
|
|
203
|
+
</ChartContainer>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LineChartProps } from '@classic-homes/charts-core';
|
|
2
|
+
interface Props extends Omit<LineChartProps, 'class'> {
|
|
3
|
+
class?: string;
|
|
4
|
+
}
|
|
5
|
+
declare const LineChart: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type LineChart = ReturnType<typeof LineChart>;
|
|
7
|
+
export default LineChart;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as echarts from 'echarts/core';
|
|
3
|
+
import { PieChart as EChartsPieChart } from 'echarts/charts';
|
|
4
|
+
import { TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components';
|
|
5
|
+
import { CanvasRenderer } from 'echarts/renderers';
|
|
6
|
+
import type { EChartsOption } from 'echarts';
|
|
7
|
+
|
|
8
|
+
import type { PieChartProps, SliceEventParams } from '@classic-homes/charts-core';
|
|
9
|
+
import { generatePieChartDescription } from '@classic-homes/charts-core';
|
|
10
|
+
|
|
11
|
+
import { cn } from '../../utils.js';
|
|
12
|
+
import { useChartTheme } from '../../composables/useChartTheme.svelte.js';
|
|
13
|
+
import { useReducedMotion } from '../../composables/useReducedMotion.svelte.js';
|
|
14
|
+
import ChartContainer from '../base/ChartContainer.svelte';
|
|
15
|
+
|
|
16
|
+
// Register required ECharts modules
|
|
17
|
+
echarts.use([EChartsPieChart, TooltipComponent, LegendComponent, TitleComponent, CanvasRenderer]);
|
|
18
|
+
|
|
19
|
+
interface Props extends Omit<PieChartProps, 'class'> {
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
title,
|
|
25
|
+
description,
|
|
26
|
+
data,
|
|
27
|
+
height = 400,
|
|
28
|
+
loading,
|
|
29
|
+
error,
|
|
30
|
+
emptyMessage,
|
|
31
|
+
theme = 'auto',
|
|
32
|
+
animation = true,
|
|
33
|
+
showLegend = true,
|
|
34
|
+
showTooltip = true,
|
|
35
|
+
showLabels = true,
|
|
36
|
+
labelPosition = 'outside',
|
|
37
|
+
innerRadius = 0,
|
|
38
|
+
onClick,
|
|
39
|
+
class: className,
|
|
40
|
+
}: Props = $props();
|
|
41
|
+
|
|
42
|
+
let containerEl: HTMLDivElement | null = $state(null);
|
|
43
|
+
let chart: echarts.ECharts | null = null;
|
|
44
|
+
|
|
45
|
+
const chartTheme = useChartTheme(() => theme);
|
|
46
|
+
const reducedMotion = useReducedMotion();
|
|
47
|
+
|
|
48
|
+
const isEmpty = $derived(!data?.length);
|
|
49
|
+
|
|
50
|
+
const option: EChartsOption = $derived.by(() => {
|
|
51
|
+
if (isEmpty) return {};
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
tooltip: showTooltip
|
|
55
|
+
? {
|
|
56
|
+
trigger: 'item',
|
|
57
|
+
formatter: '{a} <br/>{b}: {c} ({d}%)',
|
|
58
|
+
}
|
|
59
|
+
: undefined,
|
|
60
|
+
legend: showLegend
|
|
61
|
+
? {
|
|
62
|
+
orient: 'horizontal',
|
|
63
|
+
bottom: 0,
|
|
64
|
+
data: data.map((d) => d.name),
|
|
65
|
+
}
|
|
66
|
+
: undefined,
|
|
67
|
+
series: [
|
|
68
|
+
{
|
|
69
|
+
name: title,
|
|
70
|
+
type: 'pie',
|
|
71
|
+
radius: innerRadius === 0 ? '70%' : ['50%', '70%'],
|
|
72
|
+
center: ['50%', '45%'],
|
|
73
|
+
data: data.map((item) => ({
|
|
74
|
+
name: item.name,
|
|
75
|
+
value: item.value,
|
|
76
|
+
itemStyle: item.color ? { color: item.color } : undefined,
|
|
77
|
+
})),
|
|
78
|
+
label: showLabels
|
|
79
|
+
? {
|
|
80
|
+
show: true,
|
|
81
|
+
position: labelPosition,
|
|
82
|
+
formatter: labelPosition === 'inside' ? '{d}%' : '{b}: {d}%',
|
|
83
|
+
}
|
|
84
|
+
: {
|
|
85
|
+
show: false,
|
|
86
|
+
},
|
|
87
|
+
emphasis: {
|
|
88
|
+
itemStyle: {
|
|
89
|
+
shadowBlur: 10,
|
|
90
|
+
shadowOffsetX: 0,
|
|
91
|
+
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
animation: animation && !reducedMotion.value,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const accessibilityDescription = $derived(
|
|
101
|
+
description || (!isEmpty ? generatePieChartDescription(title, data, false) : undefined)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Initialize chart instance (only depends on container and theme)
|
|
105
|
+
$effect(() => {
|
|
106
|
+
if (!containerEl) return;
|
|
107
|
+
|
|
108
|
+
if (chart) {
|
|
109
|
+
chart.dispose();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
chart = echarts.init(containerEl, chartTheme.theme);
|
|
113
|
+
|
|
114
|
+
if (onClick) {
|
|
115
|
+
chart.on('click', (params) => {
|
|
116
|
+
onClick({
|
|
117
|
+
type: params.type || 'click',
|
|
118
|
+
componentType: params.componentType || 'series',
|
|
119
|
+
name: params.name || '',
|
|
120
|
+
value: params.value as number,
|
|
121
|
+
percent: params.percent,
|
|
122
|
+
color: params.color as string,
|
|
123
|
+
} as SliceEventParams);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const handleResize = () => chart?.resize();
|
|
128
|
+
window.addEventListener('resize', handleResize);
|
|
129
|
+
|
|
130
|
+
return () => {
|
|
131
|
+
window.removeEventListener('resize', handleResize);
|
|
132
|
+
chart?.dispose();
|
|
133
|
+
chart = null;
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Update options when they change (handles both initial and subsequent updates)
|
|
138
|
+
$effect(() => {
|
|
139
|
+
if (chart && !isEmpty) {
|
|
140
|
+
chart.setOption(option, { notMerge: true, lazyUpdate: true });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
<ChartContainer
|
|
146
|
+
{title}
|
|
147
|
+
description={accessibilityDescription}
|
|
148
|
+
{height}
|
|
149
|
+
{loading}
|
|
150
|
+
{error}
|
|
151
|
+
empty={isEmpty}
|
|
152
|
+
{emptyMessage}
|
|
153
|
+
class={cn(className)}
|
|
154
|
+
>
|
|
155
|
+
<div bind:this={containerEl} class="h-full w-full"></div>
|
|
156
|
+
</ChartContainer>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PieChartProps } from '@classic-homes/charts-core';
|
|
2
|
+
interface Props extends Omit<PieChartProps, 'class'> {
|
|
3
|
+
class?: string;
|
|
4
|
+
}
|
|
5
|
+
declare const PieChart: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type PieChart = ReturnType<typeof PieChart>;
|
|
7
|
+
export default PieChart;
|