diff --git a/packages/charts/src/vaadin-chart-mixin.js b/packages/charts/src/vaadin-chart-mixin.js index a701b584b19..97a942a681f 100644 --- a/packages/charts/src/vaadin-chart-mixin.js +++ b/packages/charts/src/vaadin-chart-mixin.js @@ -344,6 +344,10 @@ export const ChartMixin = (superClass) => beta: 15, depth: 50, }; + + // Threshold for the number of "addPoint" calls to detach and re-attach the chart. + this.__addPointThreshold = 200; + this.__addPointCounter = 0; } /** @@ -953,6 +957,30 @@ export const ChartMixin = (superClass) => this._jsonConfigurationBuffer = null; } + /** + * Detaches and re-attaches the chart. This process destroys and recreates the configuration, which prevents the listeners from piling up. This is a workaround for HighCharts memory leak bug on addPoint calls. Should be removed after the underlying issue is resolved. + * @private + */ + __detachReattachChart() { + const parent = this.parentElement; + let index; + if (parent) { + index = Array.prototype.indexOf.call(parent.children, this); + parent.removeChild(this); + } + queueMicrotask(() => { + if (!parent || index == null) { + return; + } + if (index === parent.children.length) { + parent.appendChild(this); + } else { + parent.insertBefore(this, parent.children[index]); + } + }); + this.__addPointCounter = 0; + } + /** * Search for axis with given `id`. * @@ -1578,8 +1606,14 @@ export const ChartMixin = (superClass) => const series = this.configuration.series[seriesIndex]; const functionToCall = series[functionName]; if (functionToCall && typeof functionToCall === 'function') { + if (functionName === 'addPoint') { + this.__addPointCounter += 1; + } args.forEach((arg) => inflateFunctions(arg)); functionToCall.apply(series, args); + if (functionName === 'addPoint' && this.__addPointCounter >= this.__addPointThreshold) { + this.__detachReattachChart(); + } } } } diff --git a/packages/charts/test/private-api.test.js b/packages/charts/test/private-api.test.js index c7c8260e370..dcd3359f1e6 100644 --- a/packages/charts/test/private-api.test.js +++ b/packages/charts/test/private-api.test.js @@ -1,5 +1,5 @@ import { expect } from '@vaadin/chai-plugins'; -import { fixtureSync, nextFrame, oneEvent } from '@vaadin/testing-helpers'; +import { fixtureSync, nextFrame, nextRender, oneEvent } from '@vaadin/testing-helpers'; import '../vaadin-chart.js'; import { inflateFunctions } from '../src/helpers.js'; @@ -102,6 +102,21 @@ describe('vaadin-chart private API', () => { const { dataLabels } = chart.configuration.series[0].userOptions; expect(dataLabels.formatter).to.be.a('function'); }); + + it('should destroy and recreate the chart after a large number of addPoint calls', async () => { + const initialNumberOfPoints = chart.configuration.series[0].data.length; + const oldChartInstance = chart.configuration; + // Use internal threshold + const numberOfPointsToAdd = chart.__addPointThreshold * 2; + for (let i = 0; i < numberOfPointsToAdd; i++) { + chart.__callSeriesFunction('addPoint', 0, i); + await nextRender(chart); + } + expect(chart.configuration).to.exist; + expect(chart.configuration).to.not.equal(oldChartInstance); + expect(chart.configuration.series[0]).to.exist; + expect(chart.configuration.series[0].data.length).to.be.greaterThan(initialNumberOfPoints); + }); }); describe('__callAxisFunction', () => {