Repository: tradingview/lightweight-charts (ts)

Issue:
Cleared and reset data appears at visually different location
**Lightweight Charts Version:** 3.3.0

**Steps/code to reproduce:**

1. Create a chart
2. Add a line series
3. Set the series data to be non-empty
4. Set the series data to be empty
5. Set the series data to be non-empty

```js
const data = [
  { time: 1609459200, value: 500 },
  { time: 1609545600, value: 600 },
  { time: 1609632000, value: 700 },
];

const container = document.getElementById("container");
const chart = LightweightCharts.createChart(container, {
  timeScale: {
    barSpacing: 40,
  },
});

const series = chart.addLineSeries();
series.setData(data);
series.setData([]);
series.setData(data);
```

**Actual behavior:**

The series data disappears and reappears at different points on the chart.

**Expected behavior:**

The series data disappears and reappears at the same points on the chart.

**Screenshots:**

Before:

<img width="1368" alt="Screenshot 2021-06-11 at 14 07 41" src="https://user-images.githubusercontent.com/7424084/121692090-a0300d00-cabf-11eb-92f7-0cb966a8f0ab.png">

After:

<img width="1368" alt="Screenshot 2021-06-11 at 14 08 25" src="https://user-images.githubusercontent.com/7424084/121692155-b6d66400-cabf-11eb-99c4-23eb8289318c.png">


**CodeSandbox/JSFiddle/etc link:**

https://codesandbox.io/s/beautiful-hawking-ue92u


Pull request:
Fix #757 by resetting base index value to null
**Type of PR:** Bugfix

**PR checklist:**

- [x] Addresses an existing issue: fixes #757 
- [x] Includes tests
- [ ] Documentation update

**Overview of change:**

`DataLayer` now returns an updated `baseIndex` value of `null` when all series data has been removed. This causes the time scale to properly calculate the right offset value.

<!-- optional -->


