@classic-homes/charts-svelte 0.1.37 → 0.2.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.
Files changed (40) hide show
  1. package/dist/lib/components/base/ChartContainer.svelte +51 -11
  2. package/dist/lib/components/base/ChartContainer.svelte.d.ts +8 -0
  3. package/dist/lib/components/base/ChartError.svelte +3 -1
  4. package/dist/lib/components/base/ChartLegend.svelte +59 -0
  5. package/dist/lib/components/base/ChartLegend.svelte.d.ts +10 -0
  6. package/dist/lib/components/base/index.d.ts +12 -0
  7. package/dist/lib/components/base/index.js +1 -0
  8. package/dist/lib/components/core/AreaChart.svelte +126 -61
  9. package/dist/lib/components/core/AreaChart.svelte.d.ts +2 -1
  10. package/dist/lib/components/core/BarChart.svelte +91 -35
  11. package/dist/lib/components/core/BarChart.svelte.d.ts +2 -1
  12. package/dist/lib/components/core/DonutChart.svelte +155 -82
  13. package/dist/lib/components/core/DonutChart.svelte.d.ts +2 -1
  14. package/dist/lib/components/core/LineChart.svelte +118 -48
  15. package/dist/lib/components/core/LineChart.svelte.d.ts +2 -1
  16. package/dist/lib/components/core/PieChart.svelte +100 -38
  17. package/dist/lib/components/core/PieChart.svelte.d.ts +2 -1
  18. package/dist/lib/components/core/ScatterChart.svelte +115 -38
  19. package/dist/lib/components/core/ScatterChart.svelte.d.ts +2 -1
  20. package/dist/lib/components/extended/CandlestickChart.svelte +53 -26
  21. package/dist/lib/components/extended/FunnelChart.svelte +102 -32
  22. package/dist/lib/components/extended/FunnelChart.svelte.d.ts +2 -1
  23. package/dist/lib/components/extended/GaugeChart.svelte +62 -29
  24. package/dist/lib/components/extended/HeatmapChart.svelte +59 -29
  25. package/dist/lib/components/extended/RadarChart.svelte +76 -22
  26. package/dist/lib/components/extended/RadarChart.svelte.d.ts +2 -1
  27. package/dist/lib/components/extended/SankeyChart.svelte +50 -21
  28. package/dist/lib/components/extended/TreemapChart.svelte +71 -26
  29. package/dist/lib/components/index.d.ts +1 -1
  30. package/dist/lib/components/index.js +1 -1
  31. package/dist/lib/composables/index.d.ts +1 -0
  32. package/dist/lib/composables/index.js +1 -0
  33. package/dist/lib/composables/useChart.svelte.js +20 -3
  34. package/dist/lib/composables/useChartLegend.svelte.d.ts +37 -0
  35. package/dist/lib/composables/useChartLegend.svelte.js +54 -0
  36. package/dist/lib/composables/useChartTheme.svelte.d.ts +2 -1
  37. package/dist/lib/composables/useChartTheme.svelte.js +2 -6
  38. package/dist/lib/index.d.ts +3 -3
  39. package/dist/lib/index.js +2 -2
  40. package/package.json +1 -1
@@ -6,9 +6,24 @@
6
6
  import type { EChartsOption } from 'echarts';
7
7
 
8
8
  import type { FunnelChartProps, SliceEventParams } from '@classic-homes/charts-core';
9
+ import {
10
+ buildDataLabel,
11
+ buildLegend,
12
+ buildTooltip,
13
+ escapeHtml,
14
+ formatTooltipValue,
15
+ getChartColors,
16
+ itemLabelLookup,
17
+ mergeChartOption,
18
+ pointLabel,
19
+ } from '@classic-homes/charts-core';
9
20
 
10
21
  import { cn } from '../../utils.js';
11
22
  import { useChart } from '../../composables/useChart.svelte.js';
23
+ import {
24
+ useChartLegend,
25
+ type CustomLegendProps,
26
+ } from '../../composables/useChartLegend.svelte.js';
12
27
  import ChartContainer from '../base/ChartContainer.svelte';
13
28
 
14
29
  echarts.use([
@@ -19,7 +34,7 @@
19
34
  CanvasRenderer,
20
35
  ]);
21
36
 
