@classic-homes/charts-svelte 0.1.38 → 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 (36) hide show
  1. package/dist/lib/components/base/ChartContainer.svelte +47 -8
  2. package/dist/lib/components/base/ChartContainer.svelte.d.ts +8 -0
  3. package/dist/lib/components/base/ChartLegend.svelte +59 -0
  4. package/dist/lib/components/base/ChartLegend.svelte.d.ts +10 -0
  5. package/dist/lib/components/base/index.d.ts +3 -0
  6. package/dist/lib/components/base/index.js +1 -0
  7. package/dist/lib/components/core/AreaChart.svelte +121 -54
  8. package/dist/lib/components/core/AreaChart.svelte.d.ts +2 -1
  9. package/dist/lib/components/core/BarChart.svelte +91 -35
  10. package/dist/lib/components/core/BarChart.svelte.d.ts +2 -1
  11. package/dist/lib/components/core/DonutChart.svelte +152 -82
  12. package/dist/lib/components/core/DonutChart.svelte.d.ts +2 -1
  13. package/dist/lib/components/core/LineChart.svelte +114 -47
  14. package/dist/lib/components/core/LineChart.svelte.d.ts +2 -1
  15. package/dist/lib/components/core/PieChart.svelte +100 -38
  16. package/dist/lib/components/core/PieChart.svelte.d.ts +2 -1
  17. package/dist/lib/components/core/ScatterChart.svelte +102 -34
  18. package/dist/lib/components/core/ScatterChart.svelte.d.ts +2 -1
  19. package/dist/lib/components/extended/CandlestickChart.svelte +44 -22
  20. package/dist/lib/components/extended/FunnelChart.svelte +102 -33
  21. package/dist/lib/components/extended/FunnelChart.svelte.d.ts +2 -1
  22. package/dist/lib/components/extended/GaugeChart.svelte +59 -38
  23. package/dist/lib/components/extended/HeatmapChart.svelte +47 -27
  24. package/dist/lib/components/extended/RadarChart.svelte +76 -22
  25. package/dist/lib/components/extended/RadarChart.svelte.d.ts +2 -1
  26. package/dist/lib/components/extended/SankeyChart.svelte +50 -21
  27. package/dist/lib/components/extended/TreemapChart.svelte +68 -27
  28. package/dist/lib/components/index.d.ts +1 -1
  29. package/dist/lib/components/index.js +1 -1
  30. package/dist/lib/composables/index.d.ts +1 -0
  31. package/dist/lib/composables/index.js +1 -0
  32. package/dist/lib/composables/useChartLegend.svelte.d.ts +37 -0
  33. package/dist/lib/composables/useChartLegend.svelte.js +54 -0
  34. package/dist/lib/index.d.ts +3 -3
  35. package/dist/lib/index.js +2 -2
  36. package/package.json +1 -1
@@ -6,16 +6,32 @@
6
6
  import type { EChartsOption } from 'echarts';
7
7
 
8
8
  import type { PieChartProps, 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
+ legendCenter,
19
+ mergeChartOption,
20
+ pointLabel,
21
+ } from '@classic-homes/charts-core';
10
22
 
11
23
  import { cn } from '../../utils.js';
12
24
  import { useChart } from '../../composables/useChart.svelte.js';
25
+ import {
26
+ useChartLegend,
27
+ type CustomLegendProps,
28
+ } from '../../composables/useChartLegend.svelte.js';
13
29
  import ChartContainer from '../base/ChartContainer.svelte';
14
30
 
15
31
  // Register required ECharts modules
16
32
  echarts.use([EChartsPieChart, TooltipComponent, LegendComponent, TitleComponent, CanvasRenderer]);
17
33
 
18
- interface Props extends Omit<PieChartProps, 'class'> {
34
+ interface Props extends Omit<PieChartProps, 'class'>, CustomLegendProps {
19
35
  class?: string;
20
36
  }
21
37
 
@@ -34,6 +50,13 @@
34
50
  showLabels = true,
35
51
  labelPosition = 'outside',
36
52
  innerRadius = 0,
53
+ labelFormatter,
54
+ tooltipFormatter,
55
+ tooltipValueFormatter,
56
+ legend: legendConfig,
57
+ renderLegend,
58
+ onLegendChange,
59
+ option: optionOverride,
37
60
  onClick,
38
61
  class: className,
39
62
  }: Props = $props();
