From 2b4cad362cd4f182c0a24f810e371913fd6861c8 Mon Sep 17 00:00:00 2001 From: Mark Silverwood <3482679+SlicedSilver@users.noreply.github.com> Date: Wed, 31 Jul 2024 09:33:07 +0100 Subject: [PATCH 01/17] historicalUpdate for series.update --- src/api/chart-api.ts | 4 +- src/api/iseries-api.ts | 4 +- src/api/series-api.ts | 4 +- src/model/data-consumer.ts | 2 +- src/model/data-layer.ts | 36 ++++- src/model/series.ts | 1 + .../coverage/test-cases/series/update-data.js | 17 +++ .../test-cases/historical-data-updates.js | 45 ++++++ tests/unittests/data-layer.spec.ts | 132 ++++++++++++++++-- 9 files changed, 222 insertions(+), 23 deletions(-) create mode 100644 tests/e2e/graphics/test-cases/historical-data-updates.js diff --git a/src/api/chart-api.ts b/src/api/chart-api.ts index aaa060b372..3d19706232 100644 --- a/src/api/chart-api.ts +++ b/src/api/chart-api.ts @@ -261,8 +261,8 @@ export class ChartApi implements IChartApiBase, Da this._sendUpdateToChart(this._dataLayer.setSeriesData(series, data)); } - public updateData(series: Series, data: SeriesDataItemTypeMap[TSeriesType]): void { - this._sendUpdateToChart(this._dataLayer.updateSeriesData(series, data)); + public updateData(series: Series, data: SeriesDataItemTypeMap[TSeriesType], historicalUpdate: boolean): void { + this._sendUpdateToChart(this._dataLayer.updateSeriesData(series, data, historicalUpdate)); } public subscribeClick(handler: MouseEventHandler): void { diff --git a/src/api/iseries-api.ts b/src/api/iseries-api.ts index 28521cc824..cafb940690 100644 --- a/src/api/iseries-api.ts +++ b/src/api/iseries-api.ts @@ -158,6 +158,8 @@ export interface ISeriesApi< * * @param bar - A single data item to be added. Time of the new item must be greater or equal to the latest existing time point. * If the new item's time is equal to the last existing item's time, then the existing item is replaced with the new one. + * @param historicalUpdate - If true, allows updating an existing data point that is not the latest bar. Default is false. + * Updating older data using `historicalUpdate` will be slower than updating the most recent data point. * @example Updating line series data * ```js * lineSeries.update({ @@ -176,7 +178,7 @@ export interface ISeriesApi< * }); * ``` */ - update(bar: TData): void; + update(bar: TData, historicalUpdate?: boolean): void; /** * Returns a bar data by provided logical index. diff --git a/src/api/series-api.ts b/src/api/series-api.ts index 608f88b89f..f21af462b3 100644 --- a/src/api/series-api.ts +++ b/src/api/series-api.ts @@ -155,10 +155,10 @@ export class SeriesApi< this._onDataChanged('full'); } - public update(bar: TData): void { + public update(bar: TData, historicalUpdate: boolean = false): void { checkSeriesValuesType(this._series.seriesType(), [bar]); - this._dataUpdatesConsumer.updateData(this._series, bar); + this._dataUpdatesConsumer.updateData(this._series, bar, historicalUpdate); this._onDataChanged('update'); } diff --git a/src/model/data-consumer.ts b/src/model/data-consumer.ts index f189ebcc82..8fd3caa53f 100644 --- a/src/model/data-consumer.ts +++ b/src/model/data-consumer.ts @@ -228,5 +228,5 @@ export interface SeriesDataItemTypeMap { export interface DataUpdatesConsumer { applyNewData(series: Series, data: SeriesDataItemTypeMap[TSeriesType][]): void; - updateData(series: Series, data: SeriesDataItemTypeMap[TSeriesType]): void; + updateData(series: Series, data: SeriesDataItemTypeMap[TSeriesType], historicalUpdate: boolean): void; } diff --git a/src/model/data-layer.ts b/src/model/data-layer.ts index b1b9af0cc7..515cc7a8cd 100644 --- a/src/model/data-layer.ts +++ b/src/model/data-layer.ts @@ -96,6 +96,7 @@ function seriesUpdateInfo(seriesR const prevFirstAndLastTime = seriesRowsFirstAndLastTime(prevSeriesRows, bh); if (firstAndLastTime !== undefined && prevFirstAndLastTime !== undefined) { return { + historicalUpdate: false, lastBarUpdatedOrNewBarsAddedToTheRight: firstAndLastTime.lastTime >= prevFirstAndLastTime.lastTime && firstAndLastTime.firstTime >= prevFirstAndLastTime.firstTime, @@ -242,7 +243,7 @@ export class DataLayer { return this.setSeriesData(series, []); } - public updateSeriesData(series: Series, data: SeriesDataItemTypeMap[TSeriesType]): DataUpdateResponse { + public updateSeriesData(series: Series, data: SeriesDataItemTypeMap[TSeriesType], historicalUpdate: boolean): DataUpdateResponse { const extendedData = data as SeriesDataItemWithOriginalTime; saveOriginalTime(extendedData); // convertStringToBusinessDay(data); @@ -252,13 +253,17 @@ export class DataLayer { const time = timeConverter(data.time); const lastSeriesTime = this._seriesLastTimePoint.get(series); - if (lastSeriesTime !== undefined && this._horzScaleBehavior.key(time) < this._horzScaleBehavior.key(lastSeriesTime)) { + if (!historicalUpdate && lastSeriesTime !== undefined && this._horzScaleBehavior.key(time) < this._horzScaleBehavior.key(lastSeriesTime)) { // eslint-disable-next-line @typescript-eslint/no-base-to-string throw new Error(`Cannot update oldest data, last time=${lastSeriesTime}, new time=${time}`); } let pointDataAtTime = this._pointDataByTimePoint.get(this._horzScaleBehavior.key(time)); + if (historicalUpdate && pointDataAtTime === undefined) { + throw new Error('Cannot update non-existing data point when historicalUpdate is true'); + } + // if no point data found for the new data item // that means that we need to update scale const affectsTimeScale = pointDataAtTime === undefined; @@ -276,9 +281,16 @@ export class DataLayer { pointDataAtTime.mapping.set(series, plotRow); - this._updateLastSeriesRow(series, plotRow); + if (historicalUpdate) { + this._updateHistoricalSeriesRow(series, plotRow, pointDataAtTime.index); + } else { + this._updateLastSeriesRow(series, plotRow); + } - const info: SeriesUpdateInfo = { lastBarUpdatedOrNewBarsAddedToTheRight: isSeriesPlotRow(plotRow) }; + const info: SeriesUpdateInfo = { + lastBarUpdatedOrNewBarsAddedToTheRight: isSeriesPlotRow(plotRow), + historicalUpdate, + }; // if point already exist on the time scale - we don't need to make a full update and just make an incremental one if (!affectsTimeScale) { @@ -332,6 +344,22 @@ export class DataLayer { this._seriesLastTimePoint.set(series, plotRow.time); } + private _updateHistoricalSeriesRow(series: Series, plotRow: SeriesPlotRow | WhitespacePlotRow, pointDataIndex: number): void { + const seriesData = this._seriesRowsBySeries.get(series); + if (seriesData === undefined) { + return; + } + // binary search for actual index in array. + const index = lowerBound(seriesData, pointDataIndex, (row: SeriesPlotRow, currentIndex: number): boolean => + row.index < currentIndex + ); + if (isSeriesPlotRow(plotRow)) { + seriesData[index] = plotRow; + } else { + seriesData.splice(index, 1); + } + } + private _setRowsToSeries(series: Series, seriesRows: (SeriesPlotRow | WhitespacePlotRow)[]): void { if (seriesRows.length !== 0) { this._seriesRowsBySeries.set(series, seriesRows.filter(isSeriesPlotRow)); diff --git a/src/model/series.ts b/src/model/series.ts index 51eca43058..9367daa124 100644 --- a/src/model/series.ts +++ b/src/model/series.ts @@ -124,6 +124,7 @@ export interface SeriesDataAtTypeMap { export interface SeriesUpdateInfo { lastBarUpdatedOrNewBarsAddedToTheRight: boolean; + historicalUpdate: boolean; } // note that if would like to use `Omit` here - you can't due https://github.com/microsoft/TypeScript/issues/36981 diff --git a/tests/e2e/coverage/test-cases/series/update-data.js b/tests/e2e/coverage/test-cases/series/update-data.js index 97c2461eda..b38f6d4e17 100644 --- a/tests/e2e/coverage/test-cases/series/update-data.js +++ b/tests/e2e/coverage/test-cases/series/update-data.js @@ -88,5 +88,22 @@ async function afterInteractions() { ...barData[barData.length - 1], time: barData[barData.length - 1].time + 3600, }); + + await awaitNewFrame(); + + lineSeries.update( + { + ...barData[barData.length - 5], + value: 1234, + }, + true // historical update + ); + lineSeries.update( + { + time: barData[barData.length - 6], + }, + true // historical update + ); + return Promise.resolve(); } diff --git a/tests/e2e/graphics/test-cases/historical-data-updates.js b/tests/e2e/graphics/test-cases/historical-data-updates.js new file mode 100644 index 0000000000..802341b3d8 --- /dev/null +++ b/tests/e2e/graphics/test-cases/historical-data-updates.js @@ -0,0 +1,45 @@ +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 50; ++i) { + res.push({ + time: time.getTime() / 1000, + value: i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function runTestCase(container) { + const chart = (window.chart = LightweightCharts.createChart(container, { + layout: { + attributionLogo: true, + }, + })); + + const mainSeries = chart.addLineSeries(); + + const data = generateData(); + const thirdLastPoint = { + ...data[data.length - 4], + }; + mainSeries.setData(data); + + try { + mainSeries.update(thirdLastPoint); + + console.assert( + false, + 'should fail if older update and not setting historicalUpdate to true' + ); + } catch (e) { + // passed + } + mainSeries.update( + { ...thirdLastPoint, value: thirdLastPoint.value - 10 }, + true + ); + chart.timeScale().fitContent(); +} diff --git a/tests/unittests/data-layer.spec.ts b/tests/unittests/data-layer.spec.ts index d3fbc393a7..0c0ec9e340 100644 --- a/tests/unittests/data-layer.spec.ts +++ b/tests/unittests/data-layer.spec.ts @@ -35,6 +35,10 @@ function dataItemAt(time: Time): BarData