Skip to content

Commit 79b643d

Browse files
persist-series-brush (#819)
* persist-series-brush - add 2 examples showing using bind:context for external programmatic control for localStorage persistance of series selection and brush/zoom state. - update guide:brush and guide:series referencing these examples. * persist-series-brush spelling fix * Fix reactivity of peristed examples * update catalog and screenshots * Revert "update catalog and screenshots" This reverts commit 21076af. --------- Co-authored-by: Sean Lynch <techniq35@gmail.com>
1 parent 1fc2b94 commit 79b643d

4 files changed

Lines changed: 151 additions & 5 deletions

File tree

docs/src/content/guides/brush.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ Multiple charts can share the same `xDomain` state to stay synchronized:
113113

114114
## Programmatic control
115115

116-
Access the brush state via the chart context to control it from code:
116+
Access the brush state via the chart context to control it from code. For example use localStorage to [persist the brush range/zoom](/docs/components/LineChart/persist-brush-zoom) between reloads. Below is an example of a custom UI for programmatic control.
117117

118118
```svelte
119119
<Chart {data} x="date" y="value" brush>
@@ -128,7 +128,6 @@ Access the brush state via the chart context to control it from code:
128128
:example{ component="BrushContext" name="programmatic-control" }
129129

130130
### Methods
131-
132131
| Method | Description |
133132
| ------------------ | ---------------------------------- |
134133
| `move({ x?, y? })` | Set the selection programmatically |
@@ -216,12 +215,12 @@ An overview chart below the main chart can act as a navigation scrollbar. The ov
216215
| Visual brush | `brush` | [basic](/docs/components/BrushContext/basic) |
217216
| Brush-to-zoom | `brush={{ onBrushEnd: (e) => { ... e.brush.reset() } }}` | [integrated-brush](</docs/components/BrushContext/integrated-brush-(x-axis)>) |
218217
| Simplified chart zoom | `brush` on LineChart/AreaChart/etc. | [brush](/docs/components/LineChart/brush) |
219-
| Brush on categories | `brush` on BarChart with band scale | [brush-band](/docs/components/BarChart/brush-band) |
218+
| Brush on categories | `brush` on BarChart with band scale | [brush-band](/docs/components/BarChart/brush-band) |
220219
| Minimap (focus+context) | `brush={{ x: xDomain, onChange: ... }}` | [minimap](/docs/components/BrushContext/minimap) |
221220
| Synced multi-chart | Shared `xDomain` state with `x` and `onChange` | [sync-brushes](/docs/components/BrushContext/synced-brushes) |
222221
| Programmatic control | `context.brush.move()`, `.reset()`, `.selectAll()` | [programmatic-control](/docs/components/BrushContext/programmatic-control) |
223222
| Brush + pan/zoom | `brush` + `transform={{ mode: 'domain' }}` | [brush-pan-zoom](/docs/components/LineChart/brush-pan-zoom) |
224-
| Brush + pan/zoom (band) | `brush` + `transform` on band scale | [brush-pan-zoom-band](/docs/components/BarChart/brush-pan-zoom-band) |
223+
| Brush + pan/zoom (band) | `brush` + `transform` on band scale | [brush-pan-zoom-band](/docs/components/BarChart/brush-pan-zoom-band) |
225224
| Overview + pan/zoom | `brush.x` synced to main chart's `context.xDomain` | [pan-zoom-with-overview](/docs/components/LineChart/pan-zoom-with-overview) |
226225
| Point selection | `brush={{ axis: 'both', onChange: ... }}` | [selection](/docs/components/BrushContext/selection) |
227226
| Custom styling | `brush={{ classes: { range: '...', handle: '...' } }}` | [simple-styling](/docs/components/BrushContext/simple-styling) |

docs/src/content/guides/series.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ You can also read the highlight state in custom mark snippets:
178178

179179
## Programmatic control
180180

181-
Access the series state via `bind:context` to build your own series toggle UI, set initial visibility, or control series from external components.
181+
Access the series state via `bind:context` to build your own series toggle UI, set initial visibility, or control series from external components. See the [persist-series example](/docs/components/LineChart/persist-series) for an example using localStorage to persist series selection.
182182

183183
### Custom legend
184184

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<script lang="ts">
2+
import { untrack } from 'svelte';
3+
import { LineChart, Chart, Area, Layer, defaultChartPadding, type ChartState } from 'layerchart';
4+
import { getAppleStock } from '$lib/data.remote';
5+
6+
const STORAGE_KEY = 'layerchart:persist-brush-zoom:range';
7+
8+
const data = $derived(await getAppleStock());
9+
let context = $state<ChartState>();
10+
let loaded = false;
11+
12+
// load once when context is ready
13+
$effect(() => {
14+
if (!context?.isMounted) return;
15+
untrack(() => {
16+
const saved = localStorage.getItem(STORAGE_KEY);
17+
if (saved) {
18+
const parsed = JSON.parse(saved);
19+
if (Array.isArray(parsed) && parsed.length === 2) {
20+
context.zoomToBrush(
21+
{ x: [new Date(parsed[0]), new Date(parsed[1])], y: [null, null] },
22+
'x'
23+
);
24+
}
25+
}
26+
loaded = true;
27+
});
28+
});
29+
30+
// save whenever brush range changes (after initial load)
31+
$effect(() => {
32+
const range = context?.xDomain;
33+
if (loaded && range) {
34+
localStorage.setItem(STORAGE_KEY, JSON.stringify(range));
35+
}
36+
});
37+
</script>
38+
39+
<div class="text-center pb-4 text-sm">
40+
Select desired brush range, reload the page, and it will persist.
41+
</div>
42+
<LineChart
43+
bind:context
44+
{data}
45+
x="date"
46+
y="value"
47+
yDomain={[0, null]}
48+
transform={{
49+
mode: 'domain',
50+
axis: 'x',
51+
scaleExtent: [1, 50],
52+
domainExtent: {
53+
x: { min: 'data', max: 'data', minRange: 7 * 24 * 60 * 60 * 1000 }
54+
}
55+
}}
56+
clip
57+
padding={defaultChartPadding({ left: 25, bottom: 24 })}
58+
height={300}
59+
/>
60+
61+
<Chart
62+
{data}
63+
x="date"
64+
y="value"
65+
padding={{ left: 16 }}
66+
brush={{
67+
x: context?.xDomain,
68+
onChange: (e) => {
69+
if (context && e.brush.active) {
70+
context.zoomToBrush(e.brush, 'x');
71+
}
72+
},
73+
onBrushEnd: (e) => {
74+
if (context && !e.brush.active) {
75+
context.transform.reset();
76+
localStorage.removeItem(STORAGE_KEY);
77+
}
78+
}
79+
}}
80+
height={40}
81+
>
82+
<Layer>
83+
<Area line={{ class: 'stroke-2 stroke-primary' }} class="fill-primary/20" />
84+
</Layer>
85+
</Chart>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<script lang="ts">
2+
import { LineChart, defaultChartPadding } from 'layerchart';
3+
import { createDateSeries } from '$lib/utils/data.js';
4+
5+
const STORAGE_KEY = 'layerchart:persist-series:selected-keys';
6+
7+
const data = createDateSeries({
8+
count: 30,
9+
min: 10,
10+
max: 100,
11+
value: 'integer',
12+
keys: ['apples', 'bananas', 'oranges']
13+
});
14+
export { data };
15+
16+
let context: any = $state();
17+
let loaded = false;
18+
19+
// load once when context is ready
20+
$effect(() => {
21+
if (!context?.isMounted) return;
22+
const saved = localStorage.getItem(STORAGE_KEY);
23+
if (saved) {
24+
const keys = JSON.parse(saved);
25+
if (Array.isArray(keys)) {
26+
context.series.selectedKeys.current = keys;
27+
}
28+
}
29+
loaded = true;
30+
});
31+
32+
// save whenever selected keys change (after initial load)
33+
$effect(() => {
34+
const keys = context?.series?.selectedKeys?.current;
35+
if (loaded && keys) {
36+
localStorage.setItem(STORAGE_KEY, JSON.stringify(keys));
37+
}
38+
});
39+
</script>
40+
41+
<div class="text-center pb-4 text-sm">
42+
Select desired series on the legend, reload the page, and they will persist.
43+
</div>
44+
<LineChart
45+
bind:context
46+
{data}
47+
x="date"
48+
series={[
49+
{ key: 'apples', color: 'var(--color-apples)' },
50+
{
51+
key: 'bananas',
52+
color: 'var(--color-bananas)'
53+
},
54+
{
55+
key: 'oranges',
56+
color: 'var(--color-oranges)'
57+
}
58+
]}
59+
padding={defaultChartPadding({ legend: true, right: 10 })}
60+
height={300}
61+
legend
62+
/>

0 commit comments

Comments
 (0)