@classic-homes/charts-svelte 0.2.0 → 0.3.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 (54) hide show
  1. package/dist/lib/components/base/ChartContainer.svelte +141 -14
  2. package/dist/lib/components/base/ChartContainer.svelte.d.ts +19 -1
  3. package/dist/lib/components/base/ChartDataTable.svelte +59 -0
  4. package/dist/lib/components/base/ChartDataTable.svelte.d.ts +15 -0
  5. package/dist/lib/components/base/ChartEmpty.svelte +1 -0
  6. package/dist/lib/components/base/ChartSkeleton.svelte +7 -2
  7. package/dist/lib/components/base/ChartSkeleton.svelte.d.ts +2 -0
  8. package/dist/lib/components/base/index.d.ts +3 -0
  9. package/dist/lib/components/base/index.js +1 -0
  10. package/dist/lib/components/core/AreaChart.svelte +115 -70
  11. package/dist/lib/components/core/BarChart.svelte +134 -62
  12. package/dist/lib/components/core/DonutChart.svelte +41 -22
  13. package/dist/lib/components/core/LineChart.svelte +104 -73
  14. package/dist/lib/components/core/PieChart.svelte +41 -22
  15. package/dist/lib/components/core/ScatterChart.svelte +72 -68
  16. package/dist/lib/components/extended/CandlestickChart.svelte +67 -44
  17. package/dist/lib/components/extended/FunnelChart.svelte +41 -27
  18. package/dist/lib/components/extended/GaugeChart.svelte +61 -10
  19. package/dist/lib/components/extended/HeatmapChart.svelte +62 -33
  20. package/dist/lib/components/extended/RadarChart.svelte +39 -19
  21. package/dist/lib/components/extended/SankeyChart.svelte +50 -20
  22. package/dist/lib/components/extended/TreemapChart.svelte +53 -21
  23. package/dist/lib/components/index.d.ts +2 -1
  24. package/dist/lib/components/index.js +3 -1
  25. package/dist/lib/components/report/BoxPlotChart.svelte +253 -0
  26. package/dist/lib/components/report/BoxPlotChart.svelte.d.ts +7 -0
  27. package/dist/lib/components/report/BulletChart.svelte +231 -0
  28. package/dist/lib/components/report/BulletChart.svelte.d.ts +7 -0
  29. package/dist/lib/components/report/ComboChart.svelte +327 -0
  30. package/dist/lib/components/report/ComboChart.svelte.d.ts +8 -0
  31. package/dist/lib/components/report/DotPlotChart.svelte +325 -0
  32. package/dist/lib/components/report/DotPlotChart.svelte.d.ts +8 -0
  33. package/dist/lib/components/report/HistogramChart.svelte +226 -0
  34. package/dist/lib/components/report/HistogramChart.svelte.d.ts +7 -0
  35. package/dist/lib/components/report/Sparkline.svelte +181 -0
  36. package/dist/lib/components/report/Sparkline.svelte.d.ts +4 -0
  37. package/dist/lib/components/report/StatCard.svelte +146 -0
  38. package/dist/lib/components/report/StatCard.svelte.d.ts +4 -0
  39. package/dist/lib/components/report/StatGroup.svelte +35 -0
  40. package/dist/lib/components/report/StatGroup.svelte.d.ts +12 -0
  41. package/dist/lib/components/report/WaterfallChart.svelte +271 -0
  42. package/dist/lib/components/report/WaterfallChart.svelte.d.ts +7 -0
  43. package/dist/lib/components/report/index.d.ts +12 -0
  44. package/dist/lib/components/report/index.js +9 -0
  45. package/dist/lib/composables/index.d.ts +1 -1
  46. package/dist/lib/composables/index.js +1 -1
  47. package/dist/lib/composables/useChart.svelte.d.ts +34 -0
  48. package/dist/lib/composables/useChart.svelte.js +121 -9
  49. package/dist/lib/composables/useChartTheme.svelte.js +14 -11
  50. package/dist/lib/events.d.ts +15 -0
  51. package/dist/lib/events.js +56 -0
  52. package/dist/lib/index.d.ts +4 -3
  53. package/dist/lib/index.js +7 -3
  54. package/package.json +10 -4
