@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
@@ -4,32 +4,32 @@
4
4
  ScatterChart as EChartsScatterChart,
5
5
  LineChart as EChartsLineChart,
6
6
  } from 'echarts/charts';
7
- import {
8
- GridComponent,
9
- TooltipComponent,
10
- LegendComponent,
11
- TitleComponent,
12
- } from 'echarts/components';
13
- import { CanvasRenderer } from 'echarts/renderers';
7
+ import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components';
14
8
  import type { EChartsOption } from 'echarts';
15
9
 
16
- import type { ScatterChartProps, DataPointEventParams } from '@classic-homes/charts-core';
10
+ import type { ScatterChartProps } from '@classic-homes/charts-core';
17
11
  import {
18
12
  buildAxis,
19
13
  buildDataLabel,
20
14
  buildLegend,
21
15
  buildTooltip,
16
+ chartFormatters,
22
17
  escapeHtml,
23
18
  formatTooltipValue,
24
19
  generateScatterChartDescription,
25
20
  getChartColors,
26
21
  gridInset,
27
22
  mergeChartOption,
23
+ resolveValueFormatter,
24
+ scatterTable,
28
25
  seriesLabelLookup,
26
+ trendLine,
29
27
  withPointLabels,
28
+ withReferenceMarks,
30
29
  } from '@classic-homes/charts-core';
31
30
 
32
31
  import { cn } from '../../utils.js';
32
+ import { chartEvents, toDataPointParams } from '../../events.js';
33
33
  import { useChart } from '../../composables/useChart.svelte.js';
34
34
  import {
35
35
  useChartLegend,
@@ -45,8 +45,6 @@
45
45
  GridComponent,
46
46
  TooltipComponent,
47
47
  LegendComponent,
48
- TitleComponent,
49
- CanvasRenderer,
50
48
  ]);
51
49
 
52
50
  interface Props extends Omit<ScatterChartProps, 'class'>, CustomLegendProps {
@@ -58,8 +56,11 @@
58
56
  description,
59
57
  data,
60
58
  height = 400,
59
+ minHeight,
60
+ maxHeight,
61
61
  loading,
62
62
  error,
63
+ onRetry,
63
64
  emptyMessage,
64
65
  theme = 'auto',
65
66
  animation = true,
@@ -69,35 +70,53 @@
69
70
  showTrendLine = false,
70
71
  symbolSize = 10,
71
72
  showLabels = false,
73
+ valueFormat,
74
+ locale,
72
75
  labelFormatter,
73
76
  xAxisConfig,
74
77
  yAxisConfig,
78
+ referenceLines,
79
+ referenceBands,
75
80
  tooltipFormatter,
76
81
  tooltipValueFormatter,
77
82
  legend: legendConfig,
78
83
  renderLegend,
79
84
  onLegendChange,
80
85
  option: optionOverride,
86
+ onReady,
87
+ events,
88
+ showDataTable,
89
+ exportable,
90
+ patterns,
81
91
  onClick,
92
+ onHover,
82
93
  class: className,
83
94
  }: Props = $props();
84
95
 
85
96
  const isEmpty = $derived(!data?.series?.length || data.series.every((s) => !s.data?.length));
86
97
 
