@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.
Files changed (53) hide show
  1. package/dist/lib/components/base/ChartContainer.svelte +63 -0
  2. package/dist/lib/components/base/ChartContainer.svelte.d.ts +17 -0
  3. package/dist/lib/components/base/ChartEmpty.svelte +39 -0
  4. package/dist/lib/components/base/ChartEmpty.svelte.d.ts +8 -0
  5. package/dist/lib/components/base/ChartError.svelte +49 -0
  6. package/dist/lib/components/base/ChartError.svelte.d.ts +9 -0
  7. package/dist/lib/components/base/ChartSkeleton.svelte +37 -0
  8. package/dist/lib/components/base/ChartSkeleton.svelte.d.ts +7 -0
  9. package/dist/lib/components/base/index.d.ts +4 -0
  10. package/dist/lib/components/base/index.js +4 -0
  11. package/dist/lib/components/core/AreaChart.svelte +198 -0
  12. package/dist/lib/components/core/AreaChart.svelte.d.ts +7 -0
  13. package/dist/lib/components/core/BarChart.svelte +186 -0
  14. package/dist/lib/components/core/BarChart.svelte.d.ts +7 -0
  15. package/dist/lib/components/core/DonutChart.svelte +207 -0
  16. package/dist/lib/components/core/DonutChart.svelte.d.ts +7 -0
  17. package/dist/lib/components/core/LineChart.svelte +203 -0
  18. package/dist/lib/components/core/LineChart.svelte.d.ts +7 -0
  19. package/dist/lib/components/core/PieChart.svelte +156 -0
  20. package/dist/lib/components/core/PieChart.svelte.d.ts +7 -0
  21. package/dist/lib/components/core/ScatterChart.svelte +224 -0
  22. package/dist/lib/components/core/ScatterChart.svelte.d.ts +7 -0
  23. package/dist/lib/components/core/index.d.ts +6 -0
  24. package/dist/lib/components/core/index.js +6 -0
  25. package/dist/lib/components/extended/CandlestickChart.svelte +200 -0
  26. package/dist/lib/components/extended/CandlestickChart.svelte.d.ts +7 -0
  27. package/dist/lib/components/extended/FunnelChart.svelte +142 -0
  28. package/dist/lib/components/extended/FunnelChart.svelte.d.ts +7 -0
  29. package/dist/lib/components/extended/GaugeChart.svelte +113 -0
  30. package/dist/lib/components/extended/GaugeChart.svelte.d.ts +7 -0
  31. package/dist/lib/components/extended/HeatmapChart.svelte +159 -0
  32. package/dist/lib/components/extended/HeatmapChart.svelte.d.ts +7 -0
  33. package/dist/lib/components/extended/RadarChart.svelte +131 -0
  34. package/dist/lib/components/extended/RadarChart.svelte.d.ts +7 -0
  35. package/dist/lib/components/extended/SankeyChart.svelte +129 -0
  36. package/dist/lib/components/extended/SankeyChart.svelte.d.ts +7 -0
  37. package/dist/lib/components/extended/TreemapChart.svelte +133 -0
  38. package/dist/lib/components/extended/TreemapChart.svelte.d.ts +7 -0
  39. package/dist/lib/components/extended/index.d.ts +7 -0
  40. package/dist/lib/components/extended/index.js +7 -0
  41. package/dist/lib/components/index.d.ts +3 -0
  42. package/dist/lib/components/index.js +6 -0
  43. package/dist/lib/composables/index.d.ts +2 -0
  44. package/dist/lib/composables/index.js +2 -0
  45. package/dist/lib/composables/useChartTheme.svelte.d.ts +11 -0
  46. package/dist/lib/composables/useChartTheme.svelte.js +66 -0
  47. package/dist/lib/composables/useReducedMotion.svelte.d.ts +6 -0
  48. package/dist/lib/composables/useReducedMotion.svelte.js +26 -0
  49. package/dist/lib/index.d.ts +9 -0
  50. package/dist/lib/index.js +17 -0
  51. package/dist/lib/utils.d.ts +2 -0
  52. package/dist/lib/utils.js +5 -0
  53. package/package.json +45 -0