Patch:
diff --git a/src/api/data-layer.ts b/src/api/data-layer.ts
index 1037b4492..bf847012c 100644
--- a/src/api/data-layer.ts
+++ b/src/api/data-layer.ts
@@ -110,7 +110,7 @@ export interface TimeScaleChanges {
 	/**
 	 * In terms of time scale "base index" means the latest time scale point with data (there might be whitespaces)
 	 */
-	baseIndex: TimePointIndex;
+	baseIndex: TimePointIndex | null;
 }
 
 export interface SeriesChanges {
@@ -381,7 +381,12 @@ export class DataLayer {
 		return firstChangedPointIndex;
 	}
 
-	private _getBaseIndex(): TimePointIndex {
+	private _getBaseIndex(): TimePointIndex | null {
+		if (this._seriesRowsBySeries.size === 0) {
+			// if we have no data then 'reset' the base index to null
+			return null;
+		}
+
 		let baseIndex = 0 as TimePointIndex;
 
 		this._seriesRowsBySeries.forEach((data: SeriesPlotRow[]) => {
diff --git a/src/model/chart-model.ts b/src/model/chart-model.ts
index f7861a2ed..bec32831b 100644
--- a/src/model/chart-model.ts
+++ b/src/model/chart-model.ts
@@ -448,7 +448,7 @@ export class ChartModel implements IDestroyable {
 		this._crosshair.updateAllViews();
 	}
 
-	public updateTimeScale(newBaseIndex: TimePointIndex, newPoints?: readonly TimeScalePoint[]): void {
+	public updateTimeScale(newBaseIndex: TimePointIndex | null, newPoints?: readonly TimeScalePoint[]): void {
 		const oldFirstTime = this._timeScale.indexToTime(0 as TimePointIndex);
 
 		if (newPoints !== undefined) {
@@ -466,11 +466,11 @@ export class ChartModel implements IDestroyable {
 		if (visibleBars !== null && oldFirstTime !== null && newFirstTime !== null) {
 			const isLastSeriesBarVisible = visibleBars.contains(currentBaseIndex);
 			const isLeftBarShiftToLeft = oldFirstTime.timestamp > newFirstTime.timestamp;
-			const isSeriesPointsAdded = newBaseIndex > currentBaseIndex;
+			const isSeriesPointsAdded = newBaseIndex !== null && newBaseIndex > currentBaseIndex;
 			const isSeriesPointsAddedToRight = isSeriesPointsAdded && !isLeftBarShiftToLeft;
 
 			const needShiftVisibleRangeOnNewBar = isLastSeriesBarVisible && this._timeScale.options().shiftVisibleRangeOnNewBar;
-			if (isSeriesPointsAddedToRight && !needShiftVisibleRangeOnNewBar) {
+			if (isSeriesPointsAddedToRight && !needShiftVisibleRangeOnNewBar && newBaseIndex !== null) {
 				const compensationShift = newBaseIndex - currentBaseIndex;
 				this._timeScale.setRightOffset(this._timeScale.rightOffset() - compensationShift);
 			}
diff --git a/src/model/time-scale.ts b/src/model/time-scale.ts
index cd010a567..8eee00701 100644
--- a/src/model/time-scale.ts
+++ b/src/model/time-scale.ts
@@ -396,7 +396,7 @@ export class TimeScale {
 		this.setRightOffset(this._options.rightOffset);
 	}
 
-	public setBaseIndex(baseIndex: TimePointIndex): void {
+	public setBaseIndex(baseIndex: TimePointIndex | null): void {
 		this._visibleRangeInvalidated = true;
 		this._baseIndexOrNull = baseIndex;
 		this._correctOffset();


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
index ca9f81f25..abdc62a45 100644
--- a/tests/e2e/graphics/test-cases/series/update-removed-series-data.js
+++ b/tests/e2e/graphics/test-cases/series/update-removed-series-data.js
@@ -1,19 +1,34 @@
-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 data1 = [
+		{
+			time: 1514764800,
+			value: 0,
+		},
+		{
+			time: 1514851200,
+			value: 0.29081914410775955,
+		},
+		{
+			time: 1514937600,
+			value: 1.6789955502713652,
+		},
+	];
+
+	const data2 = [
+		{
+			time: 1514764800,
+			value: 0,
+		},
+		{
+			time: 1514851200,
+			value: 0.7242371327593901,
+		},
+		{
+			time: 1514937600,
+			value: 0.3558113114519652,
+		},
+	];
+
 	const chart = LightweightCharts.createChart(container, {
 		width: 600,
 		height: 300,
@@ -23,8 +38,6 @@ function runTestCase(container) {
 		},
 	});
 
-	const data1 = generateData();
-	const data2 = generateData();
 	const series1 = chart.addLineSeries({ title: 'Series 1' });
 	const series2 = chart.addLineSeries({ title: 'Series 2' });
 
diff --git a/tests/e2e/graphics/test-cases/time-scale/set-and-clear-series.js b/tests/e2e/graphics/test-cases/time-scale/set-and-clear-series.js
new file mode 100644
index 000000000..b7a6960b7
--- /dev/null
+++ b/tests/e2e/graphics/test-cases/time-scale/set-and-clear-series.js
@@ -0,0 +1,18 @@
+function runTestCase(container) {
+	const data = [
+		{ time: 1609459200, value: 500 },
+		{ time: 1609545600, value: 600 },
+		{ time: 1609632000, value: 700 },
+	];
+
+	const chart = LightweightCharts.createChart(container, {
+		timeScale: {
+			barSpacing: 40,
+		},
+	});
+
+	const series = chart.addLineSeries();
+	series.setData(data);
+	series.setData([]);
+	series.setData(data);
+}
diff --git a/tests/unittests/data-layer.spec.ts b/tests/unittests/data-layer.spec.ts
index 0a07f705f..e77373e8f 100644
--- a/tests/unittests/data-layer.spec.ts
+++ b/tests/unittests/data-layer.spec.ts
@@ -449,7 +449,7 @@ describe('DataLayer', () => {
 
 		const updateResult2 = dataLayer.setSeriesData(series2, []);
 
-		expect(updateResult2.timeScale.baseIndex).to.be.equal(0, 'expected base index to be 0');
+		expect(updateResult2.timeScale.baseIndex).to.be.equal(null, 'expected base index to be null');
 		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');
@@ -469,6 +469,85 @@ describe('DataLayer', () => {
 		expect(updateResult4.series.get(series2)?.data.length).to.be.equal(data2.length, 'expected series1 data to be non-empty');
 	});
 
+	describe('should update base index to null when all series data is cleared gh#757', () => {
+		const data: LineData[] = [
+			{
+				time: 1609459200 as UTCTimestamp,
+				value: 31.533026412262345,
+			},
+			{
+				time: 1609545600 as UTCTimestamp,
+				value: 6.568118452269189,
+			},
+			{
+				time: 1609632000 as UTCTimestamp,
+				value: 98.62539451897008,
+			},
+			{
+				time: 1609718400 as UTCTimestamp,
+				value: 46.767718860541606,
+			},
+			{
+				time: 1609804800 as UTCTimestamp,
+				value: 36.955748002496655,
+			},
+			{
+				time: 1609891200 as UTCTimestamp,
+				value: 85.96192548047124,
+			},
+			{
+				time: 1609977600 as UTCTimestamp,
+				value: 72.75990512152876,
+			},
+			{
+				time: 1610064000 as UTCTimestamp,
+				value: 2.993469032310503,
+			},
+			{
+				time: 1610150400 as UTCTimestamp,
+				value: 4.258319318756176,
+			},
+			{
+				time: 1610236800 as UTCTimestamp,
+				value: 60.0150296893859,
+			},
+		];
+
+		it('single series', () => {
+			const dataLayer = new DataLayer();
+			const series = createSeriesMock();
+
+			dataLayer.setSeriesData(series, data);
+
+			const updateResult = dataLayer.setSeriesData(series, []);
+
+			expect(updateResult.timeScale.baseIndex).to.be.equal(null);
+		});
+
+		it('multiple series', () => {
+			const seriesCount = 5;
+			const dataLayer = new DataLayer();
+			const series = [];
+
+			for (let i = 0; i < seriesCount; i++) {
+				series[i] = createSeriesMock();
+				dataLayer.setSeriesData(series[i], data);
+			}
+
+			for (let i = 0; i < series.length; i++) {
+				const updateResult = dataLayer.setSeriesData(series[i], []);
+
+				if (i === series.length - 1) {
+					// the last series was cleared so we expect a null base index
+					expect(updateResult.timeScale.baseIndex).to.be.equal(null);
+				} else {
+					// some series still have data so we expected a non-null base index
+					expect(updateResult.timeScale.baseIndex).not.to.be.equal(null);
+				}
+			}
+		});
+	});
+
 	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[] {