98
+ // `valueFormat` applies to y values; format x with `xAxisConfig.formatter`
99
+ const formatters = $derived(
100
+ chartFormatters({ valueFormat, locale, tooltipValueFormatter, labelFormatter })
101
+ );
102
+
103
+ // Per-element colours that follow the resolved chart theme
104
+ const chartColors = $derived.by(() => getChartColors(chart.theme));
105
+
87
106
  const option: EChartsOption = $derived.by(() => {
88
107
  if (isEmpty) return {};
89
108
 
90
109
  const getLabel = seriesLabelLookup(data.series);
91
110
 
92
- const series: EChartsOption['series'] = data.series.map((s) => ({
111
+ const series: Record<string, unknown>[] = data.series.map((s) => ({
93
112
  name: s.name,
94
113
  type: 'scatter',
95
- data: withPointLabels(s.data, s.labels, labelFormatter, { position: 'top' }),
96
- symbolSize: typeof symbolSize === 'function' ? symbolSize : symbolSize,
114
+ data: withPointLabels(s.data, s.labels, formatters.label, { position: 'top' }),
115
+ symbolSize,
97
116
  itemStyle: s.color ? { color: s.color } : undefined,
98
117
  label: buildDataLabel({ show: true, position: 'top' }, undefined, {
99
118
  show: showLabels,
100
- formatter: labelFormatter,
119
+ formatter: formatters.label,
101
120
  getLabel,
102
121
  }),
103
122
  emphasis: {
@@ -109,47 +128,24 @@
109
128
  },
110
129
  }));
111
130
 
112
- // Add trend lines if enabled
131
+ // Trend lines share their series' name and colour, so the legend toggles
132
+ // both together; they ignore the pointer
113
133
  if (showTrendLine) {
114
134
  data.series.forEach((s, index) => {
115
- if (s.data.length < 2) return;
116
-
117
- const n = s.data.length;
118
- let sumX = 0,
119
- sumY = 0,
120
- sumXY = 0,
121
- sumX2 = 0;
122
-
123
- s.data.forEach(([x, y]) => {
124
- sumX += x;
125
- sumY += y;
126
- sumXY += x * y;
127
- sumX2 += x * x;
128
- });
129
-
130
- const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
131
- const intercept = (sumY - slope * sumX) / n;
132
-
133
- // A loop: spreading large arrays into Math.min/max overflows the stack
134
- let minX = Infinity;
135
- let maxX = -Infinity;
136
- for (const [x] of s.data) {
137
- if (x < minX) minX = x;
138
- if (x > maxX) maxX = x;
139
- }
140
-
141
- (series as unknown[]).push({
142
- name: `${s.name} Trend`,
135
+ const ends = trendLine(s.data);
136
+ if (!ends) return;
137
+ series.push({
138
+ name: s.name,
143
139
  type: 'line',
144
- data: [
145
- [minX, slope * minX + intercept],
146
- [maxX, slope * maxX + intercept],
147
- ],
140
+ data: ends,
148
141
  smooth: false,
149
142
  showSymbol: false,
143
+ silent: true,
144
+ tooltip: { show: false },
150
145
  lineStyle: {
151
146
  type: 'dashed',
152
147
  opacity: 0.7,
148
+ color: s.color || chartColors.series[index % chartColors.series.length],
153
149
  },
154
150
  emphasis: {
155
151
  disabled: true,
@@ -175,14 +171,14 @@
175
171
  const p = params as { seriesName?: string; value?: number[] };
176
172
  if (!p.value) return '';
177
173
  const x = formatTooltipValue(p.value[0], tooltipValueFormatter);
178
- const y = formatTooltipValue(p.value[1], tooltipValueFormatter);
174
+ const y = formatTooltipValue(p.value[1], formatters.tooltip);
179
175
  return `${escapeHtml(p.seriesName)}<br/>X: ${escapeHtml(x)}<br/>Y: ${escapeHtml(y)}`;
180
176
  },
181
177
  },
182
178
  {
183
179
  show: showTooltip,
184
180
  formatter: tooltipFormatter,
185
- valueFormatter: tooltipValueFormatter,
181
+ valueFormatter: formatters.tooltip,
186
182
  getLabel,
187
183
  }
188
184
  ),
@@ -207,9 +203,14 @@
207
203
  show: showGrid,
208
204
  },
209
205
  },
210
- yAxisConfig
206
+ formatters.axis(yAxisConfig)
211
207
  ),
212
- series,
208
+ series: withReferenceMarks(series, referenceLines, referenceBands, {
209
+ valueAxis: 'y',
210
+ format: formatters.format,
211
+ color: chartColors.reference,
212
+ bandColor: chartColors.referenceBand,
213
+ }),
213
214
  } as EChartsOption,
214
215
  optionOverride
215
216
  );
@@ -219,6 +220,10 @@
219
220
  description || (!isEmpty ? generateScatterChartDescription(title, data) : undefined)
220
221
  );
221
222
 
223
+ const dataTable = $derived(
224
+ isEmpty ? undefined : scatterTable(data, xAxisConfig?.name, yAxisConfig?.name)
225
+ );
226
+
222
227
  // Legend selection state, shared with a custom legend
223
228
  const legend = useChartLegend({
224
229
  names: () => data?.series?.map((s) => s.name) ?? [],
@@ -235,23 +240,14 @@
235
240
  theme: () => theme,
236
241
  animation: () => animation,
237
242
  isEmpty: () => isEmpty,
238
- onReady: (instance) => {
239
- legend.attach(instance);
240
- if (onClick) {
241
- instance.on('click', (params) => {
242
- onClick({
243
- type: params.type || 'click',
244
- componentType: params.componentType || 'series',
245
- seriesIndex: params.seriesIndex,
246
- seriesName: params.seriesName || '',
247
- dataIndex: params.dataIndex || 0,
248
- name: params.name || '',
249
- value: params.value as number[],
250
- color: params.color as string,
251
- } as DataPointEventParams);
252
- });
253
- }
254
- },
243
+ patterns: () => patterns,
244
+ events: () =>
245
+ chartEvents(events, {
246
+ click: onClick && ((params) => onClick(toDataPointParams(params, 'click'))),
247
+ mouseover: onHover && ((params) => onHover(toDataPointParams(params, 'mouseover'))),
248
+ }),
249
+ onReady: (instance) => legend.attach(instance),
250
+ onCreated: () => onReady,
255
251
  });
256
252
  </script>
257
253
 
@@ -263,13 +259,21 @@
263
259
  {title}
264
260
  description={accessibilityDescription}
265
261
  {height}
262
+ {minHeight}
263
+ {maxHeight}
266
264
  {loading}
267
265
  {error}
266
+ {onRetry}
268
267
  empty={isEmpty}
269
268
  {emptyMessage}
270
269
  class={cn(className)}
271
270
  legend={renderLegend ? customLegend : undefined}
272
271
  legendPosition={legendConfig?.position}
272
+ {dataTable}
273
+ {showDataTable}
274
+ format={resolveValueFormatter(valueFormat, locale)}
275
+ {exportable}
276
+ exportImage={chart.exportImage}
273
277
  >
274
278
  <div bind:this={chart.container} class="h-full w-full"></div>
275
279
  </ChartContainer>
@@ -4,36 +4,25 @@
4
4
  CandlestickChart as EChartsCandlestickChart,
5
5
  BarChart as EChartsBarChart,
6
6
  } from 'echarts/charts';
7
- import {
8
- GridComponent,
9
- TooltipComponent,
10
- TitleComponent,
11
- DataZoomComponent,
12
- } from 'echarts/components';
13
- import { CanvasRenderer } from 'echarts/renderers';
7
+ import { GridComponent, TooltipComponent } from 'echarts/components';
14
8
  import type { EChartsOption } from 'echarts';
15
9
 
16
10
  import type { CandlestickChartProps, CandlestickEventParams } from '@classic-homes/charts-core';
17
11
  import {
18
12
  buildAxis,
19
13
  buildTooltip,
14
+ candlestickTable,
20
15
  getChartColors,
21
16
  mergeChartOption,
17
+ resolveValueFormatter,
22
18
  } from '@classic-homes/charts-core';
23
19
 
24
20
  import { cn } from '../../utils.js';
21
+ import { chartEvents } from '../../events.js';
25
22
  import { useChart } from '../../composables/useChart.svelte.js';
26
23
  import ChartContainer from '../base/ChartContainer.svelte';
27
24
 
28
- echarts.use([
29
- EChartsCandlestickChart,
30
- EChartsBarChart,
31
- GridComponent,
32
- TooltipComponent,
33
- TitleComponent,
34
- DataZoomComponent,
35
- CanvasRenderer,
36
- ]);
25
+ echarts.use([EChartsCandlestickChart, EChartsBarChart, GridComponent, TooltipComponent]);
37
26
 
38
27
  interface Props extends Omit<CandlestickChartProps, 'class'> {
39
28
  class?: string;
@@ -44,22 +33,39 @@
44
33
  description,
45
34
  data,
46
35
  height = 400,
36
+ minHeight,
37
+ maxHeight,
47
38
  loading,
48
39
  error,
40
+ onRetry,
49
41
  emptyMessage,
50
42
  theme = 'auto',
51
43
  animation = true,
52
44
  showTooltip = true,
53
45
  showVolume = false,
46
+ showGrid = true,
47
+ zoomStart = 80,
54
48
  xAxisConfig,
55
49
  yAxisConfig,
56
50
  tooltipFormatter,
57
51
  tooltipValueFormatter,
58
52
  option: optionOverride,
53
+ valueFormat,
54
+ locale,
55
+ onReady,
56
+ events,
57
+ showDataTable,
58
+ exportable,
59
+ patterns,
59
60
  onClick,
60
61
  class: className,
61
62
  }: Props = $props();
62
63
 
64
+ // `valueFormat` writes values in tooltips and the data table
65
+ const valueFormatter = $derived(
66
+ tooltipValueFormatter ?? resolveValueFormatter(valueFormat, locale)
67
+ );
68
+
63
69
  const isEmpty = $derived(!data?.dates?.length || !data?.data?.length);
64
70
 
65
71
  // Per-element colours that follow the resolved chart theme
@@ -75,11 +81,12 @@
75
81
  name: title,
76
82
  type: 'candlestick',
77
83
  data: candlestickData,
84
+ // ECharts draws rising candles with `color` and falling ones with `color0`
78
85
  itemStyle: {
79
- color: chartColors.destructive,
80
- color0: chartColors.success,
81
- borderColor: chartColors.destructive,
82
- borderColor0: chartColors.success,
86
+ color: chartColors.success,
87
+ color0: chartColors.destructive,
88
+ borderColor: chartColors.success,
89
+ borderColor0: chartColors.destructive,
83
90
  },
84
91
  },
85
92
  ];
@@ -95,7 +102,7 @@
95
102
  data: data.dates,
96
103
  boundaryGap: false,
97
104
  axisLine: { onZero: false },
98
- splitLine: { show: false },
105
+ splitLine: { show: showGrid },
99
106
  min: 'dataMin',
100
107
  max: 'dataMax',
101
108
  },
