@loidolt/theme-charts 0.7.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/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/components/BarChart.svelte +64 -0
- package/dist/components/BarChart.svelte.d.ts +14 -0
- package/dist/components/CandlestickChart.svelte +58 -0
- package/dist/components/CandlestickChart.svelte.d.ts +21 -0
- package/dist/components/Chart.svelte +282 -0
- package/dist/components/Chart.svelte.d.ts +73 -0
- package/dist/components/ChartDataTable.svelte +79 -0
- package/dist/components/ChartDataTable.svelte.d.ts +23 -0
- package/dist/components/FunnelChart.svelte +50 -0
- package/dist/components/FunnelChart.svelte.d.ts +18 -0
- package/dist/components/GaugeChart.svelte +54 -0
- package/dist/components/GaugeChart.svelte.d.ts +18 -0
- package/dist/components/HeatmapChart.svelte +51 -0
- package/dist/components/HeatmapChart.svelte.d.ts +14 -0
- package/dist/components/LineChart.svelte +66 -0
- package/dist/components/LineChart.svelte.d.ts +14 -0
- package/dist/components/PieChart.svelte +62 -0
- package/dist/components/PieChart.svelte.d.ts +18 -0
- package/dist/components/RadarChart.svelte +48 -0
- package/dist/components/RadarChart.svelte.d.ts +14 -0
- package/dist/components/SankeyChart.svelte +48 -0
- package/dist/components/SankeyChart.svelte.d.ts +18 -0
- package/dist/components/ScatterChart.svelte +61 -0
- package/dist/components/ScatterChart.svelte.d.ts +18 -0
- package/dist/components/TreemapChart.svelte +48 -0
- package/dist/components/TreemapChart.svelte.d.ts +17 -0
- package/dist/core/builders/category.d.ts +37 -0
- package/dist/core/builders/category.js +109 -0
- package/dist/core/builders/more.d.ts +119 -0
- package/dist/core/builders/more.js +530 -0
- package/dist/core/builders/slices.d.ts +36 -0
- package/dist/core/builders/slices.js +117 -0
- package/dist/core/describe.d.ts +27 -0
- package/dist/core/describe.js +69 -0
- package/dist/core/format.d.ts +15 -0
- package/dist/core/format.js +39 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.js +12 -0
- package/dist/core/theme.d.ts +30 -0
- package/dist/core/theme.js +137 -0
- package/dist/core/types.d.ts +98 -0
- package/dist/core/types.js +1 -0
- package/dist/core/validate.d.ts +4 -0
- package/dist/core/validate.js +10 -0
- package/dist/echarts/all.d.ts +3 -0
- package/dist/echarts/all.js +23 -0
- package/dist/echarts/bar.d.ts +3 -0
- package/dist/echarts/bar.js +2 -0
- package/dist/echarts/base.d.ts +3 -0
- package/dist/echarts/base.js +13 -0
- package/dist/echarts/candlestick.d.ts +3 -0
- package/dist/echarts/candlestick.js +3 -0
- package/dist/echarts/funnel.d.ts +3 -0
- package/dist/echarts/funnel.js +2 -0
- package/dist/echarts/gauge.d.ts +3 -0
- package/dist/echarts/gauge.js +2 -0
- package/dist/echarts/heatmap.d.ts +3 -0
- package/dist/echarts/heatmap.js +3 -0
- package/dist/echarts/line.d.ts +3 -0
- package/dist/echarts/line.js +2 -0
- package/dist/echarts/pie.d.ts +3 -0
- package/dist/echarts/pie.js +2 -0
- package/dist/echarts/radar.d.ts +3 -0
- package/dist/echarts/radar.js +3 -0
- package/dist/echarts/sankey.d.ts +3 -0
- package/dist/echarts/sankey.js +2 -0
- package/dist/echarts/scatter.d.ts +3 -0
- package/dist/echarts/scatter.js +2 -0
- package/dist/echarts/treemap.d.ts +3 -0
- package/dist/echarts/treemap.js +2 -0
- package/dist/echarts.d.ts +22 -0
- package/dist/echarts.js +24 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +15 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Loidolt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# @loidolt/theme-charts
|
|
2
|
+
|
|
3
|
+
Accessible, themed [ECharts](https://echarts.apache.org) charts for Svelte 5, painted from the
|
|
4
|
+
Loidolt tokens.
|
|
5
|
+
|
|
6
|
+
- **Themed from the tokens.** Series take the eight categorical chart colours, heatmaps the
|
|
7
|
+
sequential ramp, candlesticks the gain and loss colours, axes and tooltips the surface, border
|
|
8
|
+
and text roles — read live off the page, so light, dark and your own theme all reach the canvas.
|
|
9
|
+
A theme change recolours the chart in place.
|
|
10
|
+
- **Accessible by default.** Every chart is a `<figure>` whose caption names the picture. A
|
|
11
|
+
plain-language description is generated from the data (trend, extremes, largest share), and the
|
|
12
|
+
data itself is rendered as a real table for assistive tech — or behind a "Show data table"
|
|
13
|
+
button, or always. `decal` adds patterns so series never rely on colour alone. Animation is off
|
|
14
|
+
when the user prefers reduced motion.
|
|
15
|
+
- **Server-rendered.** ECharts loads only in the browser, from an effect. The server renders the
|
|
16
|
+
caption, the description and the data table, so the numbers are in the page before any script
|
|
17
|
+
runs.
|
|
18
|
+
- **Pay for what you draw.** Each chart registers only the ECharts pieces it uses.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install @loidolt/theme-charts echarts
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`echarts` (5.5 or 6) is a peer dependency. The chart styles ship with `@loidolt/theme-styles`,
|
|
27
|
+
which you already import for the rest of the system.
|
|
28
|
+
|
|
29
|
+
```svelte
|
|
30
|
+
<script lang="ts">
|
|
31
|
+
import { LineChart } from '@loidolt/theme-charts';
|
|
32
|
+
|
|
33
|
+
const data = {
|
|
34
|
+
categories: ['Jan', 'Feb', 'Mar', 'Apr'],
|
|
35
|
+
series: [
|
|
36
|
+
{ name: 'Cut', data: [42, 48, 51, 63] },
|
|
37
|
+
{ name: 'Scrap', data: [6, 5, 7, 4] },
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<LineChart title="Sheets per month" {data} dataTable="toggle" />
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Components
|
|
46
|
+
|
|
47
|
+
| Component | Draws |
|
|
48
|
+
| ------------------ | ----------------------------------------------------------------------- |
|
|
49
|
+
| `LineChart` | Lines over shared categories; `area` fills under them, `stacked` stacks |
|
|
50
|
+
| `BarChart` | Bars, vertical or horizontal, side by side or stacked |
|
|
51
|
+
| `PieChart` | Parts of a whole; `innerRadius` makes a donut with a centre label |
|
|
52
|
+
| `FunnelChart` | Stages narrowing from first to last |
|
|
53
|
+
| `ScatterChart` | `[x, y]` points, with optional least-squares trend lines |
|
|
54
|
+
| `RadarChart` | Several series across the same measures |
|
|
55
|
+
| `GaugeChart` | One reading on a scale |
|
|
56
|
+
| `HeatmapChart` | A grid of values coloured on the sequential ramp |
|
|
57
|
+
| `TreemapChart` | Nested sizes; click a group to zoom into it |
|
|
58
|
+
| `CandlestickChart` | Open, high, low and close per period, with optional volume and zoom |
|
|
59
|
+
| `SankeyChart` | Flows between named nodes |
|
|
60
|
+
| `Chart` | Any ECharts option, themed — the base every chart above is built on |
|
|
61
|
+
| `ChartDataTable` | A chart's data as a table, visible or for assistive tech only |
|
|
62
|
+
|
|
63
|
+
Every chart takes the same frame props as `Chart`: `title`, `hideTitle`, `description`,
|
|
64
|
+
`height`, `loading`, `error` with `onRetry`, `empty`, `dataTable`, `renderer`, `decal`,
|
|
65
|
+
`animation`, `onItemClick` and a bindable `instance`. User-facing strings (`showTableLabel`,
|
|
66
|
+
`emptyTitle`, table headings…) are props with English defaults.
|
|
67
|
+
|
|
68
|
+
## Any option, themed
|
|
69
|
+
|
|
70
|
+
`Chart` takes a plain ECharts option, or a function of the resolved theme colours:
|
|
71
|
+
|
|
72
|
+
```svelte
|
|
73
|
+
<Chart
|
|
74
|
+
title="Cuts against target"
|
|
75
|
+
option={(colors) => ({
|
|
76
|
+
xAxis: { type: 'category', data: days },
|
|
77
|
+
yAxis: { type: 'value' },
|
|
78
|
+
series: [
|
|
79
|
+
{ type: 'bar', data: cuts },
|
|
80
|
+
{ type: 'line', data: target, lineStyle: { color: colors.accent, type: 'dashed' } },
|
|
81
|
+
],
|
|
82
|
+
})}
|
|
83
|
+
table={{ columns: ['Day', 'Cuts', 'Target'], rows }}
|
|
84
|
+
/>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Whatever the option leaves unset — the palette, axis and gridline colours, the tooltip, legend
|
|
88
|
+
text — is filled in from the theme. Updates replace the option, so a series removed from the data
|
|
89
|
+
leaves the chart; pass `merge` to keep zoom and legend state across updates instead.
|
|
90
|
+
|
|
91
|
+
## Building options without a component
|
|
92
|
+
|
|
93
|
+
`@loidolt/theme-charts/core` is plain TypeScript with no Svelte and no ECharts import — safe in
|
|
94
|
+
a server `load`, a worker or a test:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { buildBarOption, describeBar, categoryTable } from '@loidolt/theme-charts/core';
|
|
98
|
+
import { chartColors } from '@loidolt/theme-tokens';
|
|
99
|
+
|
|
100
|
+
const option = buildBarOption(data, { stacked: true }, chartColors('dark'));
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Each chart has a builder (`buildLineOption`, `buildPieOption`, …), a describer (`describeLine`,
|
|
104
|
+
…) and a table helper (`categoryTable`, `sliceTable`, …). Builders throw a `RangeError` on data
|
|
105
|
+
that cannot be drawn — a series shorter than its categories, a negative slice, a candle whose
|
|
106
|
+
range does not hold its body. `applyTheme(option, colors)` themes any option;
|
|
107
|
+
`formatNumber`, `formatCompact`, `formatPercent`, `formatCurrency` and `formatDate` take an
|
|
108
|
+
explicit `locale`, which you should pass when a chart is server-rendered.
|
|
109
|
+
|
|
110
|
+
## Loading ECharts yourself
|
|
111
|
+
|
|
112
|
+
Charts import `echarts/core` on first use. To use a global build from a script tag, or a stand-in
|
|
113
|
+
under test, register a loader once:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { setEChartsLoader } from '@loidolt/theme-charts';
|
|
117
|
+
setEChartsLoader(() => window.echarts);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
If ECharts cannot be loaded at all, the chart says so and shows its data table instead.
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ComponentProps } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
buildBarOption,
|
|
5
|
+
describeBar,
|
|
6
|
+
categoryTable,
|
|
7
|
+
type BarOptions,
|
|
8
|
+
} from '../core/builders/category.js';
|
|
9
|
+
import type { CategoryData } from '../core/types.js';
|
|
10
|
+
import Chart from './Chart.svelte';
|
|
11
|
+
|
|
12
|
+
type ChartProps = Omit<ComponentProps<typeof Chart>, 'option' | 'table' | 'extensions'>;
|
|
13
|
+
|
|
14
|
+
interface Props extends ChartProps, BarOptions {
|
|
15
|
+
/** Categories and a series of values for each. */
|
|
16
|
+
data: CategoryData;
|
|
17
|
+
/** Heading of the first column in the data table. */
|
|
18
|
+
categoryLabel?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
data,
|
|
23
|
+
orientation,
|
|
24
|
+
stacked,
|
|
25
|
+
showValues,
|
|
26
|
+
showLegend,
|
|
27
|
+
showTooltip,
|
|
28
|
+
showGrid,
|
|
29
|
+
valueFormat,
|
|
30
|
+
categoryAxisName,
|
|
31
|
+
valueAxisName,
|
|
32
|
+
categoryLabel = 'Category',
|
|
33
|
+
description,
|
|
34
|
+
empty,
|
|
35
|
+
instance = $bindable(null),
|
|
36
|
+
ref = $bindable(null),
|
|
37
|
+
...rest
|
|
38
|
+
}: Props = $props();
|
|
39
|
+
|
|
40
|
+
const options = $derived({
|
|
41
|
+
orientation,
|
|
42
|
+
stacked,
|
|
43
|
+
showValues,
|
|
44
|
+
showLegend,
|
|
45
|
+
showTooltip,
|
|
46
|
+
showGrid,
|
|
47
|
+
valueFormat,
|
|
48
|
+
categoryAxisName,
|
|
49
|
+
valueAxisName,
|
|
50
|
+
});
|
|
51
|
+
const extensions = () => import('../echarts/bar.js').then((module) => module.default);
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<Chart
|
|
55
|
+
bind:instance
|
|
56
|
+
bind:ref
|
|
57
|
+
option={(colors) => buildBarOption(data, options, colors)}
|
|
58
|
+
table={categoryTable(data, categoryLabel)}
|
|
59
|
+
description={description ?? describeBar(data, valueFormat)}
|
|
60
|
+
empty={empty ?? (data.series.length === 0 || data.categories.length === 0)}
|
|
61
|
+
{extensions}
|
|
62
|
+
tableFormat={valueFormat}
|
|
63
|
+
{...rest}
|
|
64
|
+
/>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentProps } from 'svelte';
|
|
2
|
+
import { type BarOptions } from '../core/builders/category.js';
|
|
3
|
+
import type { CategoryData } from '../core/types.js';
|
|
4
|
+
import Chart from './Chart.svelte';
|
|
5
|
+
type ChartProps = Omit<ComponentProps<typeof Chart>, 'option' | 'table' | 'extensions'>;
|
|
6
|
+
interface Props extends ChartProps, BarOptions {
|
|
7
|
+
/** Categories and a series of values for each. */
|
|
8
|
+
data: CategoryData;
|
|
9
|
+
/** Heading of the first column in the data table. */
|
|
10
|
+
categoryLabel?: string;
|
|
11
|
+
}
|
|
12
|
+
declare const BarChart: import("svelte").Component<Props, {}, "ref" | "instance">;
|
|
13
|
+
type BarChart = ReturnType<typeof BarChart>;
|
|
14
|
+
export default BarChart;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ComponentProps } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
buildCandlestickOption,
|
|
5
|
+
describeCandlestick,
|
|
6
|
+
candlestickTable,
|
|
7
|
+
type CandlestickOptions,
|
|
8
|
+
} from '../core/builders/more.js';
|
|
9
|
+
import type { CandlestickDatum } from '../core/types.js';
|
|
10
|
+
import Chart from './Chart.svelte';
|
|
11
|
+
|
|
12
|
+
type ChartProps = Omit<ComponentProps<typeof Chart>, 'option' | 'table' | 'extensions'>;
|
|
13
|
+
|
|
14
|
+
interface Props extends ChartProps, CandlestickOptions {
|
|
15
|
+
/** One open, high, low and close per period, optionally with volume. */
|
|
16
|
+
data: CandlestickDatum[];
|
|
17
|
+
/** Headings of the data table columns. */
|
|
18
|
+
tableLabels?: {
|
|
19
|
+
dateLabel?: string;
|
|
20
|
+
openLabel?: string;
|
|
21
|
+
highLabel?: string;
|
|
22
|
+
lowLabel?: string;
|
|
23
|
+
closeLabel?: string;
|
|
24
|
+
volumeLabel?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
data,
|
|
30
|
+
showVolume,
|
|
31
|
+
zoom,
|
|
32
|
+
showTooltip,
|
|
33
|
+
upColor,
|
|
34
|
+
downColor,
|
|
35
|
+
valueFormat,
|
|
36
|
+
tableLabels = {},
|
|
37
|
+
description,
|
|
38
|
+
empty,
|
|
39
|
+
instance = $bindable(null),
|
|
40
|
+
ref = $bindable(null),
|
|
41
|
+
...rest
|
|
42
|
+
}: Props = $props();
|
|
43
|
+
|
|
44
|
+
const options = $derived({ showVolume, zoom, showTooltip, upColor, downColor, valueFormat });
|
|
45
|
+
const extensions = () => import('../echarts/candlestick.js').then((module) => module.default);
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<Chart
|
|
49
|
+
bind:instance
|
|
50
|
+
bind:ref
|
|
51
|
+
option={(colors) => buildCandlestickOption(data, options, colors)}
|
|
52
|
+
table={candlestickTable(data, tableLabels)}
|
|
53
|
+
description={description ?? describeCandlestick(data, valueFormat)}
|
|
54
|
+
empty={empty ?? data.length === 0}
|
|
55
|
+
{extensions}
|
|
56
|
+
tableFormat={valueFormat}
|
|
57
|
+
{...rest}
|
|
58
|
+
/>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ComponentProps } from 'svelte';
|
|
2
|
+
import { type CandlestickOptions } from '../core/builders/more.js';
|
|
3
|
+
import type { CandlestickDatum } from '../core/types.js';
|
|
4
|
+
import Chart from './Chart.svelte';
|
|
5
|
+
type ChartProps = Omit<ComponentProps<typeof Chart>, 'option' | 'table' | 'extensions'>;
|
|
6
|
+
interface Props extends ChartProps, CandlestickOptions {
|
|
7
|
+
/** One open, high, low and close per period, optionally with volume. */
|
|
8
|
+
data: CandlestickDatum[];
|
|
9
|
+
/** Headings of the data table columns. */
|
|
10
|
+
tableLabels?: {
|
|
11
|
+
dateLabel?: string;
|
|
12
|
+
openLabel?: string;
|
|
13
|
+
highLabel?: string;
|
|
14
|
+
lowLabel?: string;
|
|
15
|
+
closeLabel?: string;
|
|
16
|
+
volumeLabel?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
declare const CandlestickChart: import("svelte").Component<Props, {}, "ref" | "instance">;
|
|
20
|
+
type CandlestickChart = ReturnType<typeof CandlestickChart>;
|
|
21
|
+
export default CandlestickChart;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import { chartRoles } from '@loidolt/theme-tokens';
|
|
5
|
+
import {
|
|
6
|
+
Alert,
|
|
7
|
+
Button,
|
|
8
|
+
EmptyState,
|
|
9
|
+
Skeleton,
|
|
10
|
+
createMediaQuery,
|
|
11
|
+
createTokenColors,
|
|
12
|
+
cx,
|
|
13
|
+
} from '@loidolt/theme-svelte';
|
|
14
|
+
import type { FormatOptions } from '../core/format.js';
|
|
15
|
+
import { applyTheme } from '../core/theme.js';
|
|
16
|
+
import type { ChartItemEvent, ChartOptionSource, ChartTable } from '../core/types.js';
|
|
17
|
+
import { loadECharts, type ChartExtensions, type EChartsInstance } from '../echarts.js';
|
|
18
|
+
import ChartDataTable from './ChartDataTable.svelte';
|
|
19
|
+
|
|
20
|
+
type TableMode = 'hidden' | 'toggle' | 'visible' | 'none';
|
|
21
|
+
|
|
22
|
+
interface Props extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
|
|
23
|
+
/** Names the chart — its caption, and the accessible name of the picture. */
|
|
24
|
+
title: string;
|
|
25
|
+
/** Keep the caption for assistive tech only, when a heading nearby already names the chart. */
|
|
26
|
+
hideTitle?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* What the chart shows, in a sentence or two, read after the title. The wrappers write one
|
|
29
|
+
* from the data; say what matters most if you know better.
|
|
30
|
+
*/
|
|
31
|
+
description?: string;
|
|
32
|
+
/** An ECharts option, or a function of the theme colours that builds one. */
|
|
33
|
+
option: ChartOptionSource;
|
|
34
|
+
/** The data as a table: the chart's non-visual alternative. */
|
|
35
|
+
table?: ChartTable;
|
|
36
|
+
/**
|
|
37
|
+
* How the table appears: `hidden` for assistive tech only, `toggle` behind a button,
|
|
38
|
+
* `visible` always, `none` not at all.
|
|
39
|
+
*/
|
|
40
|
+
dataTable?: TableMode;
|
|
41
|
+
/** How numbers in the table are written. */
|
|
42
|
+
tableFormat?: FormatOptions;
|
|
43
|
+
showTableLabel?: string;
|
|
44
|
+
hideTableLabel?: string;
|
|
45
|
+
/** Chart height: pixels, or any CSS length. */
|
|
46
|
+
height?: number | string;
|
|
47
|
+
/** Shows a placeholder while the data is on its way. */
|
|
48
|
+
loading?: boolean;
|
|
49
|
+
loadingLabel?: string;
|
|
50
|
+
/** Shows the message in place of the chart. */
|
|
51
|
+
error?: string | null;
|
|
52
|
+
errorTitle?: string;
|
|
53
|
+
/** Offers a retry button with the error. */
|
|
54
|
+
onRetry?: () => void;
|
|
55
|
+
retryLabel?: string;
|
|
56
|
+
/** Shows the empty state in place of the chart. The wrappers set it when there is no data. */
|
|
57
|
+
empty?: boolean;
|
|
58
|
+
emptyTitle?: string;
|
|
59
|
+
emptyDescription?: string;
|
|
60
|
+
/** Shown if ECharts itself cannot be loaded. */
|
|
61
|
+
unavailableText?: string;
|
|
62
|
+
/** `svg` stays crisp in print and at any zoom; `canvas` is faster with many points. */
|
|
63
|
+
renderer?: 'canvas' | 'svg';
|
|
64
|
+
/** Transitions. Always off when the user prefers reduced motion. */
|
|
65
|
+
animation?: boolean;
|
|
66
|
+
/** Patterns on fills, so series are told apart without relying on colour. */
|
|
67
|
+
decal?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Merge updates into the chart rather than replacing the option — keeps zoom and legend
|
|
70
|
+
* state across data updates. Series and datasets are still replaced whole.
|
|
71
|
+
*/
|
|
72
|
+
merge?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* The ECharts modules this chart draws with. Defaults to every chart type; the wrappers
|
|
75
|
+
* pass only their own.
|
|
76
|
+
*/
|
|
77
|
+
extensions?: ChartExtensions;
|
|
78
|
+
/** A click on a data point, slice or node. */
|
|
79
|
+
onItemClick?: (event: ChartItemEvent) => void;
|
|
80
|
+
/** Called once the ECharts instance exists, for anything the props do not cover. */
|
|
81
|
+
onReady?: (instance: EChartsInstance) => void;
|
|
82
|
+
/** The live ECharts instance. Bindable; `null` until loaded. */
|
|
83
|
+
instance?: EChartsInstance | null;
|
|
84
|
+
class?: string;
|
|
85
|
+
ref?: HTMLElement | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const id = $props.id();
|
|
89
|
+
|
|
90
|
+
let {
|
|
91
|
+
title,
|
|
92
|
+
hideTitle = false,
|
|
93
|
+
description,
|
|
94
|
+
option,
|
|
95
|
+
table,
|
|
96
|
+
dataTable = 'hidden',
|
|
97
|
+
tableFormat,
|
|
98
|
+
showTableLabel = 'Show data table',
|
|
99
|
+
hideTableLabel = 'Hide data table',
|
|
100
|
+
height = 320,
|
|
101
|
+
loading = false,
|
|
102
|
+
loadingLabel = 'Loading chart',
|
|
103
|
+
error = null,
|
|
104
|
+
errorTitle = 'The chart could not be shown',
|
|
105
|
+
onRetry,
|
|
106
|
+
retryLabel = 'Try again',
|
|
107
|
+
empty = false,
|
|
108
|
+
emptyTitle = 'No data to show',
|
|
109
|
+
emptyDescription,
|
|
110
|
+
unavailableText = 'Charts could not be loaded. The data is in the table below.',
|
|
111
|
+
renderer = 'canvas',
|
|
112
|
+
animation = true,
|
|
113
|
+
decal = false,
|
|
114
|
+
merge = false,
|
|
115
|
+
extensions = () => import('../echarts/all.js').then((module) => module.default),
|
|
116
|
+
onItemClick,
|
|
117
|
+
onReady,
|
|
118
|
+
instance = $bindable(null),
|
|
119
|
+
class: className,
|
|
120
|
+
ref = $bindable(null),
|
|
121
|
+
...rest
|
|
122
|
+
}: Props = $props();
|
|
123
|
+
|
|
124
|
+
let surface = $state<HTMLDivElement | null>(null);
|
|
125
|
+
let chart = $state.raw<EChartsInstance | null>(null);
|
|
126
|
+
let unavailable = $state(false);
|
|
127
|
+
let tableOpen = $state(false);
|
|
128
|
+
|
|
129
|
+
const palette = createTokenColors(chartRoles, { element: () => ref });
|
|
130
|
+
const reducedMotion = createMediaQuery('(prefers-reduced-motion: reduce)');
|
|
131
|
+
|
|
132
|
+
const resolved = $derived(
|
|
133
|
+
applyTheme(typeof option === 'function' ? option(palette.colors) : option, palette.colors, {
|
|
134
|
+
animation: animation && !reducedMotion.matches,
|
|
135
|
+
decal,
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const showChart = $derived(!loading && !error && !empty);
|
|
140
|
+
const cssHeight = $derived(typeof height === 'number' ? `${height}px` : height);
|
|
141
|
+
const [titleId, descriptionId, tableId] = [`${id}-title`, `${id}-description`, `${id}-table`];
|
|
142
|
+
|
|
143
|
+
// Read the palette off the mounted figure, so a scoped theme around it applies.
|
|
144
|
+
$effect(() => {
|
|
145
|
+
if (ref) untrack(() => palette.refresh());
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Settled through a derived, so only an actual change of renderer re-creates the instance.
|
|
149
|
+
const rendererMode = $derived(renderer);
|
|
150
|
+
|
|
151
|
+
// One ECharts instance per mounted surface; a new renderer needs a new instance.
|
|
152
|
+
$effect(() => {
|
|
153
|
+
const element = surface;
|
|
154
|
+
const mode = rendererMode;
|
|
155
|
+
if (!element) return;
|
|
156
|
+
const load = untrack(() => extensions);
|
|
157
|
+
let disposed = false;
|
|
158
|
+
let current: EChartsInstance | null = null;
|
|
159
|
+
let frame = 0;
|
|
160
|
+
const observer = new ResizeObserver(() => {
|
|
161
|
+
cancelAnimationFrame(frame);
|
|
162
|
+
frame = requestAnimationFrame(() => {
|
|
163
|
+
if (current && !current.isDisposed()) current.resize();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
observer.observe(element);
|
|
167
|
+
|
|
168
|
+
void (async () => {
|
|
169
|
+
const core = await loadECharts();
|
|
170
|
+
const modules = core
|
|
171
|
+
? await Promise.all([import('../echarts/base.js').then((module) => module.default), load()])
|
|
172
|
+
: null;
|
|
173
|
+
if (disposed) return;
|
|
174
|
+
if (!core || !modules) {
|
|
175
|
+
unavailable = true;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
core.use(modules.flat());
|
|
179
|
+
current = core.init(element, null, { renderer: mode });
|
|
180
|
+
// Handlers read the latest prop when they fire, so one registration serves every render.
|
|
181
|
+
current.on('click', (params) =>
|
|
182
|
+
onItemClick?.({
|
|
183
|
+
componentType: String(params.componentType ?? ''),
|
|
184
|
+
seriesName: params.seriesName as string | undefined,
|
|
185
|
+
seriesIndex: params.seriesIndex as number | undefined,
|
|
186
|
+
name: String(params.name ?? ''),
|
|
187
|
+
dataIndex: params.dataIndex as number | undefined,
|
|
188
|
+
value: params.value,
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
unavailable = false;
|
|
192
|
+
chart = current;
|
|
193
|
+
instance = current;
|
|
194
|
+
onReady?.(current);
|
|
195
|
+
})();
|
|
196
|
+
|
|
197
|
+
return () => {
|
|
198
|
+
disposed = true;
|
|
199
|
+
observer.disconnect();
|
|
200
|
+
cancelAnimationFrame(frame);
|
|
201
|
+
current?.dispose();
|
|
202
|
+
chart = null;
|
|
203
|
+
instance = null;
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Replace, not merge, by default: series removed from the data must leave the chart too.
|
|
208
|
+
$effect(() => {
|
|
209
|
+
if (!chart) return;
|
|
210
|
+
chart.setOption(
|
|
211
|
+
resolved,
|
|
212
|
+
merge
|
|
213
|
+
? { replaceMerge: ['series', 'dataset'], lazyUpdate: true }
|
|
214
|
+
: { notMerge: true, lazyUpdate: true }
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
</script>
|
|
218
|
+
|
|
219
|
+
<figure
|
|
220
|
+
bind:this={ref}
|
|
221
|
+
class={cx('ldt-chart', className)}
|
|
222
|
+
style:--ldt-chart-height={cssHeight}
|
|
223
|
+
aria-busy={loading || undefined}
|
|
224
|
+
{...rest}
|
|
225
|
+
>
|
|
226
|
+
<figcaption id={titleId} class={cx('ldt-chart__title', hideTitle && 'ldt-sr-only')}>
|
|
227
|
+
{title}
|
|
228
|
+
</figcaption>
|
|
229
|
+
|
|
230
|
+
{#if loading}
|
|
231
|
+
<div class="ldt-chart__state">
|
|
232
|
+
<Skeleton variant="block" height="100%" />
|
|
233
|
+
<span class="ldt-sr-only" role="status">{loadingLabel}</span>
|
|
234
|
+
</div>
|
|
235
|
+
{:else if error}
|
|
236
|
+
<Alert variant="error" title={errorTitle} class="ldt-chart__alert">
|
|
237
|
+
<p>{error}</p>
|
|
238
|
+
{#if onRetry}
|
|
239
|
+
<Button size="sm" onclick={onRetry}>{retryLabel}</Button>
|
|
240
|
+
{/if}
|
|
241
|
+
</Alert>
|
|
242
|
+
{:else if empty}
|
|
243
|
+
<EmptyState title={emptyTitle} description={emptyDescription} class="ldt-chart__state" />
|
|
244
|
+
{:else}
|
|
245
|
+
{#if unavailable}
|
|
246
|
+
<Alert variant="warning" class="ldt-chart__alert">{unavailableText}</Alert>
|
|
247
|
+
{/if}
|
|
248
|
+
<div
|
|
249
|
+
bind:this={surface}
|
|
250
|
+
class="ldt-chart__canvas"
|
|
251
|
+
role="img"
|
|
252
|
+
aria-labelledby={titleId}
|
|
253
|
+
aria-describedby={description ? descriptionId : undefined}
|
|
254
|
+
hidden={unavailable}
|
|
255
|
+
></div>
|
|
256
|
+
{#if description}<p id={descriptionId} class="ldt-sr-only">{description}</p>{/if}
|
|
257
|
+
{/if}
|
|
258
|
+
|
|
259
|
+
{#if table && showChart && dataTable !== 'none'}
|
|
260
|
+
{#if dataTable === 'toggle'}
|
|
261
|
+
<Button
|
|
262
|
+
size="sm"
|
|
263
|
+
variant="quiet"
|
|
264
|
+
class="ldt-chart__table-toggle"
|
|
265
|
+
aria-expanded={tableOpen}
|
|
266
|
+
aria-controls={tableId}
|
|
267
|
+
onclick={() => (tableOpen = !tableOpen)}
|
|
268
|
+
>
|
|
269
|
+
{tableOpen ? hideTableLabel : showTableLabel}
|
|
270
|
+
</Button>
|
|
271
|
+
{/if}
|
|
272
|
+
<ChartDataTable
|
|
273
|
+
id={tableId}
|
|
274
|
+
{table}
|
|
275
|
+
caption={title}
|
|
276
|
+
valueFormat={tableFormat}
|
|
277
|
+
visuallyHidden={dataTable === 'hidden' && !unavailable}
|
|
278
|
+
hidden={dataTable === 'toggle' && !tableOpen && !unavailable}
|
|
279
|
+
class="ldt-chart__table"
|
|
280
|
+
/>
|
|
281
|
+
{/if}
|
|
282
|
+
</figure>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import type { FormatOptions } from '../core/format.js';
|
|
3
|
+
import type { ChartItemEvent, ChartOptionSource, ChartTable } from '../core/types.js';
|
|
4
|
+
import { type ChartExtensions, type EChartsInstance } from '../echarts.js';
|
|
5
|
+
type TableMode = 'hidden' | 'toggle' | 'visible' | 'none';
|
|
6
|
+
interface Props extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
|
|
7
|
+
/** Names the chart — its caption, and the accessible name of the picture. */
|
|
8
|
+
title: string;
|
|
9
|
+
/** Keep the caption for assistive tech only, when a heading nearby already names the chart. */
|
|
10
|
+
hideTitle?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* What the chart shows, in a sentence or two, read after the title. The wrappers write one
|
|
13
|
+
* from the data; say what matters most if you know better.
|
|
14
|
+
*/
|
|
15
|
+
description?: string;
|
|
16
|
+
/** An ECharts option, or a function of the theme colours that builds one. */
|
|
17
|
+
option: ChartOptionSource;
|
|
18
|
+
/** The data as a table: the chart's non-visual alternative. */
|
|
19
|
+
table?: ChartTable;
|
|
20
|
+
/**
|
|
21
|
+
* How the table appears: `hidden` for assistive tech only, `toggle` behind a button,
|
|
22
|
+
* `visible` always, `none` not at all.
|
|
23
|
+
*/
|
|
24
|
+
dataTable?: TableMode;
|
|
25
|
+
/** How numbers in the table are written. */
|
|
26
|
+
tableFormat?: FormatOptions;
|
|
27
|
+
showTableLabel?: string;
|
|
28
|
+
hideTableLabel?: string;
|
|
29
|
+
/** Chart height: pixels, or any CSS length. */
|
|
30
|
+
height?: number | string;
|
|
31
|
+
/** Shows a placeholder while the data is on its way. */
|
|
32
|
+
loading?: boolean;
|
|
33
|
+
loadingLabel?: string;
|
|
34
|
+
/** Shows the message in place of the chart. */
|
|
35
|
+
error?: string | null;
|
|
36
|
+
errorTitle?: string;
|
|
37
|
+
/** Offers a retry button with the error. */
|
|
38
|
+
onRetry?: () => void;
|
|
39
|
+
retryLabel?: string;
|
|
40
|
+
/** Shows the empty state in place of the chart. The wrappers set it when there is no data. */
|
|
41
|
+
empty?: boolean;
|
|
42
|
+
emptyTitle?: string;
|
|
43
|
+
emptyDescription?: string;
|
|
44
|
+
/** Shown if ECharts itself cannot be loaded. */
|
|
45
|
+
unavailableText?: string;
|
|
46
|
+
/** `svg` stays crisp in print and at any zoom; `canvas` is faster with many points. */
|
|
47
|
+
renderer?: 'canvas' | 'svg';
|
|
48
|
+
/** Transitions. Always off when the user prefers reduced motion. */
|
|
49
|
+
animation?: boolean;
|
|
50
|
+
/** Patterns on fills, so series are told apart without relying on colour. */
|
|
51
|
+
decal?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Merge updates into the chart rather than replacing the option — keeps zoom and legend
|
|
54
|
+
* state across data updates. Series and datasets are still replaced whole.
|
|
55
|
+
*/
|
|
56
|
+
merge?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* The ECharts modules this chart draws with. Defaults to every chart type; the wrappers
|
|
59
|
+
* pass only their own.
|
|
60
|
+
*/
|
|
61
|
+
extensions?: ChartExtensions;
|
|
62
|
+
/** A click on a data point, slice or node. */
|
|
63
|
+
onItemClick?: (event: ChartItemEvent) => void;
|
|
64
|
+
/** Called once the ECharts instance exists, for anything the props do not cover. */
|
|
65
|
+
onReady?: (instance: EChartsInstance) => void;
|
|
66
|
+
/** The live ECharts instance. Bindable; `null` until loaded. */
|
|
67
|
+
instance?: EChartsInstance | null;
|
|
68
|
+
class?: string;
|
|
69
|
+
ref?: HTMLElement | null;
|
|
70
|
+
}
|
|
71
|
+
declare const Chart: import("svelte").Component<Props, {}, "ref" | "instance">;
|
|
72
|
+
type Chart = ReturnType<typeof Chart>;
|
|
73
|
+
export default Chart;
|