@@ -1,11 +1,10 @@
1
1
  <script lang="ts">
2
2
  import * as echarts from 'echarts/core';
3
3
  import { PieChart as EChartsPieChart } from 'echarts/charts';
4
- import { TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components';
5
- import { CanvasRenderer } from 'echarts/renderers';
4
+ import { TooltipComponent, LegendComponent } from 'echarts/components';
6
5
  import type { EChartsOption } from 'echarts';
7
6
 
8
- import type { DonutChartProps, SliceEventParams } from '@classic-homes/charts-core';
7
+ import type { DonutChartProps } from '@classic-homes/charts-core';
9
8
  import {
10
9
  buildDataLabel,
11
10
  buildLegend,
@@ -19,9 +18,12 @@
19
18
  legendCenter,
20
19
  mergeChartOption,
21
20
  pointLabel,
21
+ resolveValueFormatter,
22
+ sliceTable,
22
23
  } from '@classic-homes/charts-core';
23
24
 
24
25
  import { cn } from '../../utils.js';
26
+ import { chartEvents, toSliceParams } from '../../events.js';
25
27
  import { useChart } from '../../composables/useChart.svelte.js';
26
28
  import {
27
29
  useChartLegend,
@@ -30,7 +32,7 @@
30
32
  import ChartContainer from '../base/ChartContainer.svelte';
31
33
 
32
34
  // Register required ECharts modules
33
- echarts.use([EChartsPieChart, TooltipComponent, LegendComponent, TitleComponent, CanvasRenderer]);
35
+ echarts.use([EChartsPieChart, TooltipComponent, LegendComponent]);
34
36
 
35
37
  interface Props extends Omit<DonutChartProps, 'class'>, CustomLegendProps {
36
38
  class?: string;
@@ -41,8 +43,11 @@
41
43
  description,
42
44
  data,
43
45
  height = 400,
46
+ minHeight,
47
+ maxHeight,
44
48
  loading,
45
49
  error,
50
+ onRetry,
46
51
  emptyMessage,
47
52
  theme = 'auto',
48
53
  animation = true,
@@ -53,6 +58,8 @@
53
58
  innerRadius = '50%',
54
59
  centerLabel,
55
60
  centerValue,
61
+ valueFormat,
62
+ locale,
56
63
  labelFormatter,
57
64
  tooltipFormatter,
58
65
  tooltipValueFormatter,
@@ -60,12 +67,22 @@
60
67
  renderLegend,
61
68
  onLegendChange,
62
69
  option: optionOverride,
70
+ onReady,
71
+ events,
72
+ showDataTable,
73
+ exportable,
74
+ patterns,
63
75
  onClick,
64
76
  class: className,
65
77
  }: Props = $props();
66
78
 
67
79
  const isEmpty = $derived(!data?.length);
68
80
 
81
+ // Slice labels show shares; `valueFormat` writes the values in tooltips and the data table
82
+ const valueFormatter = $derived(
83
+ tooltipValueFormatter ?? resolveValueFormatter(valueFormat, locale)
84
+ );
85
+
69
86
  // Per-element colours that follow the resolved chart theme
70
87
  const chartColors = $derived.by(() => getChartColors(chart.theme));
71
88
 
@@ -131,7 +148,7 @@
131
148
  trigger: 'item',
132
149
  // A string template ignores valueFormatter, so format the value here
133
150
  formatter:
134
- tooltipValueFormatter && !tooltipFormatter
151
+ valueFormatter && !tooltipFormatter
135
152
  ? (params: unknown) => {
136
153
  const p = params as {
137
154
  seriesName?: string;
@@ -139,14 +156,14 @@
139
156
  value?: unknown;
140
157
  percent?: number;
141
158
  };
142
- return `${escapeHtml(p.seriesName)} <br/>${escapeHtml(p.name)}: ${escapeHtml(formatTooltipValue(p.value, tooltipValueFormatter))} (${p.percent}%)`;
159
+ return `${escapeHtml(p.seriesName)} <br/>${escapeHtml(p.name)}: ${escapeHtml(formatTooltipValue(p.value, valueFormatter))} (${p.percent}%)`;
143
160
  }
144
161
  : '{a} <br/>{b}: {c} ({d}%)',
145
162
  },
146
163
  {
147
164
  show: showTooltip,
148
165
  formatter: tooltipFormatter,
149
- valueFormatter: tooltipValueFormatter,
166
+ valueFormatter,
150
167
  getLabel,
151
168
  }
152
169
  ),
