Repository: tradingview/lightweight-charts (ts)

Issue:
Removing data for the last series doesn't actually remove the data
**Lightweight Charts Version:** 3.3.0

**Steps/code to reproduce:**
Take 2 lines series : serie 1 and serie 2 both line type series of a couple hundred points.

1. Draw serie 1 `serie1.setData([... data points serie 1 ...])`
2. Draw serie 2 `serie2.setData([... data points serie 2 ...])`
3. Clear serie 1 `serie1.setData([])`
4. Clear serie 2 `serie2.setData([])`
5. Draw serie 1 `serie1.setData([... data points serie 1 ...])`

**Actual behavior:**

Serie 1 appear on chart as expected but serie 2 is rendered too (although I _deleted_ the data using setData([]))

**Expected behavior:**

Only serie 1 is drawn as a result of the serie1.setData([... points ...])

**CodeSandbox/JSFiddle/etc link:**

https://jsfiddle.net/kjg1zvnx/

![2021-06-06 03 57 41](https://user-images.githubusercontent.com/5738336/120910011-6e601600-c67b-11eb-8019-36bc2b6c91c6.gif)

Maybe setData([]) isn't made for deleting the data? 

This works :
Instead of calling setData([]) draw a fake point
```
serie1.setData([
"time": 1622933040, // first time from other series, avoid 'invalid date' error on time axis...
"value": undefined
}])
```

Pull request:
Fix #752 Series data still in memory after using setdata
**Type of PR:** Bugfix

**PR checklist:**

- [x] Addresses an existing issue: fixes #752 
- [x] Includes tests
- [ ] Documentation update

**Overview of change:**

Currently when a series is remove (i.e. it is updated with empty data `[]`) the library does not properly handle removing that data.

Internally this is because `DataLayer._syncIndexesAndApplyChanges` doesn't include the removed series in the returned `DataUpdateResponse`.

This PR fixes the logic so that removed series are included in the update response. 


Patch:
diff --git a/src/api/data-layer.ts b/src/api/data-layer.ts
index f58744b5f..1037b4492 100644
--- a/src/api/data-layer.ts
+++ b/src/api/data-layer.ts
@@ -420,6 +420,13 @@ export class DataLayer {
 				dataUpdateResponse.series.set(s, { data, fullUpdate: true });
 			});
 
