@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.
Files changed (36) hide show
  1. package/dist/lib/components/base/ChartContainer.svelte +47 -8
  2. package/dist/lib/components/base/ChartContainer.svelte.d.ts +8 -0
  3. package/dist/lib/components/base/ChartLegend.svelte +59 -0
  4. package/dist/lib/components/base/ChartLegend.svelte.d.ts +10 -0
  5. package/dist/lib/components/base/index.d.ts +3 -0
  6. package/dist/lib/components/base/index.js +1 -0
  7. package/dist/lib/components/core/AreaChart.svelte +121 -54
  8. package/dist/lib/components/core/AreaChart.svelte.d.ts +2 -1
  9. package/dist/lib/components/core/BarChart.svelte +91 -35
  10. package/dist/lib/components/core/BarChart.svelte.d.ts +2 -1
  11. package/dist/lib/components/core/DonutChart.svelte +152 -82
  12. package/dist/lib/components/core/DonutChart.svelte.d.ts +2 -1
  13. package/dist/lib/components/core/LineChart.svelte +114 -47
  14. package/dist/lib/components/core/LineChart.svelte.d.ts +2 -1
  15. package/dist/lib/components/core/PieChart.svelte +100 -38
  16. package/dist/lib/components/core/PieChart.svelte.d.ts +2 -1
  17. package/dist/lib/components/core/ScatterChart.svelte +102 -34
  18. package/dist/lib/components/core/ScatterChart.svelte.d.ts +2 -1
  19. package/dist/lib/components/extended/CandlestickChart.svelte +44 -22
  20. package/dist/lib/components/extended/FunnelChart.svelte +102 -33
  21. package/dist/lib/components/extended/FunnelChart.svelte.d.ts +2 -1
  22. package/dist/lib/components/extended/GaugeChart.svelte +59 -38
  23. package/dist/lib/components/extended/HeatmapChart.svelte +47 -27
  24. package/dist/lib/components/extended/RadarChart.svelte +76 -22
  25. package/dist/lib/components/extended/RadarChart.svelte.d.ts +2 -1
  26. package/dist/lib/components/extended/SankeyChart.svelte +50 -21
  27. package/dist/lib/components/extended/TreemapChart.svelte +68 -27
  28. package/dist/lib/components/index.d.ts +1 -1
  29. package/dist/lib/components/index.js +1 -1
  30. package/dist/lib/composables/index.d.ts +1 -0
  31. package/dist/lib/composables/index.js +1 -0
  32. package/dist/lib/composables/useChartLegend.svelte.d.ts +37 -0
  33. package/dist/lib/composables/useChartLegend.svelte.js +54 -0
  34. package/dist/lib/index.d.ts +3 -3
  35. package/dist/lib/index.js +2 -2
  36. 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
- {#if loading}
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={cn('rounded-lg border border-border bg-card', wrapperClass)}
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 { colorWithAlpha, getChartColors } 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';
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
- return {
65
- grid: {
66
- left: '3%',
67
- right: '4%',
68
- bottom: '3%',
69
- containLabel: true,
70
- },
71
- tooltip: showTooltip
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
- : undefined,
82
- legend: showLegend
83
- ? {
84
- data: data.series.map((s) => s.name),
85
- bottom: 0,
86
- }
87
- : undefined,
88
- xAxis: {
89
- type: 'category',
90
- boundaryGap: false,
91
- data: data.categories,
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
- showSymbol: false,
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
- interface Props extends Omit<AreaChartProps, 'class'> {
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 { generateBarChartDescription } from '@classic-homes/charts-core';
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
- return {
79
- grid: {
80
- left: '3%',
81
- right: '4%',
82
- bottom: '3%',
83
- containLabel: true,
84
- },
85
- tooltip: showTooltip
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
- : undefined,
93
- legend: showLegend
94
- ? {
95
- data: data.series.map((s) => s.name),
96
- bottom: 0,
97
- }
98
- : undefined,
99
- xAxis: isHorizontal ? valueAxis : categoryAxis,
100
- yAxis: isHorizontal ? categoryAxis : valueAxis,
101
- series: data.series.map((series) => ({
102
- name: series.name,
103
- type: 'bar',
104
- data: series.data,
105
- stack: stacked ? 'Total' : undefined,
106
- itemStyle: series.color ? { color: series.color } : undefined,
107
- label: showValues
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
- : undefined,
113
- emphasis: {
114
- focus: 'series',
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
- interface Props extends Omit<BarChartProps, 'class'> {
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, {}, "">;