@@ -103,7 +110,10 @@
103
110
  ),
104
111
  ];
105
112
  const yAxisArr: unknown[] = [
106
- buildAxis({ scale: true, splitArea: { show: true } }, yAxisConfig),
113
+ buildAxis(
114
+ { scale: true, splitArea: { show: showGrid }, splitLine: { show: showGrid } },
115
+ yAxisConfig
116
+ ),
107
117
  ];
108
118
 
109
119
  if (showVolume && data.volume) {
@@ -143,14 +153,14 @@
143
153
  {
144
154
  tooltip: buildTooltip(
145
155
  { trigger: 'axis', axisPointer: { type: 'cross' } },
146
- { show: showTooltip, formatter: tooltipFormatter, valueFormatter: tooltipValueFormatter }
156
+ { show: showTooltip, formatter: tooltipFormatter, valueFormatter }
147
157
  ),
148
158
  grid: gridArr,
149
159
  xAxis: xAxisArr as EChartsOption['xAxis'],
150
160
  yAxis: yAxisArr as EChartsOption['yAxis'],
151
161
  dataZoom: [
152
- { type: 'inside', start: 80, end: 100 },
153
- { show: true, type: 'slider', top: '90%', start: 80, end: 100 },
162
+ { type: 'inside', start: zoomStart, end: 100 },
163
+ { show: true, type: 'slider', top: '90%', start: zoomStart, end: 100 },
154
164
  ],
155
165
  series: seriesArr,
156
166
  } as EChartsOption,
@@ -163,31 +173,36 @@
163
173
  (!isEmpty ? `${title}. Candlestick chart with ${data.dates.length} data points.` : undefined)
164
174
  );
