@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.
- package/dist/lib/components/base/ChartContainer.svelte +51 -11
- package/dist/lib/components/base/ChartContainer.svelte.d.ts +8 -0
- package/dist/lib/components/base/ChartError.svelte +3 -1
- package/dist/lib/components/base/ChartLegend.svelte +59 -0
- package/dist/lib/components/base/ChartLegend.svelte.d.ts +10 -0
- package/dist/lib/components/base/index.d.ts +12 -0
- package/dist/lib/components/base/index.js +1 -0
- package/dist/lib/components/core/AreaChart.svelte +126 -61
- package/dist/lib/components/core/AreaChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/BarChart.svelte +91 -35
- package/dist/lib/components/core/BarChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/DonutChart.svelte +155 -82
- package/dist/lib/components/core/DonutChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/LineChart.svelte +118 -48
- package/dist/lib/components/core/LineChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/PieChart.svelte +100 -38
- package/dist/lib/components/core/PieChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/ScatterChart.svelte +115 -38
- package/dist/lib/components/core/ScatterChart.svelte.d.ts +2 -1
- package/dist/lib/components/extended/CandlestickChart.svelte +53 -26
- package/dist/lib/components/extended/FunnelChart.svelte +102 -32
- package/dist/lib/components/extended/FunnelChart.svelte.d.ts +2 -1
- package/dist/lib/components/extended/GaugeChart.svelte +62 -29
- package/dist/lib/components/extended/HeatmapChart.svelte +59 -29
- package/dist/lib/components/extended/RadarChart.svelte +76 -22
- package/dist/lib/components/extended/RadarChart.svelte.d.ts +2 -1
- package/dist/lib/components/extended/SankeyChart.svelte +50 -21
- package/dist/lib/components/extended/TreemapChart.svelte +71 -26
- package/dist/lib/components/index.d.ts +1 -1
- package/dist/lib/components/index.js +1 -1
- package/dist/lib/composables/index.d.ts +1 -0
- package/dist/lib/composables/index.js +1 -0
- package/dist/lib/composables/useChart.svelte.js +20 -3
- package/dist/lib/composables/useChartLegend.svelte.d.ts +37 -0
- package/dist/lib/composables/useChartLegend.svelte.js +54 -0
- package/dist/lib/composables/useChartTheme.svelte.d.ts +2 -1
- package/dist/lib/composables/useChartTheme.svelte.js +2 -6
- package/dist/lib/index.d.ts +3 -3
- package/dist/lib/index.js +2 -2
- 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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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, {}, "">;
|