fix: update ApexCharts data in place instead of re-rendering (#23)
CI / build-and-test (pull_request) Successful in 2m21s

Every data change went through renderChart, which rebuilt the chart from a
full option set: animations restarted and zoom/selection state was lost.

Add an updateData path that patches the live chart via ApexCharts'
updateSeries (and updateOptions only when categories/labels actually
change), exposed as AxisChart.updateData / PieChart.updateData. Before the
first render there is nothing to patch, so those fall back to setData.
The theme overlay is re-applied to the merged options, so a rebuild after
a detach starts from the patched data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69
This commit is contained in:
Pit Friedrich
2026-07-28 19:29:09 +02:00
parent 1b846751dd
commit 640616c60a
7 changed files with 257 additions and 0 deletions
@@ -20,6 +20,10 @@ function resolveColor(varName: string): string {
return resolved;
}
function sameJson(a: unknown, b: unknown): boolean {
return JSON.stringify(a) === JSON.stringify(b);
}
function isDarkScheme(): boolean {
return getComputedStyle(document.documentElement).colorScheme.includes('dark');
}
@@ -113,6 +117,49 @@ export class ApexChart extends LitElement {
}
}
/**
* Data-only update: patches the live chart via ApexCharts' own
* updateSeries/updateOptions instead of rebuilding it from a full option
* set, so the SVG animates from its previous values and zoom/selection
* state survives. The patch carries `series` plus, depending on the chart
* type, `categories` (axis charts) or `labels` (pie).
*/
async updateData(patchJson: string) {
await this.updateComplete;
const patch = JSON.parse(patchJson);
// Nothing rendered yet — there is no option set to patch onto. The
// server only calls this after a full render, so this is a no-op guard.
if (!this.lastOptions) return;
const options = this.lastOptions;
// Categories/labels are structural: they only reach the chart through
// updateOptions, which redraws. Skip it when they are unchanged, which
// is the common case for a pure data refresh.
const structural: any = {};
if (patch.categories && !sameJson(patch.categories, options.xaxis?.categories)) {
structural.xaxis = { ...options.xaxis, categories: patch.categories };
}
if (patch.labels && !sameJson(patch.labels, options.labels)) {
structural.labels = patch.labels;
}
options.series = patch.series;
if (patch.categories) options.xaxis = { ...options.xaxis, categories: patch.categories };
if (patch.labels) options.labels = patch.labels;
// Re-resolve the theme-varying keys on the merged options, so a rebuild
// after a detach (see connectedCallback) starts from the patched data.
const themed = applyThemeOverlay(options);
this.lastOptionsJson = JSON.stringify(themed);
if (!this.chart) return;
if (Object.keys(structural).length > 0) {
await this.chart.updateOptions(structural, false, true);
}
await this.chart.updateSeries(themed.series, true);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('dialect-theme-change', this.onThemeChange);