165
175
 
176
+ const dataTable = $derived(isEmpty ? undefined : candlestickTable(data));
177
+
166
178
  // Manage ECharts instance lifecycle (init, theme, resize, option updates)
167
179
  const chart = useChart({
168
180
  option: () => option,
169
181
  theme: () => theme,
170
182
  animation: () => animation,
171
183
  isEmpty: () => isEmpty,
172
- onReady: (instance) => {
173
- if (onClick) {
174
- instance.on('click', (params) => {
175
- const dataIndex = params.dataIndex || 0;
176
- const item = data.data[dataIndex];
177
- if (!item) return;
178
- onClick({
179
- type: params.type || 'click',
180
- componentType: params.componentType || 'series',
181
- date: data.dates[dataIndex],
182
- open: item.open,
183
- close: item.close,
184
- high: item.high,
185
- low: item.low,
186
- volume: data.volume?.[dataIndex],
187
- } as CandlestickEventParams);
188
- });
189
- }
190
- },
184
+ patterns: () => patterns,
185
+ events: () =>
186
+ chartEvents(events, {
187
+ click:
188
+ onClick &&
189
+ ((params) => {
190
+ const dataIndex = params.dataIndex || 0;
191
+ const item = data.data[dataIndex];
192
+ if (!item) return;
193
+ onClick({
194
+ type: params.type || 'click',
195
+ componentType: params.componentType || 'series',
196
+ date: data.dates[dataIndex],
197
+ open: item.open,
198
+ close: item.close,
199
+ high: item.high,
200
+ low: item.low,
201
+ volume: data.volume?.[dataIndex],
202
+ } as CandlestickEventParams);
203
+ }),
204
+ }),
205
+ onCreated: () => onReady,
191
206
  });
