Skip to content

Commit

Permalink
Merge pull request #1323 from tradingview/horz-scale-as-numbers
Browse files Browse the repository at this point in the history
Implemented option to use non time-based values for the horizontal scale
  • Loading branch information
SlicedSilver committed Aug 16, 2023
2 parents 981ca1b + 5e6320e commit 174f6c3
Show file tree
Hide file tree
Showing 93 changed files with 2,132 additions and 1,349 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.07 KB',
limit: '47.58 KB',
},
{
name: 'ESM',
path: 'dist/lightweight-charts.production.mjs',
limit: '47.01 KB',
limit: '47.53 KB',
},
{
name: 'Standalone-ESM',
path: 'dist/lightweight-charts.standalone.production.mjs',
limit: '48.70 KB',
limit: '49.25 KB',
},
{
name: 'Standalone',
path: 'dist/lightweight-charts.standalone.production.js',
limit: '48.74 KB',
limit: '49.3 KB',
},
];
2 changes: 1 addition & 1 deletion src/api/candlestick-series-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {

import { SeriesApi } from './series-api';

export class CandlestickSeriesApi extends SeriesApi<'Candlestick'> {
export class CandlestickSeriesApi<HorzScaleItem> extends SeriesApi<'Candlestick', HorzScaleItem> {
public override applyOptions(options: CandlestickSeriesPartialOptions): void {
fillUpDownCandlesticksColors(options);
super.applyOptions(options);
Expand Down
125 changes: 65 additions & 60 deletions src/api/chart-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { Delegate } from '../helpers/delegate';
import { warn } from '../helpers/logger';
import { clone, DeepPartial, isBoolean, merge } from '../helpers/strict-type-checks';

import { ChartOptions, ChartOptionsInternal } from '../model/chart-model';
import { ChartOptionsImpl, ChartOptionsInternal } from '../model/chart-model';
import { DataUpdatesConsumer, isFulfilledData, SeriesDataItemTypeMap, WhitespaceData } from '../model/data-consumer';
import { DataLayer, DataUpdateResponse, SeriesChanges } from '../model/data-layer';
import { CustomData, ICustomSeriesPaneView } from '../model/icustom-series';
import { IHorzScaleBehavior } from '../model/ihorz-scale-behavior';
import { Series } from '../model/series';
import { SeriesPlotRow } from '../model/series-data';
import {
Expand All @@ -28,12 +31,10 @@ import {
SeriesStyleOptionsMap,
SeriesType,
} from '../model/series-options';
import { Logical, Time } from '../model/time-data';
import { Logical } from '../model/time-data';

import { DataUpdatesConsumer, isFulfilledData, SeriesDataItemTypeMap, WhitespaceData } from './data-consumer';
import { DataLayer, DataUpdateResponse, SeriesChanges } from './data-layer';
import { getSeriesDataCreator } from './get-series-data-creator';
import { IChartApi, MouseEventHandler, MouseEventParams } from './ichart-api';
import { IChartApiBase, MouseEventHandler, MouseEventParams } from './ichart-api';
import { IPriceScaleApi } from './iprice-scale-api';
import { ISeriesApi } from './iseries-api';
import { ITimeScaleApi } from './itime-scale-api';
Expand Down Expand Up @@ -62,7 +63,7 @@ function patchPriceFormat(priceFormat?: DeepPartial<PriceFormat>): void {
}
}

function migrateHandleScaleScrollOptions(options: DeepPartial<ChartOptions>): void {
function migrateHandleScaleScrollOptions<HorzScaleItem>(options: DeepPartial<ChartOptionsImpl<HorzScaleItem>>): void {
if (isBoolean(options.handleScale)) {
const handleScale = options.handleScale;
options.handleScale = {
Expand Down Expand Up @@ -104,32 +105,36 @@ function migrateHandleScaleScrollOptions(options: DeepPartial<ChartOptions>): vo
}
}

function toInternalOptions(options: DeepPartial<ChartOptions>): DeepPartial<ChartOptionsInternal> {
function toInternalOptions<HorzScaleItem>(options: DeepPartial<ChartOptionsImpl<HorzScaleItem>>): DeepPartial<ChartOptionsInternal<HorzScaleItem>> {
migrateHandleScaleScrollOptions(options);

return options as DeepPartial<ChartOptionsInternal>;
return options as DeepPartial<ChartOptionsInternal<HorzScaleItem>>;
}

export type IPriceScaleApiProvider = Pick<IChartApi, 'priceScale'>;
export type IPriceScaleApiProvider<HorzScaleItem> = Pick<IChartApiBase<HorzScaleItem>, 'priceScale'>;

export class ChartApi implements IChartApi, DataUpdatesConsumer<SeriesType> {
private _chartWidget: ChartWidget;
private _dataLayer: DataLayer = new DataLayer();
private readonly _seriesMap: Map<SeriesApi<SeriesType>, Series> = new Map();
private readonly _seriesMapReversed: Map<Series, SeriesApi<SeriesType>> = new Map();
export class ChartApi<HorzScaleItem> implements IChartApiBase<HorzScaleItem>, DataUpdatesConsumer<SeriesType, HorzScaleItem> {
private _chartWidget: ChartWidget<HorzScaleItem>;
private _dataLayer: DataLayer<HorzScaleItem>;
private readonly _seriesMap: Map<SeriesApi<SeriesType, HorzScaleItem>, Series<SeriesType>> = new Map();
private readonly _seriesMapReversed: Map<Series<SeriesType>, SeriesApi<SeriesType, HorzScaleItem>> = new Map();

private readonly _clickedDelegate: Delegate<MouseEventParams> = new Delegate();
private readonly _dblClickedDelegate: Delegate<MouseEventParams> = new Delegate();
private readonly _crosshairMovedDelegate: Delegate<MouseEventParams> = new Delegate();
private readonly _clickedDelegate: Delegate<MouseEventParams<HorzScaleItem>> = new Delegate();
private readonly _dblClickedDelegate: Delegate<MouseEventParams<HorzScaleItem>> = new Delegate();
private readonly _crosshairMovedDelegate: Delegate<MouseEventParams<HorzScaleItem>> = new Delegate();

private readonly _timeScaleApi: TimeScaleApi;
private readonly _timeScaleApi: TimeScaleApi<HorzScaleItem>;

public constructor(container: HTMLElement, options?: DeepPartial<ChartOptions>) {
private readonly _horzScaleBehavior: IHorzScaleBehavior<HorzScaleItem>;

public constructor(container: HTMLElement, horzScaleBehavior: IHorzScaleBehavior<HorzScaleItem>, options?: DeepPartial<ChartOptionsImpl<HorzScaleItem>>) {
this._dataLayer = new DataLayer<HorzScaleItem>(horzScaleBehavior);
const internalOptions = (options === undefined) ?
clone(chartOptionsDefaults) :
merge(clone(chartOptionsDefaults), toInternalOptions(options)) as ChartOptionsInternal;
clone(chartOptionsDefaults<HorzScaleItem>()) :
merge(clone(chartOptionsDefaults()), toInternalOptions(options)) as ChartOptionsInternal<HorzScaleItem>;

this._chartWidget = new ChartWidget(container, internalOptions);
this._horzScaleBehavior = horzScaleBehavior;
this._chartWidget = new ChartWidget(container, internalOptions, horzScaleBehavior);

this._chartWidget.clicked().subscribe(
(paramSupplier: MouseEventParamsImplSupplier) => {
Expand Down Expand Up @@ -157,7 +162,7 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer<SeriesType> {
);

const model = this._chartWidget.model();
this._timeScaleApi = new TimeScaleApi(model, this._chartWidget.timeAxisWidget());
this._timeScaleApi = new TimeScaleApi(model, this._chartWidget.timeAxisWidget(), this._horzScaleBehavior);
}

public remove(): void {
Expand Down Expand Up @@ -188,13 +193,13 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer<SeriesType> {
}

public addCustomSeries<
TData extends CustomData,
TData extends CustomData<HorzScaleItem>,
TOptions extends CustomSeriesOptions,
TPartialOptions extends CustomSeriesPartialOptions = SeriesPartialOptions<TOptions>
>(
customPaneView: ICustomSeriesPaneView<TData, TOptions>,
customPaneView: ICustomSeriesPaneView<HorzScaleItem, TData, TOptions>,
options?: SeriesPartialOptions<TOptions>
): ISeriesApi<'Custom', TData, TOptions, TPartialOptions> {
): ISeriesApi<'Custom', HorzScaleItem, TData, TOptions, TPartialOptions> {
const paneView = ensure(customPaneView);
const defaults = {
...customStyleDefaults,
Expand All @@ -208,33 +213,33 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer<SeriesType> {
);
}

public addAreaSeries(options?: AreaSeriesPartialOptions): ISeriesApi<'Area'> {
public addAreaSeries(options?: AreaSeriesPartialOptions): ISeriesApi<'Area', HorzScaleItem> {
return this._addSeriesImpl('Area', areaStyleDefaults, options);
}

public addBaselineSeries(options?: BaselineSeriesPartialOptions): ISeriesApi<'Baseline'> {
public addBaselineSeries(options?: BaselineSeriesPartialOptions): ISeriesApi<'Baseline', HorzScaleItem> {
return this._addSeriesImpl('Baseline', baselineStyleDefaults, options);
}

public addBarSeries(options?: BarSeriesPartialOptions): ISeriesApi<'Bar'> {
public addBarSeries(options?: BarSeriesPartialOptions): ISeriesApi<'Bar', HorzScaleItem> {
return this._addSeriesImpl('Bar', barStyleDefaults, options);
}

public addCandlestickSeries(options: CandlestickSeriesPartialOptions = {}): ISeriesApi<'Candlestick'> {
public addCandlestickSeries(options: CandlestickSeriesPartialOptions = {}): ISeriesApi<'Candlestick', HorzScaleItem> {
fillUpDownCandlesticksColors(options);

return this._addSeriesImpl('Candlestick', candlestickStyleDefaults, options);
}

public addHistogramSeries(options?: HistogramSeriesPartialOptions): ISeriesApi<'Histogram'> {
public addHistogramSeries(options?: HistogramSeriesPartialOptions): ISeriesApi<'Histogram', HorzScaleItem> {
return this._addSeriesImpl('Histogram', histogramStyleDefaults, options);
}

public addLineSeries(options?: LineSeriesPartialOptions): ISeriesApi<'Line'> {
public addLineSeries(options?: LineSeriesPartialOptions): ISeriesApi<'Line', HorzScaleItem> {
return this._addSeriesImpl('Line', lineStyleDefaults, options);
}

public removeSeries(seriesApi: SeriesApi<SeriesType>): void {
public removeSeries(seriesApi: SeriesApi<SeriesType, HorzScaleItem>): void {
const series = ensureDefined(this._seriesMap.get(seriesApi));

const update = this._dataLayer.removeSeries(series);
Expand All @@ -247,52 +252,52 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer<SeriesType> {
this._seriesMapReversed.delete(series);
}

public applyNewData<TSeriesType extends SeriesType>(series: Series<TSeriesType>, data: SeriesDataItemTypeMap[TSeriesType][]): void {
public applyNewData<TSeriesType extends SeriesType>(series: Series<TSeriesType>, data: SeriesDataItemTypeMap<HorzScaleItem>[TSeriesType][]): void {
this._sendUpdateToChart(this._dataLayer.setSeriesData(series, data));
}

public updateData<TSeriesType extends SeriesType>(series: Series<TSeriesType>, data: SeriesDataItemTypeMap[TSeriesType]): void {
public updateData<TSeriesType extends SeriesType>(series: Series<TSeriesType>, data: SeriesDataItemTypeMap<HorzScaleItem>[TSeriesType]): void {
this._sendUpdateToChart(this._dataLayer.updateSeriesData(series, data));
}

public subscribeClick(handler: MouseEventHandler): void {
public subscribeClick(handler: MouseEventHandler<HorzScaleItem>): void {
this._clickedDelegate.subscribe(handler);
}

public unsubscribeClick(handler: MouseEventHandler): void {
public unsubscribeClick(handler: MouseEventHandler<HorzScaleItem>): void {
this._clickedDelegate.unsubscribe(handler);
}

public subscribeDblClick(handler: MouseEventHandler): void {
this._dblClickedDelegate.subscribe(handler);
public subscribeCrosshairMove(handler: MouseEventHandler<HorzScaleItem>): void {
this._crosshairMovedDelegate.subscribe(handler);
}

public unsubscribeDblClick(handler: MouseEventHandler): void {
this._dblClickedDelegate.unsubscribe(handler);
public unsubscribeCrosshairMove(handler: MouseEventHandler<HorzScaleItem>): void {
this._crosshairMovedDelegate.unsubscribe(handler);
}

public subscribeCrosshairMove(handler: MouseEventHandler): void {
this._crosshairMovedDelegate.subscribe(handler);
public subscribeDblClick(handler: MouseEventHandler<HorzScaleItem>): void {
this._dblClickedDelegate.subscribe(handler);
}

public unsubscribeCrosshairMove(handler: MouseEventHandler): void {
this._crosshairMovedDelegate.unsubscribe(handler);
public unsubscribeDblClick(handler: MouseEventHandler<HorzScaleItem>): void {
this._dblClickedDelegate.unsubscribe(handler);
}

public priceScale(priceScaleId: string): IPriceScaleApi {
return new PriceScaleApi(this._chartWidget, priceScaleId);
}

public timeScale(): ITimeScaleApi {
public timeScale(): ITimeScaleApi<HorzScaleItem> {
return this._timeScaleApi;
}

public applyOptions(options: DeepPartial<ChartOptions>): void {
public applyOptions(options: DeepPartial<ChartOptionsImpl<HorzScaleItem>>): void {
this._chartWidget.applyOptions(toInternalOptions(options));
}

public options(): Readonly<ChartOptions> {
return this._chartWidget.options() as Readonly<ChartOptions>;
public options(): Readonly<ChartOptionsImpl<HorzScaleItem>> {
return this._chartWidget.options() as Readonly<ChartOptionsImpl<HorzScaleItem>>;
}

public takeScreenshot(): HTMLCanvasElement {
Expand All @@ -309,21 +314,21 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer<SeriesType> {

private _addSeriesImpl<
TSeries extends SeriesType,
TData extends WhitespaceData = SeriesDataItemTypeMap[TSeries],
TData extends WhitespaceData<HorzScaleItem> = SeriesDataItemTypeMap<HorzScaleItem>[TSeries],
TOptions extends SeriesOptionsMap[TSeries] = SeriesOptionsMap[TSeries],
TPartialOptions extends SeriesPartialOptionsMap[TSeries] = SeriesPartialOptionsMap[TSeries]
>(
type: TSeries,
styleDefaults: SeriesStyleOptionsMap[TSeries],
options: SeriesPartialOptionsMap[TSeries] = {},
customPaneView?: ICustomSeriesPaneView
): ISeriesApi<TSeries, TData, TOptions, TPartialOptions> {
customPaneView?: ICustomSeriesPaneView<HorzScaleItem>
): ISeriesApi<TSeries, HorzScaleItem, TData, TOptions, TPartialOptions> {
patchPriceFormat(options.priceFormat);

const strictOptions = merge(clone(seriesOptionsDefaults), clone(styleDefaults), options) as SeriesOptionsMap[TSeries];
const series = this._chartWidget.model().createSeries(type, strictOptions, customPaneView);

const res = new SeriesApi<TSeries, TData, TOptions, TPartialOptions>(series, this, this, this);
const res = new SeriesApi<TSeries, HorzScaleItem, TData, TOptions, TPartialOptions>(series, this, this, this, this._horzScaleBehavior);
this._seriesMap.set(res, series);
this._seriesMapReversed.set(series, res);

Expand All @@ -334,20 +339,20 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer<SeriesType> {
const model = this._chartWidget.model();

model.updateTimeScale(update.timeScale.baseIndex, update.timeScale.points, update.timeScale.firstChangedPointIndex);
update.series.forEach((value: SeriesChanges, series: Series) => series.setData(value.data, value.info));
update.series.forEach((value: SeriesChanges, series: Series<SeriesType>) => series.setData(value.data, value.info));

model.recalculateAllPanes();
}

private _mapSeriesToApi(series: Series): ISeriesApi<SeriesType> {
private _mapSeriesToApi(series: Series<SeriesType>): ISeriesApi<SeriesType, HorzScaleItem> {
return ensureDefined(this._seriesMapReversed.get(series));
}

private _convertMouseParams(param: MouseEventParamsImpl): MouseEventParams {
const seriesData: MouseEventParams['seriesData'] = new Map();
param.seriesData.forEach((plotRow: SeriesPlotRow, series: Series) => {
private _convertMouseParams(param: MouseEventParamsImpl): MouseEventParams<HorzScaleItem> {
const seriesData: MouseEventParams<HorzScaleItem>['seriesData'] = new Map();
param.seriesData.forEach((plotRow: SeriesPlotRow<SeriesType>, series: Series<SeriesType>) => {
const seriesType = series.seriesType();
const data = getSeriesDataCreator(seriesType)(plotRow);
const data = getSeriesDataCreator<SeriesType, HorzScaleItem>(seriesType)(plotRow);
if (seriesType !== 'Custom') {
assert(isFulfilledData(data));
} else {
Expand All @@ -360,7 +365,7 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer<SeriesType> {
const hoveredSeries = param.hoveredSeries === undefined ? undefined : this._mapSeriesToApi(param.hoveredSeries);

return {
time: param.time as Time | undefined,
time: param.originalTime as HorzScaleItem,
logical: param.index as Logical | undefined,
point: param.point,
hoveredSeries,
Expand Down
56 changes: 51 additions & 5 deletions src/api/create-chart.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
import { assert } from '../helpers/assertions';
import { DeepPartial, isString } from '../helpers/strict-type-checks';

import { ChartOptions } from '../model/chart-model';
import { HorzScaleBehaviorTime } from '../model/horz-scale-behavior-time/horz-scale-behavior-time';
import { TimeChartOptions } from '../model/horz-scale-behavior-time/time-based-chart-options';
import { Time } from '../model/horz-scale-behavior-time/types';
import { IHorzScaleBehavior } from '../model/ihorz-scale-behavior';

import { ChartApi } from './chart-api';
import { IChartApi } from './ichart-api';
import { IChartApiBase } from './ichart-api';

/**
* This function is the main entry point of the Lightweight Charting Library.
* This function is the main entry point of the Lightweight Charting Library. If you are using time values
* for the horizontal scale then it is recommended that you rather use the {@link createChart} function.
*
* @template HorzScaleItem - type of points on the horizontal scale
* @template THorzScaleBehavior - type of horizontal axis strategy that encapsulate all the specific behaviors of the horizontal scale type
*
* @param container - ID of HTML element or element itself
* @param horzScaleBehavior - Horizontal scale behavior
* @param options - Any subset of options to be applied at start.
* @returns An interface to the created chart
*/
export function createChart(container: string | HTMLElement, options?: DeepPartial<ChartOptions>): IChartApi {
export function createChartEx<HorzScaleItem, THorzScaleBehavior extends IHorzScaleBehavior<HorzScaleItem>>(
container: string | HTMLElement,
horzScaleBehavior: THorzScaleBehavior,
options?: DeepPartial<ReturnType<THorzScaleBehavior['options']>>
): IChartApiBase<HorzScaleItem> {
let htmlElement: HTMLElement;
if (isString(container)) {
const element = document.getElementById(container);
Expand All @@ -23,5 +35,39 @@ export function createChart(container: string | HTMLElement, options?: DeepParti
htmlElement = container;
}

return new ChartApi(htmlElement, options);
const res = new ChartApi<HorzScaleItem>(htmlElement, horzScaleBehavior, options);
horzScaleBehavior.setOptions(res.options());
return res;
}

/**
* Structure describing options of the chart with time points at the horizontal scale. Series options are to be set separately
*/
export type ChartOptions = TimeChartOptions;

/**
* The main interface of a single chart using time for horizontal scale.
*/
export interface IChartApi extends IChartApiBase<Time> {
/**
* Applies new options to the chart
*
* @param options - Any subset of options.
*/
applyOptions(options: DeepPartial<ChartOptions>): void;
}

/**
* This function is the simplified main entry point of the Lightweight Charting Library with time points for the horizontal scale.
*
* @param container - ID of HTML element or element itself
* @param options - Any subset of options to be applied at start.
* @returns An interface to the created chart
*/
export function createChart(container: string | HTMLElement, options?: DeepPartial<ChartOptions>): IChartApi {
return createChartEx<Time, HorzScaleBehaviorTime>(
container,
new HorzScaleBehaviorTime(),
HorzScaleBehaviorTime.applyDefaults(options)
);
}
Loading

0 comments on commit 174f6c3

Please sign in to comment.