22
- interface Props extends Omit<FunnelChartProps, 'class'> {
37
+ interface Props extends Omit<FunnelChartProps, 'class'>, CustomLegendProps {
23
38
  class?: string;
24
39
  }
25
40
 
@@ -38,6 +53,13 @@
38
53
  sort = 'descending',
39
54
  orientation = 'vertical',
40
55
  showLabels = true,
56
+ labelFormatter,
57
+ tooltipFormatter,
58
+ tooltipValueFormatter,
59
+ legend: legendConfig,
60
+ renderLegend,
61
+ onLegendChange,
62
+ option: optionOverride,
41
63
  onClick,
42
64
  class: className,
43
65
  }: Props = $props();
@@ -47,43 +69,84 @@
47
69
  const option: EChartsOption = $derived.by(() => {
48
70
  if (isEmpty) return {};
49
71
 
50
- return {
51
- tooltip: showTooltip ? { trigger: 'item', formatter: '{a} <br/>{b}: {c}' } : undefined,
52
- legend: showLegend ? { data: data.map((d) => d.name), bottom: 0 } : undefined,
53
- series: [
54
- {
55
- name: title,
56
- type: 'funnel',
57
- left: '10%',
58
- top: 60,
59
- bottom: 60,
60
- width: '80%',
61
- min: 0,
62
- max: Math.max(...data.map((d) => d.value)),
63
- minSize: '0%',
64
- maxSize: '100%',
65
- sort,
66
- orient: orientation,
67
- gap: 2,
68
- label: showLabels
69
- ? { show: true, position: 'inside', formatter: '{b}: {c}' }
70
- : { show: false },
71
- labelLine: { show: false },
72
- emphasis: { label: { fontSize: 16 } },
73
- data: data.map((item) => ({
74
- name: item.name,
75
- value: item.value,
76
- itemStyle: item.color ? { color: item.color } : undefined,
77
- })),
78
- },
79
- ],
80
- };
72
+ const getLabel = itemLabelLookup(data);
73
+
74
+ return mergeChartOption(
75
+ {
76
+ tooltip: buildTooltip(
77
+ {
78
+ trigger: 'item',
79
+ // A string template ignores valueFormatter, so format the value here
80
+ formatter:
81
+ tooltipValueFormatter && !tooltipFormatter
82
+ ? (params: unknown) => {
83
+ const p = params as { seriesName?: string; name?: string; value?: unknown };
84
+ return `${escapeHtml(p.seriesName)} <br/>${escapeHtml(p.name)}: ${escapeHtml(formatTooltipValue(p.value, tooltipValueFormatter))}`;
85
+ }
86
+ : '{a} <br/>{b}: {c}',
87
+ },
88
+ {
89
+ show: showTooltip,
90
+ formatter: tooltipFormatter,
91
+ valueFormatter: tooltipValueFormatter,
92
+ getLabel,
93
+ }
94
+ ),
95
+ legend: buildLegend(
96
+ data.map((d) => d.name),
97
+ legendConfig,
98
+ { show: showLegend, custom: !!renderLegend }
99
+ ),
100
+ series: [
101
+ {
102
+ name: title,
103
+ type: 'funnel',
104
+ left: '10%',
105
+ top: 60,
106
+ bottom: 60,
107
+ width: '80%',
108
+ min: 0,
109
+ // reduce, not Math.max(...): spreading large arrays overflows the stack
110
+ max: data.reduce((m, d) => (d.value > m ? d.value : m), -Infinity),
111
+ minSize: '0%',
112
+ maxSize: '100%',
113
+ sort,
114
+ orient: orientation,
115
+ gap: 2,
116
+ label: buildDataLabel(
117
+ { show: true, position: 'inside', formatter: '{b}: {c}' },
118
+ { show: false },
119
+ { show: showLabels, formatter: labelFormatter, getLabel }
120
+ ),
121
+ labelLine: { show: false },
122
+ emphasis: { label: { fontSize: 16 } },
123
+ data: data.map((item) => ({
124
+ name: item.name,
125
+ value: item.value,
126
+ itemStyle: item.color ? { color: item.color } : undefined,
127
+ label: pointLabel(item.label, labelFormatter, { position: 'inside' }),
128
+ })),
129
+ },
130
+ ],
131
+ } as EChartsOption,
132
+ optionOverride
133
+ );
81
134
  });
82
135
 
83
136
  const accessibilityDescription = $derived(
84
137
  description || (!isEmpty ? `${title}. Funnel chart with ${data.length} stages.` : undefined)
85
138
  );