192
207
  </script>
193
208
 
@@ -195,11 +210,19 @@
195
210
  {title}
196
211
  description={accessibilityDescription}
197
212
  {height}
213
+ {minHeight}
214
+ {maxHeight}
198
215
  {loading}
199
216
  {error}
217
+ {onRetry}
200
218
  empty={isEmpty}
201
219
  {emptyMessage}
202
220
  class={cn(className)}
221
+ {dataTable}
222
+ {showDataTable}
223
+ format={valueFormatter}
224
+ {exportable}
225
+ exportImage={chart.exportImage}
203
226
  >
204
227
  <div bind:this={chart.container} class="h-full w-full"></div>
205
228
  </ChartContainer>
@@ -1,11 +1,10 @@
1
1
  <script lang="ts">
2
2
  import * as echarts from 'echarts/core';
3
3
  import { FunnelChart as EChartsFunnelChart } 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 { FunnelChartProps, SliceEventParams } from '@classic-homes/charts-core';
7
+ import type { FunnelChartProps } from '@classic-homes/charts-core';
9
8
  import {
10
9
  buildDataLabel,
11
10
  buildLegend,
@@ -16,9 +15,12 @@
16
15
  itemLabelLookup,
17
16
  mergeChartOption,
18
17
  pointLabel,
18
+ resolveValueFormatter,
19
+ sliceTable,
19
20
  } from '@classic-homes/charts-core';
20
21
 
21
22
  import { cn } from '../../utils.js';
23
+ import { chartEvents, toSliceParams } from '../../events.js';
22
24
  import { useChart } from '../../composables/useChart.svelte.js';
23
25
  import {
24
26
  useChartLegend,
@@ -26,13 +28,7 @@
26
28
  } from '../../composables/useChartLegend.svelte.js';
27
29
  import ChartContainer from '../base/ChartContainer.svelte';
28
30
 
29
- echarts.use([
30
- EChartsFunnelChart,
31
- TooltipComponent,
32
- LegendComponent,
33
- TitleComponent,
34
- CanvasRenderer,
35
- ]);
31
+ echarts.use([EChartsFunnelChart, TooltipComponent, LegendComponent]);
36
32
 