@@ -200,6 +217,8 @@
200
217
  description || (!isEmpty ? generatePieChartDescription(title, data, true) : undefined)
201
218
  );
202
219
 
220
+ const dataTable = $derived(isEmpty ? undefined : sliceTable(data));
221
+
203
222
  // Legend selection state, shared with a custom legend
204
223
  const legend = useChartLegend({
205
224
  names: () => data?.map((d) => d.name) ?? [],
@@ -216,21 +235,13 @@
216
235
  theme: () => theme,
217
236
  animation: () => animation,
218
237
  isEmpty: () => isEmpty,
219
- onReady: (instance) => {
220
- legend.attach(instance);
221
- if (onClick) {
222
- instance.on('click', (params) => {
223
- onClick({
224
- type: params.type || 'click',
225
- componentType: params.componentType || 'series',
226
- name: params.name || '',
227
- value: params.value as number,
228
- percent: params.percent,
229
- color: params.color as string,
230
- } as SliceEventParams);
231
- });
232
- }
233
- },
238
+ patterns: () => patterns,
239
+ events: () =>
240
+ chartEvents(events, {
241
+ click: onClick && ((params) => onClick(toSliceParams(params, 'click'))),
242
+ }),
243
+ onReady: (instance) => legend.attach(instance),
244
+ onCreated: () => onReady,
234
245
  });
235
246
  </script>
236
247
 
@@ -242,13 +253,21 @@
242
253
  {title}
243
254
  description={accessibilityDescription}
244
255
  {height}
256
+ {minHeight}
257
+ {maxHeight}
245
258
  {loading}
246
259
  {error}
260
+ {onRetry}
247
261
  empty={isEmpty}
248
262
  {emptyMessage}
249
263
  class={cn(className)}
250
264
  legend={renderLegend ? customLegend : undefined}
251
265
  legendPosition={legendConfig?.position}
266
+ {dataTable}
267
+ {showDataTable}
268
+ format={resolveValueFormatter(valueFormat, locale)}
269
+ {exportable}
270
+ exportImage={chart.exportImage}
252
271
  >
253
272
  <div bind:this={chart.container} class="h-full w-full"></div>
254
273
  </ChartContainer>
@@ -1,31 +1,35 @@
1
1
  <script lang="ts">
2
2
  import * as echarts from 'echarts/core';
3
3
  import { LineChart as EChartsLineChart } from 'echarts/charts';
4
- import {
5
- GridComponent,
6
- TooltipComponent,
7
- LegendComponent,
8
- TitleComponent,
9
- } from 'echarts/components';
10
- import { CanvasRenderer } from 'echarts/renderers';
4
+ import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components';
11
5
  import type { EChartsOption } from 'echarts';
12
6
 
13
- import type { LineChartProps, DataPointEventParams } from '@classic-homes/charts-core';
7
+ import type { LineChartProps } from '@classic-homes/charts-core';
14
8
  import {
9
+ allSeriesEmpty,
15
10
  buildAxis,
16
11
  buildDataLabel,
12
+ buildDataZoom,
17
13
  buildLegend,
18
14
  buildTooltip,
15
+ categoryTable,
16
+ chartFormatters,
19
17
  generateLineChartDescription,
20
18
  getChartColors,
21
19
  hasPointLabels,
22
20
  gridInset,
23
21
  mergeChartOption,
22
+ pairWithCategories,
23
+ resolveValueFormatter,
24
24
  seriesLabelLookup,
25
+ toPercentStack,
25
26
  withPointLabels,
27
+ withReferenceMarks,
28
+ withZoomInset,
26
29
  } from '@classic-homes/charts-core';