@@ -43,56 +66,88 @@
43
66
  const option: EChartsOption = $derived.by(() => {
44
67
  if (isEmpty) return {};
45
68
 
46
- return {
47
- tooltip: showTooltip
48
- ? {
69
+ const getLabel = itemLabelLookup(data);
70
+
71
+ return mergeChartOption(
72
+ {
73
+ tooltip: buildTooltip(
74
+ {
49
75
  trigger: 'item',
50
- formatter: '{a} <br/>{b}: {c} ({d}%)',
51
- }
52
- : undefined,
53
- legend: showLegend
54
- ? {
55
- orient: 'horizontal',
56
- bottom: 0,
57
- data: data.map((d) => d.name),
76
+ // A string template ignores valueFormatter, so format the value here
77
+ formatter:
78
+ tooltipValueFormatter && !tooltipFormatter
79
+ ? (params: unknown) => {
80
+ const p = params as {
81
+ seriesName?: string;
82
+ name?: string;
83
+ value?: unknown;
84
+ percent?: number;
85
+ };
86
+ return `${escapeHtml(p.seriesName)} <br/>${escapeHtml(p.name)}: ${escapeHtml(formatTooltipValue(p.value, tooltipValueFormatter))} (${p.percent}%)`;
87
+ }
88
+ : '{a} <br/>{b}: {c} ({d}%)',
89
+ },
90
+ {
91
+ show: showTooltip,
92
+ formatter: tooltipFormatter,
93
+ valueFormatter: tooltipValueFormatter,
94
+ getLabel,
58
95
  }
59
- : undefined,
60
- series: [
61
- {
62
- name: title,
63
- type: 'pie',
64
- radius: innerRadius ? [innerRadius, '70%'] : '70%',
65
- center: ['50%', '45%'],
66
- data: data.map((item) => ({
67
- name: item.name,
68
- value: item.value,
69
- itemStyle: item.color ? { color: item.color } : undefined,
70
- })),
71
- label: showLabels
72
- ? {
96
+ ),
97
+ legend: buildLegend(
98
+ data.map((d) => d.name),
99
+ legendConfig,
100
+ { show: showLegend, custom: !!renderLegend, defaults: { orient: 'horizontal' } }
101
+ ),
102
+ series: [
103
+ {
104
+ name: title,
105
+ type: 'pie',
106
+ radius: innerRadius ? [innerRadius, '70%'] : '70%',
107
+ center: legendCenter(legendConfig, showLegend && !renderLegend, ['50%', '45%']),
108
+ data: data.map((item) => ({
109
+ name: item.name,
110
+ value: item.value,
111
+ itemStyle: item.color ? { color: item.color } : undefined,
112
+ label: pointLabel(item.label, labelFormatter, { position: labelPosition }),
113
+ })),
114
+ label: buildDataLabel(
115
+ {
73
116
  show: true,
74
117
  position: labelPosition,
75
118
  formatter: labelPosition === 'inside' ? '{d}%' : '{b}: {d}%',
76
- }
77
- : {
78
- show: false,
79
119
  },
80
- emphasis: {
81
- itemStyle: {
82
- shadowBlur: 10,
83
- shadowOffsetX: 0,
84
- shadowColor: 'rgba(0, 0, 0, 0.5)',
120
+ { show: false },
121
+ { show: showLabels, formatter: labelFormatter, getLabel }
122
+ ),
123
+ emphasis: {
124
+ itemStyle: {
125
+ shadowBlur: 10,
126
+ shadowOffsetX: 0,
127
+ shadowColor: 'rgba(0, 0, 0, 0.5)',
128
+ },
85
129
  },
86
130
  },
87
- },
88
- ],
89
- };
131
+ ],
132
+ } as EChartsOption,
133
+ optionOverride
134
+ );
90
135
  });
91
136
 
92
137
  const accessibilityDescription = $derived(
93
138
  description || (!isEmpty ? generatePieChartDescription(title, data, false) : undefined)
94
139
  );
95
140
 
141
+ // Legend selection state, shared with a custom legend
142
+ const legend = useChartLegend({
143
+ names: () => data?.map((d) => d.name) ?? [],
144
+ colors: () => data?.map((d) => d.color) ?? [],
145
+ palette: () => getChartColors(chart.theme).series,
146
+ config: () => legendConfig,
147
+ title: () => title,
148
+ onLegendChange: () => onLegendChange,
149
+ });
150
+
96
151
  // Manage ECharts instance lifecycle (init, theme, resize, option updates)
97
152
  const chart = useChart({
98
153
  option: () => option,
@@ -100,6 +155,7 @@
100
155
  animation: () => animation,
101
156
  isEmpty: () => isEmpty,
102
157
  onReady: (instance) => {
158
+ legend.attach(instance);
103
159
  if (onClick) {
104
160
  instance.on('click', (params) => {
105
161
  onClick({
@@ -116,6 +172,10 @@
116
172
  });
117
173
  </script>
118
174
 
175
+ {#snippet customLegend()}
176
+ {@render renderLegend?.(legend.renderProps)}
177
+ {/snippet}
178
+
119
179
  <ChartContainer
120
180
  {title}
121
181
  description={accessibilityDescription}
@@ -125,6 +185,8 @@
125
185
  empty={isEmpty}
126
186
  {emptyMessage}
127
187
  class={cn(className)}
188
+ legend={renderLegend ? customLegend : undefined}
189
+ legendPosition={legendConfig?.position}
128
190
  >
129
191
  <div bind:this={chart.container} class="h-full w-full"></div>
130
192
  </ChartContainer>
@@ -1,5 +1,6 @@
1
1
  import type { PieChartProps } from '@classic-homes/charts-core';
2
- interface Props extends Omit<PieChartProps, 'class'> {
2
+ import { type CustomLegendProps } from '../../composables/useChartLegend.svelte.js';
3
+ interface Props extends Omit<PieChartProps, 'class'>, CustomLegendProps {
3
4
  class?: string;
4
5
  }
5
6
  declare const PieChart: import("svelte").Component<Props, {}, "">;
@@ -14,10 +14,27 @@
14
14
  import type { EChartsOption } from 'echarts';
15
15
 
16
16
  import type { ScatterChartProps, DataPointEventParams } from '@classic-homes/charts-core';
17
- import { escapeHtml, generateScatterChartDescription } from '@classic-homes/charts-core';
17
+ import {
18
+ buildAxis,
19
+ buildDataLabel,
20
+ buildLegend,
21
+ buildTooltip,
22
+ escapeHtml,
23
+ formatTooltipValue,
24
+ generateScatterChartDescription,
25
+ getChartColors,
26
+ gridInset,
27
+ mergeChartOption,
28
+ seriesLabelLookup,
29
+ withPointLabels,
30
+ } from '@classic-homes/charts-core';
18
31
 
19
32
  import { cn } from '../../utils.js';
20
33
  import { useChart } from '../../composables/useChart.svelte.js';
34
+ import {
35
+ useChartLegend,
36
+ type CustomLegendProps,
37
+ } from '../../composables/useChartLegend.svelte.js';
21
38
  import ChartContainer from '../base/ChartContainer.svelte';
22
39
 
23
40
  // Register required ECharts modules
@@ -32,7 +49,7 @@
32
49
  CanvasRenderer,
33
50
  ]);
34
51
 
35
- interface Props extends Omit<ScatterChartProps, 'class'> {
52
+ interface Props extends Omit<ScatterChartProps, 'class'>, CustomLegendProps {
36
53
  class?: string;
37
54
  }
38
55
 
@@ -51,6 +68,16 @@
51
68
  showGrid = true,
52
69
  showTrendLine = false,
53
70
  symbolSize = 10,
71
+ showLabels = false,
72
+ labelFormatter,
73
+ xAxisConfig,
74
+ yAxisConfig,
75
+ tooltipFormatter,
76
+ tooltipValueFormatter,
77
+ legend: legendConfig,
78
+ renderLegend,
79
+ onLegendChange,
80
+ option: optionOverride,
54
81
  onClick,
55
82
  class: className,
56
83
  }: Props = $props();
@@ -60,12 +87,19 @@
60
87
  const option: EChartsOption = $derived.by(() => {
61
88
  if (isEmpty) return {};
62
89
 
90
+ const getLabel = seriesLabelLookup(data.series);
91
+
63
92
  const series: EChartsOption['series'] = data.series.map((s) => ({
64
93
  name: s.name,
65
94
  type: 'scatter',
66
- data: s.data,
95
+ data: withPointLabels(s.data, s.labels, labelFormatter, { position: 'top' }),
67
96
  symbolSize: typeof symbolSize === 'function' ? symbolSize : symbolSize,
68
97
  itemStyle: s.color ? { color: s.color } : undefined,
98
+ label: buildDataLabel({ show: true, position: 'top' }, undefined, {
99
+ show: showLabels,
100
+ formatter: labelFormatter,
101
+ getLabel,
102
+ }),
69
103
  emphasis: {
70
104
  focus: 'series',
71
105
  itemStyle: {
@@ -125,49 +159,76 @@
125
159
  });
126
160
  }
127
161
 
128
- return {
129
- grid: {
130
- left: '3%',
131
- right: '4%',
132
- bottom: '3%',
133
- containLabel: true,
134
- },
135
- tooltip: showTooltip
136
- ? {
162
+ return mergeChartOption(
163
+ {
164
+ grid: {
165
+ left: '3%',
166
+ right: '4%',
167
+ bottom: '3%',
168
+ containLabel: true,
169
+ ...gridInset(legendConfig, showLegend && !renderLegend, xAxisConfig, yAxisConfig),
170
+ },
171
+ tooltip: buildTooltip(
172
+ {
137
173
  trigger: 'item' as const,
138
174
  formatter: (params: unknown) => {
139
175
  const p = params as { seriesName?: string; value?: number[] };
140
176
  if (!p.value) return '';
141
- return `${escapeHtml(p.seriesName)}<br/>X: ${escapeHtml(p.value[0])}<br/>Y: ${escapeHtml(p.value[1])}`;
177
+ const x = formatTooltipValue(p.value[0], tooltipValueFormatter);
178
+ const y = formatTooltipValue(p.value[1], tooltipValueFormatter);
179
+ return `${escapeHtml(p.seriesName)}<br/>X: ${escapeHtml(x)}<br/>Y: ${escapeHtml(y)}`;
142
180
  },
181
+ },
182
+ {
183
+ show: showTooltip,
184
+ formatter: tooltipFormatter,
185
+ valueFormatter: tooltipValueFormatter,
186
+ getLabel,
143
187
  }
144
- : undefined,
145
- legend: showLegend
146
- ? {
147
- data: data.series.map((s) => s.name),
148
- bottom: 0,
149
- }
150
- : undefined,
151
- xAxis: {
152
- type: 'value',
153
- splitLine: {
154
- show: showGrid,
155
- },
156
- },
157
- yAxis: {
158
- type: 'value',
159
- splitLine: {
160
- show: showGrid,
161
- },
162
- },
163
- series,
164
- };
188
+ ),
189
+ legend: buildLegend(
190
+ data.series.map((s) => s.name),
191
+ legendConfig,
192
+ { show: showLegend, custom: !!renderLegend }
193
+ ),
194
+ xAxis: buildAxis(
195
+ {
196
+ type: 'value',
197
+ splitLine: {
198
+ show: showGrid,
199
+ },
200
+ },
201
+ xAxisConfig
202
+ ),
203
+ yAxis: buildAxis(
204
+ {
205
+ type: 'value',
206
+ splitLine: {
207
+ show: showGrid,
208
+ },
209
+ },
210
+ yAxisConfig
211
+ ),
212
+ series,
213
+ } as EChartsOption,
214
+ optionOverride
215
+ );
165
216
  });
166
217
 
167
218
  const accessibilityDescription = $derived(
168
219
  description || (!isEmpty ? generateScatterChartDescription(title, data) : undefined)
169
220
  );
170
221
 
222
+ // Legend selection state, shared with a custom legend
223
+ const legend = useChartLegend({
224
+ names: () => data?.series?.map((s) => s.name) ?? [],
225
+ colors: () => data?.series?.map((s) => s.color) ?? [],
226
+ palette: () => getChartColors(chart.theme).series,
227
+ config: () => legendConfig,
228
+ title: () => title,
229
+ onLegendChange: () => onLegendChange,
230
+ });
231
+
171
232
  // Manage ECharts instance lifecycle (init, theme, resize, option updates)
172
233
  const chart = useChart({
173
234
  option: () => option,
@@ -175,6 +236,7 @@
175
236
  animation: () => animation,
176
237
  isEmpty: () => isEmpty,
177
238
  onReady: (instance) => {
239
+ legend.attach(instance);
178
240
  if (onClick) {
179
241
  instance.on('click', (params) => {
180
242
  onClick({
@@ -193,6 +255,10 @@
193
255
  });
194
256
  </script>
195
257
 
258
+ {#snippet customLegend()}
259
+ {@render renderLegend?.(legend.renderProps)}
260
+ {/snippet}
261
+
196
262
  <ChartContainer
197
263
  {title}
198
264
  description={accessibilityDescription}
@@ -202,6 +268,8 @@
202
268
  empty={isEmpty}
203
269
  {emptyMessage}
204
270
  class={cn(className)}
271
+ legend={renderLegend ? customLegend : undefined}
272
+ legendPosition={legendConfig?.position}
205
273
  >
206
274
  <div bind:this={chart.container} class="h-full w-full"></div>
207
275
  </ChartContainer>
@@ -1,5 +1,6 @@
1
1
  import type { ScatterChartProps } from '@classic-homes/charts-core';
2
- interface Props extends Omit<ScatterChartProps, 'class'> {
2
+ import { type CustomLegendProps } from '../../composables/useChartLegend.svelte.js';
3
+ interface Props extends Omit<ScatterChartProps, 'class'>, CustomLegendProps {
3
4
  class?: string;
4
5
  }
5
6
  declare const ScatterChart: import("svelte").Component<Props, {}, "">;
@@ -14,7 +14,12 @@
14
14
  import type { EChartsOption } from 'echarts';
15
15
 
16
16
  import type { CandlestickChartProps, CandlestickEventParams } from '@classic-homes/charts-core';
17
- import { getChartColors } from '@classic-homes/charts-core';
17
+ import {
18
+ buildAxis,
19
+ buildTooltip,
20
+ getChartColors,
21
+ mergeChartOption,
22
+ } from '@classic-homes/charts-core';
18
23
 
19
24
  import { cn } from '../../utils.js';
20
25
  import { useChart } from '../../composables/useChart.svelte.js';
@@ -46,6 +51,11 @@
46
51
  animation = true,
47
52
  showTooltip = true,
48
53
  showVolume = false,
54
+ xAxisConfig,
55
+ yAxisConfig,
56
+ tooltipFormatter,
57
+ tooltipValueFormatter,
58
+ option: optionOverride,
49
59
  onClick,
50
60
  class: className,
51
61
  }: Props = $props();
@@ -77,18 +87,24 @@
77
87
  const gridArr: { left: string; right: string; top: number | string; height: string }[] = [
78
88
  { left: '10%', right: '10%', top: 60, height: showVolume ? '50%' : '70%' },
79
89
  ];
90
+ // Axis customisation applies to the main (price) axes only
80
91
  const xAxisArr: unknown[] = [
81
- {
82
- type: 'category',
83
- data: data.dates,
84
- boundaryGap: false,
85
- axisLine: { onZero: false },
86
- splitLine: { show: false },
87
- min: 'dataMin',
88
- max: 'dataMax',
89
- },
92
+ buildAxis(
93
+ {
94
+ type: 'category',
95
+ data: data.dates,
96
+ boundaryGap: false,
97
+ axisLine: { onZero: false },
98
+ splitLine: { show: false },
99
+ min: 'dataMin',
100
+ max: 'dataMax',
101
+ },
102
+ xAxisConfig
103
+ ),
104
+ ];
105
+ const yAxisArr: unknown[] = [
106
+ buildAxis({ scale: true, splitArea: { show: true } }, yAxisConfig),
90
107
  ];
91
- const yAxisArr: unknown[] = [{ scale: true, splitArea: { show: true } }];
92
108
 
93
109
  if (showVolume && data.volume) {
94
110
  gridArr.push({ left: '10%', right: '10%', top: '73%', height: '12%' });
@@ -123,17 +139,23 @@
123
139
  });
124
140
  }
125
141
 
126
- return {
127
- tooltip: showTooltip ? { trigger: 'axis', axisPointer: { type: 'cross' } } : undefined,
128
- grid: gridArr,
129
- xAxis: xAxisArr as EChartsOption['xAxis'],
130
- yAxis: yAxisArr as EChartsOption['yAxis'],
131
- dataZoom: [
132
- { type: 'inside', start: 80, end: 100 },
133
- { show: true, type: 'slider', top: '90%', start: 80, end: 100 },
134
- ],
135
- series: seriesArr,
136
- };
142
+ return mergeChartOption(
143
+ {
144
+ tooltip: buildTooltip(
145
+ { trigger: 'axis', axisPointer: { type: 'cross' } },
146
+ { show: showTooltip, formatter: tooltipFormatter, valueFormatter: tooltipValueFormatter }
147
+ ),
148
+ grid: gridArr,
149
+ xAxis: xAxisArr as EChartsOption['xAxis'],
150
+ yAxis: yAxisArr as EChartsOption['yAxis'],
151
+ dataZoom: [
152
+ { type: 'inside', start: 80, end: 100 },
153
+ { show: true, type: 'slider', top: '90%', start: 80, end: 100 },
154
+ ],
155
+ series: seriesArr,
156
+ } as EChartsOption,
157
+ optionOverride
158
+ );
137
159
  });
138
160
 
139
161
  const accessibilityDescription = $derived(