@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
@@ -0,0 +1,327 @@
1
+ <script lang="ts">
2
+ import * as echarts from 'echarts/core';
3
+ import { BarChart as EChartsBarChart, LineChart as EChartsLineChart } from 'echarts/charts';
4
+ import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components';
5
+ import type { EChartsOption } from 'echarts';
6
+
7
+ import type { ComboChartProps } from '@classic-homes/charts-core';
8
+ import {
9
+ allSeriesEmpty,
10
+ buildAxis,
11
+ buildDataLabel,
12
+ buildDataZoom,
13
+ buildLegend,
14
+ buildTooltip,
15
+ categoryTable,
16
+ chartFormatters,
17
+ colorWithAlpha,
18
+ escapeHtml,
19
+ generateComboChartDescription,
20
+ getChartColors,
21
+ gridInset,
22
+ hasPointLabels,
23
+ mergeChartOption,
24
+ resolveValueFormatter,
25
+ seriesLabelLookup,
26
+ valueAxisConfig,
27
+ valueLabelFormatter,
28
+ withPointLabels,
29
+ withReferenceMarks,
30
+ withZoomInset,
31
+ } from '@classic-homes/charts-core';
32
+
33
+ import { cn } from '../../utils.js';
34
+ import { chartEvents, toDataPointParams } from '../../events.js';
35
+ import { useChart } from '../../composables/useChart.svelte.js';
36
+ import {
37
+ useChartLegend,
38
+ type CustomLegendProps,
39
+ } from '../../composables/useChartLegend.svelte.js';
40
+ import ChartContainer from '../base/ChartContainer.svelte';
41
+
42
+ // Register required ECharts modules
43
+ echarts.use([
44
+ EChartsBarChart,
45
+ EChartsLineChart,
46
+ GridComponent,
47
+ TooltipComponent,
48
+ LegendComponent,
49
+ ]);
50
+
51
+ interface Props extends Omit<ComboChartProps, 'class'>, CustomLegendProps {
52
+ class?: string;
53
+ }
54
+
55
+ let {
56
+ title,
57
+ description,
58
+ data,
59
+ height = 400,
60
+ minHeight,
61
+ maxHeight,
62
+ loading,
63
+ error,
64
+ onRetry,
65
+ emptyMessage,
66
+ theme = 'auto',
67
+ animation = true,
68
+ showLegend = true,
69
+ showTooltip = true,
70
+ showGrid = true,
71
+ stacked = false,
72
+ smooth = false,
73
+ showLabels = false,
74
+ connectNulls = false,
75
+ valueFormat,
76
+ secondaryValueFormat,
77
+ locale,
78
+ labelFormatter,
79
+ xAxisConfig,
80
+ yAxisConfig,
81
+ secondaryYAxisConfig,
82
+ referenceLines,
83
+ referenceBands,
84
+ dataZoom,
85
+ tooltipFormatter,
86
+ tooltipValueFormatter,
87
+ legend: legendConfig,
88
+ renderLegend,
89
+ onLegendChange,
90
+ option: optionOverride,
91
+ onReady,
92
+ events,
93
+ showDataTable,
94
+ exportable,
95
+ patterns,
96
+ onClick,
97
+ onHover,
98
+ class: className,
99
+ }: Props = $props();
100
+
101
+ const isEmpty = $derived(!data?.categories?.length || allSeriesEmpty(data?.series));
102
+
103
+ const formatters = $derived(
104
+ chartFormatters({ valueFormat, locale, tooltipValueFormatter, labelFormatter })
105
+ );
106
+ const secondaryFormat = $derived(
107
+ resolveValueFormatter(secondaryValueFormat, locale) ?? formatters.format
108
+ );
109
+ const secondaryTitled = $derived(
110
+ secondaryYAxisConfig?.name !== undefined &&
111
+ (secondaryYAxisConfig.nameLocation ?? 'middle') === 'middle'
112
+ );
113
+ const hasSecondary = $derived(data?.series?.some((s) => s.axis === 'secondary') ?? false);
114
+
115
+ // Per-element colours that follow the resolved chart theme
116
+ const chartColors = $derived.by(() => getChartColors(chart.theme));
117
+
118
+ const builtInLegend = $derived(showLegend && !renderLegend);
119
+
120
+ const option: EChartsOption = $derived.by(() => {
121
+ if (isEmpty) return {};
122
+
123
+ const getLabel = seriesLabelLookup(data.series);
124
+ const legendBottom = builtInLegend && (legendConfig?.position ?? 'bottom') === 'bottom';
125
+ const anyBars = data.series.some((s) => (s.type ?? 'bar') === 'bar');
126
+
127
+ const series = data.series.map((source, index) => {
128
+ const type = source.type ?? 'bar';
129
+ const secondary = source.axis === 'secondary';
130
+ // Each axis has its own format, so each series labels with its own
131
+ const label =
132
+ labelFormatter ??
133
+ (secondary && secondaryFormat ? valueLabelFormatter(secondaryFormat) : formatters.label);
134
+ const color = source.color || chartColors.series[index % chartColors.series.length];
135
+ const common = {
136
+ name: source.name,
137
+ yAxisIndex: secondary ? 1 : 0,
138
+ data: withPointLabels(source.data, source.labels, label, { position: 'top' }),
139
+ itemStyle: source.color ? { color: source.color } : undefined,
140
+ label: buildDataLabel({ show: true, position: 'top' }, undefined, {
141
+ show: showLabels,
142
+ formatter: label,
143
+ getLabel,
144
+ }),
145
+ emphasis: { focus: 'series' },
146
+ };
147
+ if (type === 'bar') {
148
+ return { ...common, type: 'bar', stack: source.stack ?? (stacked ? 'Total' : undefined) };
149
+ }
150
+ return {
151
+ ...common,
152
+ type: 'line',
153
+ smooth,
154
+ connectNulls,
155
+ showSymbol: true,
156
+ symbolSize: showLabels || hasPointLabels([source]) ? 6 : 5,
157
+ // Lines sit above bars
158
+ z: 3,
159
+ stack: source.stack,
160
+ areaStyle: type === 'area' ? { color: colorWithAlpha(color, 0.25) } : undefined,
161
+ };
162
+ });
163
+
164
+ // A per-series value formatter, so tooltips write each value in its axis' format
165
+ const tooltipValue = (value: number, seriesIndex: number) => {
166
+ if (tooltipValueFormatter) return tooltipValueFormatter(value);
167
+ const fmt =
168
+ data.series[seriesIndex]?.axis === 'secondary' ? secondaryFormat : formatters.format;
169
+ return fmt ? fmt(value) : String(value);
170
+ };
171
+
172
+ return mergeChartOption(
173
+ {
174
+ grid: {
175
+ left: '3%',
176
+ right: '4%',
177
+ bottom: '3%',
178
+ containLabel: true,
179
+ ...withZoomInset(
180
+ {
181
+ ...gridInset(legendConfig, builtInLegend, xAxisConfig, yAxisConfig),
182
+ // Room for a centred secondary axis title beside its tick labels
183
+ ...(secondaryTitled && { right: 36 }),
184
+ },
185
+ dataZoom,
186
+ 'x'
187
+ ),
188
+ },
189
+ tooltip: buildTooltip(
190
+ {
191
+ trigger: 'axis',
192
+ axisPointer: { type: anyBars ? 'shadow' : 'line' },
193
+ formatter: tooltipFormatter
194
+ ? undefined
195
+ : (params: unknown) => {
196
+ const list = (Array.isArray(params) ? params : [params]) as {
197
+ axisValueLabel?: string;
198
+ marker?: string;
199
+ seriesName?: string;
200
+ seriesIndex?: number;
201
+ value?: unknown;
202
+ }[];
203
+ const rows = list.map((p) => {
204
+ // Missing values show as a dash
205
+ const value =
206
+ typeof p.value === 'number' ? tooltipValue(p.value, p.seriesIndex ?? 0) : '-';
207
+ return `${p.marker ?? ''}${escapeHtml(p.seriesName ?? '')}<span style="float:right;margin-left:16px;font-weight:600">${escapeHtml(value)}</span>`;
208
+ });
209
+ return [escapeHtml(list[0]?.axisValueLabel ?? ''), ...rows].join('<br/>');
210
+ },
211
+ },
212
+ { show: showTooltip, formatter: tooltipFormatter, getLabel }
213
+ ),
214
+ legend: buildLegend(
215
+ data.series.map((s) => s.name),
216
+ legendConfig,
217
+ { show: showLegend, custom: !!renderLegend }
218
+ ),
219
+ dataZoom: buildDataZoom(dataZoom, 'x', legendBottom ? 36 : 0),
220
+ xAxis: buildAxis(
221
+ {
222
+ type: 'category',
223
+ data: data.categories,
224
+ axisTick: { alignWithLabel: true },
225
+ },
226
+ xAxisConfig
227
+ ),
228
+ yAxis: [
229
+ buildAxis({ type: 'value', splitLine: { show: showGrid } }, formatters.axis(yAxisConfig)),
230
+ ...(hasSecondary
231
+ ? [
232
+ buildAxis(
233
+ // The secondary axis' grid lines would not line up with the primary's
234
+ { type: 'value', position: 'right', splitLine: { show: false } },
235
+ valueAxisConfig(
236
+ secondaryTitled
237
+ ? // Clear wide tick labels such as "$500,000"
238
+ { nameGap: 64, ...secondaryYAxisConfig }
239
+ : secondaryYAxisConfig,
240
+ secondaryFormat
241
+ )
242
+ ),
243
+ ]
244
+ : []),
245
+ ],
246
+ series: withReferenceMarks(series, referenceLines, referenceBands, {
247
+ valueAxis: 'y',
248
+ format: formatters.format,
249
+ color: chartColors.reference,
250
+ bandColor: chartColors.referenceBand,
251
+ }),
252
+ } as EChartsOption,
253
+ optionOverride
254
+ );
255
+ });
256
+
257
+ const accessibilityDescription = $derived(
258
+ description ||
259
+ (!isEmpty ? generateComboChartDescription(title, data, formatters.format) : undefined)
260
+ );
261
+
262
+ // Secondary-axis columns are written as plain numbers when their format differs
263
+ const dataTable = $derived.by(() => {
264
+ if (isEmpty) return undefined;
265
+ const table = categoryTable(data);
266
+ if (!secondaryValueFormat) return table;
267
+ return {
268
+ ...table,
269
+ columns: table.columns?.map((column, index) =>
270
+ index > 0 && data.series[index - 1]?.axis === 'secondary' ? ('number' as const) : column
271
+ ),
272
+ };
273
+ });
274
+
275
+ // Legend selection state, shared with a custom legend
276
+ const legend = useChartLegend({
277
+ names: () => data?.series?.map((s) => s.name) ?? [],
278
+ colors: () => data?.series?.map((s) => s.color) ?? [],
279
+ palette: () => getChartColors(chart.theme).series,
280
+ config: () => legendConfig,
281
+ title: () => title,
282
+ onLegendChange: () => onLegendChange,
283
+ });
284
+
285
+ // Manage ECharts instance lifecycle (init, theme, resize, option updates)
286
+ const chart = useChart({
287
+ option: () => option,
288
+ theme: () => theme,
289
+ animation: () => animation,
290
+ isEmpty: () => isEmpty,
291
+ patterns: () => patterns,
292
+ events: () =>
293
+ chartEvents(events, {
294
+ click: onClick && ((params) => onClick(toDataPointParams(params, 'click'))),
295
+ mouseover: onHover && ((params) => onHover(toDataPointParams(params, 'mouseover'))),
296
+ }),
297
+ onReady: (instance) => legend.attach(instance),
298
+ onCreated: () => onReady,
299
+ });
300
+ </script>
301
+
302
+ {#snippet customLegend()}
303
+ {@render renderLegend?.(legend.renderProps)}
304
+ {/snippet}
305
+
306
+ <ChartContainer
307
+ {title}
308
+ description={accessibilityDescription}
309
+ {height}
310
+ {minHeight}
311
+ {maxHeight}
312
+ {loading}
313
+ {error}
314
+ {onRetry}
315
+ empty={isEmpty}
316
+ {emptyMessage}
317
+ class={cn(className)}
318
+ legend={renderLegend ? customLegend : undefined}
319
+ legendPosition={legendConfig?.position}
320
+ {dataTable}
321
+ {showDataTable}
322
+ format={formatters.format}
323
+ {exportable}
324
+ exportImage={chart.exportImage}
325
+ >
326
+ <div bind:this={chart.container} class="h-full w-full"></div>
327
+ </ChartContainer>
@@ -0,0 +1,8 @@
1
+ import type { ComboChartProps } from '@classic-homes/charts-core';
2
+ import { type CustomLegendProps } from '../../composables/useChartLegend.svelte.js';
3
+ interface Props extends Omit<ComboChartProps, 'class'>, CustomLegendProps {
4
+ class?: string;
5
+ }
6
+ declare const ComboChart: import("svelte").Component<Props, {}, "">;
7
+ type ComboChart = ReturnType<typeof ComboChart>;
8
+ export default ComboChart;
@@ -0,0 +1,325 @@
1
+ <script lang="ts">
2
+ import * as echarts from 'echarts/core';
3
+ import { ScatterChart as EChartsScatterChart, LinesChart } from 'echarts/charts';
4
+ import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components';
5
+ import type { EChartsOption } from 'echarts';
6
+
7
+ import type { DotPlotChartProps } from '@classic-homes/charts-core';
8
+ import {
9
+ allSeriesEmpty,
10
+ buildAxis,
11
+ buildDataLabel,
12
+ buildLegend,
13
+ buildTooltip,
14
+ categoryTable,
15
+ chartFormatters,
16
+ escapeHtml,
17
+ formatValue,
18
+ generateDotPlotDescription,
19
+ getChartColors,
20
+ gridInset,
21
+ mergeChartOption,
22
+ pointLabel,
23
+ resolveChartHeight,
24
+ seriesLabelLookup,
25
+ withReferenceMarks,
26
+ } from '@classic-homes/charts-core';
27
+
28
+ import { cn } from '../../utils.js';
29
+ import { chartEvents, toDataPointParams } from '../../events.js';
30
+ import { useChart } from '../../composables/useChart.svelte.js';
31
+ import {
32
+ useChartLegend,
33
+ type CustomLegendProps,
34
+ } from '../../composables/useChartLegend.svelte.js';
35
+ import ChartContainer from '../base/ChartContainer.svelte';
36
+
37
+ // Register required ECharts modules
38
+ echarts.use([EChartsScatterChart, LinesChart, GridComponent, TooltipComponent, LegendComponent]);
39
+
40
+ interface Props extends Omit<DotPlotChartProps, 'class'>, CustomLegendProps {
41
+ class?: string;
42
+ }
43
+
44
+ let {
45
+ title,
46
+ description,
47
+ data,
48
+ height = 'auto',
49
+ minHeight,
50
+ maxHeight,
51
+ rowHeight,
52
+ loading,
53
+ error,
54
+ onRetry,
55
+ emptyMessage,
56
+ theme = 'auto',
57
+ animation = true,
58
+ showLegend = true,
59
+ showTooltip = true,
60
+ showGrid = true,
61
+ dotSize = 12,
62
+ connect = false,
63
+ showLabels = false,
64
+ valueFormat,
65
+ locale,
66
+ labelFormatter,
67
+ valueAxisConfig,
68
+ categoryAxisConfig,
69
+ referenceLines,
70
+ referenceBands,
71
+ tooltipFormatter,
72
+ tooltipValueFormatter,
73
+ legend: legendConfig,
74
+ renderLegend,
75
+ onLegendChange,
76
+ option: optionOverride,
77
+ onReady,
78
+ events,
79
+ showDataTable,
80
+ exportable,
81
+ patterns,
82
+ onClick,
83
+ class: className,
84
+ }: Props = $props();
85
+
86
+ const isEmpty = $derived(!data?.categories?.length || allSeriesEmpty(data?.series));
87
+
88
+ const formatters = $derived(
89
+ chartFormatters({ valueFormat, locale, tooltipValueFormatter, labelFormatter })
90
+ );
91
+
92
+ // Points are [value, row index], so labels format the first element
93
+ const dotLabel = $derived<DotPlotChartProps['labelFormatter']>(
94
+ labelFormatter ??
95
+ ((params) =>
96
+ params.label ??
97
+ formatValue(
98
+ Array.isArray(params.value) ? params.value[0] : params.value,
99
+ formatters.format
100
+ ))
101
+ );
102
+
103
+ // Per-element colours that follow the resolved chart theme
104
+ const chartColors = $derived.by(() => getChartColors(chart.theme));
105
+
106
+ const builtInLegend = $derived(showLegend && !renderLegend);
107
+
108
+ // Dot diameter for a weight: dot area grows with the weight
109
+ const sizeScale = $derived.by(() => {
110
+ const [smallest, largest] = Array.isArray(dotSize) ? dotSize : [dotSize, dotSize];
111
+ let maxWeight = 0;
112
+ for (const s of data?.series ?? []) {
113
+ for (const weight of s.sizes ?? []) {
114
+ if (typeof weight === 'number' && weight > maxWeight) maxWeight = weight;
115
+ }
116
+ }
117
+ return (weight: number | null | undefined) =>
118
+ typeof weight === 'number' && maxWeight > 0 && smallest !== largest
119
+ ? smallest + Math.sqrt(Math.max(weight, 0) / maxWeight) * (largest - smallest)
120
+ : largest;
121
+ });
122
+
123
+ const option: EChartsOption = $derived.by(() => {
124
+ if (isEmpty) return {};
125
+
126
+ const getLabel = seriesLabelLookup(data.series);
127
+
128
+ const series: Record<string, unknown>[] = data.series.map((source) => ({
129
+ name: source.name,
130
+ type: 'scatter',
131
+ // Missing values leave the row empty
132
+ data: source.data.flatMap((value, index) =>
133
+ value == null
134
+ ? []
135
+ : [
136
+ {
137
+ name: data.categories[index],
138
+ value: [value, index],
139
+ symbolSize: sizeScale(source.sizes?.[index]),
140
+ weight: source.sizes?.[index],
141
+ label: pointLabel(source.labels?.[index], dotLabel, { position: 'right' }),
142
+ },
143
+ ]
144
+ ),
145
+ itemStyle: source.color ? { color: source.color } : undefined,
146
+ label: buildDataLabel({ show: true, position: 'right', distance: 8 }, undefined, {
147
+ show: showLabels,
148
+ formatter: dotLabel,
149
+ getLabel: (seriesIndex, _dataIndex, name) =>
150
+ getLabel(seriesIndex, data.categories.indexOf(name), name),
151
+ }),
152
+ emphasis: { scale: 1.3 },
153
+ z: 3,
154
+ }));
155
+
156
+ // A line through each row's dots, for comparing series (a dumbbell plot)
157
+ if (connect && data.series.length > 1) {
158
+ const segments = data.categories.flatMap((_, index) => {
159
+ const values = data.series
160
+ .map((s) => s.data[index])
161
+ .filter((value): value is number => typeof value === 'number');
162
+ if (values.length < 2) return [];
163
+ return [
164
+ {
165
+ coords: [
166
+ [Math.min(...values), index],
167
+ [Math.max(...values), index],
168
+ ],
169
+ },
170
+ ];
171
+ });
172
+ series.push({
173
+ type: 'lines',
174
+ coordinateSystem: 'cartesian2d',
175
+ silent: true,
176
+ data: segments,
177
+ lineStyle: { color: chartColors.mutedForeground, width: 2, opacity: 0.5 },
178
+ z: 2,
179
+ });
180
+ }
181
+
182
+ return mergeChartOption(
183
+ {
184
+ grid: {
185
+ left: '3%',
186
+ right: '6%',
187
+ bottom: '3%',
188
+ // Reference line labels sit above the plot
189
+ top: referenceLines?.length ? 32 : 16,
190
+ containLabel: true,
191
+ ...gridInset(legendConfig, builtInLegend, valueAxisConfig, categoryAxisConfig),
192
+ },
193
+ tooltip: buildTooltip(
194
+ {
195
+ trigger: 'item',
196
+ formatter: (params: unknown) => {
197
+ const p = params as {
198
+ name?: string;
199
+ marker?: string;
200
+ seriesName?: string;
201
+ value?: [number, number];
202
+ data?: { weight?: number | null };
203
+ };
204
+ if (!p.value) return '';
205
+ const value = formatValue(p.value[0], formatters.tooltip);
206
+ const weight =
207
+ typeof p.data?.weight === 'number' ? ` (n = ${formatValue(p.data.weight)})` : '';
208
+ return `${escapeHtml(p.name ?? '')}<br/>${p.marker ?? ''}${escapeHtml(p.seriesName ?? '')}: <strong>${escapeHtml(value)}</strong>${escapeHtml(weight)}`;
209
+ },
210
+ },
211
+ { show: showTooltip, formatter: tooltipFormatter, getLabel }
212
+ ),
213
+ legend: buildLegend(
214
+ data.series.map((s) => s.name),
215
+ legendConfig,
216
+ { show: showLegend && data.series.length > 1, custom: !!renderLegend }
217
+ ),
218
+ xAxis: buildAxis(
219
+ {
220
+ type: 'value',
221
+ // A zero baseline would hide the differences a dot plot is for
222
+ scale: true,
223
+ splitLine: { show: showGrid },
224
+ },
225
+ formatters.axis(valueAxisConfig)
226
+ ),
227
+ yAxis: buildAxis(
228
+ {
229
+ type: 'category',
230
+ data: data.categories,
231
+ // First category at the top
232
+ inverse: true,
233
+ axisTick: { show: false },
234
+ axisLine: { show: false },
235
+ splitLine: { show: showGrid, lineStyle: { type: 'dotted' } },
236
+ },
237
+ categoryAxisConfig
238
+ ),
239
+ series: withReferenceMarks(series, referenceLines, referenceBands, {
240
+ valueAxis: 'x',
241
+ inverse: true,
242
+ format: formatters.format,
243
+ color: chartColors.reference,
244
+ bandColor: chartColors.referenceBand,
245
+ }),
246
+ } as EChartsOption,
247
+ optionOverride
248
+ );
249
+ });
250
+
251
+ const accessibilityDescription = $derived(
252
+ description ||
253
+ (!isEmpty ? generateDotPlotDescription(title, data, formatters.format) : undefined)
254
+ );
255
+
256
+ const dataTable = $derived(isEmpty ? undefined : categoryTable(data));
257
+
258
+ const resolvedHeight = $derived(
259
+ resolveChartHeight(height, data?.categories?.length ?? 0, {
260
+ rowHeight,
261
+ minHeight,
262
+ maxHeight,
263
+ chrome: 56 + (builtInLegend && (data?.series?.length ?? 0) > 1 ? 36 : 0),
264
+ })
265
+ );
266
+
267
+ // Legend selection state, shared with a custom legend
268
+ const legend = useChartLegend({
269
+ names: () => data?.series?.map((s) => s.name) ?? [],
270
+ colors: () => data?.series?.map((s) => s.color) ?? [],
271
+ palette: () => getChartColors(chart.theme).series,
272
+ config: () => legendConfig,
273
+ title: () => title,
274
+ onLegendChange: () => onLegendChange,
275
+ });
276
+
277
+ // Manage ECharts instance lifecycle (init, theme, resize, option updates)
278
+ const chart = useChart({
279
+ option: () => option,
280
+ theme: () => theme,
281
+ animation: () => animation,
282
+ isEmpty: () => isEmpty,
283
+ patterns: () => patterns,
284
+ events: () =>
285
+ chartEvents(events, {
286
+ click:
287
+ onClick &&
288
+ ((params) => {
289
+ const mapped = toDataPointParams(params, 'click');
290
+ // Report the category's index and the plain value
291
+ const value = mapped.value as unknown as [number, number];
292
+ onClick({ ...mapped, dataIndex: value[1], value: value[0] });
293
+ }),
294
+ }),
295
+ onReady: (instance) => legend.attach(instance),
296
+ onCreated: () => onReady,
297
+ });
298
+ </script>
299
+
300
+ {#snippet customLegend()}
301
+ {@render renderLegend?.(legend.renderProps)}
302
+ {/snippet}
303
+
304
+ <ChartContainer
305
+ {title}
306
+ description={accessibilityDescription}
307
+ height={resolvedHeight}
308
+ {minHeight}
309
+ {maxHeight}
310
+ {loading}
311
+ {error}
312
+ {onRetry}
313
+ empty={isEmpty}
314
+ {emptyMessage}
315
+ class={cn(className)}
316
+ legend={renderLegend ? customLegend : undefined}
317
+ legendPosition={legendConfig?.position}
318
+ {dataTable}
319
+ {showDataTable}
320
+ format={formatters.format}
321
+ {exportable}
322
+ exportImage={chart.exportImage}
323
+ >
324
+ <div bind:this={chart.container} class="h-full w-full"></div>
325
+ </ChartContainer>
@@ -0,0 +1,8 @@
1
+ import type { DotPlotChartProps } from '@classic-homes/charts-core';
2
+ import { type CustomLegendProps } from '../../composables/useChartLegend.svelte.js';
3
+ interface Props extends Omit<DotPlotChartProps, 'class'>, CustomLegendProps {
4
+ class?: string;
5
+ }
6
+ declare const DotPlotChart: import("svelte").Component<Props, {}, "">;
7
+ type DotPlotChart = ReturnType<typeof DotPlotChart>;
8
+ export default DotPlotChart;