@classic-homes/charts-svelte 0.1.37 → 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 +51 -11
- package/dist/lib/components/base/ChartContainer.svelte.d.ts +8 -0
- package/dist/lib/components/base/ChartError.svelte +3 -1
- 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 +12 -0
- package/dist/lib/components/base/index.js +1 -0
- package/dist/lib/components/core/AreaChart.svelte +126 -61
- 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 +155 -82
- package/dist/lib/components/core/DonutChart.svelte.d.ts +2 -1
- package/dist/lib/components/core/LineChart.svelte +118 -48
- 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 +115 -38
- package/dist/lib/components/core/ScatterChart.svelte.d.ts +2 -1
- package/dist/lib/components/extended/CandlestickChart.svelte +53 -26
- package/dist/lib/components/extended/FunnelChart.svelte +102 -32
- package/dist/lib/components/extended/FunnelChart.svelte.d.ts +2 -1
- package/dist/lib/components/extended/GaugeChart.svelte +62 -29
- package/dist/lib/components/extended/HeatmapChart.svelte +59 -29
- 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 +71 -26
- 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/useChart.svelte.js +20 -3
- package/dist/lib/composables/useChartLegend.svelte.d.ts +37 -0
- package/dist/lib/composables/useChartLegend.svelte.js +54 -0
- package/dist/lib/composables/useChartTheme.svelte.d.ts +2 -1
- package/dist/lib/composables/useChartTheme.svelte.js +2 -6
- 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,28 +38,29 @@
|
|
|
30
38
|
onRetry,
|
|
31
39
|
class: className,
|
|
32
40
|
wrapperClass,
|
|
41
|
+
legend,
|
|
42
|
+
legendPosition = 'bottom',
|
|
33
43
|
children,
|
|
34
44
|
}: Props = $props();
|
|
35
45
|
|
|
46
|
+
// Unique per instance: two charts may share a title
|
|
47
|
+
const uid = $props.id();
|
|
48
|
+
|
|
36
49
|
const heightStyle = $derived(typeof height === 'number' ? `${height}px` : height);
|
|
37
|
-
const descriptionId = $derived(
|
|
38
|
-
|
|
39
|
-
|
|
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');
|
|
40
55
|
</script>
|
|
41
56
|
|
|
42
|
-
{#
|
|
43
|
-
<ChartSkeleton {height} class={wrapperClass} />
|
|
44
|
-
{:else if error}
|
|
45
|
-
<ChartError message={error} {height} {onRetry} class={wrapperClass} />
|
|
46
|
-
{:else if empty}
|
|
47
|
-
<ChartEmpty message={emptyMessage} {height} class={wrapperClass} />
|
|
48
|
-
{:else}
|
|
57
|
+
{#snippet chart(chartClass: string)}
|
|
49
58
|
<div
|
|
50
59
|
role="img"
|
|
51
60
|
aria-label={title}
|
|
52
61
|
aria-describedby={descriptionId}
|
|
53
62
|
data-testid="chart-container"
|
|
54
|
-
class={
|
|
63
|
+
class={chartClass}
|
|
55
64
|
>
|
|
56
65
|
{#if description}
|
|
57
66
|
<span id={descriptionId} class="sr-only">{description}</span>
|
|
@@ -60,4 +69,35 @@
|
|
|
60
69
|
{@render children()}
|
|
61
70
|
</div>
|
|
62
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))}
|
|
63
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, {}, "">;
|
|
@@ -37,9 +37,11 @@
|
|
|
37
37
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
38
38
|
/>
|
|
39
39
|
</svg>
|
|
40
|
-
|
|
40
|
+
<!-- text-destructive on the destructive tint fails AA; the icon and border carry the colour -->
|
|
41
|
+
<p class="mb-2 text-sm font-medium text-foreground">{message}</p>
|
|
41
42
|
{#if onRetry}
|
|
42
43
|
<button
|
|
44
|
+
type="button"
|
|
43
45
|
onclick={onRetry}
|
|
44
46
|
class="rounded-md bg-destructive px-3 py-1.5 text-xs font-medium text-destructive-foreground transition-colors hover:bg-destructive/90"
|
|
45
47
|
>
|
|
@@ -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,3 +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';
|
|
6
|
+
import type { ComponentProps } from 'svelte';
|
|
7
|
+
import type ChartContainer from './ChartContainer.svelte';
|
|
8
|
+
import type ChartSkeleton from './ChartSkeleton.svelte';
|
|
9
|
+
import type ChartError from './ChartError.svelte';
|
|
10
|
+
import type ChartEmpty from './ChartEmpty.svelte';
|
|
11
|
+
import type ChartLegend from './ChartLegend.svelte';
|
|
12
|
+
export type ChartContainerProps = ComponentProps<typeof ChartContainer>;
|
|
13
|
+
export type ChartSkeletonProps = ComponentProps<typeof ChartSkeleton>;
|
|
14
|
+
export type ChartErrorProps = ComponentProps<typeof ChartError>;
|
|
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,9 +11,26 @@
|
|
|
11
11
|
import type { EChartsOption } from 'echarts';
|
|
12
12
|
|
|
13
13
|
import type { AreaChartProps, DataPointEventParams } from '@classic-homes/charts-core';
|
|
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';
|
|
14
27
|
|
|
15
28
|
import { cn } from '../../utils.js';
|
|
16
29
|
import { useChart } from '../../composables/useChart.svelte.js';
|
|
30
|
+
import {
|
|
31
|
+
useChartLegend,
|
|
32
|
+
type CustomLegendProps,
|
|
33
|
+
} from '../../composables/useChartLegend.svelte.js';
|
|
17
34
|
import ChartContainer from '../base/ChartContainer.svelte';
|
|
18
35
|
|
|
19
36
|
// Register required ECharts modules
|
|
@@ -26,7 +43,7 @@
|
|
|
26
43
|
CanvasRenderer,
|
|
27
44
|
]);
|
|
28
45
|
|
|
29
|
-
interface Props extends Omit<AreaChartProps, 'class'
|
|
46
|
+
interface Props extends Omit<AreaChartProps, 'class'>, CustomLegendProps {
|
|
30
47
|
class?: string;
|
|
31
48
|
}
|
|
32
49
|
|
|
@@ -46,6 +63,16 @@
|
|
|
46
63
|
smooth = false,
|
|
47
64
|
stacked = false,
|
|
48
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,
|
|
49
76
|
onClick,
|
|
50
77
|
class: className,
|
|
51
78
|
}: Props = $props();
|
|
@@ -54,76 +81,97 @@
|
|
|
54
81
|
!data?.categories?.length || !data?.series?.length || data.series.every((s) => !s.data?.length)
|
|
55
82
|
);
|
|
56
83
|
|
|
84
|
+
// Per-element colours that follow the resolved chart theme
|
|
85
|
+
const chartColors = $derived.by(() => getChartColors(chart.theme));
|
|
86
|
+
|
|
57
87
|
const option: EChartsOption = $derived.by(() => {
|
|
58
88
|
if (isEmpty) return {};
|
|
59
89
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
{
|
|
69
105
|
trigger: 'axis',
|
|
70
106
|
axisPointer: {
|
|
71
107
|
type: 'cross',
|
|
72
108
|
label: {
|
|
73
|
-
backgroundColor:
|
|
109
|
+
backgroundColor: chartColors.axisPointerLabel,
|
|
74
110
|
},
|
|
75
111
|
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
show: showTooltip,
|
|
115
|
+
formatter: tooltipFormatter,
|
|
116
|
+
valueFormatter: tooltipValueFormatter,
|
|
117
|
+
getLabel,
|
|
76
118
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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,
|
|
130
|
+
},
|
|
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
|
+
);
|
|
127
175
|
});
|
|
128
176
|
|
|
129
177
|
const accessibilityDescription = $derived(
|
|
@@ -133,6 +181,16 @@
|
|
|
133
181
|
: undefined)
|
|
134
182
|
);
|
|
135
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
|
+
|
|
136
194
|
// Manage ECharts instance lifecycle (init, theme, resize, option updates)
|
|
137
195
|
const chart = useChart({
|
|
138
196
|
option: () => option,
|
|
@@ -140,6 +198,7 @@
|
|
|
140
198
|
animation: () => animation,
|
|
141
199
|
isEmpty: () => isEmpty,
|
|
142
200
|
onReady: (instance) => {
|
|
201
|
+
legend.attach(instance);
|
|
143
202
|
if (onClick) {
|
|
144
203
|
instance.on('click', (params) => {
|
|
145
204
|
onClick({
|
|
@@ -158,6 +217,10 @@
|
|
|
158
217
|
});
|
|
159
218
|
</script>
|
|
160
219
|
|
|
220
|
+
{#snippet customLegend()}
|
|
221
|
+
{@render renderLegend?.(legend.renderProps)}
|
|
222
|
+
{/snippet}
|
|
223
|
+
|
|
161
224
|
<ChartContainer
|
|
162
225
|
{title}
|
|
163
226
|
description={accessibilityDescription}
|
|
@@ -167,6 +230,8 @@
|
|
|
167
230
|
empty={isEmpty}
|
|
168
231
|
{emptyMessage}
|
|
169
232
|
class={cn(className)}
|
|
233
|
+
legend={renderLegend ? customLegend : undefined}
|
|
234
|
+
legendPosition={legendConfig?.position}
|
|
170
235
|
>
|
|
171
236
|
<div bind:this={chart.container} class="h-full w-full"></div>
|
|
172
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>
|