@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.
- package/dist/lib/components/base/ChartContainer.svelte +141 -14
- package/dist/lib/components/base/ChartContainer.svelte.d.ts +19 -1
- package/dist/lib/components/base/ChartDataTable.svelte +59 -0
- package/dist/lib/components/base/ChartDataTable.svelte.d.ts +15 -0
- package/dist/lib/components/base/ChartEmpty.svelte +1 -0
- package/dist/lib/components/base/ChartSkeleton.svelte +7 -2
- package/dist/lib/components/base/ChartSkeleton.svelte.d.ts +2 -0
- package/dist/lib/components/base/index.d.ts +3 -0
- package/dist/lib/components/base/index.js +1 -0
- package/dist/lib/components/core/AreaChart.svelte +115 -70
- package/dist/lib/components/core/BarChart.svelte +134 -62
- package/dist/lib/components/core/DonutChart.svelte +41 -22
- package/dist/lib/components/core/LineChart.svelte +104 -73
- package/dist/lib/components/core/PieChart.svelte +41 -22
- package/dist/lib/components/core/ScatterChart.svelte +72 -68
- package/dist/lib/components/extended/CandlestickChart.svelte +67 -44
- package/dist/lib/components/extended/FunnelChart.svelte +41 -27
- package/dist/lib/components/extended/GaugeChart.svelte +61 -10
- package/dist/lib/components/extended/HeatmapChart.svelte +62 -33
- package/dist/lib/components/extended/RadarChart.svelte +39 -19
- package/dist/lib/components/extended/SankeyChart.svelte +50 -20
- package/dist/lib/components/extended/TreemapChart.svelte +53 -21
- package/dist/lib/components/index.d.ts +2 -1
- package/dist/lib/components/index.js +3 -1
- package/dist/lib/components/report/BoxPlotChart.svelte +253 -0
- package/dist/lib/components/report/BoxPlotChart.svelte.d.ts +7 -0
- package/dist/lib/components/report/BulletChart.svelte +231 -0
- package/dist/lib/components/report/BulletChart.svelte.d.ts +7 -0
- package/dist/lib/components/report/ComboChart.svelte +327 -0
- package/dist/lib/components/report/ComboChart.svelte.d.ts +8 -0
- package/dist/lib/components/report/DotPlotChart.svelte +325 -0
- package/dist/lib/components/report/DotPlotChart.svelte.d.ts +8 -0
- package/dist/lib/components/report/HistogramChart.svelte +226 -0
- package/dist/lib/components/report/HistogramChart.svelte.d.ts +7 -0
- package/dist/lib/components/report/Sparkline.svelte +181 -0
- package/dist/lib/components/report/Sparkline.svelte.d.ts +4 -0
- package/dist/lib/components/report/StatCard.svelte +146 -0
- package/dist/lib/components/report/StatCard.svelte.d.ts +4 -0
- package/dist/lib/components/report/StatGroup.svelte +35 -0
- package/dist/lib/components/report/StatGroup.svelte.d.ts +12 -0
- package/dist/lib/components/report/WaterfallChart.svelte +271 -0
- package/dist/lib/components/report/WaterfallChart.svelte.d.ts +7 -0
- package/dist/lib/components/report/index.d.ts +12 -0
- package/dist/lib/components/report/index.js +9 -0
- package/dist/lib/composables/index.d.ts +1 -1
- package/dist/lib/composables/index.js +1 -1
- package/dist/lib/composables/useChart.svelte.d.ts +34 -0
- package/dist/lib/composables/useChart.svelte.js +121 -9
- package/dist/lib/composables/useChartTheme.svelte.js +14 -11
- package/dist/lib/events.d.ts +15 -0
- package/dist/lib/events.js +56 -0
- package/dist/lib/index.d.ts +4 -3
- package/dist/lib/index.js +7 -3
- package/package.json +10 -4
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import * as echarts from 'echarts/core';
|
|
3
3
|
import { TreemapChart as EChartsTreemapChart } from 'echarts/charts';
|
|
4
|
-
import { TooltipComponent
|
|
5
|
-
import { CanvasRenderer } from 'echarts/renderers';
|
|
4
|
+
import { TooltipComponent } from 'echarts/components';
|
|
6
5
|
import type { EChartsOption } from 'echarts';
|
|
7
6
|
|
|
8
7
|
import type {
|
|
@@ -19,13 +18,16 @@
|
|
|
19
18
|
mergeChartOption,
|
|
20
19
|
nameLabelLookup,
|
|
21
20
|
pointLabel,
|
|
21
|
+
resolveValueFormatter,
|
|
22
|
+
treemapTable,
|
|
22
23
|
} from '@classic-homes/charts-core';
|
|
23
24
|
|
|
24
25
|
import { cn } from '../../utils.js';
|
|
26
|
+
import { chartEvents } from '../../events.js';
|
|
25
27
|
import { useChart } from '../../composables/useChart.svelte.js';
|
|
26
28
|
import ChartContainer from '../base/ChartContainer.svelte';
|
|
27
29
|
|
|
28
|
-
echarts.use([EChartsTreemapChart, TooltipComponent
|
|
30
|
+
echarts.use([EChartsTreemapChart, TooltipComponent]);
|
|
29
31
|
|
|
30
32
|
interface Props extends Omit<TreemapChartProps, 'class'> {
|
|
31
33
|
class?: string;
|
|
@@ -37,8 +39,11 @@
|
|
|
37
39
|
data,
|
|
38
40
|
levels,
|
|
39
41
|
height = 400,
|
|
42
|
+
minHeight,
|
|
43
|
+
maxHeight,
|
|
40
44
|
loading,
|
|
41
45
|
error,
|
|
46
|
+
onRetry,
|
|
42
47
|
emptyMessage,
|
|
43
48
|
theme = 'auto',
|
|
44
49
|
animation = true,
|
|
@@ -47,10 +52,22 @@
|
|
|
47
52
|
tooltipFormatter,
|
|
48
53
|
tooltipValueFormatter,
|
|
49
54
|
option: optionOverride,
|
|
55
|
+
valueFormat,
|
|
56
|
+
locale,
|
|
57
|
+
onReady,
|
|
58
|
+
events,
|
|
59
|
+
showDataTable,
|
|
60
|
+
exportable,
|
|
61
|
+
patterns,
|
|
50
62
|
onClick,
|
|
51
63
|
class: className,
|
|
52
64
|
}: Props = $props();
|
|
53
65
|
|
|
66
|
+
// `valueFormat` writes values in tooltips and the data table
|
|
67
|
+
const valueFormatter = $derived(
|
|
68
|
+
tooltipValueFormatter ?? resolveValueFormatter(valueFormat, locale)
|
|
69
|
+
);
|
|
70
|
+
|
|
54
71
|
const isEmpty = $derived(!data?.length);
|
|
55
72
|
|
|
56
73
|
// Per-element colours that follow the resolved chart theme
|
|
@@ -61,8 +78,10 @@
|
|
|
61
78
|
|
|
62
79
|
const getLabel = nameLabelLookup(data);
|
|
63
80
|
// ECharts expects a label object, so swap each node's label text for one
|
|
64
|
-
|
|
81
|
+
// and a colour for a node fill (a node's own `color` is a palette for its children)
|
|
82
|
+
const toNode = ({ color, ...n }: TreemapNode): Record<string, unknown> => ({
|
|
65
83
|
...n,
|
|
84
|
+
...(color && { itemStyle: { color } }),
|
|
66
85
|
children: n.children?.map(toNode),
|
|
67
86
|
label: pointLabel(n.label, labelFormatter),
|
|
68
87
|
});
|
|
@@ -73,13 +92,13 @@
|
|
|
73
92
|
{
|
|
74
93
|
formatter: (params: unknown) => {
|
|
75
94
|
const p = params as { name?: string; value?: number };
|
|
76
|
-
return `${escapeHtml(p.name)}: ${escapeHtml(formatTooltipValue(p.value,
|
|
95
|
+
return `${escapeHtml(p.name)}: ${escapeHtml(formatTooltipValue(p.value, valueFormatter))}`;
|
|
77
96
|
},
|
|
78
97
|
},
|
|
79
98
|
{
|
|
80
99
|
show: showTooltip,
|
|
81
100
|
formatter: tooltipFormatter,
|
|
82
|
-
valueFormatter
|
|
101
|
+
valueFormatter,
|
|
83
102
|
getLabel,
|
|
84
103
|
}
|
|
85
104
|
),
|
|
@@ -118,27 +137,32 @@
|
|
|
118
137
|
description || (!isEmpty ? `${title}. Treemap chart showing hierarchical data.` : undefined)
|
|
119
138
|
);
|
|
120
139
|
|
|
140
|
+
const dataTable = $derived(isEmpty ? undefined : treemapTable(data));
|
|
141
|
+
|
|
121
142
|
// Manage ECharts instance lifecycle (init, theme, resize, option updates)
|
|
122
143
|
const chart = useChart({
|
|
123
144
|
option: () => option,
|
|
124
145
|
theme: () => theme,
|
|
125
146
|
animation: () => animation,
|
|
126
147
|
isEmpty: () => isEmpty,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
onClick
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
patterns: () => patterns,
|
|
149
|
+
events: () =>
|
|
150
|
+
chartEvents(events, {
|
|
151
|
+
click:
|
|
152
|
+
onClick &&
|
|
153
|
+
((params) => {
|
|
154
|
+
const treePathInfo = params.treePathInfo || [];
|
|
155
|
+
onClick({
|
|
156
|
+
type: params.type || 'click',
|
|
157
|
+
componentType: params.componentType || 'series',
|
|
158
|
+
name: params.name || '',
|
|
159
|
+
value: params.value as number,
|
|
160
|
+
path: treePathInfo.map((item: { name?: string }) => item.name || ''),
|
|
161
|
+
depth: treePathInfo.length - 1,
|
|
162
|
+
} as TreemapEventParams);
|
|
163
|
+
}),
|
|
164
|
+
}),
|
|
165
|
+
onCreated: () => onReady,
|
|
142
166
|
});
|
|
143
167
|
</script>
|
|
144
168
|
|
|
@@ -146,11 +170,19 @@
|
|
|
146
170
|
{title}
|
|
147
171
|
description={accessibilityDescription}
|
|
148
172
|
{height}
|
|
173
|
+
{minHeight}
|
|
174
|
+
{maxHeight}
|
|
149
175
|
{loading}
|
|
150
176
|
{error}
|
|
177
|
+
{onRetry}
|
|
151
178
|
empty={isEmpty}
|
|
152
179
|
{emptyMessage}
|
|
153
180
|
class={cn(className)}
|
|
181
|
+
{dataTable}
|
|
182
|
+
{showDataTable}
|
|
183
|
+
format={valueFormatter}
|
|
184
|
+
{exportable}
|
|
185
|
+
exportImage={chart.exportImage}
|
|
154
186
|
>
|
|
155
187
|
<div bind:this={chart.container} class="h-full w-full"></div>
|
|
156
188
|
</ChartContainer>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { ChartContainer, ChartSkeleton, ChartError, ChartEmpty, ChartLegend, type ChartContainerProps, type ChartLegendProps, type ChartSkeletonProps, type ChartErrorProps, type ChartEmptyProps, } from './base/index.js';
|
|
1
|
+
export { ChartContainer, ChartSkeleton, ChartError, ChartEmpty, ChartLegend, type ChartContainerProps, type ChartLegendProps, type ChartSkeletonProps, type ChartErrorProps, type ChartEmptyProps, ChartDataTable, type ChartDataTableProps, } from './base/index.js';
|
|
2
2
|
export { LineChart, BarChart, PieChart, AreaChart, ScatterChart, DonutChart, } from './core/index.js';
|
|
3
3
|
export { RadarChart, FunnelChart, GaugeChart, HeatmapChart, TreemapChart, CandlestickChart, SankeyChart, } from './extended/index.js';
|
|
4
|
+
export { ComboChart, DotPlotChart, BulletChart, HistogramChart, BoxPlotChart, WaterfallChart, Sparkline, StatCard, StatGroup, type StatGroupProps, } from './report/index.js';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Base components
|
|
2
|
-
export { ChartContainer, ChartSkeleton, ChartError, ChartEmpty, ChartLegend, } from './base/index.js';
|
|
2
|
+
export { ChartContainer, ChartSkeleton, ChartError, ChartEmpty, ChartLegend, ChartDataTable, } from './base/index.js';
|
|
3
3
|
// Core charts
|
|
4
4
|
export { LineChart, BarChart, PieChart, AreaChart, ScatterChart, DonutChart, } from './core/index.js';
|
|
5
5
|
// Extended charts
|
|
6
6
|
export { RadarChart, FunnelChart, GaugeChart, HeatmapChart, TreemapChart, CandlestickChart, SankeyChart, } from './extended/index.js';
|
|
7
|
+
// Report charts and stats
|
|
8
|
+
export { ComboChart, DotPlotChart, BulletChart, HistogramChart, BoxPlotChart, WaterfallChart, Sparkline, StatCard, StatGroup, } from './report/index.js';
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as echarts from 'echarts/core';
|
|
3
|
+
import {
|
|
4
|
+
BoxplotChart as EChartsBoxplotChart,
|
|
5
|
+
ScatterChart as EChartsScatterChart,
|
|
6
|
+
} from 'echarts/charts';
|
|
7
|
+
import { GridComponent, TooltipComponent } from 'echarts/components';
|
|
8
|
+
import type { EChartsOption } from 'echarts';
|
|
9
|
+
|
|
10
|
+
import type { BoxPlotChartProps } from '@classic-homes/charts-core';
|
|
11
|
+
import {
|
|
12
|
+
boxPlotTable,
|
|
13
|
+
boxplotStats,
|
|
14
|
+
buildAxis,
|
|
15
|
+
buildTooltip,
|
|
16
|
+
chartFormatters,
|
|
17
|
+
colorWithAlpha,
|
|
18
|
+
escapeHtml,
|
|
19
|
+
formatValue,
|
|
20
|
+
generateBoxPlotDescription,
|
|
21
|
+
getChartColors,
|
|
22
|
+
gridInset,
|
|
23
|
+
mergeChartOption,
|
|
24
|
+
quantile,
|
|
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 ChartContainer from '../base/ChartContainer.svelte';
|
|
32
|
+
|
|
33
|
+
// Register required ECharts modules
|
|
34
|
+
echarts.use([EChartsBoxplotChart, EChartsScatterChart, GridComponent, TooltipComponent]);
|
|
35
|
+
|
|
36
|
+
interface Props extends Omit<BoxPlotChartProps, 'class'> {
|
|
37
|
+
class?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let {
|
|
41
|
+
title,
|
|
42
|
+
description,
|
|
43
|
+
data,
|
|
44
|
+
orientation = 'vertical',
|
|
45
|
+
showOutliers = true,
|
|
46
|
+
color,
|
|
47
|
+
height = 400,
|
|
48
|
+
minHeight,
|
|
49
|
+
maxHeight,
|
|
50
|
+
loading,
|
|
51
|
+
error,
|
|
52
|
+
onRetry,
|
|
53
|
+
emptyMessage,
|
|
54
|
+
theme = 'auto',
|
|
55
|
+
animation = true,
|
|
56
|
+
showTooltip = true,
|
|
57
|
+
showGrid = true,
|
|
58
|
+
valueFormat,
|
|
59
|
+
locale,
|
|
60
|
+
xAxisConfig,
|
|
61
|
+
yAxisConfig,
|
|
62
|
+
referenceLines,
|
|
63
|
+
referenceBands,
|
|
64
|
+
tooltipFormatter,
|
|
65
|
+
tooltipValueFormatter,
|
|
66
|
+
option: optionOverride,
|
|
67
|
+
onReady,
|
|
68
|
+
events,
|
|
69
|
+
showDataTable,
|
|
70
|
+
exportable,
|
|
71
|
+
patterns,
|
|
72
|
+
onClick,
|
|
73
|
+
class: className,
|
|
74
|
+
}: Props = $props();
|
|
75
|
+
|
|
76
|
+
const groups = $derived(
|
|
77
|
+
(data ?? []).map((group) => ({ name: group.name, stats: boxplotStats(group.values) }))
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const isEmpty = $derived(!groups.some((group) => group.stats));
|
|
81
|
+
|
|
82
|
+
// A statistic of every observation, for reference lines
|
|
83
|
+
const statistic = (kind: 'average' | 'median' | 'min' | 'max') => {
|
|
84
|
+
const all = (data ?? [])
|
|
85
|
+
.flatMap((group) => group.values)
|
|
86
|
+
.filter((value) => Number.isFinite(value))
|
|
87
|
+
.sort((a, b) => a - b);
|
|
88
|
+
if (!all.length) return 0;
|
|
89
|
+
if (kind === 'min') return all[0];
|
|
90
|
+
if (kind === 'max') return all[all.length - 1];
|
|
91
|
+
if (kind === 'median') return quantile(all, 0.5);
|
|
92
|
+
return all.reduce((sum, value) => sum + value, 0) / all.length;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const isHorizontal = $derived(orientation === 'horizontal');
|
|
96
|
+
|
|
97
|
+
const formatters = $derived(chartFormatters({ valueFormat, locale, tooltipValueFormatter }));
|
|
98
|
+
|
|
99
|
+
// Per-element colours that follow the resolved chart theme
|
|
100
|
+
const chartColors = $derived.by(() => getChartColors(chart.theme));
|
|
101
|
+
|
|
102
|
+
const option: EChartsOption = $derived.by(() => {
|
|
103
|
+
if (isEmpty) return {};
|
|
104
|
+
|
|
105
|
+
const base = color ?? chartColors.series[0];
|
|
106
|
+
const point = (index: number, value: number) =>
|
|
107
|
+
isHorizontal ? [value, index] : [index, value];
|
|
108
|
+
|
|
109
|
+
const series: Record<string, unknown>[] = [
|
|
110
|
+
{
|
|
111
|
+
name: title,
|
|
112
|
+
type: 'boxplot',
|
|
113
|
+
data: groups.map(({ stats }) =>
|
|
114
|
+
stats ? [stats.low, stats.q1, stats.median, stats.q3, stats.high] : []
|
|
115
|
+
),
|
|
116
|
+
itemStyle: { color: colorWithAlpha(base, 0.2), borderColor: base, borderWidth: 1.5 },
|
|
117
|
+
emphasis: { itemStyle: { borderWidth: 2.5 } },
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
if (showOutliers) {
|
|
122
|
+
series.push({
|
|
123
|
+
name: 'Outliers',
|
|
124
|
+
type: 'scatter',
|
|
125
|
+
symbolSize: 6,
|
|
126
|
+
itemStyle: { color: 'transparent', borderColor: base, borderWidth: 1.5 },
|
|
127
|
+
data: groups.flatMap(({ stats }, index) =>
|
|
128
|
+
(stats?.outliers ?? []).map((value) => point(index, value))
|
|
129
|
+
),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const categoryAxis = {
|
|
134
|
+
type: 'category' as const,
|
|
135
|
+
data: groups.map((group) => group.name),
|
|
136
|
+
// Horizontal boxes list the first group at the top
|
|
137
|
+
inverse: isHorizontal,
|
|
138
|
+
axisTick: { alignWithLabel: true },
|
|
139
|
+
};
|
|
140
|
+
const valueAxis = { type: 'value' as const, scale: true, splitLine: { show: showGrid } };
|
|
141
|
+
|
|
142
|
+
return mergeChartOption(
|
|
143
|
+
{
|
|
144
|
+
grid: {
|
|
145
|
+
left: '3%',
|
|
146
|
+
right: '4%',
|
|
147
|
+
bottom: '3%',
|
|
148
|
+
containLabel: true,
|
|
149
|
+
...gridInset(undefined, false, xAxisConfig, yAxisConfig),
|
|
150
|
+
},
|
|
151
|
+
tooltip: buildTooltip(
|
|
152
|
+
{
|
|
153
|
+
trigger: 'item',
|
|
154
|
+
formatter: (params: unknown) => {
|
|
155
|
+
const p = params as { seriesType?: string; dataIndex?: number; value?: number[] };
|
|
156
|
+
const fmt = (value: number) => escapeHtml(formatValue(value, formatters.tooltip));
|
|
157
|
+
if (p.seriesType === 'scatter') {
|
|
158
|
+
const [x, y] = p.value ?? [];
|
|
159
|
+
const index = isHorizontal ? y : x;
|
|
160
|
+
return `${escapeHtml(groups[index]?.name ?? '')}<br/>Outlier: <strong>${fmt(isHorizontal ? x : y)}</strong>`;
|
|
161
|
+
}
|
|
162
|
+
const group = groups[p.dataIndex ?? 0];
|
|
163
|
+
if (!group?.stats) return '';
|
|
164
|
+
const { stats } = group;
|
|
165
|
+
return [
|
|
166
|
+
`<strong>${escapeHtml(group.name)}</strong> (n = ${stats.count})`,
|
|
167
|
+
`High: ${fmt(stats.high)}`,
|
|
168
|
+
`Q3: ${fmt(stats.q3)}`,
|
|
169
|
+
`Median: <strong>${fmt(stats.median)}</strong>`,
|
|
170
|
+
`Q1: ${fmt(stats.q1)}`,
|
|
171
|
+
`Low: ${fmt(stats.low)}`,
|
|
172
|
+
].join('<br/>');
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{ show: showTooltip, formatter: tooltipFormatter }
|
|
176
|
+
),
|
|
177
|
+
xAxis: buildAxis(
|
|
178
|
+
isHorizontal ? valueAxis : categoryAxis,
|
|
179
|
+
isHorizontal ? formatters.axis(xAxisConfig) : xAxisConfig
|
|
180
|
+
),
|
|
181
|
+
yAxis: buildAxis(
|
|
182
|
+
isHorizontal ? categoryAxis : valueAxis,
|
|
183
|
+
isHorizontal ? yAxisConfig : formatters.axis(yAxisConfig)
|
|
184
|
+
),
|
|
185
|
+
series: withReferenceMarks(
|
|
186
|
+
series,
|
|
187
|
+
// ECharts would compute statistics from the box series' five-number
|
|
188
|
+
// rows, so resolve them from the raw observations instead
|
|
189
|
+
referenceLines?.map((line) =>
|
|
190
|
+
typeof line.value === 'number' ? line : { ...line, value: statistic(line.value) }
|
|
191
|
+
),
|
|
192
|
+
referenceBands,
|
|
193
|
+
{
|
|
194
|
+
valueAxis: isHorizontal ? 'x' : 'y',
|
|
195
|
+
inverse: isHorizontal,
|
|
196
|
+
format: formatters.format,
|
|
197
|
+
color: chartColors.reference,
|
|
198
|
+
bandColor: chartColors.referenceBand,
|
|
199
|
+
}
|
|
200
|
+
),
|
|
201
|
+
} as EChartsOption,
|
|
202
|
+
optionOverride
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const accessibilityDescription = $derived(
|
|
207
|
+
description ||
|
|
208
|
+
(!isEmpty ? generateBoxPlotDescription(title, groups, formatters.format) : undefined)
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const dataTable = $derived(isEmpty ? undefined : boxPlotTable(groups));
|
|
212
|
+
|
|
213
|
+
// Manage ECharts instance lifecycle (init, theme, resize, option updates)
|
|
214
|
+
const chart = useChart({
|
|
215
|
+
option: () => option,
|
|
216
|
+
theme: () => theme,
|
|
217
|
+
animation: () => animation,
|
|
218
|
+
isEmpty: () => isEmpty,
|
|
219
|
+
patterns: () => patterns,
|
|
220
|
+
events: () =>
|
|
221
|
+
chartEvents(events, {
|
|
222
|
+
click:
|
|
223
|
+
onClick &&
|
|
224
|
+
((params) => {
|
|
225
|
+
const mapped = toDataPointParams(params, 'click');
|
|
226
|
+
if ((params as { seriesType?: string }).seriesType !== 'boxplot') return;
|
|
227
|
+
onClick({ ...mapped, name: groups[mapped.dataIndex]?.name ?? mapped.name });
|
|
228
|
+
}),
|
|
229
|
+
}),
|
|
230
|
+
onCreated: () => onReady,
|
|
231
|
+
});
|
|
232
|
+
</script>
|
|
233
|
+
|
|
234
|
+
<ChartContainer
|
|
235
|
+
{title}
|
|
236
|
+
description={accessibilityDescription}
|
|
237
|
+
{height}
|
|
238
|
+
{minHeight}
|
|
239
|
+
{maxHeight}
|
|
240
|
+
{loading}
|
|
241
|
+
{error}
|
|
242
|
+
{onRetry}
|
|
243
|
+
empty={isEmpty}
|
|
244
|
+
{emptyMessage}
|
|
245
|
+
class={cn(className)}
|
|
246
|
+
{dataTable}
|
|
247
|
+
{showDataTable}
|
|
248
|
+
format={formatters.format}
|
|
249
|
+
{exportable}
|
|
250
|
+
exportImage={chart.exportImage}
|
|
251
|
+
>
|
|
252
|
+
<div bind:this={chart.container} class="h-full w-full"></div>
|
|
253
|
+
</ChartContainer>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BoxPlotChartProps } from '@classic-homes/charts-core';
|
|
2
|
+
interface Props extends Omit<BoxPlotChartProps, 'class'> {
|
|
3
|
+
class?: string;
|
|
4
|
+
}
|
|
5
|
+
declare const BoxPlotChart: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type BoxPlotChart = ReturnType<typeof BoxPlotChart>;
|
|
7
|
+
export default BoxPlotChart;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as echarts from 'echarts/core';
|
|
3
|
+
import { BarChart as EChartsBarChart, ScatterChart as EChartsScatterChart } from 'echarts/charts';
|
|
4
|
+
import { GridComponent, TooltipComponent } from 'echarts/components';
|
|
5
|
+
import type { EChartsOption } from 'echarts';
|
|
6
|
+
|
|
7
|
+
import type { BulletChartProps } from '@classic-homes/charts-core';
|
|
8
|
+
import {
|
|
9
|
+
buildAxis,
|
|
10
|
+
buildDataLabel,
|
|
11
|
+
buildTooltip,
|
|
12
|
+
bulletTable,
|
|
13
|
+
chartFormatters,
|
|
14
|
+
escapeHtml,
|
|
15
|
+
formatValue,
|
|
16
|
+
generateBulletDescription,
|
|
17
|
+
getChartColors,
|
|
18
|
+
mergeChartOption,
|
|
19
|
+
pointLabel,
|
|
20
|
+
resolveChartHeight,
|
|
21
|
+
} from '@classic-homes/charts-core';
|
|
22
|
+
|
|
23
|
+
import { cn } from '../../utils.js';
|
|
24
|
+
import { chartEvents, toDataPointParams } from '../../events.js';
|
|
25
|
+
import { useChart } from '../../composables/useChart.svelte.js';
|
|
26
|
+
import ChartContainer from '../base/ChartContainer.svelte';
|
|
27
|
+
|
|
28
|
+
// Register required ECharts modules
|
|
29
|
+
echarts.use([EChartsBarChart, EChartsScatterChart, GridComponent, TooltipComponent]);
|
|
30
|
+
|
|
31
|
+
interface Props extends Omit<BulletChartProps, 'class'> {
|
|
32
|
+
class?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let {
|
|
36
|
+
title,
|
|
37
|
+
description,
|
|
38
|
+
data,
|
|
39
|
+
height = 'auto',
|
|
40
|
+
minHeight,
|
|
41
|
+
maxHeight,
|
|
42
|
+
rowHeight = 36,
|
|
43
|
+
loading,
|
|
44
|
+
error,
|
|
45
|
+
onRetry,
|
|
46
|
+
emptyMessage,
|
|
47
|
+
theme = 'auto',
|
|
48
|
+
animation = true,
|
|
49
|
+
showTooltip = true,
|
|
50
|
+
showGrid = false,
|
|
51
|
+
showLabels = true,
|
|
52
|
+
valueFormat,
|
|
53
|
+
locale,
|
|
54
|
+
labelFormatter,
|
|
55
|
+
valueAxisConfig,
|
|
56
|
+
tooltipFormatter,
|
|
57
|
+
tooltipValueFormatter,
|
|
58
|
+
option: optionOverride,
|
|
59
|
+
onReady,
|
|
60
|
+
events,
|
|
61
|
+
showDataTable,
|
|
62
|
+
exportable,
|
|
63
|
+
patterns,
|
|
64
|
+
onClick,
|
|
65
|
+
class: className,
|
|
66
|
+
}: Props = $props();
|
|
67
|
+
|
|
68
|
+
const isEmpty = $derived(!data?.length);
|
|
69
|
+
|
|
70
|
+
const formatters = $derived(
|
|
71
|
+
chartFormatters({ valueFormat, locale, tooltipValueFormatter, labelFormatter })
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Per-element colours that follow the resolved chart theme
|
|
75
|
+
const chartColors = $derived.by(() => getChartColors(chart.theme));
|
|
76
|
+
|
|
77
|
+
const option: EChartsOption = $derived.by(() => {
|
|
78
|
+
if (isEmpty) return {};
|
|
79
|
+
|
|
80
|
+
const names = data.map((item) => item.name);
|
|
81
|
+
const rangeCount = Math.max(0, ...data.map((item) => item.ranges?.length ?? 0));
|
|
82
|
+
const shades = chartColors.ranges;
|
|
83
|
+
|
|
84
|
+
// Qualitative ranges, stacked as bands behind each row; widths are the
|
|
85
|
+
// gaps between consecutive boundaries
|
|
86
|
+
const rangeSeries = Array.from({ length: rangeCount }, (_, band) => ({
|
|
87
|
+
name: `Range ${band + 1}`,
|
|
88
|
+
type: 'bar',
|
|
89
|
+
stack: 'ranges',
|
|
90
|
+
silent: true,
|
|
91
|
+
barWidth: '70%',
|
|
92
|
+
yAxisIndex: 0,
|
|
93
|
+
itemStyle: { color: shades[Math.min(band, shades.length - 1)], borderRadius: 0 },
|
|
94
|
+
tooltip: { show: false },
|
|
95
|
+
data: data.map((item) => {
|
|
96
|
+
const ranges = item.ranges ?? [];
|
|
97
|
+
if (band >= ranges.length) return null;
|
|
98
|
+
return ranges[band] - (band > 0 ? ranges[band - 1] : 0);
|
|
99
|
+
}),
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
const measure = {
|
|
103
|
+
name: 'Value',
|
|
104
|
+
type: 'bar',
|
|
105
|
+
// A second, hidden category axis, so the narrower measure bar centres
|
|
106
|
+
// on the range bands instead of sitting beside them
|
|
107
|
+
yAxisIndex: 1,
|
|
108
|
+
barWidth: '30%',
|
|
109
|
+
z: 3,
|
|
110
|
+
data: data.map((item, index) => ({
|
|
111
|
+
value: item.value,
|
|
112
|
+
name: item.name,
|
|
113
|
+
itemStyle: item.color ? { color: item.color } : undefined,
|
|
114
|
+
label: pointLabel(item.label, formatters.label, { position: 'right' }),
|
|
115
|
+
})),
|
|
116
|
+
itemStyle: { color: chartColors.foreground, borderRadius: 0 },
|
|
117
|
+
label: buildDataLabel({ show: true, position: 'right' }, undefined, {
|
|
118
|
+
show: showLabels,
|
|
119
|
+
formatter: formatters.label,
|
|
120
|
+
getLabel: (_series, index) => data[index]?.label,
|
|
121
|
+
}),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const targets = {
|
|
125
|
+
name: 'Target',
|
|
126
|
+
type: 'scatter',
|
|
127
|
+
yAxisIndex: 0,
|
|
128
|
+
symbol: 'rect',
|
|
129
|
+
symbolSize: [3, Math.round(rowHeight * 0.6)],
|
|
130
|
+
itemStyle: { color: chartColors.foreground },
|
|
131
|
+
z: 4,
|
|
132
|
+
data: data.flatMap((item, index) =>
|
|
133
|
+
typeof item.target === 'number' ? [{ value: [item.target, index], name: item.name }] : []
|
|
134
|
+
),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const categoryAxis = {
|
|
138
|
+
type: 'category' as const,
|
|
139
|
+
data: names,
|
|
140
|
+
// First row at the top
|
|
141
|
+
inverse: true,
|
|
142
|
+
axisTick: { show: false },
|
|
143
|
+
axisLine: { show: false },
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return mergeChartOption(
|
|
147
|
+
{
|
|
148
|
+
grid: { left: '3%', right: '8%', top: 8, bottom: '3%', containLabel: true },
|
|
149
|
+
tooltip: buildTooltip(
|
|
150
|
+
{
|
|
151
|
+
trigger: 'axis',
|
|
152
|
+
axisPointer: { type: 'none' },
|
|
153
|
+
formatter: (params: unknown) => {
|
|
154
|
+
const list = (Array.isArray(params) ? params : [params]) as { dataIndex?: number }[];
|
|
155
|
+
const item = data[list[0]?.dataIndex ?? 0];
|
|
156
|
+
if (!item) return '';
|
|
157
|
+
const value = formatValue(item.value, formatters.tooltip);
|
|
158
|
+
const target =
|
|
159
|
+
typeof item.target === 'number'
|
|
160
|
+
? `<br/>Target: ${escapeHtml(formatValue(item.target, formatters.tooltip))}`
|
|
161
|
+
: '';
|
|
162
|
+
return `${escapeHtml(item.name)}<br/>Value: <strong>${escapeHtml(value)}</strong>${target}`;
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{ show: showTooltip, formatter: tooltipFormatter }
|
|
166
|
+
),
|
|
167
|
+
xAxis: buildAxis(
|
|
168
|
+
{ type: 'value', min: 0, splitLine: { show: showGrid } },
|
|
169
|
+
formatters.axis(valueAxisConfig)
|
|
170
|
+
),
|
|
171
|
+
yAxis: [categoryAxis, { ...categoryAxis, show: false }],
|
|
172
|
+
series: [...rangeSeries, measure, targets],
|
|
173
|
+
} as EChartsOption,
|
|
174
|
+
optionOverride
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const accessibilityDescription = $derived(
|
|
179
|
+
description ||
|
|
180
|
+
(!isEmpty ? generateBulletDescription(title, data, formatters.format) : undefined)
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const dataTable = $derived(isEmpty ? undefined : bulletTable(data));
|
|
184
|
+
|
|
185
|
+
const resolvedHeight = $derived(
|
|
186
|
+
resolveChartHeight(height, data?.length ?? 0, { rowHeight, minHeight, maxHeight, chrome: 48 })
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Manage ECharts instance lifecycle (init, theme, resize, option updates)
|
|
190
|
+
const chart = useChart({
|
|
191
|
+
option: () => option,
|
|
192
|
+
theme: () => theme,
|
|
193
|
+
animation: () => animation,
|
|
194
|
+
isEmpty: () => isEmpty,
|
|
195
|
+
patterns: () => patterns,
|
|
196
|
+
events: () =>
|
|
197
|
+
chartEvents(events, {
|
|
198
|
+
click:
|
|
199
|
+
onClick &&
|
|
200
|
+
((params) => {
|
|
201
|
+
// Rows are reported by their item, whichever mark was clicked
|
|
202
|
+
const mapped = toDataPointParams(params, 'click');
|
|
203
|
+
const index = data.findIndex((item) => item.name === mapped.name);
|
|
204
|
+
if (index === -1) return;
|
|
205
|
+
onClick({ ...mapped, dataIndex: index, value: data[index].value });
|
|
206
|
+
}),
|
|
207
|
+
}),
|
|
208
|
+
onCreated: () => onReady,
|
|
209
|
+
});
|
|
210
|
+
</script>
|
|
211
|
+
|
|
212
|
+
<ChartContainer
|
|
213
|
+
{title}
|
|
214
|
+
description={accessibilityDescription}
|
|
215
|
+
height={resolvedHeight}
|
|
216
|
+
{minHeight}
|
|
217
|
+
{maxHeight}
|
|
218
|
+
{loading}
|
|
219
|
+
{error}
|
|
220
|
+
{onRetry}
|
|
221
|
+
empty={isEmpty}
|
|
222
|
+
{emptyMessage}
|
|
223
|
+
class={cn(className)}
|
|
224
|
+
{dataTable}
|
|
225
|
+
{showDataTable}
|
|
226
|
+
format={formatters.format}
|
|
227
|
+
{exportable}
|
|
228
|
+
exportImage={chart.exportImage}
|
|
229
|
+
>
|
|
230
|
+
<div bind:this={chart.container} class="h-full w-full"></div>
|
|
231
|
+
</ChartContainer>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BulletChartProps } from '@classic-homes/charts-core';
|
|
2
|
+
interface Props extends Omit<BulletChartProps, 'class'> {
|
|
3
|
+
class?: string;
|
|
4
|
+
}
|
|
5
|
+
declare const BulletChart: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type BulletChart = ReturnType<typeof BulletChart>;
|
|
7
|
+
export default BulletChart;
|