@@ -0,0 +1,113 @@
1
+ <script lang="ts">
2
+ import * as echarts from 'echarts/core';
3
+ import { GaugeChart as EChartsGaugeChart } from 'echarts/charts';
4
+ import { TitleComponent } from 'echarts/components';
5
+ import { CanvasRenderer } from 'echarts/renderers';
6
+ import type { EChartsOption } from 'echarts';
7
+
8
+ import type { GaugeChartProps } from '@classic-homes/charts-core';
9
+
10
+ import { cn } from '../../utils.js';
11
+ import { useChartTheme } from '../../composables/useChartTheme.svelte.js';
12
+ import { useReducedMotion } from '../../composables/useReducedMotion.svelte.js';
13
+ import ChartContainer from '../base/ChartContainer.svelte';
14
+
15
+ echarts.use([EChartsGaugeChart, TitleComponent, CanvasRenderer]);
16
+
17
+ interface Props extends Omit<GaugeChartProps, 'class'> {
18
+ class?: string;
19
+ }
20
+
21
+ let {
22
+ title,
23
+ description,
24
+ value,
25
+ min = 0,
26
+ max = 100,
27
+ height = 400,
28
+ loading,
29
+ error,
30
+ emptyMessage,
31
+ theme = 'auto',
32
+ animation = true,
33
+ splitNumber = 10,
34
+ showProgress = true,
35
+ formatValue,
36
+ class: className,
37
+ }: Props = $props();
38
+
39
+ let containerEl: HTMLDivElement | null = $state(null);
40
+ let chart: echarts.ECharts | null = null;
41
+
42
+ const chartTheme = useChartTheme(() => theme);
43
+ const reducedMotion = useReducedMotion();
44
+
45
+ const option: EChartsOption = $derived({
46
+ series: [
47
+ {
48
+ type: 'gauge',
49
+ center: ['50%', '60%'],
50
+ radius: '80%',
51
+ min,
52
+ max,
53
+ splitNumber,
54
+ progress: showProgress ? { show: true, width: 18 } : undefined,
55
+ axisLine: { lineStyle: { width: 18 } },
56
+ axisTick: { distance: -30, splitNumber: 5, lineStyle: { width: 2, color: '#999' } },
57
+ splitLine: { distance: -32, length: 10, lineStyle: { width: 3, color: '#999' } },
58
+ axisLabel: { distance: -20, color: '#999', fontSize: 12 },
59
+ anchor: { show: true, size: 20, itemStyle: { borderWidth: 2 } },
60
+ pointer: { width: 6, length: '60%' },
61
+ title: { show: true, offsetCenter: [0, '70%'], fontSize: 14 },
62
+ detail: {
63
+ valueAnimation: animation && !reducedMotion.value,
64
+ fontSize: 28,
65
+ fontWeight: 'bold',
66
+ offsetCenter: [0, '40%'],
67
+ formatter: formatValue || '{value}',
68
+ },
69
+ data: [{ value, name: title }],
70
+ },
71
+ ],
72
+ animation: animation && !reducedMotion.value,
73
+ });
74
+
75
+ const accessibilityDescription = $derived(
76
+ description || `${title}. Gauge showing value ${value} on scale from ${min} to ${max}.`
77
+ );
78
+
79
+ // Initialize chart instance (only depends on container and theme)
80
+ $effect(() => {
81
+ if (!containerEl) return;
82
+ if (chart) chart.dispose();
83
+ chart = echarts.init(containerEl, chartTheme.theme);
84
+
85
+ const handleResize = () => chart?.resize();
86
+ window.addEventListener('resize', handleResize);
87
+
88
+ return () => {
89
+ window.removeEventListener('resize', handleResize);
90
+ chart?.dispose();
91
+ chart = null;
92
+ };
93
+ });
94
+
95
+ // Update options when they change (handles both initial and subsequent updates)
96
+ $effect(() => {
97
+ if (chart) {
98
+ chart.setOption(option, { notMerge: true, lazyUpdate: true });
99
+ }
100
+ });
101
+ </script>
102
+
103
+ <ChartContainer
104
+ {title}
105
+ description={accessibilityDescription}
106
+ {height}
107
+ {loading}
108
+ {error}
109
+ {emptyMessage}
110
+ class={cn(className)}
111
+ >
112
+ <div bind:this={containerEl} class="h-full w-full"></div>
113
+ </ChartContainer>
@@ -0,0 +1,7 @@
1
+ import type { GaugeChartProps } from '@classic-homes/charts-core';
2
+ interface Props extends Omit<GaugeChartProps, 'class'> {
3
+ class?: string;
4
+ }
5
+ declare const GaugeChart: import("svelte").Component<Props, {}, "">;
6
+ type GaugeChart = ReturnType<typeof GaugeChart>;
7
+ export default GaugeChart;
@@ -0,0 +1,159 @@
1
+ <script lang="ts">
2
+ import * as echarts from 'echarts/core';
3
+ import { HeatmapChart as EChartsHeatmapChart } from 'echarts/charts';
4
+ import {
5
+ GridComponent,
6
+ TooltipComponent,
7
+ TitleComponent,
8
+ VisualMapComponent,
9
+ } from 'echarts/components';
10
+ import { CanvasRenderer } from 'echarts/renderers';
11
+ import type { EChartsOption } from 'echarts';
12
+
13
+ import type { HeatmapChartProps, HeatmapEventParams } 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
+ echarts.use([
21
+ EChartsHeatmapChart,
22
+ GridComponent,
23
+ TooltipComponent,
24
+ TitleComponent,
25
+ VisualMapComponent,
26
+ CanvasRenderer,
27
+ ]);
28
+
29
+ interface Props extends Omit<HeatmapChartProps, 'class'> {
30
+ class?: string;
31
+ }
32
+
33
+ let {
34
+ title,
35
+ description,
36
+ data,
37
+ xAxis,
38
+ yAxis,
39
+ height = 400,
40
+ loading,
41
+ error,
42
+ emptyMessage,
43
+ theme = 'auto',
44
+ animation = true,
45
+ showTooltip = true,
46
+ showValues = false,
47
+ onClick,
48
+ class: className,
49
+ }: Props = $props();
50
+
51
+ let containerEl: HTMLDivElement | null = $state(null);
52
+ let chart: echarts.ECharts | null = null;
53
+
54
+ const chartTheme = useChartTheme(() => theme);
55
+ const reducedMotion = useReducedMotion();
56
+
57
+ const isEmpty = $derived(!data?.data?.length || !xAxis?.length || !yAxis?.length);
58
+
59
+ const values = $derived(data?.data?.map(([, , value]) => value) || []);
60
+ const minValue = $derived(data?.min ?? Math.min(...values));
61
+ const maxValue = $derived(data?.max ?? Math.max(...values));
62
+
63
+ const option: EChartsOption = $derived.by(() => {
64
+ if (isEmpty) return {};
65
+
66
+ return {
67
+ tooltip: showTooltip
68
+ ? {
69
+ position: 'top' as const,
70
+ formatter: (params: unknown) => {
71
+ const p = params as { value?: [number, number, number] };
72
+ if (!p.value) return '';
73
+ const [xIdx, yIdx, value] = p.value;
74
+ return `${xAxis[xIdx]} / ${yAxis[yIdx]}: ${value}`;
75
+ },
76
+ }
77
+ : undefined,
78
+ grid: { left: '3%', right: '10%', bottom: '3%', containLabel: true },
79
+ xAxis: { type: 'category', data: xAxis, splitArea: { show: true } },
80
+ yAxis: { type: 'category', data: yAxis, splitArea: { show: true } },
81
+ visualMap: {
82
+ min: minValue,
83
+ max: maxValue,
84
+ calculable: true,
85
+ orient: 'vertical',
86
+ right: '0%',
87
+ top: 'center',
88
+ },
89
+ series: [
90
+ {
91
+ name: title,
92
+ type: 'heatmap',
93
+ data: data.data,
94
+ label: showValues ? { show: true } : undefined,
95
+ emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.5)' } },
96
+ },
97
+ ],
98
+ animation: animation && !reducedMotion.value,
99
+ };
100
+ });
101
+
102
+ const accessibilityDescription = $derived(
103
+ description ||
104
+ (!isEmpty
105
+ ? `${title}. Heatmap with ${xAxis.length} columns and ${yAxis.length} rows.`
106
+ : undefined)
107
+ );
108
+
109
+ // Initialize chart instance (only depends on container and theme)
110
+ $effect(() => {
111
+ if (!containerEl) return;
112
+ if (chart) chart.dispose();
113
+ chart = echarts.init(containerEl, chartTheme.theme);
114
+
115
+ if (onClick) {
116
+ chart.on('click', (params) => {
117
+ const [xIdx, yIdx, value] = (params.value as [number, number, number]) || [0, 0, 0];
118
+ onClick({
119
+ type: params.type || 'click',
120
+ componentType: params.componentType || 'series',
121
+ xIndex: xIdx,
122
+ yIndex: yIdx,
123
+ xLabel: xAxis[xIdx],
124
+ yLabel: yAxis[yIdx],
125
+ value,
126
+ } as HeatmapEventParams);
127
+ });
128
+ }
129
+
130
+ const handleResize = () => chart?.resize();
131
+ window.addEventListener('resize', handleResize);
132
+
133
+ return () => {
134
+ window.removeEventListener('resize', handleResize);
135
+ chart?.dispose();
136
+ chart = null;
137
+ };
138
+ });
139
+
140
+ // Update options when they change (handles both initial and subsequent updates)
141
+ $effect(() => {
142
+ if (chart && !isEmpty) {
143
+ chart.setOption(option, { notMerge: true, lazyUpdate: true });
144
+ }
145
+ });
146
+ </script>
147
+
148
+ <ChartContainer
149
+ {title}
150
+ description={accessibilityDescription}
151
+ {height}
152
+ {loading}
153
+ {error}
154
+ empty={isEmpty}
155
+ {emptyMessage}
156
+ class={cn(className)}
157
+ >
158
+ <div bind:this={containerEl} class="h-full w-full"></div>
159
+ </ChartContainer>
@@ -0,0 +1,7 @@
1
+ import type { HeatmapChartProps } from '@classic-homes/charts-core';
2
+ interface Props extends Omit<HeatmapChartProps, 'class'> {
3
+ class?: string;
4
+ }
5
+ declare const HeatmapChart: import("svelte").Component<Props, {}, "">;
6
+ type HeatmapChart = ReturnType<typeof HeatmapChart>;
7
+ export default HeatmapChart;
@@ -0,0 +1,131 @@
1
+ <script lang="ts">
2
+ import * as echarts from 'echarts/core';
3
+ import { RadarChart as EChartsRadarChart } from 'echarts/charts';
4
+ import {
5
+ TooltipComponent,
6
+ LegendComponent,
7
+ TitleComponent,
8
+ RadarComponent,
9
+ } from 'echarts/components';
10
+ import { CanvasRenderer } from 'echarts/renderers';
11
+ import type { EChartsOption } from 'echarts';
12
+
13
+ import type { RadarChartProps } 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
+ echarts.use([
21
+ EChartsRadarChart,
22
+ TooltipComponent,
23
+ LegendComponent,
24
+ TitleComponent,
25
+ RadarComponent,
26
+ CanvasRenderer,
27
+ ]);
28
+
29
+ interface Props extends Omit<RadarChartProps, 'class'> {
30
+ class?: string;
31
+ }
32
+
33
+ let {
34
+ title,
35
+ description,
36
+ data,
37
+ indicators,
38
+ height = 400,
39
+ loading,
40
+ error,
41
+ emptyMessage,
42
+ theme = 'auto',
43
+ animation = true,
44
+ showLegend = true,
45
+ showTooltip = true,
46
+ shape = 'polygon',
47
+ class: className,
48
+ }: Props = $props();
49
+
50
+ let containerEl: HTMLDivElement | null = $state(null);
51
+ let chart: echarts.ECharts | null = null;
52
+
53
+ const chartTheme = useChartTheme(() => theme);
54
+ const reducedMotion = useReducedMotion();
55
+
56
+ const isEmpty = $derived(
57
+ !data?.series?.length || !indicators?.length || data.series.every((s) => !s.data?.length)
58
+ );
59
+
60
+ const option: EChartsOption = $derived.by(() => {
61
+ if (isEmpty) return {};
62
+
63
+ return {
64
+ tooltip: showTooltip ? { trigger: 'item' } : undefined,
65
+ legend: showLegend ? { data: data.series.map((s) => s.name), bottom: 0 } : undefined,
66
+ radar: {
67
+ indicator: indicators.map((ind) => ({
68
+ name: ind.name,
69
+ max: ind.max,
70
+ min: ind.min,
71
+ })),
72
+ shape,
73
+ },
74
+ series: [
75
+ {
76
+ type: 'radar',
77
+ data: data.series.map((series) => ({
78
+ name: series.name,
79
+ value: series.data,
80
+ itemStyle: series.color ? { color: series.color } : undefined,
81
+ areaStyle: { opacity: 0.2 },
82
+ })),
83
+ },
84
+ ],
85
+ animation: animation && !reducedMotion.value,
86
+ };
87
+ });
88
+
89
+ const accessibilityDescription = $derived(
90
+ description ||
91
+ (!isEmpty
92
+ ? `${title}. Radar chart with ${indicators.length} dimensions comparing ${data.series.length} series.`
93
+ : undefined)
94
+ );
95
+
96
+ // Initialize chart instance (only depends on container and theme)
97
+ $effect(() => {
98
+ if (!containerEl) return;
99
+ if (chart) chart.dispose();
100
+ chart = echarts.init(containerEl, chartTheme.theme);
101
+
102
+ const handleResize = () => chart?.resize();
103
+ window.addEventListener('resize', handleResize);
104
+
105
+ return () => {
106
+ window.removeEventListener('resize', handleResize);
107
+ chart?.dispose();
108
+ chart = null;
109
+ };
110
+ });
111
+
112
+ // Update options when they change (handles both initial and subsequent updates)
113
+ $effect(() => {
114
+ if (chart && !isEmpty) {
115
+ chart.setOption(option, { notMerge: true, lazyUpdate: true });
116
+ }
117
+ });
118
+ </script>
119
+
120
+ <ChartContainer
121
+ {title}
122
+ description={accessibilityDescription}
123
+ {height}
124
+ {loading}
125
+ {error}
126
+ empty={isEmpty}
127
+ {emptyMessage}
128
+ class={cn(className)}
129
+ >
130
+ <div bind:this={containerEl} class="h-full w-full"></div>
131
+ </ChartContainer>
@@ -0,0 +1,7 @@
1
+ import type { RadarChartProps } from '@classic-homes/charts-core';
2
+ interface Props extends Omit<RadarChartProps, 'class'> {
3
+ class?: string;
4
+ }
5
+ declare const RadarChart: import("svelte").Component<Props, {}, "">;
6
+ type RadarChart = ReturnType<typeof RadarChart>;
7
+ export default RadarChart;
@@ -0,0 +1,129 @@
1
+ <script lang="ts">
2
+ import * as echarts from 'echarts/core';
3
+ import { SankeyChart as EChartsSankeyChart } from 'echarts/charts';
4
+ import { TooltipComponent, TitleComponent } from 'echarts/components';
5
+ import { CanvasRenderer } from 'echarts/renderers';
6
+ import type { EChartsOption } from 'echarts';
7
+
8
+ import type { SankeyChartProps, SankeyEventParams } from '@classic-homes/charts-core';
9
+
10
+ import { cn } from '../../utils.js';
11
+ import { useChartTheme } from '../../composables/useChartTheme.svelte.js';
12
+ import { useReducedMotion } from '../../composables/useReducedMotion.svelte.js';
13
+ import ChartContainer from '../base/ChartContainer.svelte';
14
+
15
+ echarts.use([EChartsSankeyChart, TooltipComponent, TitleComponent, CanvasRenderer]);
16
+
17
+ interface Props extends Omit<SankeyChartProps, 'class'> {
18
+ class?: string;
19
+ }
20
+
21
+ let {
22
+ title,
23
+ description,
24
+ data,
25
+ height = 400,
26
+ loading,
27
+ error,
28
+ emptyMessage,
29
+ theme = 'auto',
30
+ animation = true,
31
+ showTooltip = true,
32
+ orient = 'horizontal',
33
+ onClick,
34
+ class: className,
35
+ }: Props = $props();
36
+
37
+ let containerEl: HTMLDivElement | null = $state(null);
38
+ let chart: echarts.ECharts | null = null;
39
+
40
+ const chartTheme = useChartTheme(() => theme);
41
+ const reducedMotion = useReducedMotion();
42
+
43
+ const isEmpty = $derived(!data?.nodes?.length || !data?.links?.length);
44
+
45
+ const option: EChartsOption = $derived.by(() => {
46
+ if (isEmpty) return {};
47
+
48
+ return {
49
+ tooltip: showTooltip ? { trigger: 'item', triggerOn: 'mousemove' } : undefined,
50
+ series: [
51
+ {
52
+ type: 'sankey',
53
+ orient,
54
+ emphasis: { focus: 'adjacency' },
55
+ data: data.nodes.map((node) => ({
56
+ name: node.name,
57
+ itemStyle: node.color ? { color: node.color } : undefined,
58
+ })),
59
+ links: data.links.map((link) => ({
60
+ source: link.source,
61
+ target: link.target,
62
+ value: link.value,
63
+ })),
64
+ lineStyle: { color: 'gradient', curveness: 0.5 },
65
+ label: { position: orient === 'horizontal' ? 'right' : 'bottom' },
66
+ },
67
+ ],
68
+ animation: animation && !reducedMotion.value,
69
+ };
70
+ });
71
+
72
+ const accessibilityDescription = $derived(
73
+ description ||
74
+ (!isEmpty
75
+ ? `${title}. Sankey diagram with ${data.nodes.length} nodes and ${data.links.length} links.`
76
+ : undefined)
77
+ );
78
+
79
+ // Initialize chart instance (only depends on container and theme)
80
+ $effect(() => {
81
+ if (!containerEl) return;
82
+ if (chart) chart.dispose();
83
+ chart = echarts.init(containerEl, chartTheme.theme);
84
+
85
+ if (onClick) {
86
+ chart.on('click', (params) => {
87
+ const isEdge = params.dataType === 'edge';
88
+ onClick({
89
+ type: params.type || 'click',
90
+ componentType: params.componentType || 'series',
91
+ dataType: isEdge ? 'edge' : 'node',
92
+ name: params.name || '',
93
+ value: isEdge ? (params.value as number) : undefined,
94
+ source: isEdge ? (params.data as { source?: string })?.source : undefined,
95
+ target: isEdge ? (params.data as { target?: string })?.target : undefined,
96
+ } as SankeyEventParams);
97
+ });
98
+ }
99
+
100
+ const handleResize = () => chart?.resize();
101
+ window.addEventListener('resize', handleResize);
102
+
103
+ return () => {
104
+ window.removeEventListener('resize', handleResize);
105
+ chart?.dispose();
106
+ chart = null;
107
+ };
108
+ });
109
+
110
+ // Update options when they change (handles both initial and subsequent updates)
111
+ $effect(() => {
112
+ if (chart && !isEmpty) {
113
+ chart.setOption(option, { notMerge: true, lazyUpdate: true });
114
+ }
115
+ });
116
+ </script>
117
+
118
+ <ChartContainer
119
+ {title}
120
+ description={accessibilityDescription}
121
+ {height}
122
+ {loading}
123
+ {error}
124
+ empty={isEmpty}
125
+ {emptyMessage}
126
+ class={cn(className)}
127
+ >
128
+ <div bind:this={containerEl} class="h-full w-full"></div>
129
+ </ChartContainer>
@@ -0,0 +1,7 @@
1
+ import type { SankeyChartProps } from '@classic-homes/charts-core';
2
+ interface Props extends Omit<SankeyChartProps, 'class'> {
3
+ class?: string;
4
+ }
5
+ declare const SankeyChart: import("svelte").Component<Props, {}, "">;
6
+ type SankeyChart = ReturnType<typeof SankeyChart>;
7
+ export default SankeyChart;
@@ -0,0 +1,133 @@
1
+ <script lang="ts">
2
+ import * as echarts from 'echarts/core';
3
+ import { TreemapChart as EChartsTreemapChart } from 'echarts/charts';
4
+ import { TooltipComponent, TitleComponent } from 'echarts/components';
5
+ import { CanvasRenderer } from 'echarts/renderers';
6
+ import type { EChartsOption } from 'echarts';
7
+
8
+ import type { TreemapChartProps, TreemapEventParams } from '@classic-homes/charts-core';
9
+
10
+ import { cn } from '../../utils.js';
11
+ import { useChartTheme } from '../../composables/useChartTheme.svelte.js';
12
+ import { useReducedMotion } from '../../composables/useReducedMotion.svelte.js';
13
+ import ChartContainer from '../base/ChartContainer.svelte';
14
+
15
+ echarts.use([EChartsTreemapChart, TooltipComponent, TitleComponent, CanvasRenderer]);
16
+
17
+ interface Props extends Omit<TreemapChartProps, 'class'> {
18
+ class?: string;
19
+ }
20
+
21
+ let {
22
+ title,
23
+ description,
24
+ data,
25
+ levels,
26
+ height = 400,
27
+ loading,
28
+ error,
29
+ emptyMessage,
30
+ theme = 'auto',
31
+ animation = true,
32
+ showTooltip = true,
33
+ onClick,
34
+ class: className,
35
+ }: Props = $props();
36
+
37
+ let containerEl: HTMLDivElement | null = $state(null);
38
+ let chart: echarts.ECharts | null = null;
39
+
40
+ const chartTheme = useChartTheme(() => theme);
41
+ const reducedMotion = useReducedMotion();
42
+
43
+ const isEmpty = $derived(!data?.length);
44
+
45
+ const option: EChartsOption = $derived.by(() => {
46
+ if (isEmpty) return {};
47
+
48
+ return {
49
+ tooltip: showTooltip
50
+ ? {
51
+ formatter: (params: unknown) => {
52
+ const p = params as { name?: string; value?: number };
53
+ return `${p.name}: ${p.value}`;
54
+ },
55
+ }
56
+ : undefined,
57
+ series: [
58
+ {
59
+ type: 'treemap',
60
+ data,
61
+ roam: false,
62
+ levels: (levels || [
63
+ { itemStyle: { borderColor: '#555', borderWidth: 4, gapWidth: 4 } },
64
+ {
65
+ colorSaturation: [0.3, 0.6],
66
+ itemStyle: { borderColorSaturation: 0.7, gapWidth: 2, borderWidth: 2 },
67
+ },
68
+ { colorSaturation: [0.3, 0.5], itemStyle: { borderColorSaturation: 0.6, gapWidth: 1 } },
69
+ { colorSaturation: [0.3, 0.5] },
70
+ ]) as unknown[],
71
+ label: { show: true, formatter: '{b}' },
72
+ upperLabel: { show: true, height: 30 },
73
+ breadcrumb: { show: true },
74
+ },
75
+ ] as EChartsOption['series'],
76
+ animation: animation && !reducedMotion.value,
77
+ };
78
+ });
79
+
80
+ const accessibilityDescription = $derived(
81
+ description || (!isEmpty ? `${title}. Treemap chart showing hierarchical data.` : undefined)
82
+ );
83
+
84
+ // Initialize chart instance (only depends on container and theme)
85
+ $effect(() => {
86
+ if (!containerEl) return;
87
+ if (chart) chart.dispose();
88
+ chart = echarts.init(containerEl, chartTheme.theme);
89
+
90
+ if (onClick) {
91
+ chart.on('click', (params) => {
92
+ const treePathInfo = params.treePathInfo || [];
93
+ onClick({
94
+ type: params.type || 'click',
95
+ componentType: params.componentType || 'series',
96
+ name: params.name || '',
97
+ value: params.value as number,
98
+ path: treePathInfo.map((item: { name?: string }) => item.name || ''),
99
+ depth: treePathInfo.length - 1,
100
+ } as TreemapEventParams);
101
+ });
102
+ }
103
+
104
+ const handleResize = () => chart?.resize();
105
+ window.addEventListener('resize', handleResize);
106
+
107
+ return () => {
108
+ window.removeEventListener('resize', handleResize);
109
+ chart?.dispose();
110
+ chart = null;
111
+ };
112
+ });
113
+
114
+ // Update options when they change (handles both initial and subsequent updates)
115
+ $effect(() => {
116
+ if (chart && !isEmpty) {
117
+ chart.setOption(option, { notMerge: true, lazyUpdate: true });
118
+ }
119
+ });
120
+ </script>
121
+
122
+ <ChartContainer
123
+ {title}
124
+ description={accessibilityDescription}
125
+ {height}
126
+ {loading}
127
+ {error}
128
+ empty={isEmpty}
129
+ {emptyMessage}
130
+ class={cn(className)}
131
+ >
132
+ <div bind:this={containerEl} class="h-full w-full"></div>
133
+ </ChartContainer>