Skip to content

Commit

Permalink
Merge pull request #1444 from tradingview/fix-timescale-shifting-on-u…
Browse files Browse the repository at this point in the history
…pdates

fix shiftVisibleRangeOnNewBar behaviour
  • Loading branch information
SlicedSilver committed Oct 25, 2023
2 parents 0276ce5 + 7a99206 commit 328fed8
Show file tree
Hide file tree
Showing 10 changed files with 369 additions and 42 deletions.
8 changes: 4 additions & 4 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ module.exports = [
{
name: 'CJS',
path: 'dist/lightweight-charts.production.cjs',
limit: '47.78 KB',
limit: '47.82 KB',
},
{
name: 'ESM',
path: 'dist/lightweight-charts.production.mjs',
limit: '47.72 KB',
limit: '47.76 KB',
},
{
name: 'Standalone-ESM',
path: 'dist/lightweight-charts.standalone.production.mjs',
limit: '49.44 KB',
limit: '49.47 KB',
},
{
name: 'Standalone',
path: 'dist/lightweight-charts.standalone.production.js',
limit: '49.48 KB',
limit: '49.52 KB',
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const chart = createChart('chart', {
timeScale: {
secondsVisible: true,
timeVisible: true,
rightOffset: 20,
allowShiftVisibleRangeOnWhitespaceReplacement: true,
},
});

Expand All @@ -24,9 +26,6 @@ lineSeries.setData(initialData);

const priceAlerts = new ExpiringPriceAlerts(lineSeries, { interval: 60 });

const rightMarginPosition = 20;
chart.timeScale().scrollToPosition(rightMarginPosition, false);

// The rest simulates updates and the user adding price alerts.

const simulateUserPriceAlerts = (time: Time) => {
Expand Down Expand Up @@ -100,29 +99,13 @@ function* getNextRealtimeUpdate(realtimeData: LineData[]) {
}
const streamingDataProvider = getNextRealtimeUpdate(realtimeUpdates);

// Since we are using whitespace to draw into the future
// updates which are within that whitespace time won't
// trigger an automatic scrolling of the chart
// We are thus doing this manually we we update the series,
// but should check if the user has scrolled and then disable that.
let userScrolledChart = false;
chart.timeScale().subscribeVisibleLogicalRangeChange(() => {
const pos = chart.timeScale().scrollPosition();
if (pos !== 0 && Math.abs(pos - rightMarginPosition) > 1) {
userScrolledChart = true;
}
});

const intervalID = window.setInterval(() => {
const update = streamingDataProvider.next();
if (update.done) {
window.clearInterval(intervalID);
return;
}
lineSeries.update(update.value);
if (!userScrolledChart) {
chart.timeScale().scrollToPosition(rightMarginPosition, false);
}
simulateUserPriceAlerts(update.value.time);
}, 200);

1 change: 1 addition & 0 deletions src/api/options/time-scale-options-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const timeScaleOptionsDefaults: HorzScaleOptions = {
timeVisible: false,
secondsVisible: true,
shiftVisibleRangeOnNewBar: true,
allowShiftVisibleRangeOnWhitespaceReplacement: false,
ticksVisible: false,
uniformDistribution: false,
minimumHeight: 0,
Expand Down
14 changes: 3 additions & 11 deletions src/model/chart-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,6 @@ export class ChartModel<HorzScaleItem> implements IDestroyable, IChartModelBase
public updateTimeScale(newBaseIndex: TimePointIndex | null, newPoints?: readonly TimeScalePoint[], firstChangedPointIndex?: number): void {
const oldFirstTime = this._timeScale.indexToTime(0 as TimePointIndex);

const oldLastIndex = this._timeScale.lastIndex();
if (newPoints !== undefined && firstChangedPointIndex !== undefined) {
this._timeScale.update(newPoints, firstChangedPointIndex);
}
Expand All @@ -812,16 +811,9 @@ export class ChartModel<HorzScaleItem> implements IDestroyable, IChartModelBase
const isSeriesPointsAdded = newBaseIndex !== null && newBaseIndex > currentBaseIndex;
const isSeriesPointsAddedToRight = isSeriesPointsAdded && !isLeftBarShiftToLeft;

// If the lastIndex of the time scale hasn't changed then this means
// that the new point/s has been placed on existing time index point/s.
// This can happen when you have a whitespace series which extends past
// the baseIndex, and now you are adding a new data point at one of those
// whitespace locations. In this case we would not want the chart to
// shift the visible range. #1201
const lastIndex = this._timeScale.lastIndex();
const replacedExistingWhitespace = lastIndex === oldLastIndex;

const needShiftVisibleRangeOnNewBar = isLastSeriesBarVisible && !replacedExistingWhitespace && this._timeScale.options().shiftVisibleRangeOnNewBar;
const allowShiftWhenReplacingWhitespace = this._timeScale.options().allowShiftVisibleRangeOnWhitespaceReplacement;
const replacedExistingWhitespace = firstChangedPointIndex === undefined;
const needShiftVisibleRangeOnNewBar = isLastSeriesBarVisible && (!replacedExistingWhitespace || allowShiftWhenReplacingWhitespace) && this._timeScale.options().shiftVisibleRangeOnNewBar;
if (isSeriesPointsAddedToRight && !needShiftVisibleRangeOnNewBar) {
const compensationShift = newBaseIndex - currentBaseIndex;
this._timeScale.setRightOffset(this._timeScale.rightOffset() - compensationShift);
Expand Down
24 changes: 17 additions & 7 deletions src/model/time-scale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@ export interface HorzScaleOptions {
*/
shiftVisibleRangeOnNewBar: boolean;

/**
* Allow the visible range to be shifted to the right when a new bar is added which
* is replacing an existing whitespace time point on the chart.
*
* Note that this only applies when the last bar is visible & `shiftVisibleRangeOnNewBar` is enabled.
*
* @defaultValue `false`
*/
allowShiftVisibleRangeOnWhitespaceReplacement: boolean;

/**
* Draw small vertical line on time axis labels.
*
Expand Down Expand Up @@ -360,7 +370,7 @@ export class TimeScale<HorzScaleItem> implements ITimeScale {
const to = Math.round(range.to);

const firstIndex = ensureNotNull(this._firstIndex());
const lastIndex = ensureNotNull(this.lastIndex());
const lastIndex = ensureNotNull(this._lastIndex());

return {
from: ensureNotNull(this.indexToTimeScalePoint(Math.max(firstIndex, from) as TimePointIndex)),
Expand Down Expand Up @@ -506,7 +516,7 @@ export class TimeScale<HorzScaleItem> implements ITimeScale {
const earliestIndexOfSecondLabel = (this._firstIndex() as number) + indexPerLabel;

// according to indexPerLabel value this value means "earliest index which _might be_ used as the second last label on time scale"
const indexOfSecondLastLabel = (this.lastIndex() as number) - indexPerLabel;
const indexOfSecondLastLabel = (this._lastIndex() as number) - indexPerLabel;

const isAllScalingAndScrollingDisabled = this._isAllScalingAndScrollingDisabled();
const isLeftEdgeFixed = this._options.fixLeftEdge || isAllScalingAndScrollingDisabled;
Expand Down Expand Up @@ -734,7 +744,7 @@ export class TimeScale<HorzScaleItem> implements ITimeScale {

public fitContent(): void {
const first = this._firstIndex();
const last = this.lastIndex();
const last = this._lastIndex();
if (first === null || last === null) {
return;
}
Expand All @@ -758,10 +768,6 @@ export class TimeScale<HorzScaleItem> implements ITimeScale {
return this._horzScaleBehavior.formatHorzItem(timeScalePoint.time);
}

public lastIndex(): TimePointIndex | null {
return this._points.length === 0 ? null : (this._points.length - 1) as TimePointIndex;
}

private _isAllScalingAndScrollingDisabled(): boolean {
const { handleScroll, handleScale } = this._model.options();
return !handleScroll.horzTouchDrag
Expand All @@ -778,6 +784,10 @@ export class TimeScale<HorzScaleItem> implements ITimeScale {
return this._points.length === 0 ? null : 0 as TimePointIndex;
}

private _lastIndex(): TimePointIndex | null {
return this._points.length === 0 ? null : (this._points.length - 1) as TimePointIndex;
}

private _rightOffsetForCoordinate(x: Coordinate): number {
return (this._width - 1 - x) / this._barSpacing;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const seriesOneData = [
{ time: '2019-04-11', value: 80.01 },
{ time: '2019-04-12', value: 96.63 },
{ time: '2019-04-13', value: 76.64 },
{ time: '2019-04-14', value: 81.89 },
{ time: '2019-04-15', value: 74.43 },
{ time: '2019-04-16', value: 80.01 },
{ time: '2019-04-17' },
];

const seriesTwoWhitespaceData = [
{ time: '2019-04-17' },
{ time: '2019-04-18' },
{ time: '2019-04-19' },
{ time: '2019-04-20' },
];

function runTestCase(container) {
const chart = window.chart = LightweightCharts.createChart(container, {
timeScale: {
barSpacing: 30,
rightOffset: 0,
shiftVisibleRangeOnNewBar: true,
allowShiftVisibleRangeOnWhitespaceReplacement: true,
},
});

const s1 = chart.addAreaSeries({
lineColor: 'rgb(0, 50, 200)',
topColor: 'rgba(0, 50, 200, 0.2)',
bottomColor: 'rgba(0, 50, 200, 0.2)',
});
s1.setData(seriesOneData);

return new Promise(resolve => {
requestAnimationFrame(() => {
const s2 = chart.addLineSeries({
color: 'black',
});
s2.setData(seriesTwoWhitespaceData);
requestAnimationFrame(() => {
s1.update({ time: '2019-04-17', value: 84.43, lineColor: 'green', topColor: 'rgba(0, 200, 50, 0.2)', bottomColor: 'rgba(0, 200, 50, 0.2)' });
requestAnimationFrame(() => {
s1.update({ time: '2019-04-18', value: 86.43, lineColor: 'green', topColor: 'rgba(0, 200, 50, 0.2)', bottomColor: 'rgba(0, 200, 50, 0.2)' });
requestAnimationFrame(resolve);
});
});
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ function runTestCase(container) {
barSpacing: 30,
rightOffset: 10,
shiftVisibleRangeOnNewBar: true,
shiftVisibleRangeWhenNewBarReplacesWhitespace: false,
},
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
It is expected that 10 bars should be visible.
*/
const startData = [
{
time: '2019-05-23',
open: 59.0,
high: 59.27,
low: 58.54,
close: 58.87,
},
{
time: '2019-05-24',
open: 59.07,
high: 59.36,
low: 58.67,
close: 59.32,
},
{
time: '2019-05-28',
open: 59.21,
high: 59.66,
low: 59.02,
close: 59.57,
},
];

const updates = [
{
time: '2019-05-29',
open: 59.0,
high: 59.27,
low: 58.54,
close: 58.87,
},
{
time: '2019-05-29',
open: 59.07,
high: 59.36,
low: 58.67,
close: 59.32,
},
{
time: '2019-05-29',
open: 59.21,
high: 59.66,
low: 59.02,
close: 59.57,
},
{
time: '2019-05-30',
open: 59.0,
high: 59.27,
low: 58.54,
close: 58.87,
},
{
time: '2019-05-31',
open: 59.07,
high: 59.36,
low: 58.67,
close: 59.32,
},
{
time: '2019-06-01',
},
{
time: '2019-06-01',
open: 59.0,
high: 59.27,
low: 58.54,
close: 58.87,
},
{
time: '2019-06-02',
open: 59.0,
high: 59.27,
low: 58.54,
close: 58.87,
},
{
time: '2019-06-03',
},
{
time: '2019-06-03',
open: 59.21,
high: 59.66,
low: 59.02,
close: 59.57,
},
{
time: '2019-06-04',
open: 59.0,
high: 59.27,
low: 58.54,
close: 58.87,
color: 'rgb(0,0,255)',
wickColor: 'rgb(0,0,255)',
borderColor: 'rgb(0,0,255)',
},
];

function runTestCase(container) {
const chart = (window.chart = LightweightCharts.createChart(container, {
timeScale: {
barSpacing: 12,
shiftVisibleRangeOnNewBar: true,
/*
! NOTE !
We need to set a rightOffset to a number large enough to cover the
amount of whitespaces we will be adding. Since adding whitespace isn't
meant to shift the timescale, and shifting only works when the last
bar is visible. We need to make sure that the whitespaces are visible
so the updates are on on a visible bar.
*/
rightOffset: 2,
},
}));

const s1 = chart.addCandlestickSeries();
s1.setData(startData);

return new Promise(resolve => {
let index = 0;
const intervalId = setInterval(() => {
s1.update(updates[index]);
index += 1;
if (index >= updates.length) {
clearInterval(intervalId);
requestAnimationFrame(resolve);
}
}, 10);
});
}

0 comments on commit 328fed8

Please sign in to comment.