27
30
 
28
31
  import { cn } from '../../utils.js';
32
+ import { chartEvents, toDataPointParams } from '../../events.js';
29
33
  import { useChart } from '../../composables/useChart.svelte.js';
30
34
  import {
31
35
  useChartLegend,
@@ -34,14 +38,7 @@
34
38
  import ChartContainer from '../base/ChartContainer.svelte';
35
39
 
36
40
  // Register required ECharts modules
37
- echarts.use([
38
- EChartsLineChart,
39
- GridComponent,
40
- TooltipComponent,
41
- LegendComponent,
42
- TitleComponent,
43
- CanvasRenderer,
44
- ]);
41
+ echarts.use([EChartsLineChart, GridComponent, TooltipComponent, LegendComponent]);
45
42
 
46
43
  interface Props extends Omit<LineChartProps, 'class'>, CustomLegendProps {
47
44
  class?: string;
@@ -52,8 +49,11 @@
52
49
  description,
53
50
  data,
54
51
  height = 400,
52
+ minHeight,
53
+ maxHeight,
55
54
  loading,
56
55
  error,
56
+ onRetry,
57
57
  emptyMessage,
58
58
  theme = 'auto',
59
59
  animation = true,
@@ -64,34 +64,85 @@
64
64
  areaFilled = false,
65
65
  showDataPoints = false,
66
66
  stacked = false,
67
+ connectNulls = false,
67
68
  showLabels = false,
69
+ valueFormat,
70
+ locale,
68
71
  labelFormatter,
69
72
  xAxisConfig,
70
73
  yAxisConfig,
74
+ referenceLines,
75
+ referenceBands,
76
+ dataZoom,
71
77
  tooltipFormatter,
72
78
  tooltipValueFormatter,
73
79
  legend: legendConfig,
74
80
  renderLegend,
75
81
  onLegendChange,
76
82
  option: optionOverride,
83
+ onReady,
84
+ events,
85
+ showDataTable,
86
+ exportable,
87
+ patterns,
77
88
  onClick,
78
89
  onHover,
79
90
  class: className,
80
91
  }: Props = $props();
81
92
 
82
- const isEmpty = $derived(
83
- !data?.categories?.length || !data?.series?.length || data.series.every((s) => !s.data?.length)
93
+ const isEmpty = $derived(!data?.categories?.length || allSeriesEmpty(data?.series));
94
+
95
+ const isPercent = $derived(stacked === 'percent');
96
+
97
+ // Percent stacks plot shares, so they always read as percentages
98
+ const formatters = $derived(
99
+ chartFormatters({
100
+ valueFormat: isPercent ? 'percentValue' : valueFormat,
101
+ locale,
102
+ tooltipValueFormatter,
103
+ labelFormatter,
104
+ })
84
105
  );
85
106
 
86
107
  // Per-element colours that follow the resolved chart theme
87
108
  const chartColors = $derived.by(() => getChartColors(chart.theme));
88
109
 
110
+ const builtInLegend = $derived(showLegend && !renderLegend);
111
+
89
112
  const option: EChartsOption = $derived.by(() => {
90
113
  if (isEmpty) return {};
91
114
 
115
+ const plotted = isPercent ? toPercentStack(data.series) : data.series;
92
116
  const getLabel = seriesLabelLookup(data.series);
93
117
  // ECharts hides line labels along with the symbol
94
118
  const showSymbol = showDataPoints || showLabels || hasPointLabels(data.series);
119
+ const legendBottom = builtInLegend && (legendConfig?.position ?? 'bottom') === 'bottom';
120
+
121
+ const series = data.series.map((source, index) => ({
122
+ name: source.name,
123
+ type: 'line',
124
+ data: withPointLabels(
125
+ pairWithCategories(data.categories, plotted[index].data, xAxisConfig?.type),
126
+ source.labels,
127
+ formatters.label,
128
+ { position: 'top' }
129
+ ),
130
+ smooth,
131
+ showSymbol,
132
+ symbolSize: showDataPoints ? 6 : 0,
133
+ connectNulls,
134
+ stack: source.stack ?? (stacked ? 'Total' : undefined),
135
+ areaStyle: areaFilled ? { opacity: 0.3 } : undefined,
136
+ itemStyle: source.color ? { color: source.color } : undefined,
137
+ label: buildDataLabel({ show: true, position: 'top' }, undefined, {
138
+ show: showLabels,
139
+ formatter: formatters.label,
140
+ getLabel,
141
+ }),
142
+ emphasis: {
143
+ focus: 'series',
144
+ },
145
+ }));
95
146
 
96
147
  return mergeChartOption(
97
148
  {
@@ -100,7 +151,11 @@
100
151
  right: '4%',
101
152
  bottom: '3%',
102
153
  containLabel: true,
103
- ...gridInset(legendConfig, showLegend && !renderLegend, xAxisConfig, yAxisConfig),
154
+ ...withZoomInset(
155
+ gridInset(legendConfig, builtInLegend, xAxisConfig, yAxisConfig),
156
+ dataZoom,
157
+ 'x'
158
+ ),
104
159
  },
105
160
  tooltip: buildTooltip(
106
161
  {
@@ -115,7 +170,7 @@
115
170
  {
116
171
  show: showTooltip,
117
172
  formatter: tooltipFormatter,
118
- valueFormatter: tooltipValueFormatter,
173
+ valueFormatter: formatters.tooltip,
119
174
  getLabel,
120
175
  }
121
176
  ),
@@ -124,6 +179,7 @@
124
179
  legendConfig,
125
180
  { show: showLegend, custom: !!renderLegend }
126
181
  ),
182
+ dataZoom: buildDataZoom(dataZoom, 'x', legendBottom ? 36 : 0),
127
183
  xAxis: buildAxis(
128
184
  {
129
185
  type: 'category',
@@ -138,40 +194,31 @@
138
194
  yAxis: buildAxis(
139
195
  {
140
196
  type: 'value',
197
+ ...(isPercent && { max: 100 }),
141
198
  splitLine: {
142
199
  show: showGrid,
143
200
  },
144
201
  },
145
- yAxisConfig
202
+ formatters.axis(yAxisConfig)
146
203
  ),
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
- })),
204
+ series: withReferenceMarks(series, referenceLines, referenceBands, {
205
+ valueAxis: 'y',
206
+ format: formatters.format,
207
+ color: chartColors.reference,
208
+ bandColor: chartColors.referenceBand,
209
+ }),
166
210
  } as EChartsOption,
167
211
  optionOverride
168
212
  );
169
213
  });
