Skip to content

Commit

Permalink
Merge pull request #499 from tradingview/fix209-whitespaces
Browse files Browse the repository at this point in the history
Added whitespaces support
  • Loading branch information
timocov committed Jun 29, 2020
2 parents 88ddd43 + 2b69572 commit 2124831
Show file tree
Hide file tree
Showing 18 changed files with 570 additions and 76 deletions.
2 changes: 1 addition & 1 deletion docs/area-series.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ areaSeries.setData([

## Data format

Each area series item should have the following field:
Each area series item should be a [whitespace](./whitespace-data.md) item or an object with the following fields:

- `time` ([Time](./time.md)) - item time
- `value` (`number`) - item value
Expand Down
2 changes: 1 addition & 1 deletion docs/bar-series.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ barSeries.setData([

## Data format

Each item of the bar series is [OHLC](./ohlc.md) item.
Each item of the bar series is either an [OHLC](./ohlc.md) or a [whitespace](./whitespace-data.md) item.

## Customization

Expand Down
2 changes: 1 addition & 1 deletion docs/candlestick-series.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ candlestickSeries.setData([

## Data format

Each item of the candlestick series is an [OHLC](./ohlc.md) item.
Each item of the candlestick series is either an [OHLC](./ohlc.md) or a [whitespace](./whitespace-data.md) item.

## Customization

Expand Down
2 changes: 1 addition & 1 deletion docs/histogram-series.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ histogramSeries.setData([

## Data format

Each item of the histogram series should include the following field:
Each item of the histogram series should be a [whitespace](./whitespace-data.md) item or an object with the following fields:

- `time` ([Time](./time.md)) - item time
- `value` (`number`) - item value
Expand Down
2 changes: 1 addition & 1 deletion docs/line-series.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ lineSeries.setData([

## Data format

Each item of the line series should include the following field:
Each item of the line series should be a [whitespace](./whitespace-data.md) item or an object with the following fields:

- `time` ([Time](./time.md)) - a time of the item
- `value` (`number`) - a value of the item
Expand Down
30 changes: 30 additions & 0 deletions docs/whitespace-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Whitespace Data

A whitespace - an empty space on the chart, which extends timescale, but doesn't have a value for the series.

A whitespace item is an object with the only one field:

- `time` ([Time](./time.md)) - whitespace time

Example:

```javascript
// note it might be any type of series here
const series = chart.addHistogramSeries();

series.setData([
{ time: "2018-12-01", value: 32.51 },
{ time: "2018-12-02", value: 31.11 },
{ time: "2018-12-03", value: 27.02 },
{ time: "2018-12-04" }, // whitespace
{ time: "2018-12-05" }, // whitespace
{ time: "2018-12-06" }, // whitespace
{ time: "2018-12-07" }, // whitespace
{ time: "2018-12-08", value: 23.92 },
{ time: "2018-12-09", value: 22.68 },
{ time: "2018-12-10", value: 22.67 },
{ time: "2018-12-11", value: 27.57 },
{ time: "2018-12-12", value: 24.11 },
{ time: "2018-12-13", value: 30.74 },
]);
```
5 changes: 1 addition & 4 deletions src/api/chart-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,10 +334,7 @@ export class ChartApi implements IChartApi, IPriceScaleApiProvider, DataUpdatesC
private _sendUpdateToChart(update: DataUpdateResponse): void {
const model = this._chartWidget.model();

if (update.timeScale !== undefined) {
model.updateTimeScale(update.timeScale.points, update.timeScale.baseIndex);
}

model.updateTimeScale(update.timeScale.baseIndex, update.timeScale.points);
update.series.forEach((value: SeriesChanges, series: Series) => series.updateData(value.data, value.fullUpdate));

model.recalculateAllPanes();
Expand Down
21 changes: 16 additions & 5 deletions src/api/data-consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ export function isUTCTimestamp(time: Time): time is UTCTimestamp {
return isNumber(time);
}

/**
* Structure describing whitespace data item (data point without series' data)
*/
export interface WhitespaceData {
time: Time;
}

/**
* Structure describing single data item for series of type Line or Area
*/
Expand Down Expand Up @@ -45,12 +52,16 @@ export interface BarData {
close: number;
}

export function isWhitespaceData(data: SeriesDataItemTypeMap[SeriesType]): data is WhitespaceData {
return (data as Partial<BarData>).open === undefined && (data as Partial<LineData>).value === undefined;
}

export interface SeriesDataItemTypeMap {
Bar: BarData;
Candlestick: BarData;
Area: LineData;
Line: LineData;
Histogram: HistogramData;
Bar: BarData | WhitespaceData;
Candlestick: BarData | WhitespaceData;
Area: LineData | WhitespaceData;
Line: LineData | WhitespaceData;
Histogram: HistogramData | WhitespaceData;
}

export interface DataUpdatesConsumer<TSeriesType extends SeriesType> {
Expand Down
94 changes: 68 additions & 26 deletions src/api/data-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
SeriesDataItemTypeMap,
Time,
} from './data-consumer';
import { getSeriesPlotRowCreator } from './get-series-plot-row-creator';
import { getSeriesPlotRowCreator, isSeriesPlotRow, WhitespacePlotRow } from './get-series-plot-row-creator';
import { fillWeightsForPoints } from './time-scale-point-weight-generator';

type TimedData = Pick<SeriesDataItemTypeMap[SeriesType], 'time'>;
Expand Down Expand Up @@ -105,7 +105,7 @@ export interface TimeScaleChanges {
/**
* An array of the new time scale points
*/
points: readonly TimeScalePoint[];
points?: readonly TimeScalePoint[];

/**
* In terms of time scale "base index" means the latest time scale point with data (there might be whitespaces)
Expand Down Expand Up @@ -134,7 +134,7 @@ export interface DataUpdateResponse {
/**
* Contains optional time scale points
*/
timeScale?: TimeScaleChanges;
timeScale: TimeScaleChanges;
}

interface TimePointData {
Expand All @@ -143,7 +143,7 @@ interface TimePointData {

// actually the type of the value should be related to the series' type (generic type)
// here, in data layer all data for us is "mutable" by default, but to the chart we provide "readonly" data, to avoid modifying it
mapping: Map<Series, Mutable<SeriesPlotRow>>;
mapping: Map<Series, Mutable<SeriesPlotRow | WhitespacePlotRow>>;
}

function createEmptyTimePointData(timePoint: TimePoint): TimePointData {
Expand Down Expand Up @@ -174,7 +174,7 @@ export class DataLayer {
this._pointDataByTimePoint.forEach((pointData: TimePointData) => pointData.mapping.delete(series));
}

let seriesRows: SeriesPlotRow[] = [];
let seriesRows: (SeriesPlotRow | WhitespacePlotRow)[] = [];

if (data.length !== 0) {
convertStringsToBusinessDays(data);
Expand Down Expand Up @@ -237,21 +237,29 @@ export class DataLayer {
const plotRow = createPlotRow(time, pointDataAtTime.index, data);
pointDataAtTime.mapping.set(series, plotRow);

this._updateLastSeriesRow(series, plotRow);
const seriesChanges = this._updateLastSeriesRow(series, plotRow);

// 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) {
const seriesUpdate = new Map<Series, SeriesChanges>();
const seriesData = ensureDefined(this._seriesRowsBySeries.get(series));
seriesUpdate.set(series, { data: [seriesData[seriesData.length - 1]], fullUpdate: false });
return { series: seriesUpdate };
if (seriesChanges !== null) {
seriesUpdate.set(series, seriesChanges);
}

return {
series: seriesUpdate,
timeScale: {
// base index might be updated even if no time scale point is changed
baseIndex: this._getBaseIndex(),
},
};
}

// but if we don't have such point on the time scale - we need to generate "full" update (including time scale update)
return this._syncIndexesAndApplyChanges(series);
}

private _updateLastSeriesRow(series: Series, plotRow: SeriesPlotRow): void {
private _updateLastSeriesRow(series: Series, plotRow: SeriesPlotRow | WhitespacePlotRow): SeriesChanges | null {
let seriesData = this._seriesRowsBySeries.get(series);
if (seriesData === undefined) {
seriesData = [];
Expand All @@ -260,18 +268,44 @@ export class DataLayer {

const lastSeriesRow = seriesData.length !== 0 ? seriesData[seriesData.length - 1] : null;

let result: SeriesChanges | null = null;

if (lastSeriesRow === null || plotRow.time.timestamp > lastSeriesRow.time.timestamp) {
seriesData.push(plotRow);
if (isSeriesPlotRow(plotRow)) {
seriesData.push(plotRow);

result = {
fullUpdate: false,
data: [plotRow],
};
}
} else {
seriesData[seriesData.length - 1] = plotRow;
if (isSeriesPlotRow(plotRow)) {
seriesData[seriesData.length - 1] = plotRow;

result = {
fullUpdate: false,
data: [plotRow],
};
} else {
seriesData.splice(-1, 1);

// we just removed point from series - needs generate full update
result = {
fullUpdate: true,
data: seriesData,
};
}
}

this._seriesLastTimePoint.set(series, plotRow.time);

return result;
}

private _setRowsToSeries(series: Series, seriesRows: SeriesPlotRow[]): void {
private _setRowsToSeries(series: Series, seriesRows: (SeriesPlotRow | WhitespacePlotRow)[]): void {
if (seriesRows.length !== 0) {
this._seriesRowsBySeries.set(series, seriesRows);
this._seriesRowsBySeries.set(series, seriesRows.filter(isSeriesPlotRow));
this._seriesLastTimePoint.set(series, seriesRows[seriesRows.length - 1].time);
} else {
this._seriesRowsBySeries.delete(series);
Expand Down Expand Up @@ -333,7 +367,7 @@ export class DataLayer {
pointData.index = index as TimePointIndex;

// and then we need to sync indexes for all series
pointData.mapping.forEach((seriesRow: Mutable<SeriesPlotRow>) => {
pointData.mapping.forEach((seriesRow: Mutable<SeriesPlotRow | WhitespacePlotRow>) => {
seriesRow.index = index as TimePointIndex;
});
}
Expand All @@ -346,6 +380,18 @@ export class DataLayer {
return firstChangedPointIndex;
}

private _getBaseIndex(): TimePointIndex {
let baseIndex = 0 as TimePointIndex;

this._seriesRowsBySeries.forEach((data: SeriesPlotRow[]) => {
if (data.length !== 0) {
baseIndex = Math.max(baseIndex, data[data.length - 1].index) as TimePointIndex;
}
});

return baseIndex;
}

/**
* Methods syncs indexes (recalculates them applies them to point/series data) between time scale, point data and series point
* and returns generated update for applied change.
Expand All @@ -358,26 +404,22 @@ export class DataLayer {

const firstChangedPointIndex = this._updateTimeScalePoints(newTimeScalePoints);

const dataUpdateResponse: DataUpdateResponse = { series: new Map() };
const dataUpdateResponse: DataUpdateResponse = {
series: new Map(),
timeScale: {
baseIndex: this._getBaseIndex(),
},
};

if (firstChangedPointIndex !== -1) {
let baseIndex = 0 as TimePointIndex;

// time scale is changed, so we need to make "full" update for every series
// TODO: it's possible to make perf improvements by checking what series has data after firstChangedPointIndex
// but let's skip for now
this._seriesRowsBySeries.forEach((data: SeriesPlotRow[], s: Series) => {
dataUpdateResponse.series.set(s, { data, fullUpdate: true });

if (data.length !== 0) {
baseIndex = Math.max(baseIndex, data[data.length - 1].index) as TimePointIndex;
}
});

dataUpdateResponse.timeScale = {
points: this._sortedTimePoints,
baseIndex,
};
dataUpdateResponse.timeScale.points = this._sortedTimePoints;
} else {
const seriesData = this._seriesRowsBySeries.get(series);
// if no seriesData found that means that we just removed the series
Expand Down
37 changes: 27 additions & 10 deletions src/api/get-series-plot-row-creator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { PlotRow } from '../model/plot-data';
import { SeriesPlotRow } from '../model/series-data';
import { SeriesType } from '../model/series-options';
import { TimePoint, TimePointIndex } from '../model/time-data';

import { BarData, HistogramData, LineData, SeriesDataItemTypeMap } from './data-consumer';
import { BarData, HistogramData, isWhitespaceData, LineData, SeriesDataItemTypeMap } from './data-consumer';

function getLineBasedSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: LineData | HistogramData): Mutable<SeriesPlotRow<'Line' | 'Histogram'>> {
const val = item.value;
Expand All @@ -15,26 +16,42 @@ function getLineBasedSeriesPlotRow(time: TimePoint, index: TimePointIndex, item:
return res;
}

function getOHLCBasedSeriesPlotRow(time: TimePoint, index: TimePointIndex, bar: BarData): Mutable<SeriesPlotRow> {
return { index, time, value: [bar.open, bar.high, bar.low, bar.close] };
function getOHLCBasedSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: BarData): Mutable<SeriesPlotRow> {
return { index, time, value: [item.open, item.high, item.low, item.close] };
}

export type WhitespacePlotRow = Omit<PlotRow, 'value'>;

export function isSeriesPlotRow(row: SeriesPlotRow | WhitespacePlotRow): row is SeriesPlotRow {
return 'value' in row;
}

// we want to have compile-time checks that the type of the functions is correct
// but due contravariance we cannot easily use type of values of the SeriesItemValueFnMap map itself
// so let's use TimedSeriesItemValueFn for shut up the compiler in seriesItemValueFn
// we need to be sure (and we're sure actually) that stored data has correct type for it's according series object
type SeriesItemValueFnMap = {
[T in keyof SeriesDataItemTypeMap]: (time: TimePoint, index: TimePointIndex, item: SeriesDataItemTypeMap[T]) => Mutable<SeriesPlotRow>;
[T in keyof SeriesDataItemTypeMap]: (time: TimePoint, index: TimePointIndex, item: SeriesDataItemTypeMap[T]) => Mutable<SeriesPlotRow | WhitespacePlotRow>;
};

export type TimedSeriesItemValueFn = (time: TimePoint, index: TimePointIndex, item: SeriesDataItemTypeMap[SeriesType]) => Mutable<SeriesPlotRow>;
export type TimedSeriesItemValueFn = (time: TimePoint, index: TimePointIndex, item: SeriesDataItemTypeMap[SeriesType]) => Mutable<SeriesPlotRow | WhitespacePlotRow>;

function wrapWhitespaceData(createPlotRowFn: (typeof getLineBasedSeriesPlotRow) | (typeof getOHLCBasedSeriesPlotRow)): TimedSeriesItemValueFn {
return (time: TimePoint, index: TimePointIndex, bar: SeriesDataItemTypeMap[SeriesType]) => {
if (isWhitespaceData(bar)) {
return { time, index };
}

return createPlotRowFn(time, index, bar);
};
}

const seriesPlotRowFnMap: SeriesItemValueFnMap = {
Candlestick: getOHLCBasedSeriesPlotRow,
Bar: getOHLCBasedSeriesPlotRow,
Area: getLineBasedSeriesPlotRow,
Histogram: getLineBasedSeriesPlotRow,
Line: getLineBasedSeriesPlotRow,
Candlestick: wrapWhitespaceData(getOHLCBasedSeriesPlotRow),
Bar: wrapWhitespaceData(getOHLCBasedSeriesPlotRow),
Area: wrapWhitespaceData(getLineBasedSeriesPlotRow),
Histogram: wrapWhitespaceData(getLineBasedSeriesPlotRow),
Line: wrapWhitespaceData(getLineBasedSeriesPlotRow),
};

export function getSeriesPlotRowCreator(seriesType: SeriesType): TimedSeriesItemValueFn {
Expand Down
6 changes: 4 additions & 2 deletions src/model/chart-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,8 +461,10 @@ export class ChartModel implements IDestroyable {
}
}

public updateTimeScale(newPoints: readonly TimeScalePoint[], newBaseIndex: TimePointIndex): void {
this._timeScale.update(newPoints);
public updateTimeScale(newBaseIndex: TimePointIndex, newPoints?: readonly TimeScalePoint[]): void {
if (newPoints !== undefined) {
this._timeScale.update(newPoints);
}

const currentBaseIndex = this._timeScale.baseIndex();
const visibleBars = this._timeScale.visibleStrictRange();
Expand Down
Loading

0 comments on commit 2124831

Please sign in to comment.