@classic-homes/charts-svelte 0.1.38 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/components/base/ChartContainer.svelte +47 -8
- package/dist/lib/components/base/ChartContainer.svelte.d.ts +8 -0
- package/dist/lib/components/base/ChartLegend.svelte +59 -0
- package/dist/lib/components/base/ChartLegend.svelte.d.ts +10 -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 +121 -54
- package/dist/lib/components/core/AreaChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/BarChart.svelte +91 -35
- package/dist/lib/components/core/BarChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/DonutChart.svelte +152 -82
- package/dist/lib/components/core/DonutChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/LineChart.svelte +114 -47
- package/dist/lib/components/core/LineChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/PieChart.svelte +100 -38
- package/dist/lib/components/core/PieChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/ScatterChart.svelte +102 -34
- package/dist/lib/components/core/ScatterChart.svelte.d.ts +2 -1
- package/dist/lib/components/extended/CandlestickChart.svelte +44 -22
- package/dist/lib/components/extended/FunnelChart.svelte +102 -33
- package/dist/lib/components/extended/FunnelChart.svelte.d.ts +2 -1
- package/dist/lib/components/extended/GaugeChart.svelte +59 -38
- package/dist/lib/components/extended/HeatmapChart.svelte +47 -27
- package/dist/lib/components/extended/RadarChart.svelte +76 -22
- package/dist/lib/components/extended/RadarChart.svelte.d.ts +2 -1
- package/dist/lib/components/extended/SankeyChart.svelte +50 -21
- package/dist/lib/components/extended/TreemapChart.svelte +68 -27
- package/dist/lib/components/index.d.ts +1 -1
- package/dist/lib/components/index.js +1 -1
- package/dist/lib/composables/index.d.ts +1 -0
- package/dist/lib/composables/index.js +1 -0
- package/dist/lib/composables/useChartLegend.svelte.d.ts +37 -0
- package/dist/lib/composables/useChartLegend.svelte.js +54 -0
- package/dist/lib/index.d.ts +3 -3
- package/dist/lib/index.js +2 -2
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { ChartLegendPosition } from '@classic-homes/charts-core';
|
|
3
4
|
import { cn } from '../../utils.js';
|
|
4
5
|
import ChartSkeleton from './ChartSkeleton.svelte';
|
|
5
6
|
import ChartError from './ChartError.svelte';
|
|
@@ -16,6 +17,13 @@
|
|
|
16
17
|
onRetry?: () => void;
|
|
17
18
|
class?: string;
|
|
18
19
|
wrapperClass?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Custom legend, rendered beside the chart rather than inside the
|
|
22
|
+
* `role="img"` element so its controls stay accessible
|
|
23
|
+
*/
|
|
24
|
+
legend?: Snippet;
|
|
25
|
+
/** Where the custom legend sits (defaults to 'bottom') */
|
|
26
|
+
legendPosition?: ChartLegendPosition;
|
|
19
27
|
children: Snippet;
|
|
20
28
|
}
|
|
21
29
|
|
|
@@ -30,6 +38,8 @@
|
|
|
30
38
|
onRetry,
|
|
31
39
|
class: className,
|
|
32
40
|
wrapperClass,
|
|
41
|
+
legend,
|
|
42
|
+
legendPosition = 'bottom',
|
|
33
43
|
children,
|
|
34
44
|
}: Props = $props();
|
|
35
45
|
|
|
@@ -38,21 +48,19 @@
|
|
|
38
48
|
|
|
39
49
|
const heightStyle = $derived(typeof height === 'number' ? `${height}px` : height);
|
|
40
50
|
const descriptionId = $derived(description ? `${uid}-desc` : undefined);
|
|
51
|
+
|
|
52
|
+
// The legend's DOM order follows its visual position
|
|
53
|
+
const side = $derived(legendPosition === 'left' || legendPosition === 'right');
|
|
54
|
+
const leading = $derived(legendPosition === 'top' || legendPosition === 'left');
|
|
41
55
|
</script>
|
|
42
56
|
|
|
43
|
-
{#
|
|
44
|
-
<ChartSkeleton {height} class={wrapperClass} />
|
|
45
|
-
{:else if error}
|
|
46
|
-
<ChartError message={error} {height} {onRetry} class={wrapperClass} />
|
|
47
|
-
{:else if empty}
|
|
48
|
-
<ChartEmpty message={emptyMessage} {height} class={wrapperClass} />
|
|
49
|
-
{:else}
|
|
57
|
+
{#snippet chart(chartClass: string)}
|
|
50
58
|
<div
|
|
51
59
|
role="img"
|
|
52
60
|
aria-label={title}
|
|
53
61
|
aria-describedby={descriptionId}
|
|
54
62
|
data-testid="chart-container"
|
|
55
|
-
class={
|
|
63
|
+
class={chartClass}
|
|
56
64
|
>
|
|
57
65
|
{#if description}
|
|
58
66
|
<span id={descriptionId} class="sr-only">{description}</span>
|
|
@@ -61,4 +69,35 @@
|
|
|
61
69
|
{@render children()}
|
|
62
70
|
</div>
|
|
63
71
|
</div>
|
|
72
|
+
{/snippet}
|
|
73
|
+
|
|
74
|
+
{#snippet legendArea(content: Snippet)}
|
|
75
|
+
<div
|
|
76
|
+
data-testid="chart-legend"
|
|
77
|
+
class={cn('flex shrink-0 p-2', side ? 'items-center' : 'justify-center')}
|
|
78
|
+
>
|
|
79
|
+
{@render content()}
|
|
80
|
+
</div>
|
|
81
|
+
{/snippet}
|
|
82
|
+
|
|
83
|
+
{#if loading}
|
|
84
|
+
<ChartSkeleton {height} class={wrapperClass} />
|
|
85
|
+
{:else if error}
|
|
86
|
+
<ChartError message={error} {height} {onRetry} class={wrapperClass} />
|
|
87
|
+
{:else if empty}
|
|
88
|
+
<ChartEmpty message={emptyMessage} {height} class={wrapperClass} />
|
|
89
|
+
{:else if legend}
|
|
90
|
+
<div
|
|
91
|
+
class={cn(
|
|
92
|
+
'flex rounded-lg border border-border bg-card',
|
|
93
|
+
side ? 'flex-row' : 'flex-col',
|
|
94
|
+
wrapperClass
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
{#if leading}{@render legendArea(legend)}{/if}
|
|
98
|
+
{@render chart('min-h-0 min-w-0 flex-1')}
|
|
99
|
+
{#if !leading}{@render legendArea(legend)}{/if}
|
|
100
|
+
</div>
|
|
101
|
+
{:else}
|
|
102
|
+
{@render chart(cn('rounded-lg border border-border bg-card', wrapperClass))}
|
|
64
103
|
{/if}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { ChartLegendPosition } from '@classic-homes/charts-core';
|
|
2
3
|
interface Props {
|
|
3
4
|
title: string;
|
|
4
5
|
description?: string;
|
|
@@ -10,6 +11,13 @@ interface Props {
|
|
|
10
11
|
onRetry?: () => void;
|
|
11
12
|
class?: string;
|
|
12
13
|
wrapperClass?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Custom legend, rendered beside the chart rather than inside the
|
|
16
|
+
* `role="img"` element so its controls stay accessible
|
|
17
|
+
*/
|
|
18
|
+
legend?: Snippet;
|
|
19
|
+
/** Where the custom legend sits (defaults to 'bottom') */
|
|
20
|
+
legendPosition?: ChartLegendPosition;
|
|
13
21
|
children: Snippet;
|
|
14
22
|
}
|
|
15
23
|
declare const ChartContainer: import("svelte").Component<Props, {}, "">;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { LegendRenderProps } from '@classic-homes/charts-core';
|
|
3
|
+
import { cn } from '../../utils.js';
|
|
4
|
+
|
|
5
|
+
interface Props
|
|
6
|
+
extends
|
|
7
|
+
Pick<LegendRenderProps, 'items' | 'toggle'>,
|
|
8
|
+
Partial<Omit<LegendRenderProps, 'items' | 'toggle'>> {
|
|
9
|
+
/** Lay items out in a column (defaults to vertical for left/right positions) */
|
|
10
|
+
orientation?: 'horizontal' | 'vertical';
|
|
11
|
+
/** Additional classes for the list */
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
items,
|
|
17
|
+
toggle,
|
|
18
|
+
position = 'bottom',
|
|
19
|
+
chartTitle,
|
|
20
|
+
orientation,
|
|
21
|
+
class: className,
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
|
|
24
|
+
const vertical = $derived(
|
|
25
|
+
(orientation ?? (position === 'left' || position === 'right' ? 'vertical' : 'horizontal')) ===
|
|
26
|
+
'vertical'
|
|
27
|
+
);
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<!--
|
|
31
|
+
Accessible HTML legend for use with a chart's `renderLegend` snippet. Each
|
|
32
|
+
item is a toggle button that shows or hides its series.
|
|
33
|
+
-->
|
|
34
|
+
<ul
|
|
35
|
+
aria-label={chartTitle ? `${chartTitle} legend` : 'Chart legend'}
|
|
36
|
+
class={cn('flex gap-1 text-sm', vertical ? 'flex-col' : 'flex-wrap justify-center', className)}
|
|
37
|
+
>
|
|
38
|
+
{#each items as item (item.name)}
|
|
39
|
+
<li>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
aria-pressed={item.visible}
|
|
43
|
+
onclick={() => toggle(item.name)}
|
|
44
|
+
class={cn(
|
|
45
|
+
'inline-flex items-center gap-2 rounded-md px-2 py-1 text-left hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
46
|
+
item.visible ? 'text-foreground' : 'text-muted-foreground line-through'
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
<span
|
|
50
|
+
aria-hidden="true"
|
|
51
|
+
class="h-3 w-3 shrink-0 rounded-sm"
|
|
52
|
+
style:background-color={item.color}
|
|
53
|
+
style:opacity={item.visible ? 1 : 0.35}
|
|
54
|
+
></span>
|
|
55
|
+
{item.name}
|
|
56
|
+
</button>
|
|
57
|
+
</li>
|
|
58
|
+
{/each}
|
|
59
|
+
</ul>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LegendRenderProps } from '@classic-homes/charts-core';
|
|
2
|
+
interface Props extends Pick<LegendRenderProps, 'items' | 'toggle'>, Partial<Omit<LegendRenderProps, 'items' | 'toggle'>> {
|
|
3
|
+
/** Lay items out in a column (defaults to vertical for left/right positions) */
|
|
4
|
+
orientation?: 'horizontal' | 'vertical';
|
|
5
|
+
/** Additional classes for the list */
|
|
6
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const ChartLegend: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type ChartLegend = ReturnType<typeof ChartLegend>;
|
|
10
|
+
export default ChartLegend;
|
|
@@ -2,12 +2,15 @@ export { default as ChartContainer } from './ChartContainer.svelte';
|
|
|
2
2
|
export { default as ChartSkeleton } from './ChartSkeleton.svelte';
|
|
3
3
|
export { default as ChartError } from './ChartError.svelte';
|
|
4
4
|
export { default as ChartEmpty } from './ChartEmpty.svelte';
|
|
5
|
+
export { default as ChartLegend } from './ChartLegend.svelte';
|
|
5
6
|
import type { ComponentProps } from 'svelte';
|
|
6
7
|
import type ChartContainer from './ChartContainer.svelte';
|
|
7
8
|
import type ChartSkeleton from './ChartSkeleton.svelte';
|
|
8
9
|
import type ChartError from './ChartError.svelte';
|
|
9
10
|
import type ChartEmpty from './ChartEmpty.svelte';
|
|
11
|
+
import type ChartLegend from './ChartLegend.svelte';
|
|
10
12
|
export type ChartContainerProps = ComponentProps<typeof ChartContainer>;
|
|
11
13
|
export type ChartSkeletonProps = ComponentProps<typeof ChartSkeleton>;
|
|
12
14
|
export type ChartErrorProps = ComponentProps<typeof ChartError>;
|
|
13
15
|
export type ChartEmptyProps = ComponentProps<typeof ChartEmpty>;
|
|
16
|
+
export type ChartLegendProps = ComponentProps<typeof ChartLegend>;
|
|
@@ -2,3 +2,4 @@ export { default as ChartContainer } from './ChartContainer.svelte';
|
|
|
2
2
|
export { default as ChartSkeleton } from './ChartSkeleton.svelte';
|
|
3
3
|
export { default as ChartError } from './ChartError.svelte';
|
|
4
4
|
export { default as ChartEmpty } from './ChartEmpty.svelte';
|
|
5
|
+
export { default as ChartLegend } from './ChartLegend.svelte';
|
|
@@ -11,10 +11,26 @@
|
|
|
11
11
|
import type { EChartsOption } from 'echarts';
|
|
12
12
|
|
|
13
13
|
import type { AreaChartProps, DataPointEventParams } from '@classic-homes/charts-core';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
buildAxis,
|
|
16
|
+
buildDataLabel,
|
|
17
|
+
buildLegend,
|
|
18
|
+
buildTooltip,
|
|
19
|
+
colorWithAlpha,
|
|
20
|
+
getChartColors,
|
|
21
|
+
hasPointLabels,
|
|
22
|
+
gridInset,
|
|
23
|
+
mergeChartOption,
|
|
24
|
+
seriesLabelLookup,
|
|
25
|
+
withPointLabels,
|
|
26
|
+
} from '@classic-homes/charts-core';
|
|
15
27
|
|
|
16
28
|
import { cn } from '../../utils.js';
|
|
17
29
|
import { useChart } from '../../composables/useChart.svelte.js';
|
|
30
|
+
import {
|
|
31
|
+
useChartLegend,
|
|
32
|
+
type CustomLegendProps,
|
|
33
|
+
} from '../../composables/useChartLegend.svelte.js';
|
|
18
34
|
import ChartContainer from '../base/ChartContainer.svelte';
|
|
19
35
|
|
|
20
36
|
// Register required ECharts modules
|
|
@@ -27,7 +43,7 @@
|
|
|
27
43
|
CanvasRenderer,
|
|
28
44
|
]);
|
|
29
45
|
|
|
30
|
-
interface Props extends Omit<AreaChartProps, 'class'
|
|
46
|
+
interface Props extends Omit<AreaChartProps, 'class'>, CustomLegendProps {
|
|
31
47
|
class?: string;
|
|
32
48
|
}
|
|
33
49
|
|
|
@@ -47,6 +63,16 @@
|
|
|
47
63
|
smooth = false,
|
|
48
64
|
stacked = false,
|
|
49
65
|
gradient = true,
|
|
66
|
+
showLabels = false,
|
|
67
|
+
labelFormatter,
|
|
68
|
+
xAxisConfig,
|
|
69
|
+
yAxisConfig,
|
|
70
|
+
tooltipFormatter,
|
|
71
|
+
tooltipValueFormatter,
|
|
72
|
+
legend: legendConfig,
|
|
73
|
+
renderLegend,
|
|
74
|
+
onLegendChange,
|
|
75
|
+
option: optionOverride,
|
|
50
76
|
onClick,
|
|
51
77
|
class: className,
|
|
52
78
|
}: Props = $props();
|
|
@@ -61,15 +87,21 @@
|
|
|
61
87
|
const option: EChartsOption = $derived.by(() => {
|
|
62
88
|
if (isEmpty) return {};
|
|
63
89
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
const getLabel = seriesLabelLookup(data.series);
|
|
91
|
+
// ECharts hides line labels along with the symbol
|
|
92
|
+
const showSymbol = showLabels || hasPointLabels(data.series);
|
|
93
|
+
|
|
94
|
+
return mergeChartOption(
|
|
95
|
+
{
|
|
96
|
+
grid: {
|
|
97
|
+
left: '3%',
|
|
98
|
+
right: '4%',
|
|
99
|
+
bottom: '3%',
|
|
100
|
+
containLabel: true,
|
|
101
|
+
...gridInset(legendConfig, showLegend && !renderLegend, xAxisConfig, yAxisConfig),
|
|
102
|
+
},
|
|
103
|
+
tooltip: buildTooltip(
|
|
104
|
+
{
|
|
73
105
|
trigger: 'axis',
|
|
74
106
|
axisPointer: {
|
|
75
107
|
type: 'cross',
|
|
@@ -77,51 +109,69 @@
|
|
|
77
109
|
backgroundColor: chartColors.axisPointerLabel,
|
|
78
110
|
},
|
|
79
111
|
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
show: showTooltip,
|
|
115
|
+
formatter: tooltipFormatter,
|
|
116
|
+
valueFormatter: tooltipValueFormatter,
|
|
117
|
+
getLabel,
|
|
80
118
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
},
|
|
93
|
-
yAxis: {
|
|
94
|
-
type: 'value',
|
|
95
|
-
splitLine: {
|
|
96
|
-
show: showGrid,
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
series: data.series.map((series, index) => {
|
|
100
|
-
// Fade the series colour (theme palette unless overridden) downwards
|
|
101
|
-
const base = series.color || chartColors.series[index % chartColors.series.length];
|
|
102
|
-
return {
|
|
103
|
-
name: series.name,
|
|
104
|
-
type: 'line',
|
|
105
|
-
data: series.data,
|
|
106
|
-
smooth,
|
|
107
|
-
stack: stacked ? 'Total' : undefined,
|
|
108
|
-
areaStyle: gradient
|
|
109
|
-
? {
|
|
110
|
-
opacity: 0.8,
|
|
111
|
-
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
112
|
-
{ offset: 0, color: colorWithAlpha(base, 0.8) },
|
|
113
|
-
{ offset: 1, color: colorWithAlpha(base, 0.1) },
|
|
114
|
-
]),
|
|
115
|
-
}
|
|
116
|
-
: { opacity: 0.5 },
|
|
117
|
-
itemStyle: series.color ? { color: series.color } : undefined,
|
|
118
|
-
emphasis: {
|
|
119
|
-
focus: 'series',
|
|
119
|
+
),
|
|
120
|
+
legend: buildLegend(
|
|
121
|
+
data.series.map((s) => s.name),
|
|
122
|
+
legendConfig,
|
|
123
|
+
{ show: showLegend, custom: !!renderLegend }
|
|
124
|
+
),
|
|
125
|
+
xAxis: buildAxis(
|
|
126
|
+
{
|
|
127
|
+
type: 'category',
|
|
128
|
+
boundaryGap: false,
|
|
129
|
+
data: data.categories,
|
|
120
130
|
},
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
xAxisConfig
|
|
132
|
+
),
|
|
133
|
+
yAxis: buildAxis(
|
|
134
|
+
{
|
|
135
|
+
type: 'value',
|
|
136
|
+
splitLine: {
|
|
137
|
+
show: showGrid,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
yAxisConfig
|
|
141
|
+
),
|
|
142
|
+
series: data.series.map((series, index) => {
|
|
143
|
+
// Fade the series colour (theme palette unless overridden) downwards
|
|
144
|
+
const base = series.color || chartColors.series[index % chartColors.series.length];
|
|
145
|
+
return {
|
|
146
|
+
name: series.name,
|
|
147
|
+
type: 'line',
|
|
148
|
+
data: withPointLabels(series.data, series.labels, labelFormatter, { position: 'top' }),
|
|
149
|
+
smooth,
|
|
150
|
+
stack: stacked ? 'Total' : undefined,
|
|
151
|
+
areaStyle: gradient
|
|
152
|
+
? {
|
|
153
|
+
opacity: 0.8,
|
|
154
|
+
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
155
|
+
{ offset: 0, color: colorWithAlpha(base, 0.8) },
|
|
156
|
+
{ offset: 1, color: colorWithAlpha(base, 0.1) },
|
|
157
|
+
]),
|
|
158
|
+
}
|
|
159
|
+
: { opacity: 0.5 },
|
|
160
|
+
itemStyle: series.color ? { color: series.color } : undefined,
|
|
161
|
+
label: buildDataLabel({ show: true, position: 'top' }, undefined, {
|
|
162
|
+
show: showLabels,
|
|
163
|
+
formatter: labelFormatter,
|
|
164
|
+
getLabel,
|
|
165
|
+
}),
|
|
166
|
+
emphasis: {
|
|
167
|
+
focus: 'series',
|
|
168
|
+
},
|
|
169
|
+
showSymbol,
|
|
170
|
+
};
|
|
171
|
+
}),
|
|
172
|
+
} as EChartsOption,
|
|
173
|
+
optionOverride
|
|
174
|
+
);
|
|
125
175
|
});
|
|
126
176
|
|
|
127
177
|
const accessibilityDescription = $derived(
|
|
@@ -131,6 +181,16 @@
|
|
|
131
181
|
: undefined)
|
|
132
182
|
);
|
|
133
183
|
|
|
184
|
+
// Legend selection state, shared with a custom legend
|
|
185
|
+
const legend = useChartLegend({
|
|
186
|
+
names: () => data?.series?.map((s) => s.name) ?? [],
|
|
187
|
+
colors: () => data?.series?.map((s) => s.color) ?? [],
|
|
188
|
+
palette: () => getChartColors(chart.theme).series,
|
|
189
|
+
config: () => legendConfig,
|
|
190
|
+
title: () => title,
|
|
191
|
+
onLegendChange: () => onLegendChange,
|
|
192
|
+
});
|
|
193
|
+
|
|
134
194
|
// Manage ECharts instance lifecycle (init, theme, resize, option updates)
|
|
135
195
|
const chart = useChart({
|
|
136
196
|
option: () => option,
|
|
@@ -138,6 +198,7 @@
|
|
|
138
198
|
animation: () => animation,
|
|
139
199
|
isEmpty: () => isEmpty,
|
|
140
200
|
onReady: (instance) => {
|
|
201
|
+
legend.attach(instance);
|
|
141
202
|
if (onClick) {
|
|
142
203
|
instance.on('click', (params) => {
|
|
143
204
|
onClick({
|
|
@@ -156,6 +217,10 @@
|
|
|
156
217
|
});
|
|
157
218
|
</script>
|
|
158
219
|
|
|
220
|
+
{#snippet customLegend()}
|
|
221
|
+
{@render renderLegend?.(legend.renderProps)}
|
|
222
|
+
{/snippet}
|
|
223
|
+
|
|
159
224
|
<ChartContainer
|
|
160
225
|
{title}
|
|
161
226
|
description={accessibilityDescription}
|
|
@@ -165,6 +230,8 @@
|
|
|
165
230
|
empty={isEmpty}
|
|
166
231
|
{emptyMessage}
|
|
167
232
|
class={cn(className)}
|
|
233
|
+
legend={renderLegend ? customLegend : undefined}
|
|
234
|
+
legendPosition={legendConfig?.position}
|
|
168
235
|
>
|
|
169
236
|
<div bind:this={chart.container} class="h-full w-full"></div>
|
|
170
237
|
</ChartContainer>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AreaChartProps } from '@classic-homes/charts-core';
|
|
2
|
-
|
|
2
|
+
import { type CustomLegendProps } from '../../composables/useChartLegend.svelte.js';
|
|
3
|
+
interface Props extends Omit<AreaChartProps, 'class'>, CustomLegendProps {
|
|
3
4
|
class?: string;
|
|
4
5
|
}
|
|
5
6
|
declare const AreaChart: import("svelte").Component<Props, {}, "">;
|
|
@@ -11,10 +11,25 @@
|
|
|
11
11
|
import type { EChartsOption } from 'echarts';
|
|
12
12
|
|
|
13
13
|
import type { BarChartProps, DataPointEventParams } from '@classic-homes/charts-core';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
buildAxis,
|
|
16
|
+
buildDataLabel,
|
|
17
|
+
buildLegend,
|
|
18
|
+
buildTooltip,
|
|
19
|
+
generateBarChartDescription,
|
|
20
|
+
getChartColors,
|
|
21
|
+
gridInset,
|
|
22
|
+
mergeChartOption,
|
|
23
|
+
seriesLabelLookup,
|
|
24
|
+
withPointLabels,
|
|
25
|
+
} from '@classic-homes/charts-core';
|
|
15
26
|
|
|
16
27
|
import { cn } from '../../utils.js';
|
|
17
28
|
import { useChart } from '../../composables/useChart.svelte.js';
|
|
29
|
+
import {
|
|
30
|
+
useChartLegend,
|
|
31
|
+
type CustomLegendProps,
|
|
32
|
+
} from '../../composables/useChartLegend.svelte.js';
|
|
18
33
|
import ChartContainer from '../base/ChartContainer.svelte';
|
|
19
34
|
|
|
20
35
|
// Register required ECharts modules
|
|
@@ -27,7 +42,7 @@
|
|
|
27
42
|
CanvasRenderer,
|
|
28
43
|
]);
|
|
29
44
|
|
|
30
|
-
interface Props extends Omit<BarChartProps, 'class'
|
|
45
|
+
interface Props extends Omit<BarChartProps, 'class'>, CustomLegendProps {
|
|
31
46
|
class?: string;
|
|
32
47
|
}
|
|
33
48
|
|
|
@@ -47,6 +62,15 @@
|
|
|
47
62
|
orientation = 'vertical',
|
|
48
63
|
stacked = false,
|
|
49
64
|
showValues = false,
|
|
65
|
+
labelFormatter,
|
|
66
|
+
xAxisConfig,
|
|
67
|
+
yAxisConfig,
|
|
68
|
+
tooltipFormatter,
|
|
69
|
+
tooltipValueFormatter,
|
|
70
|
+
legend: legendConfig,
|
|
71
|
+
renderLegend,
|
|
72
|
+
onLegendChange,
|
|
73
|
+
option: optionOverride,
|
|
50
74
|
onClick,
|
|
51
75
|
class: className,
|
|
52
76
|
}: Props = $props();
|
|
@@ -75,52 +99,77 @@
|
|
|
75
99
|
},
|
|
76
100
|
};
|
|
77
101
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
102
|
+
const getLabel = seriesLabelLookup(data.series);
|
|
103
|
+
|
|
104
|
+
return mergeChartOption(
|
|
105
|
+
{
|
|
106
|
+
grid: {
|
|
107
|
+
left: '3%',
|
|
108
|
+
right: '4%',
|
|
109
|
+
bottom: '3%',
|
|
110
|
+
containLabel: true,
|
|
111
|
+
...gridInset(legendConfig, showLegend && !renderLegend, xAxisConfig, yAxisConfig),
|
|
112
|
+
},
|
|
113
|
+
tooltip: buildTooltip(
|
|
114
|
+
{
|
|
87
115
|
trigger: 'axis',
|
|
88
116
|
axisPointer: {
|
|
89
117
|
type: 'shadow',
|
|
90
118
|
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
show: showTooltip,
|
|
122
|
+
formatter: tooltipFormatter,
|
|
123
|
+
valueFormatter: tooltipValueFormatter,
|
|
124
|
+
getLabel,
|
|
91
125
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
126
|
+
),
|
|
127
|
+
legend: buildLegend(
|
|
128
|
+
data.series.map((s) => s.name),
|
|
129
|
+
legendConfig,
|
|
130
|
+
{ show: showLegend, custom: !!renderLegend }
|
|
131
|
+
),
|
|
132
|
+
xAxis: buildAxis(isHorizontal ? valueAxis : categoryAxis, xAxisConfig),
|
|
133
|
+
yAxis: buildAxis(isHorizontal ? categoryAxis : valueAxis, yAxisConfig),
|
|
134
|
+
series: data.series.map((series) => ({
|
|
135
|
+
name: series.name,
|
|
136
|
+
type: 'bar',
|
|
137
|
+
data: withPointLabels(series.data, series.labels, labelFormatter, {
|
|
138
|
+
position: isHorizontal ? 'right' : 'top',
|
|
139
|
+
}),
|
|
140
|
+
stack: stacked ? 'Total' : undefined,
|
|
141
|
+
itemStyle: series.color ? { color: series.color } : undefined,
|
|
142
|
+
label: buildDataLabel(
|
|
143
|
+
{
|
|
109
144
|
show: true,
|
|
110
145
|
position: isHorizontal ? 'right' : 'top',
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
146
|
+
},
|
|
147
|
+
undefined,
|
|
148
|
+
{ show: showValues, formatter: labelFormatter, getLabel }
|
|
149
|
+
),
|
|
150
|
+
emphasis: {
|
|
151
|
+
focus: 'series',
|
|
152
|
+
},
|
|
153
|
+
})),
|
|
154
|
+
} as EChartsOption,
|
|
155
|
+
optionOverride
|
|
156
|
+
);
|
|
118
157
|
});
|
|
119
158
|
|
|
120
159
|
const accessibilityDescription = $derived(
|
|
121
160
|
description || (!isEmpty ? generateBarChartDescription(title, data) : undefined)
|
|
122
161
|
);
|
|
123
162
|
|
|
163
|
+
// Legend selection state, shared with a custom legend
|
|
164
|
+
const legend = useChartLegend({
|
|
165
|
+
names: () => data?.series?.map((s) => s.name) ?? [],
|
|
166
|
+
colors: () => data?.series?.map((s) => s.color) ?? [],
|
|
167
|
+
palette: () => getChartColors(chart.theme).series,
|
|
168
|
+
config: () => legendConfig,
|
|
169
|
+
title: () => title,
|
|
170
|
+
onLegendChange: () => onLegendChange,
|
|
171
|
+
});
|
|
172
|
+
|
|
124
173
|
// Manage ECharts instance lifecycle (init, theme, resize, option updates)
|
|
125
174
|
const chart = useChart({
|
|
126
175
|
option: () => option,
|
|
@@ -128,6 +177,7 @@
|
|
|
128
177
|
animation: () => animation,
|
|
129
178
|
isEmpty: () => isEmpty,
|
|
130
179
|
onReady: (instance) => {
|
|
180
|
+
legend.attach(instance);
|
|
131
181
|
if (onClick) {
|
|
132
182
|
instance.on('click', (params) => {
|
|
133
183
|
onClick({
|
|
@@ -146,6 +196,10 @@
|
|
|
146
196
|
});
|
|
147
197
|
</script>
|
|
148
198
|
|
|
199
|
+
{#snippet customLegend()}
|
|
200
|
+
{@render renderLegend?.(legend.renderProps)}
|
|
201
|
+
{/snippet}
|
|
202
|
+
|
|
149
203
|
<ChartContainer
|
|
150
204
|
{title}
|
|
151
205
|
description={accessibilityDescription}
|
|
@@ -155,6 +209,8 @@
|
|
|
155
209
|
empty={isEmpty}
|
|
156
210
|
{emptyMessage}
|
|
157
211
|
class={cn(className)}
|
|
212
|
+
legend={renderLegend ? customLegend : undefined}
|
|
213
|
+
legendPosition={legendConfig?.position}
|
|
158
214
|
>
|
|
159
215
|
<div bind:this={chart.container} class="h-full w-full"></div>
|
|
160
216
|
</ChartContainer>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BarChartProps } from '@classic-homes/charts-core';
|
|
2
|
-
|
|
2
|
+
import { type CustomLegendProps } from '../../composables/useChartLegend.svelte.js';
|
|
3
|
+
interface Props extends Omit<BarChartProps, 'class'>, CustomLegendProps {
|
|
3
4
|
class?: string;
|
|
4
5
|
}
|
|
5
6
|
declare const BarChart: import("svelte").Component<Props, {}, "">;
|