170
214
 
171
215
  const accessibilityDescription = $derived(
172
- description || (!isEmpty ? generateLineChartDescription(title, data) : undefined)
216
+ description ||
217
+ (!isEmpty ? generateLineChartDescription(title, data, formatters.format) : undefined)
173
218
  );
174
219
 
220
+ const dataTable = $derived(isEmpty ? undefined : categoryTable(data));
221
+
175
222
  // Legend selection state, shared with a custom legend
176
223
  const legend = useChartLegend({
177
224
  names: () => data?.series?.map((s) => s.name) ?? [],
@@ -188,38 +235,14 @@
188
235
  theme: () => theme,
189
236
  animation: () => animation,
190
237
  isEmpty: () => isEmpty,
191
- onReady: (instance) => {
192
- legend.attach(instance);
193
- if (onClick) {
194
- instance.on('click', (params) => {
195
- onClick({
196
- type: params.type || 'click',
197
- componentType: params.componentType || 'series',
198
- seriesIndex: params.seriesIndex,
199
- seriesName: params.seriesName || '',
200
- dataIndex: params.dataIndex || 0,
201
- name: params.name || '',
202
- value: params.value as number,
203
- color: params.color as string,
204
- } as DataPointEventParams);
205
- });
206
- }
207
-
208
- if (onHover) {
209
- instance.on('mouseover', (params) => {
210
- onHover({
211
- type: params.type || 'mouseover',
212
- componentType: params.componentType || 'series',
213
- seriesIndex: params.seriesIndex,
214
- seriesName: params.seriesName || '',
215
- dataIndex: params.dataIndex || 0,
216
- name: params.name || '',
217
- value: params.value as number,
218
- color: params.color as string,
219
- } as DataPointEventParams);
220
- });
221
- }
222
- },
238
+ patterns: () => patterns,
239
+ events: () =>
240
+ chartEvents(events, {
241
+ click: onClick && ((params) => onClick(toDataPointParams(params, 'click'))),
242
+ mouseover: onHover && ((params) => onHover(toDataPointParams(params, 'mouseover'))),
243
+ }),
244
+ onReady: (instance) => legend.attach(instance),
245
+ onCreated: () => onReady,
223
246
  });