86
139
 
140
+ // Legend selection state, shared with a custom legend
141
+ const legend = useChartLegend({
142
+ names: () => data?.map((d) => d.name) ?? [],
143
+ colors: () => data?.map((d) => d.color) ?? [],
144
+ palette: () => getChartColors(chart.theme).series,
145
+ config: () => legendConfig,
146
+ title: () => title,
147
+ onLegendChange: () => onLegendChange,
148
+ });
149
+
87
150
  // Manage ECharts instance lifecycle (init, theme, resize, option updates)
88
151
  const chart = useChart({
89
152
  option: () => option,
@@ -91,6 +154,7 @@
91
154
  animation: () => animation,
92
155
  isEmpty: () => isEmpty,
93
156
  onReady: (instance) => {
157
+ legend.attach(instance);
94
158
  if (onClick) {
95
159
  instance.on('click', (params) => {
96
160
  onClick({
@@ -106,6 +170,10 @@
106
170
  });
107
171
  </script>
108
172
 
173
+ {#snippet customLegend()}
174
+ {@render renderLegend?.(legend.renderProps)}
175
+ {/snippet}
176
+
109
177
  <ChartContainer
110
178
  {title}
111
179
  description={accessibilityDescription}
@@ -115,6 +183,8 @@
115
183
  empty={isEmpty}
116
184
  {emptyMessage}
117
185
  class={cn(className)}
186
+ legend={renderLegend ? customLegend : undefined}
187
+ legendPosition={legendConfig?.position}
118
188
  >
119
189
  <div bind:this={chart.container} class="h-full w-full"></div>
120
190
  </ChartContainer>
@@ -1,5 +1,6 @@
1
1
  import type { FunnelChartProps } from '@classic-homes/charts-core';
2
- interface Props extends Omit<FunnelChartProps, 'class'> {
2
+ import { type CustomLegendProps } from '../../composables/useChartLegend.svelte.js';
3
+ interface Props extends Omit<FunnelChartProps, 'class'>, CustomLegendProps {
3
4
  class?: string;
4
5
  }
5
6
  declare const FunnelChart: import("svelte").Component<Props, {}, "">;
@@ -1,17 +1,18 @@
1
1
  <script lang="ts">
2
2
  import * as echarts from 'echarts/core';
3
3
  import { GaugeChart as EChartsGaugeChart } from 'echarts/charts';
4
- import { TitleComponent } from 'echarts/components';
4
+ import { TitleComponent, TooltipComponent } from 'echarts/components';
5
5
  import { CanvasRenderer } from 'echarts/renderers';
6
6
  import type { EChartsOption } from 'echarts';
7
7
 
8
8
  import type { GaugeChartProps } from '@classic-homes/charts-core';
9
+ import { buildTooltip, getChartColors, mergeChartOption } from '@classic-homes/charts-core';
9
10
 
10
11
  import { cn } from '../../utils.js';
11
12
  import { useChart } from '../../composables/useChart.svelte.js';
12
13
  import ChartContainer from '../base/ChartContainer.svelte';
13
14
 
14
- echarts.use([EChartsGaugeChart, TitleComponent, CanvasRenderer]);
15
+ echarts.use([EChartsGaugeChart, TitleComponent, TooltipComponent, CanvasRenderer]);
15
16
 
16
17
  interface Props extends Omit<GaugeChartProps, 'class'> {
17
18
  class?: string;
@@ -32,6 +33,10 @@
32
33
  splitNumber = 10,
33
34
  showProgress = true,
34
35
  formatValue,
36
+ showTooltip = true,
37
+ tooltipFormatter,
38
+ tooltipValueFormatter,
39
+ option: optionOverride,
35
40
  class: className,
36
41
  }: Props = $props();
37
42
 
@@ -42,34 +47,62 @@
42
47
  animation: () => animation,
43
48
  });
44
49
 
45
- const option: EChartsOption = $derived({
46
- series: [
50
+ // Per-element colours that follow the resolved chart theme
51
+ const chartColors = $derived.by(() => getChartColors(chart.theme));
52
+
53
+ const option: EChartsOption = $derived.by(() =>
54
+ mergeChartOption(
47
55
  {
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 && !chart.reducedMotion,
64
- fontSize: 28,
65
- fontWeight: 'bold',
66
- offsetCenter: [0, '40%'],
67
- formatter: formatValue || '{value}',
68
- },
69
- data: [{ value, name: title }],
70
- },
71
- ],
72
- });
56
+ // No tooltip by default; one appears only when a formatter is set
57
+ tooltip:
58
+ tooltipFormatter || tooltipValueFormatter
59
+ ? buildTooltip(
60
+ { trigger: 'item' },
61
+ {
62
+ show: showTooltip,
63
+ formatter: tooltipFormatter,
64
+ valueFormatter: tooltipValueFormatter,
65
+ }
66
+ )
67
+ : undefined,
68
+ series: [
69
+ {
70
+ type: 'gauge',
71
+ center: ['50%', '60%'],
72
+ radius: '80%',
73
+ min,
74
+ max,
75
+ splitNumber,
76
+ progress: showProgress ? { show: true, width: 18 } : undefined,
77
+ axisLine: { lineStyle: { width: 18 } },
78
+ axisTick: {
79
+ distance: -30,
80
+ splitNumber: 5,
81
+ lineStyle: { width: 2, color: chartColors.mutedForeground },
82
+ },
83
+ splitLine: {
84
+ distance: -32,
85
+ length: 10,
86
+ lineStyle: { width: 3, color: chartColors.mutedForeground },
87
+ },
88
+ axisLabel: { distance: -20, color: chartColors.mutedForeground, fontSize: 12 },
89
+ anchor: { show: true, size: 20, itemStyle: { borderWidth: 2 } },
90
+ pointer: { width: 6, length: '60%' },
91
+ title: { show: true, offsetCenter: [0, '70%'], fontSize: 14 },
92
+ detail: {
93
+ valueAnimation: animation && !chart.reducedMotion,
94
+ fontSize: 28,
95
+ fontWeight: 'bold',
96
+ offsetCenter: [0, '40%'],
97
+ formatter: formatValue || '{value}',
98
+ },
99
+ data: [{ value, name: title }],
100
+ },
101
+ ],
102
+ } as EChartsOption,
103
+ optionOverride
104
+ )
105
+ );
73
106
 
74
107
  const accessibilityDescription = $derived(
75
108
  description || `${title}. Gauge showing value ${value} on scale from ${min} to ${max}.`
@@ -11,6 +11,14 @@
11
11
  import type { EChartsOption } from 'echarts';
12
12
 
13
13
  import type { HeatmapChartProps, HeatmapEventParams } from '@classic-homes/charts-core';
14
+ import {
15
+ buildAxis,
16
+ buildDataLabel,
17
+ buildTooltip,
18
+ escapeHtml,
19
+ formatTooltipValue,
20
+ mergeChartOption,
21
+ } from '@classic-homes/charts-core';
14
22
 
15
23
  import { cn } from '../../utils.js';
16
24
  import { useChart } from '../../composables/useChart.svelte.js';
@@ -43,52 +51,74 @@
43
51
  animation = true,
44
52
  showTooltip = true,
45
53
  showValues = false,
54
+ labelFormatter,
55
+ xAxisConfig,
56
+ yAxisConfig,
57
+ tooltipFormatter,
58
+ tooltipValueFormatter,
59
+ option: optionOverride,
46
60
  onClick,
47
61
  class: className,
48
62
  }: Props = $props();
49
63
 
50
64
  const isEmpty = $derived(!data?.data?.length || !xAxis?.length || !yAxis?.length);
51
65
 
52
- const values = $derived(data?.data?.map(([, , value]) => value) || []);
53
- const minValue = $derived(data?.min ?? Math.min(...values));
54
- const maxValue = $derived(data?.max ?? Math.max(...values));
66
+ // A loop: spreading large arrays into Math.min/max overflows the call stack
67
+ const extent = $derived.by(() => {
68
+ let min = Infinity;
69
+ let max = -Infinity;
70
+ for (const [, , value] of data?.data ?? []) {
71
+ if (value < min) min = value;
72
+ if (value > max) max = value;
73
+ }
74
+ return { min, max };
75
+ });
76
+ const minValue = $derived(data?.min ?? extent.min);
77
+ const maxValue = $derived(data?.max ?? extent.max);
55
78
 
56
79
  const option: EChartsOption = $derived.by(() => {
57
80
  if (isEmpty) return {};
58
81
 
59
- return {
60
- tooltip: showTooltip
61
- ? {
82
+ return mergeChartOption(
83
+ {
84
+ tooltip: buildTooltip(
85
+ {
62
86
  position: 'top' as const,
63
87
  formatter: (params: unknown) => {
64
88
  const p = params as { value?: [number, number, number] };
65
89
  if (!p.value) return '';
66
90
  const [xIdx, yIdx, value] = p.value;
67
- return `${xAxis[xIdx]} / ${yAxis[yIdx]}: ${value}`;
91
+ return `${escapeHtml(xAxis[xIdx])} / ${escapeHtml(yAxis[yIdx])}: ${escapeHtml(formatTooltipValue(value, tooltipValueFormatter))}`;
68
92
  },
69
- }
70
- : undefined,
71
- grid: { left: '3%', right: '10%', bottom: '3%', containLabel: true },
72
- xAxis: { type: 'category', data: xAxis, splitArea: { show: true } },
73
- yAxis: { type: 'category', data: yAxis, splitArea: { show: true } },
74
- visualMap: {
75
- min: minValue,
76
- max: maxValue,
77
- calculable: true,
78
- orient: 'vertical',
79
- right: '0%',
80
- top: 'center',
81
- },
82
- series: [
83
- {
84
- name: title,
85
- type: 'heatmap',
86
- data: data.data,
87
- label: showValues ? { show: true } : undefined,
88
- emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.5)' } },
93
+ },
94
+ { show: showTooltip, formatter: tooltipFormatter, valueFormatter: tooltipValueFormatter }
95
+ ),
96
+ grid: { left: '3%', right: '10%', bottom: '3%', containLabel: true },
97
+ xAxis: buildAxis({ type: 'category', data: xAxis, splitArea: { show: true } }, xAxisConfig),
98
+ yAxis: buildAxis({ type: 'category', data: yAxis, splitArea: { show: true } }, yAxisConfig),
99
+ visualMap: {
100
+ min: minValue,
101
+ max: maxValue,
102
+ calculable: true,
103
+ orient: 'vertical',
104
+ right: '0%',
105
+ top: 'center',
89
106
  },
90
- ],
91
- };
107
+ series: [
108
+ {
109
+ name: title,
110
+ type: 'heatmap',
111
+ data: data.data,
112
+ label: buildDataLabel({ show: true }, undefined, {
113
+ show: showValues,
114
+ formatter: labelFormatter,
115
+ }),
116
+ emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.5)' } },
117
+ },
118
+ ],
119
+ } as EChartsOption,
120
+ optionOverride
121
+ );
92
122
  });
93
123
 
94
124
  const accessibilityDescription = $derived(
@@ -11,9 +11,21 @@
11
11
  import type { EChartsOption } from 'echarts';
12
12
 
13
13
  import type { RadarChartProps } from '@classic-homes/charts-core';
14
+ import {
15
+ buildDataLabel,
16
+ buildLegend,
17
+ buildTooltip,
18
+ getChartColors,
19
+ legendCenter,
20
+ mergeChartOption,
21
+ } from '@classic-homes/charts-core';
14
22
 
15
23
  import { cn } from '../../utils.js';
16
24
  import { useChart } from '../../composables/useChart.svelte.js';
25
+ import {
26
+ useChartLegend,
27
+ type CustomLegendProps,
28
+ } from '../../composables/useChartLegend.svelte.js';
17
29
  import ChartContainer from '../base/ChartContainer.svelte';
18
30
 
19
31
  echarts.use([
@@ -25,7 +37,7 @@
25
37
  CanvasRenderer,
26
38
  ]);
27
39
 
28
- interface Props extends Omit<RadarChartProps, 'class'> {
40
+ interface Props extends Omit<RadarChartProps, 'class'>, CustomLegendProps {
29
41
  class?: string;
30
42
  }
31
43
 
@@ -43,6 +55,14 @@
43
55
  showLegend = true,
44
56
  showTooltip = true,
45
57
  shape = 'polygon',
58
+ showLabels = false,
59
+ labelFormatter,
60
+ tooltipFormatter,
61
+ tooltipValueFormatter,
62
+ legend: legendConfig,
63
+ renderLegend,
64
+ onLegendChange,
65
+ option: optionOverride,
46
66
  class: className,
47
67
  }: Props = $props();
48
68
 
@@ -53,29 +73,44 @@
53
73
  const option: EChartsOption = $derived.by(() => {
54
74
  if (isEmpty) return {};
55
75
 
56
- return {
57
- tooltip: showTooltip ? { trigger: 'item' } : undefined,
58
- legend: showLegend ? { data: data.series.map((s) => s.name), bottom: 0 } : undefined,
59
- radar: {
60
- indicator: indicators.map((ind) => ({
61
- name: ind.name,
62
- max: ind.max,
63
- min: ind.min,
64
- })),
65
- shape,
66
- },
67
- series: [
68
- {
69
- type: 'radar',
70
- data: data.series.map((series) => ({
71
- name: series.name,
72
- value: series.data,
73
- itemStyle: series.color ? { color: series.color } : undefined,
74
- areaStyle: { opacity: 0.2 },
76
+ return mergeChartOption(
77
+ {
78
+ tooltip: buildTooltip(
79
+ { trigger: 'item' },
80
+ { show: showTooltip, formatter: tooltipFormatter, valueFormatter: tooltipValueFormatter }
81
+ ),
82
+ legend: buildLegend(
83
+ data.series.map((s) => s.name),
84
+ legendConfig,
85
+ { show: showLegend, custom: !!renderLegend }
86
+ ),
87
+ radar: {
88
+ center: legendCenter(legendConfig, showLegend && !renderLegend, ['50%', '50%']),
89
+ indicator: indicators.map((ind) => ({
90
+ name: ind.name,
91
+ max: ind.max,
92
+ min: ind.min,
75
93
  })),
94
+ shape,
76
95
  },
77
- ],
78
- };
96
+ series: [
97
+ {
98
+ type: 'radar',
99
+ label: buildDataLabel({ show: true }, undefined, {
100
+ show: showLabels,
101
+ formatter: labelFormatter,
102
+ }),
103
+ data: data.series.map((series) => ({
104
+ name: series.name,
105
+ value: series.data,
106
+ itemStyle: series.color ? { color: series.color } : undefined,
107
+ areaStyle: { opacity: 0.2 },
108
+ })),
109
+ },
110
+ ],
111
+ } as EChartsOption,
112
+ optionOverride
113
+ );
79
114
  });
80
115
 
81
116
  const accessibilityDescription = $derived(
@@ -85,15 +120,32 @@
85
120
  : undefined)
86
121
  );
87
122
 
123
+ // Legend selection state, shared with a custom legend
124
+ const legend = useChartLegend({
125
+ names: () => data?.series?.map((s) => s.name) ?? [],
126
+ colors: () => data?.series?.map((s) => s.color) ?? [],
127
+ palette: () => getChartColors(chart.theme).series,
128
+ config: () => legendConfig,
129
+ title: () => title,
130
+ onLegendChange: () => onLegendChange,
131
+ });
132
+
88
133
  // Manage ECharts instance lifecycle (init, theme, resize, option updates)
89
134
  const chart = useChart({
90
135
  option: () => option,
91
136
  theme: () => theme,
92
137
  animation: () => animation,
93
138
  isEmpty: () => isEmpty,
139
+ onReady: (instance) => {
140
+ legend.attach(instance);
141
+ },
94
142
  });
95
143
  </script>
96
144
 
145
+ {#snippet customLegend()}
146
+ {@render renderLegend?.(legend.renderProps)}
147
+ {/snippet}
148
+
97
149
  <ChartContainer
98
150
  {title}
99
151
  description={accessibilityDescription}
@@ -103,6 +155,8 @@
103
155
  empty={isEmpty}
104
156
  {emptyMessage}
105
157
  class={cn(className)}
158
+ legend={renderLegend ? customLegend : undefined}
159
+ legendPosition={legendConfig?.position}
106
160
  >
107
161
  <div bind:this={chart.container} class="h-full w-full"></div>
108
162
  </ChartContainer>
@@ -1,5 +1,6 @@
1
1
  import type { RadarChartProps } from '@classic-homes/charts-core';
2
- interface Props extends Omit<RadarChartProps, 'class'> {
2
+ import { type CustomLegendProps } from '../../composables/useChartLegend.svelte.js';
3
+ interface Props extends Omit<RadarChartProps, 'class'>, CustomLegendProps {
3
4
  class?: string;
4
5
  }
5
6
  declare const RadarChart: import("svelte").Component<Props, {}, "">;