37
33
  interface Props extends Omit<FunnelChartProps, 'class'>, CustomLegendProps {
38
34
  class?: string;
@@ -43,8 +39,11 @@
43
39
  description,
44
40
  data,
45
41
  height = 400,
42
+ minHeight,
43
+ maxHeight,
46
44
  loading,
47
45
  error,
46
+ onRetry,
48
47
  emptyMessage,
49
48
  theme = 'auto',
50
49
  animation = true,
@@ -60,10 +59,22 @@
60
59
  renderLegend,
61
60
  onLegendChange,
62
61
  option: optionOverride,
62
+ valueFormat,
63
+ locale,
64
+ onReady,
65
+ events,
66
+ showDataTable,
67
+ exportable,
68
+ patterns,
63
69
  onClick,
64
70
  class: className,
65
71
  }: Props = $props();
66
72
 
73
+ // `valueFormat` writes values in tooltips and the data table
74
+ const valueFormatter = $derived(
75
+ tooltipValueFormatter ?? resolveValueFormatter(valueFormat, locale)
76
+ );
77
+
67
78
  const isEmpty = $derived(!data?.length);
68
79
 
69
80
  const option: EChartsOption = $derived.by(() => {
@@ -78,17 +89,17 @@
78
89
  trigger: 'item',
79
90
  // A string template ignores valueFormatter, so format the value here
80
91
  formatter:
81
- tooltipValueFormatter && !tooltipFormatter
92
+ valueFormatter && !tooltipFormatter
82
93
  ? (params: unknown) => {
83
94
  const p = params as { seriesName?: string; name?: string; value?: unknown };
84
- return `${escapeHtml(p.seriesName)} <br/>${escapeHtml(p.name)}: ${escapeHtml(formatTooltipValue(p.value, tooltipValueFormatter))}`;
95
+ return `${escapeHtml(p.seriesName)} <br/>${escapeHtml(p.name)}: ${escapeHtml(formatTooltipValue(p.value, valueFormatter))}`;
85
96
  }
86
97
  : '{a} <br/>{b}: {c}',
87
98
  },
88
99
  {
89
100
  show: showTooltip,
90
101
  formatter: tooltipFormatter,
91
- valueFormatter: tooltipValueFormatter,
102
+ valueFormatter,
92
103
  getLabel,
93
104
  }
94
105
  ),
@@ -147,26 +158,21 @@
147
158
  onLegendChange: () => onLegendChange,
148
159
  });
149
160
 
161
+ const dataTable = $derived(isEmpty ? undefined : sliceTable(data, 'Stage'));
162
+
150
163
  // Manage ECharts instance lifecycle (init, theme, resize, option updates)
151
164
  const chart = useChart({
152
165
  option: () => option,
153
166
  theme: () => theme,
154
167
  animation: () => animation,
155
168
  isEmpty: () => isEmpty,
156
- onReady: (instance) => {
157
- legend.attach(instance);
158
- if (onClick) {
159
- instance.on('click', (params) => {
160
- onClick({
161
- type: params.type || 'click',
162
- componentType: params.componentType || 'series',
163
- name: params.name || '',
164
- value: params.value as number,
165
- color: params.color as string,
166
- } as SliceEventParams);
167
- });
168
- }
169
- },
169
+ patterns: () => patterns,
170
+ events: () =>
171
+ chartEvents(events, {
172
+ click: onClick && ((params) => onClick(toSliceParams(params, 'click'))),
173
+ }),
174
+ onReady: (instance) => legend.attach(instance),
175
+ onCreated: () => onReady,
170
176
  });
171
177
  </script>
172
178
 
@@ -178,13 +184,21 @@
178
184
  {title}
179
185
  description={accessibilityDescription}
180
186
  {height}
187
+ {minHeight}
188
+ {maxHeight}
181
189
  {loading}
182
190
  {error}
191
+ {onRetry}
183
192
  empty={isEmpty}
184
193
  {emptyMessage}
185
194
  class={cn(className)}
186
195
  legend={renderLegend ? customLegend : undefined}
187
196
  legendPosition={legendConfig?.position}
197
+ {dataTable}
198
+ {showDataTable}
199
+ format={valueFormatter}
200
+ {exportable}
201
+ exportImage={chart.exportImage}
188
202
  >
189
203
  <div bind:this={chart.container} class="h-full w-full"></div>
190
204
  </ChartContainer>