@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
@@ -1,5 +1,6 @@
1
1
  import type { BarChartProps } from '@classic-homes/charts-core';
2
- interface Props extends Omit<BarChartProps, 'class'> {
2
+ import { type CustomLegendProps } from '../../composables/useChartLegend.svelte.js';
3
+ interface Props extends Omit<BarChartProps, 'class'>, CustomLegendProps {
3
4
  class?: string;
4
5
  }
5
6
  declare const BarChart: import("svelte").Component<Props, {}, "">;
@@ -6,16 +6,33 @@
6
6
  import type { EChartsOption } from 'echarts';
7
7
 
8
8
  import type { DonutChartProps, SliceEventParams } from '@classic-homes/charts-core';
9
- import { generatePieChartDescription } from '@classic-homes/charts-core';
9
+ import {
10
+ buildDataLabel,
11
+ buildLegend,
12
+ buildTooltip,
13
+ escapeHtml,
14
+ formatTooltipValue,
15
+ generatePieChartDescription,
16
+ getChartColors,
17
+ itemLabelLookup,
18
+ anchorCenterGraphic,
19
+ legendCenter,
20
+ mergeChartOption,
21
+ pointLabel,
22
+ } from '@classic-homes/charts-core';
10
23
 
11
24
  import { cn } from '../../utils.js';
12
25
  import { useChart } from '../../composables/useChart.svelte.js';
26
+ import {
27
+ useChartLegend,
28
+ type CustomLegendProps,
29
+ } from '../../composables/useChartLegend.svelte.js';
13
30
  import ChartContainer from '../base/ChartContainer.svelte';
14
31
 
15
32
  // Register required ECharts modules
16
33
  echarts.use([EChartsPieChart, TooltipComponent, LegendComponent, TitleComponent, CanvasRenderer]);
17
34
 
18
- interface Props extends Omit<DonutChartProps, 'class'> {
35
+ interface Props extends Omit<DonutChartProps, 'class'>, CustomLegendProps {
19
36
  class?: string;
20
37
  }
21
38
 
@@ -36,114 +53,163 @@
36
53
  innerRadius = '50%',
37
54
  centerLabel,
38
55
  centerValue,
56
+ labelFormatter,
57
+ tooltipFormatter,
58
+ tooltipValueFormatter,
59
+ legend: legendConfig,
60
+ renderLegend,
61
+ onLegendChange,
62
+ option: optionOverride,
39
63
  onClick,
40
64
  class: className,
41
65
  }: Props = $props();
42
66
 
43
67
  const isEmpty = $derived(!data?.length);
44
68
 
69
+ // Per-element colours that follow the resolved chart theme
70
+ const chartColors = $derived.by(() => getChartColors(chart.theme));
71
+
45
72
  const option: EChartsOption = $derived.by(() => {
46
73
  if (isEmpty) return {};
47
74
 
75
+ const getLabel = itemLabelLookup(data);
76
+
77
+ const pieCenter = legendCenter(legendConfig, showLegend && !renderLegend, ['50%', '45%']);
48
78
  const graphicElements: unknown[] = [];
49
79
 
50
80
  if (centerLabel || centerValue) {
51
- graphicElements.push({
52
- type: 'group',
53
- left: 'center',
54
- top: '40%',
55
- children: [
56
- ...(centerValue !== undefined
57
- ? [
58
- {
59
- type: 'text',
60
- style: {
61
- text: String(centerValue),
62
- fontSize: 28,
63
- fontWeight: 'bold',
64
- fill: 'currentColor',
65
- align: 'center',
66
- },
67
- top: 0,
68
- left: 'center',
69
- },
70
- ]
71
- : []),
72
- ...(centerLabel
73
- ? [
74
- {
75
- type: 'text',
76
- style: {
77
- text: centerLabel,
78
- fontSize: 14,
79
- fill: '#888',
80
- align: 'center',
81
- },
82
- top: centerValue !== undefined ? 35 : 0,
83
- left: 'center',
84
- },
85
- ]
86
- : []),
87
- ],
88
- });
81
+ graphicElements.push(
82
+ anchorCenterGraphic(
83
+ {
84
+ type: 'group',
85
+ left: 'center',
86
+ top: '40%',
87
+ children: [
88
+ ...(centerValue !== undefined
89
+ ? [
90
+ {
91
+ type: 'text',
92
+ style: {
93
+ text: String(centerValue),
94
+ fontSize: 28,
95
+ fontWeight: 'bold',
96
+ fill: chartColors.foreground,
97
+ align: 'center',
98
+ },
99
+ top: 0,
100
+ left: 'center',
101
+ },
102
+ ]
103
+ : []),
104
+ ...(centerLabel
105
+ ? [
106
+ {
107
+ type: 'text',
108
+ style: {
109
+ text: centerLabel,
110
+ fontSize: 14,
111
+ fill: chartColors.mutedForeground,
112
+ align: 'center',
113
+ },
114
+ top: centerValue !== undefined ? 35 : 0,
115
+ left: 'center',
116
+ },
117
+ ]
118
+ : []),
119
+ ],
120
+ },
121
+ pieCenter,
122
+ ['50%', '45%']
123
+ )
124
+ );
89
125
  }
90
126
 
91
- return {
92
- tooltip: showTooltip
93
- ? {
127
+ return mergeChartOption(
128
+ {
129
+ tooltip: buildTooltip(
130
+ {
94
131
  trigger: 'item',
95
- formatter: '{a} <br/>{b}: {c} ({d}%)',
96
- }
97
- : undefined,
98
- legend: showLegend
99
- ? {
100
- orient: 'horizontal',
101
- bottom: 0,
102
- data: data.map((d) => d.name),
132
+ // A string template ignores valueFormatter, so format the value here
133
+ formatter:
134
+ tooltipValueFormatter && !tooltipFormatter
135
+ ? (params: unknown) => {
136
+ const p = params as {
137
+ seriesName?: string;
138
+ name?: string;
139
+ value?: unknown;
140
+ percent?: number;
141
+ };
142
+ return `${escapeHtml(p.seriesName)} <br/>${escapeHtml(p.name)}: ${escapeHtml(formatTooltipValue(p.value, tooltipValueFormatter))} (${p.percent}%)`;
143
+ }
144
+ : '{a} <br/>{b}: {c} ({d}%)',
145
+ },
146
+ {
147
+ show: showTooltip,
148
+ formatter: tooltipFormatter,
149
+ valueFormatter: tooltipValueFormatter,
150
+ getLabel,
103
151
  }
104
- : undefined,
105
- graphic:
106
- graphicElements.length > 0 ? (graphicElements as EChartsOption['graphic']) : undefined,
107
- series: [
108
- {
109
- name: title,
110
- type: 'pie',
111
- radius: [innerRadius, '70%'],
112
- center: ['50%', '45%'],
113
- avoidLabelOverlap: true,
114
- data: data.map((item) => ({
115
- name: item.name,
116
- value: item.value,
117
- itemStyle: item.color ? { color: item.color } : undefined,
118
- })),
119
- label: showLabels
120
- ? {
152
+ ),
153
+ legend: buildLegend(
154
+ data.map((d) => d.name),
155
+ legendConfig,
156
+ { show: showLegend, custom: !!renderLegend, defaults: { orient: 'horizontal' } }
157
+ ),
158
+ graphic:
159
+ graphicElements.length > 0 ? (graphicElements as EChartsOption['graphic']) : undefined,
160
+ series: [
161
+ {
162
+ name: title,
163
+ type: 'pie',
164
+ radius: [innerRadius, '70%'],
165
+ center: pieCenter,
166
+ avoidLabelOverlap: true,
167
+ data: data.map((item) => ({
168
+ name: item.name,
169
+ value: item.value,
170
+ itemStyle: item.color ? { color: item.color } : undefined,
171
+ label: pointLabel(item.label, labelFormatter, { position: labelPosition }),
172
+ })),
173
+ label: buildDataLabel(
174
+ {
121
175
  show: true,
122
176
  position: labelPosition,
123
177
  formatter: labelPosition === 'inside' ? '{d}%' : '{b}: {d}%',
124
- }
125
- : {
126
- show: false,
127
178
  },
128
- labelLine: {
129
- show: showLabels && labelPosition === 'outside',
130
- },
131
- emphasis: {
132
- itemStyle: {
133
- shadowBlur: 10,
134
- shadowOffsetX: 0,
135
- shadowColor: 'rgba(0, 0, 0, 0.5)',
179
+ { show: false },
180
+ { show: showLabels, formatter: labelFormatter, getLabel }
181
+ ),
182
+ labelLine: {
183
+ show: showLabels && labelPosition === 'outside',
184
+ },
185
+ emphasis: {
186
+ itemStyle: {
187
+ shadowBlur: 10,
188
+ shadowOffsetX: 0,
189
+ shadowColor: 'rgba(0, 0, 0, 0.5)',
190
+ },
136
191
  },
137
192
  },
138
- },
139
- ],
140
- };
193
+ ],
194
+ } as EChartsOption,
195
+ optionOverride
196
+ );
141
197
  });
142
198
 
143
199
  const accessibilityDescription = $derived(
144
200
  description || (!isEmpty ? generatePieChartDescription(title, data, true) : undefined)
145
201
  );
146
202
 
203
+ // Legend selection state, shared with a custom legend
204
+ const legend = useChartLegend({
205
+ names: () => data?.map((d) => d.name) ?? [],
206
+ colors: () => data?.map((d) => d.color) ?? [],
207
+ palette: () => getChartColors(chart.theme).series,
208
+ config: () => legendConfig,
209
+ title: () => title,
210
+ onLegendChange: () => onLegendChange,
211
+ });
212
+
147
213
  // Manage ECharts instance lifecycle (init, theme, resize, option updates)
148
214
  const chart = useChart({
149
215
  option: () => option,
@@ -151,6 +217,7 @@
151
217
  animation: () => animation,
152
218
  isEmpty: () => isEmpty,
153
219
  onReady: (instance) => {
220
+ legend.attach(instance);
154
221
  if (onClick) {
155
222
  instance.on('click', (params) => {
156
223
  onClick({
@@ -167,6 +234,10 @@
167
234
  });
168
235
  </script>
169
236
 
237
+ {#snippet customLegend()}
238
+ {@render renderLegend?.(legend.renderProps)}
239
+ {/snippet}
240
+
170
241
  <ChartContainer
171
242
  {title}
172
243
  description={accessibilityDescription}
@@ -176,6 +247,8 @@
176
247
  empty={isEmpty}
177
248
  {emptyMessage}
178
249
  class={cn(className)}
250
+ legend={renderLegend ? customLegend : undefined}
251
+ legendPosition={legendConfig?.position}
179
252
  >
180
253
  <div bind:this={chart.container} class="h-full w-full"></div>
181
254
  </ChartContainer>
@@ -1,5 +1,6 @@
1
1
  import type { DonutChartProps } from '@classic-homes/charts-core';
2
- interface Props extends Omit<DonutChartProps, 'class'> {
2
+ import { type CustomLegendProps } from '../../composables/useChartLegend.svelte.js';
3
+ interface Props extends Omit<DonutChartProps, 'class'>, CustomLegendProps {
3
4
  class?: string;
4
5
  }
5
6
  declare const DonutChart: import("svelte").Component<Props, {}, "">;
@@ -11,10 +11,26 @@
11
11
  import type { EChartsOption } from 'echarts';
12
12
 
13
13
  import type { LineChartProps, DataPointEventParams } from '@classic-homes/charts-core';
14
- import { generateLineChartDescription } from '@classic-homes/charts-core';
14
+ import {
15
+ buildAxis,
16
+ buildDataLabel,
17
+ buildLegend,
18
+ buildTooltip,
19
+ generateLineChartDescription,
20
+ getChartColors,
21
+ hasPointLabels,
22
+ gridInset,
23
+ mergeChartOption,
24
+ seriesLabelLookup,
25
+ withPointLabels,
26
+ } from '@classic-homes/charts-core';
15
27
 
16
28
  import { cn } from '../../utils.js';
17
29
  import { useChart } from '../../composables/useChart.svelte.js';
30
+ import {
31
+ useChartLegend,
32
+ type CustomLegendProps,
33
+ } from '../../composables/useChartLegend.svelte.js';
18
34
  import ChartContainer from '../base/ChartContainer.svelte';
19
35
 
20
36
  // Register required ECharts modules
@@ -27,7 +43,7 @@
27
43
  CanvasRenderer,
28
44
  ]);
29
45
 
30
- interface Props extends Omit<LineChartProps, 'class'> {
46
+ interface Props extends Omit<LineChartProps, 'class'>, CustomLegendProps {
31
47
  class?: string;
32
48
  }
33
49
 
@@ -48,6 +64,16 @@
48
64
  areaFilled = false,
49
65
  showDataPoints = false,
50
66
  stacked = false,
67
+ showLabels = false,
68
+ labelFormatter,
69
+ xAxisConfig,
70
+ yAxisConfig,
71
+ tooltipFormatter,
72
+ tooltipValueFormatter,
73
+ legend: legendConfig,
74
+ renderLegend,
75
+ onLegendChange,
76
+ option: optionOverride,
51
77
  onClick,
52
78
  onHover,
53
79
  class: className,
@@ -57,68 +83,105 @@
57
83
  !data?.categories?.length || !data?.series?.length || data.series.every((s) => !s.data?.length)
58
84
  );
59
85
 
86
+ // Per-element colours that follow the resolved chart theme
87
+ const chartColors = $derived.by(() => getChartColors(chart.theme));
88
+
60
89
  const option: EChartsOption = $derived.by(() => {
61
90
  if (isEmpty) return {};
62
91
 
63
- return {
64
- grid: {
65
- left: '3%',
66
- right: '4%',
67
- bottom: '3%',
68
- containLabel: true,
69
- },
70
- tooltip: showTooltip
71
- ? {
92
+ const getLabel = seriesLabelLookup(data.series);
93
+ // ECharts hides line labels along with the symbol
94
+ const showSymbol = showDataPoints || showLabels || hasPointLabels(data.series);
95
+
96
+ return mergeChartOption(
97
+ {
98
+ grid: {
99
+ left: '3%',
100
+ right: '4%',
101
+ bottom: '3%',
102
+ containLabel: true,
103
+ ...gridInset(legendConfig, showLegend && !renderLegend, xAxisConfig, yAxisConfig),
104
+ },
105
+ tooltip: buildTooltip(
106
+ {
72
107
  trigger: 'axis',
73
108
  axisPointer: {
74
109
  type: 'cross',
75
110
  label: {
76
- backgroundColor: '#6a7985',
111
+ backgroundColor: chartColors.axisPointerLabel,
77
112
  },
78
113
  },
114
+ },
115
+ {
116
+ show: showTooltip,
117
+ formatter: tooltipFormatter,
118
+ valueFormatter: tooltipValueFormatter,
119
+ getLabel,
79
120
  }
80
- : undefined,
81
- legend: showLegend
82
- ? {
83
- data: data.series.map((s) => s.name),
84
- bottom: 0,
85
- }
86
- : undefined,
87
- xAxis: {
88
- type: 'category',
89
- boundaryGap: false,
90
- data: data.categories,
91
- splitLine: {
92
- show: showGrid,
93
- },
94
- },
95
- yAxis: {
96
- type: 'value',
97
- splitLine: {
98
- show: showGrid,
99
- },
100
- },
101
- series: data.series.map((series) => ({
102
- name: series.name,
103
- type: 'line',
104
- data: series.data,
105
- smooth,
106
- showSymbol: showDataPoints,
107
- symbolSize: showDataPoints ? 6 : 0,
108
- stack: stacked ? 'Total' : undefined,
109
- areaStyle: areaFilled ? { opacity: 0.3 } : undefined,
110
- itemStyle: series.color ? { color: series.color } : undefined,
111
- emphasis: {
112
- focus: 'series',
113
- },
114
- })),
115
- };
121
+ ),
122
+ legend: buildLegend(
123
+ data.series.map((s) => s.name),
124
+ legendConfig,
125
+ { show: showLegend, custom: !!renderLegend }
126
+ ),
127
+ xAxis: buildAxis(
128
+ {
129
+ type: 'category',
130
+ boundaryGap: false,
131
+ data: data.categories,
132
+ splitLine: {
133
+ show: showGrid,
134
+ },
135
+ },
136
+ xAxisConfig
137
+ ),
138
+ yAxis: buildAxis(
139
+ {
140
+ type: 'value',
141
+ splitLine: {
142
+ show: showGrid,
143
+ },
144
+ },
145
+ yAxisConfig
146
+ ),
147
+ series: data.series.map((series) => ({
148
+ name: series.name,
149
+ type: 'line',
150
+ data: withPointLabels(series.data, series.labels, labelFormatter, { position: 'top' }),
151
+ smooth,
152
+ showSymbol,
153
+ symbolSize: showDataPoints ? 6 : 0,
154
+ stack: stacked ? 'Total' : undefined,
155
+ areaStyle: areaFilled ? { opacity: 0.3 } : undefined,
156
+ itemStyle: series.color ? { color: series.color } : undefined,
157
+ label: buildDataLabel({ show: true, position: 'top' }, undefined, {
158
+ show: showLabels,
159
+ formatter: labelFormatter,
160
+ getLabel,
161
+ }),
162
+ emphasis: {
163
+ focus: 'series',
164
+ },
165
+ })),
166
+ } as EChartsOption,
167
+ optionOverride
168
+ );
116
169
  });
117
170
 
118
171
  const accessibilityDescription = $derived(
119
172
  description || (!isEmpty ? generateLineChartDescription(title, data) : undefined)
120
173
  );
121
174
 
175
+ // Legend selection state, shared with a custom legend
176
+ const legend = useChartLegend({
177
+ names: () => data?.series?.map((s) => s.name) ?? [],
178
+ colors: () => data?.series?.map((s) => s.color) ?? [],
179
+ palette: () => getChartColors(chart.theme).series,
180
+ config: () => legendConfig,
181
+ title: () => title,
182
+ onLegendChange: () => onLegendChange,
183
+ });
184
+
122
185
  // Manage ECharts instance lifecycle (init, theme, resize, option updates)
123
186
  const chart = useChart({
124
187
  option: () => option,
@@ -126,6 +189,7 @@
126
189
  animation: () => animation,
127
190
  isEmpty: () => isEmpty,
128
191
  onReady: (instance) => {
192
+ legend.attach(instance);
129
193
  if (onClick) {
130
194
  instance.on('click', (params) => {
131
195
  onClick({
@@ -159,6 +223,10 @@
159
223
  });
160
224
  </script>
161
225
 
226
+ {#snippet customLegend()}
227
+ {@render renderLegend?.(legend.renderProps)}
228
+ {/snippet}
229
+
162
230
  <ChartContainer
163
231
  {title}
164
232
  description={accessibilityDescription}
@@ -168,6 +236,8 @@
168
236
  empty={isEmpty}
169
237
  {emptyMessage}
170
238
  class={cn(className)}
239
+ legend={renderLegend ? customLegend : undefined}
240
+ legendPosition={legendConfig?.position}
171
241
  >
172
242
  <div bind:this={chart.container} class="h-full w-full"></div>
173
243
  </ChartContainer>
@@ -1,5 +1,6 @@
1
1
  import type { LineChartProps } from '@classic-homes/charts-core';
2
- interface Props extends Omit<LineChartProps, 'class'> {
2
+ import { type CustomLegendProps } from '../../composables/useChartLegend.svelte.js';
3
+ interface Props extends Omit<LineChartProps, 'class'>, CustomLegendProps {
3
4
  class?: string;
4
5
  }
5
6
  declare const LineChart: import("svelte").Component<Props, {}, "">;