+			// if the seires data was set to [] it will have already been removed from _seriesRowBySeries
+			// meaning the forEach above won't add the series to the data update response
+			// so we handle that case here
+			if (!this._seriesRowsBySeries.has(series)) {
+				dataUpdateResponse.series.set(series, { data: [], fullUpdate: true });
+			}
+
 			dataUpdateResponse.timeScale.points = this._sortedTimePoints;
 		} else {
 			const seriesData = this._seriesRowsBySeries.get(series);


Test patch:
diff --git a/tests/e2e/graphics/test-cases/series/update-removed-series-data.js b/tests/e2e/graphics/test-cases/series/update-removed-series-data.js
new file mode 100644
index 000000000..ca9f81f25
--- /dev/null
+++ b/tests/e2e/graphics/test-cases/series/update-removed-series-data.js
@@ -0,0 +1,54 @@
+function generateData() {
+	const res = [];
+	const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
+	for (let i = 0; i < 3; ++i) {
+		res.push({
+			time: time.getTime() / 1000,
+			value: i * Math.random(),
+		});
+
+		time.setUTCDate(time.getUTCDate() + 1);
+	}
+
+	return res;
+}
+
+function runTestCase(container) {
+	const chart = LightweightCharts.createChart(container, {
+		width: 600,
+		height: 300,
+		timeScale: {
+			barSpacing: 40,
+			timeVisible: true,
+		},
+	});
+
+	const data1 = generateData();
+	const data2 = generateData();
+	const series1 = chart.addLineSeries({ title: 'Series 1' });
+	const series2 = chart.addLineSeries({ title: 'Series 2' });
+
+	return new Promise(resolve => {
+		setTimeout(() => {
+			series1.setData(data1);
+
+			setTimeout(() => {
+				series2.setData(data2);
+
+				setTimeout(() => {
+					series1.setData([]);
+
+					setTimeout(() => {
+						series2.setData([]);
+
+						setTimeout(() => {
+							series1.setData(data1);
+
+							resolve();
+						}, 50);
+					}, 50);
+				}, 50);
+			}, 50);
+		}, 50);
+	});
+}
diff --git a/tests/unittests/data-layer.spec.ts b/tests/unittests/data-layer.spec.ts
index b30458412..0a07f705f 100644
--- a/tests/unittests/data-layer.spec.ts
+++ b/tests/unittests/data-layer.spec.ts
@@ -132,23 +132,30 @@ describe('DataLayer', () => {
 			{ time: { timestamp: 5000 }, timeWeight: 30 },
 			{ time: { timestamp: 7000 }, timeWeight: 22 },
 		]);
-		expect(updateResult.series.size).to.be.equal(2);
-		updateResult.series.forEach((updatePacket: SeriesChanges, series: Series) => {
-			expect(updatePacket.data.length).to.be.equal(2);
-			expect(updatePacket.fullUpdate).to.be.equal(true);
-
-			if (series === series1) {
-				expect(updatePacket.data[0].index).to.be.equal(0 as TimePointIndex);
-				expect(updatePacket.data[0].time).to.be.deep.equal({ timestamp: 2000 });
-				expect(updatePacket.data[1].index).to.be.equal(2 as TimePointIndex);
-				expect(updatePacket.data[1].time).to.be.deep.equal({ timestamp: 5000 });
-			} else {
-				expect(updatePacket.data[0].index).to.be.equal(1 as TimePointIndex);
-				expect(updatePacket.data[0].time).to.be.deep.equal({ timestamp: 3000 });
-				expect(updatePacket.data[1].index).to.be.equal(3 as TimePointIndex);
-				expect(updatePacket.data[1].time).to.be.deep.equal({ timestamp: 7000 });
-			}
-		});
+		expect(updateResult.series.size).to.be.equal(3);
+
+		const series1Update = updateResult.series.get(series1);
+		expect(series1Update).not.to.be.equal(undefined);
+		expect(series1Update?.data.length).to.be.equal(2);
+		expect(series1Update?.fullUpdate).to.be.equal(true);
+		expect(series1Update?.data[0].index).to.be.equal(0 as TimePointIndex);
+		expect(series1Update?.data[0].time).to.be.deep.equal({ timestamp: 2000 });
+		expect(series1Update?.data[1].index).to.be.equal(2 as TimePointIndex);
+		expect(series1Update?.data[1].time).to.be.deep.equal({ timestamp: 5000 });
+
+		const series2Update = updateResult.series.get(series2);
+		expect(series2Update).not.to.be.equal(undefined);
+		expect(series2Update?.data.length).to.be.equal(2);
+		expect(series2Update?.fullUpdate).to.be.equal(true);
+		expect(series2Update?.data[0].index).to.be.equal(1 as TimePointIndex);
+		expect(series2Update?.data[0].time).to.be.deep.equal({ timestamp: 3000 });
+		expect(series2Update?.data[1].index).to.be.equal(3 as TimePointIndex);
+		expect(series2Update?.data[1].time).to.be.deep.equal({ timestamp: 7000 });
+
+		const series3Update = updateResult.series.get(series3);
+		expect(series3Update).not.to.be.equal(undefined);
+		expect(series3Update?.data.length).to.be.equal(0);
+		expect(series3Update?.fullUpdate).to.be.equal(true);
 	});
 
 	it('should be able to remove series if time scale is NOT changed', () => {
@@ -411,6 +418,57 @@ describe('DataLayer', () => {
 		}
 	});
 
+	it('should update removed series data gh#752', () => {
+		function generateData(): LineData[] {
+			const res = [];
+			const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
+
+			for (let i = 0; i < 10; ++i) {
+				const timestamp = time.getTime() / 1000;
+				res.push(dataItemAt(timestamp as UTCTimestamp));
+				time.setUTCDate(time.getUTCDate() + 1);
+			}
+
+			return res;
+		}
+
+		const dataLayer = new DataLayer();
+		const series1 = createSeriesMock();
+		const series2 = createSeriesMock();
+		const data1 = generateData();
+		const data2 = generateData();
+		dataLayer.setSeriesData(series1, data1);
+		dataLayer.setSeriesData(series2, data2);
+
+		const updateResult1 = dataLayer.setSeriesData(series1, []);
+
+		expect(updateResult1.timeScale.baseIndex).to.be.equal(9, 'expected base index to be 9');
+		expect(updateResult1.timeScale.points).to.be.equal(undefined, 'expected updated time scale points to be undefined');
+		expect(updateResult1.series.has(series1)).to.be.equal(true, 'expected to contain series1');
+		expect(updateResult1.series.get(series1)?.data.length).to.be.equal(0, 'expected series1 data to be empty');
+
+		const updateResult2 = dataLayer.setSeriesData(series2, []);
+
+		expect(updateResult2.timeScale.baseIndex).to.be.equal(0, 'expected base index to be 0');
+		expect(updateResult2.timeScale.points?.length).to.be.equal(0, 'expected updated time scale points length to equal 0');
+		expect(updateResult2.series.has(series2)).to.be.equal(true, 'expected to contain series2');
+		expect(updateResult2.series.get(series2)?.data.length).to.be.equal(0, 'expected series2 data to be empty');
+
+		const updateResult3 = dataLayer.setSeriesData(series1, data1);
+
+		expect(updateResult3.timeScale.baseIndex).to.be.equal(9, 'expected base index to be 9');
+		expect(updateResult3.timeScale.points?.length).to.be.equal(10, 'expected updated time scale points length to equal 10');
+		expect(updateResult3.series.has(series1)).to.be.equal(true, 'expected to contain series1');
+		expect(updateResult3.series.get(series1)?.data.length).to.be.equal(data1.length, 'expected series1 data to be non-empty');
+
+		const updateResult4 = dataLayer.setSeriesData(series2, data2);
+
+		expect(updateResult4.timeScale.baseIndex).to.be.equal(9, 'expected base index to be 9');
+		expect(updateResult4.timeScale.points).to.be.equal(undefined, 'expected updated time scale points to be undefined');
+		expect(updateResult4.series.has(series2)).to.be.equal(true, 'expected to contain series2');
+		expect(updateResult4.series.get(series2)?.data.length).to.be.equal(data2.length, 'expected series1 data to be non-empty');
+	});
+
 	describe('should be able to remove series and generate full update to other series if time scale is changed gh#355', () => {
 		const barsCount = 10;
 		function generateData(startTime: number, step: number): LineData[] {
@@ -437,7 +495,12 @@ describe('DataLayer', () => {
 			expect(updateResult.timeScale.points).not.to.be.equal(undefined);
 			expect(updateResult.timeScale.baseIndex).to.be.equal(barsCount - 1);
 
-			expect(updateResult.series.size).to.be.equal(1);
+			expect(updateResult.series.size).to.be.equal(2);
+
+			const series1Update = updateResult.series.get(series1);
+			expect(series1Update).not.to.be.equal(undefined);
+			expect(series1Update?.fullUpdate).to.be.equal(true);
+			expect(series1Update?.data.length).to.be.equal(0);
 
 			const series2Update = updateResult.series.get(series2);
 			expect(series2Update).not.to.be.equal(undefined);
@@ -458,12 +521,17 @@ describe('DataLayer', () => {
 			expect(updateResult.timeScale.points).not.to.be.equal(undefined);
 			expect(updateResult.timeScale.baseIndex).to.be.equal(barsCount - 1);
 
-			expect(updateResult.series.size).to.be.equal(1);
+			expect(updateResult.series.size).to.be.equal(2);
 
 			const series1Update = updateResult.series.get(series1);
 			expect(series1Update).not.to.be.equal(undefined);
 			expect(series1Update?.fullUpdate).to.be.equal(true);
 			expect(series1Update?.data.length).to.be.equal(barsCount);
+
+			const series2Update = updateResult.series.get(series2);
+			expect(series2Update).not.to.be.equal(undefined);
+			expect(series2Update?.fullUpdate).to.be.equal(true);
+			expect(series2Update?.data.length).to.be.equal(0);
 		});
 	});