fix: update ApexCharts data in place instead of re-rendering (#23)
CI / build-and-test (pull_request) Successful in 2m21s
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:
Binary file not shown.
@@ -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);
|
||||
|
||||
@@ -17,6 +17,7 @@ public abstract class ApexChart extends Component implements HasSize {
|
||||
protected static final JsonMapper MAPPER = JsonMapper.builder().build();
|
||||
|
||||
private final String chartType;
|
||||
private boolean optionsSent;
|
||||
|
||||
protected ApexChart(String chartType) {
|
||||
this.chartType = chartType;
|
||||
@@ -28,6 +29,23 @@ public abstract class ApexChart extends Component implements HasSize {
|
||||
|
||||
protected void sendOptions(Map<String, Object> options) {
|
||||
getElement().callJsFunction("renderChart", MAPPER.writeValueAsString(options));
|
||||
optionsSent = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a data-only patch ({@code series} plus {@code categories} or
|
||||
* {@code labels}) that the client applies to the running chart, instead of
|
||||
* rebuilding it from a full option set. Only valid once
|
||||
* {@link #sendOptions} has run — see {@link #hasSentOptions()}.
|
||||
*/
|
||||
protected void sendDataPatch(Map<String, Object> patch) {
|
||||
getElement().callJsFunction("updateData", MAPPER.writeValueAsString(patch));
|
||||
}
|
||||
|
||||
/** Whether a full option set has been sent, i.e. whether there is a chart
|
||||
* on the client a data patch could be applied to. */
|
||||
protected boolean hasSentOptions() {
|
||||
return optionsSent;
|
||||
}
|
||||
|
||||
@ClientCallable
|
||||
|
||||
@@ -20,4 +20,22 @@ public abstract class AxisChart extends ApexChart {
|
||||
|
||||
sendOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the data of an already rendered chart without re-rendering it:
|
||||
* the client patches series (and categories) into the live chart, so the
|
||||
* update animates from the previous values instead of flashing. Falls back
|
||||
* to {@link #setData} as long as nothing has been rendered yet.
|
||||
*/
|
||||
public void updateData(String seriesName, List<Double> values, List<String> categories) {
|
||||
if (!hasSentOptions()) {
|
||||
setData(seriesName, values, categories);
|
||||
return;
|
||||
}
|
||||
|
||||
sendDataPatch(Map.of(
|
||||
"series", List.of(Map.of("name", seriesName, "data", values)),
|
||||
"categories", categories
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,18 @@ public class PieChart extends ApexChart {
|
||||
|
||||
sendOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the data of an already rendered chart without re-rendering it —
|
||||
* see {@link AxisChart#updateData}. Falls back to {@link #setData} as long
|
||||
* as nothing has been rendered yet.
|
||||
*/
|
||||
public void updateData(List<Double> values, List<String> labels) {
|
||||
if (!hasSentOptions()) {
|
||||
setData(values, labels);
|
||||
return;
|
||||
}
|
||||
|
||||
sendDataPatch(Map.of("series", values, "labels", labels));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user