224
247
  </script>
225
248
 
@@ -231,13 +254,21 @@
231
254
  {title}
232
255
  description={accessibilityDescription}
233
256
  {height}
257
+ {minHeight}
258
+ {maxHeight}
234
259
  {loading}
235
260
  {error}
261
+ {onRetry}
236
262
  empty={isEmpty}
237
263
  {emptyMessage}
238
264
  class={cn(className)}
239
265
  legend={renderLegend ? customLegend : undefined}
240
266
  legendPosition={legendConfig?.position}
267
+ {dataTable}
268
+ {showDataTable}
269
+ format={resolveValueFormatter(valueFormat, locale)}
270
+ {exportable}
271
+ exportImage={chart.exportImage}
241
272
  >
242
273
  <div bind:this={chart.container} class="h-full w-full"></div>
243
274
  </ChartContainer>
@@ -1,11 +1,10 @@
1
1
  <script lang="ts">
2
2
  import * as echarts from 'echarts/core';
3
3
  import { PieChart as EChartsPieChart } from 'echarts/charts';
4
- import { TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components';
5
- import { CanvasRenderer } from 'echarts/renderers';
4
+ import { TooltipComponent, LegendComponent } from 'echarts/components';
6
5
  import type { EChartsOption } from 'echarts';
7
6
 
8
- import type { PieChartProps, SliceEventParams } from '@classic-homes/charts-core';
7
+ import type { PieChartProps } from '@classic-homes/charts-core';
9
8
  import {
10
9
  buildDataLabel,
11
10
  buildLegend,
@@ -18,9 +17,12 @@
18
17
  legendCenter,
19
18
  mergeChartOption,
20
19
  pointLabel,
20
+ resolveValueFormatter,
21
+ sliceTable,
21
22
  } from '@classic-homes/charts-core';
22
23
 
23
24
  import { cn } from '../../utils.js';
25
+ import { chartEvents, toSliceParams } from '../../events.js';
24
26
  import { useChart } from '../../composables/useChart.svelte.js';
25
27
  import {
26
28
  useChartLegend,
@@ -29,7 +31,7 @@
29
31
  import ChartContainer from '../base/ChartContainer.svelte';
30
32
 
31
33
  // Register required ECharts modules
32
- echarts.use([EChartsPieChart, TooltipComponent, LegendComponent, TitleComponent, CanvasRenderer]);
34
+ echarts.use([EChartsPieChart, TooltipComponent, LegendComponent]);
33
35
 
34
36
  interface Props extends Omit<PieChartProps, 'class'>, CustomLegendProps {
35
37
  class?: string;
@@ -40,8 +42,11 @@
40
42
  description,
41
43
  data,
42
44
  height = 400,
45
+ minHeight,
46
+ maxHeight,
43
47
  loading,
44
48
  error,
49
+ onRetry,
45
50
  emptyMessage,
46
51
  theme = 'auto',
47
52
  animation = true,
@@ -50,6 +55,8 @@
50
55
  showLabels = true,
51
56
  labelPosition = 'outside',
52
57
  innerRadius = 0,
58
+ valueFormat,
59
+ locale,
53
60
  labelFormatter,
54
61
  tooltipFormatter,
55
62
  tooltipValueFormatter,
@@ -57,12 +64,22 @@
57
64
  renderLegend,
58
65
  onLegendChange,
59
66
  option: optionOverride,
67
+ onReady,
68
+ events,
69
+ showDataTable,
70
+ exportable,
71
+ patterns,
60
72
  onClick,
61
73
  class: className,
62
74
  }: Props = $props();
63
75
 
64
76
  const isEmpty = $derived(!data?.length);
65
77
 
78
+ // Slice labels show shares; `valueFormat` writes the values in tooltips and the data table
79
+ const valueFormatter = $derived(
80
+ tooltipValueFormatter ?? resolveValueFormatter(valueFormat, locale)
81
+ );
82
+
66
83
  const option: EChartsOption = $derived.by(() => {
67
84
  if (isEmpty) return {};
68
85
 
@@ -75,7 +92,7 @@
75
92
  trigger: 'item',
76
93
  // A string template ignores valueFormatter, so format the value here
77
94
  formatter:
78
- tooltipValueFormatter && !tooltipFormatter
95
+ valueFormatter && !tooltipFormatter
79
96
  ? (params: unknown) => {
80
97
  const p = params as {
81
98
  seriesName?: string;
@@ -83,14 +100,14 @@
83
100
  value?: unknown;
84
101
  percent?: number;
85
102
  };
86
- return `${escapeHtml(p.seriesName)} <br/>${escapeHtml(p.name)}: ${escapeHtml(formatTooltipValue(p.value, tooltipValueFormatter))} (${p.percent}%)`;
103
+ return `${escapeHtml(p.seriesName)} <br/>${escapeHtml(p.name)}: ${escapeHtml(formatTooltipValue(p.value, valueFormatter))} (${p.percent}%)`;
87
104
  }
88
105
  : '{a} <br/>{b}: {c} ({d}%)',
89
106
  },
90
107
  {
91
108
  show: showTooltip,
92
109
  formatter: tooltipFormatter,
93
- valueFormatter: tooltipValueFormatter,
110
+ valueFormatter,
94
111
  getLabel,
95
112
  }
96
113
  ),
@@ -138,6 +155,8 @@
138
155
  description || (!isEmpty ? generatePieChartDescription(title, data, false) : undefined)
139
156
  );
140
157
 
158
+ const dataTable = $derived(isEmpty ? undefined : sliceTable(data));
159
+
141
160
  // Legend selection state, shared with a custom legend
142
161
  const legend = useChartLegend({
143
162
  names: () => data?.map((d) => d.name) ?? [],
@@ -154,21 +173,13 @@
154
173
  theme: () => theme,
155
174
  animation: () => animation,
156
175
  isEmpty: () => isEmpty,
157
- onReady: (instance) => {
158
- legend.attach(instance);
159
- if (onClick) {
160
- instance.on('click', (params) => {
161
- onClick({
162
- type: params.type || 'click',
163
- componentType: params.componentType || 'series',
164
- name: params.name || '',
165
- value: params.value as number,
166
- percent: params.percent,
167
- color: params.color as string,
168
- } as SliceEventParams);
169
- });
170
- }
171
- },
176
+ patterns: () => patterns,
177
+ events: () =>
178
+ chartEvents(events, {
179
+ click: onClick && ((params) => onClick(toSliceParams(params, 'click'))),
180
+ }),
181
+ onReady: (instance) => legend.attach(instance),
182
+ onCreated: () => onReady,
172
183
  });
173
184
  </script>
174
185
 
@@ -180,13 +191,21 @@
180
191
  {title}
181
192
  description={accessibilityDescription}
182
193
  {height}
194
+ {minHeight}
195
+ {maxHeight}
183
196
  {loading}
184
197
  {error}
198
+ {onRetry}
185
199
  empty={isEmpty}
186
200
  {emptyMessage}
187
201
  class={cn(className)}
188
202
  legend={renderLegend ? customLegend : undefined}
189
203
  legendPosition={legendConfig?.position}
204
+ {dataTable}
205
+ {showDataTable}
206
+ format={resolveValueFormatter(valueFormat, locale)}
207
+ {exportable}
208
+ exportImage={chart.exportImage}
190
209
  >
191
210
  <div bind:this={chart.container} class="h-full w-full"></div>
192
211
  </ChartContainer>