diff --git a/.circleci/config.yml b/.circleci/config.yml index 164bb31681..a1bb4255e5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -180,6 +180,13 @@ jobs: - store_test_results: path: test-results/ + type-tests: + executor: node16-executor + steps: + - checkout-with-deps + - run: npm run tsc-verify + - run: npm run type-tests + dts-changes: executor: node16-executor steps: @@ -334,6 +341,10 @@ workflows: filters: *default-filters requires: - build + - type-tests: + filters: *default-filters + requires: + - build - dts-changes: filters: *merge-based-filters requires: diff --git a/.eslintrc.js b/.eslintrc.js index 08151466d6..90afb847f8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,7 +11,7 @@ function getNamingConventionRules(additionalDefaultFormats = []) { format: ['PascalCase'], filter: { match: true, - regex: '^(Area|Baseline|Bar|Candlestick|Histogram|Line)$', + regex: '^(Area|Baseline|Bar|Candlestick|Histogram|Line|Custom)$', }, }, ]; diff --git a/package.json b/package.json index 409c6305d5..6e043e4c95 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,8 @@ "prepare-release": "npm-run-all clean build:release && npm run prepare-package-json-for-release", "prepare-package-json-for-release": "node ./scripts/clean-package-json.js", "size-limit": "size-limit", - "verify": "npm-run-all clean -p build:prod check-markdown-links -p lint check-dts-docs tsc-verify test size-limit", - "test": "mocha tests/unittests/**/*.spec.ts" + "verify": "npm-run-all clean -p build:prod check-markdown-links -p lint check-dts-docs tsc-verify test size-limit -p type-tests", + "test": "mocha tests/unittests/**/*.spec.ts", + "type-tests": "tsc -p ./tests/type-checks/tsconfig.composite.json --noEmit" } } diff --git a/src/api/chart-api.ts b/src/api/chart-api.ts index 9c638641b3..2729a00e0d 100644 --- a/src/api/chart-api.ts +++ b/src/api/chart-api.ts @@ -1,11 +1,12 @@ import { ChartWidget, MouseEventParamsImpl, MouseEventParamsImplSupplier } from '../gui/chart-widget'; -import { assert, ensureDefined } from '../helpers/assertions'; +import { assert, ensure, ensureDefined } from '../helpers/assertions'; 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 { CustomData, ICustomSeriesPaneView } from '../model/icustom-series'; import { Series } from '../model/series'; import { SeriesPlotRow } from '../model/series-data'; import { @@ -13,6 +14,8 @@ import { BarSeriesPartialOptions, BaselineSeriesPartialOptions, CandlestickSeriesPartialOptions, + CustomSeriesOptions, + CustomSeriesPartialOptions, fillUpDownCandlesticksColors, HistogramSeriesPartialOptions, LineSeriesPartialOptions, @@ -20,13 +23,14 @@ import { PriceFormat, PriceFormatBuiltIn, SeriesOptionsMap, + SeriesPartialOptions, SeriesPartialOptionsMap, SeriesStyleOptionsMap, SeriesType, } from '../model/series-options'; import { Logical, Time } from '../model/time-data'; -import { DataUpdatesConsumer, isFulfilledData, SeriesDataItemTypeMap } from './data-consumer'; +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'; @@ -39,6 +43,7 @@ import { barStyleDefaults, baselineStyleDefaults, candlestickStyleDefaults, + customStyleDefaults, histogramStyleDefaults, lineStyleDefaults, seriesOptionsDefaults, @@ -171,6 +176,27 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer { this._chartWidget.resize(width, height, forceRepaint); } + public addCustomSeries< + TData extends CustomData, + TOptions extends CustomSeriesOptions, + TPartialOptions extends CustomSeriesPartialOptions = SeriesPartialOptions + >( + customPaneView: ICustomSeriesPaneView, + options?: SeriesPartialOptions + ): ISeriesApi<'Custom', TData, TOptions, TPartialOptions> { + const paneView = ensure(customPaneView); + const defaults = { + ...customStyleDefaults, + ...paneView.defaultOptions(), + }; + return this._addSeriesImpl<'Custom', TData, TOptions, TPartialOptions>( + 'Custom', + defaults, + options, + paneView + ); + } + public addAreaSeries(options?: AreaSeriesPartialOptions): ISeriesApi<'Area'> { return this._addSeriesImpl('Area', areaStyleDefaults, options); } @@ -258,17 +284,27 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer { return this._chartWidget.autoSizeActive(); } - private _addSeriesImpl( + public chartElement(): HTMLDivElement { + return this._chartWidget.element(); + } + + private _addSeriesImpl< + TSeries extends SeriesType, + TData extends WhitespaceData = SeriesDataItemTypeMap[TSeries], + TOptions extends SeriesOptionsMap[TSeries] = SeriesOptionsMap[TSeries], + TPartialOptions extends SeriesPartialOptionsMap[TSeries] = SeriesPartialOptionsMap[TSeries] + >( type: TSeries, styleDefaults: SeriesStyleOptionsMap[TSeries], - options: SeriesPartialOptionsMap[TSeries] = {} - ): ISeriesApi { + options: SeriesPartialOptionsMap[TSeries] = {}, + customPaneView?: ICustomSeriesPaneView + ): ISeriesApi { patchPriceFormat(options.priceFormat); const strictOptions = merge(clone(seriesOptionsDefaults), clone(styleDefaults), options) as SeriesOptionsMap[TSeries]; - const series = this._chartWidget.model().createSeries(type, strictOptions); + const series = this._chartWidget.model().createSeries(type, strictOptions, customPaneView); - const res = new SeriesApi(series, this, this); + const res = new SeriesApi(series, this, this, this); this._seriesMap.set(res, series); this._seriesMapReversed.set(series, res); @@ -291,8 +327,14 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer { private _convertMouseParams(param: MouseEventParamsImpl): MouseEventParams { const seriesData: MouseEventParams['seriesData'] = new Map(); param.seriesData.forEach((plotRow: SeriesPlotRow, series: Series) => { - const data = getSeriesDataCreator(series.seriesType())(plotRow); - assert(isFulfilledData(data)); + const seriesType = series.seriesType(); + const data = getSeriesDataCreator(seriesType)(plotRow); + if (seriesType !== 'Custom') { + assert(isFulfilledData(data)); + } else { + const customWhitespaceChecker = series.customSeriesWhitespaceCheck(); + assert(!customWhitespaceChecker || customWhitespaceChecker(data) === false); + } seriesData.set(this._mapSeriesToApi(series), data); }); diff --git a/src/api/data-consumer.ts b/src/api/data-consumer.ts index c0bde4b825..be06064699 100644 --- a/src/api/data-consumer.ts +++ b/src/api/data-consumer.ts @@ -1,5 +1,6 @@ import { isNumber, isString } from '../helpers/strict-type-checks'; +import { CustomData, CustomSeriesWhitespaceData } from '../model/icustom-series'; import { Series } from '../model/series'; import { SeriesType } from '../model/series-options'; import { BusinessDay, Time, UTCTimestamp } from '../model/time-data'; @@ -45,17 +46,18 @@ export interface WhitespaceData { * The time of the data. */ time: Time; + + /** + * Additional custom values which will be ignored by the library, but + * could be used by plugins. + */ + customValues?: Record; } /** * A base interface for a data point of single-value series. */ -export interface SingleValueData { - /** - * The time of the data. - */ - time: Time; - +export interface SingleValueData extends WhitespaceData { /** * Price value of the data. */ @@ -140,12 +142,7 @@ export interface BaselineData extends SingleValueData { /** * Represents a bar with a {@link Time} and open, high, low, and close prices. */ -export interface OhlcData { - /** - * The bar time. - */ - time: Time; - +export interface OhlcData extends WhitespaceData { /** * The open price. */ @@ -235,6 +232,10 @@ export interface SeriesDataItemTypeMap { * The types of histogram series data. */ Histogram: HistogramData | WhitespaceData; + /** + * The base types of an custom series data. + */ + Custom: CustomData | CustomSeriesWhitespaceData; } export interface DataUpdatesConsumer { diff --git a/src/api/data-layer.ts b/src/api/data-layer.ts index 1fc36a5b63..0ddbfb492c 100644 --- a/src/api/data-layer.ts +++ b/src/api/data-layer.ts @@ -207,17 +207,6 @@ function timeScalePointTime(mergedPointData: Map(data: SeriesDataItemWithOriginalTime): void { - // eslint-disable-next-line @typescript-eslint/tslint/config - if (data.originalTime === undefined) { - data.originalTime = data.time as unknown as OriginalTime; - } -} - -type SeriesDataItemWithOriginalTime = SeriesDataItemTypeMap[TSeriesType] & { - originalTime: OriginalTime; -}; - export class DataLayer { // note that _pointDataByTimePoint and _seriesRowsBySeries shares THE SAME objects in their values between each other // it's just different kind of maps to make usages/perf better @@ -263,15 +252,16 @@ export class DataLayer { let seriesRows: (SeriesPlotRow | WhitespacePlotRow)[] = []; if (data.length !== 0) { - const extendedData = data as SeriesDataItemWithOriginalTime[]; - extendedData.forEach((i: SeriesDataItemWithOriginalTime) => saveOriginalTime(i)); + const originalTimes = data.map((d: SeriesDataItemTypeMap[TSeriesType]) => d.time as unknown as OriginalTime); convertStringsToBusinessDays(data); const timeConverter = ensureNotNull(selectTimeConverter(data)); const createPlotRow = getSeriesPlotRowCreator(series.seriesType()); + const dataToPlotRow = series.customSeriesPlotValuesBuilder(); + const customWhitespaceChecker = series.customSeriesWhitespaceCheck(); - seriesRows = extendedData.map((item: SeriesDataItemWithOriginalTime) => { + seriesRows = data.map((item: SeriesDataItemTypeMap[TSeriesType], index: number) => { const time = timeConverter(item.time); let timePointData = this._pointDataByTimePoint.get(time.timestamp); @@ -282,7 +272,7 @@ export class DataLayer { isTimeScaleAffected = true; } - const row = createPlotRow(time, timePointData.index, item, item.originalTime); + const row = createPlotRow(time, timePointData.index, item, originalTimes[index], dataToPlotRow, customWhitespaceChecker); timePointData.mapping.set(series, row); return row; }); @@ -327,8 +317,7 @@ export class DataLayer { } public updateSeriesData(series: Series, data: SeriesDataItemTypeMap[TSeriesType]): DataUpdateResponse { - const extendedData = data as SeriesDataItemWithOriginalTime; - saveOriginalTime(extendedData); + const originalTime = data.time as unknown as OriginalTime; convertStringToBusinessDay(data); const time = ensureNotNull(selectTimeConverter([data]))(data.time); @@ -351,7 +340,9 @@ export class DataLayer { } const createPlotRow = getSeriesPlotRowCreator(series.seriesType()); - const plotRow = createPlotRow(time, pointDataAtTime.index, data, extendedData.originalTime); + const dataToPlotRow = series.customSeriesPlotValuesBuilder(); + const customWhitespaceChecker = series.customSeriesWhitespaceCheck(); + const plotRow = createPlotRow(time, pointDataAtTime.index, data, originalTime, dataToPlotRow, customWhitespaceChecker); pointDataAtTime.mapping.set(series, plotRow); this._updateLastSeriesRow(series, plotRow); diff --git a/src/api/data-validators.ts b/src/api/data-validators.ts index 2dc2ef94aa..d4b56a0066 100644 --- a/src/api/data-validators.ts +++ b/src/api/data-validators.ts @@ -45,7 +45,7 @@ export function checkSeriesValuesType(type: SeriesType, data: readonly SeriesDat type Checker = (item: SeriesDataItemTypeMap[SeriesType]) => void; -function getChecker(type: SeriesType): Checker { +export function getChecker(type: SeriesType): Checker { switch (type) { case 'Bar': case 'Candlestick': @@ -56,6 +56,9 @@ function getChecker(type: SeriesType): Checker { case 'Line': case 'Histogram': return checkLineItem.bind(null, type); + + case 'Custom': + return checkCustomItem.bind(null, type); } } @@ -113,3 +116,11 @@ function checkLineItem( }` ); } + +function checkCustomItem( + // type: 'Custom', + // customItem: SeriesDataItemTypeMap[typeof type] +): void { + // Nothing to check yet... + return; +} diff --git a/src/api/get-series-data-creator.ts b/src/api/get-series-data-creator.ts index 26448936c7..55f6f2873b 100644 --- a/src/api/get-series-data-creator.ts +++ b/src/api/get-series-data-creator.ts @@ -1,9 +1,11 @@ +import { CustomData } from '../model/icustom-series'; import { PlotRow, PlotRowValueIndex } from '../model/plot-data'; import { AreaPlotRow, BarPlotRow, BaselinePlotRow, CandlestickPlotRow, + CustomPlotRow, LinePlotRow, SeriesPlotRow, } from '../model/series-data'; @@ -22,14 +24,18 @@ import { } from './data-consumer'; type SeriesPlotRowToDataMap = { - [T in keyof SeriesDataItemTypeMap]: (plotRow: SeriesPlotRow) => SeriesDataItemTypeMap[T]; + [T in keyof SeriesDataItemTypeMap]: (plotRow: SeriesPlotRow) => SeriesDataItemTypeMap[T]; }; function singleValueData(plotRow: PlotRow): SingleValueData { - return { + const data: SingleValueData = { value: plotRow.value[PlotRowValueIndex.Close], time: plotRow.originalTime as unknown as Time, }; + if (plotRow.customValues !== undefined) { + data.customValues = plotRow.customValues; + } + return data; } function lineData(plotRow: LinePlotRow): LineData { @@ -91,13 +97,17 @@ function baselineData(plotRow: BaselinePlotRow): BaselineData { } function ohlcData(plotRow: PlotRow): OhlcData { - return { + const data: OhlcData = { open: plotRow.value[PlotRowValueIndex.Open], high: plotRow.value[PlotRowValueIndex.High], low: plotRow.value[PlotRowValueIndex.Low], close: plotRow.value[PlotRowValueIndex.Close], time: plotRow.originalTime as unknown as Time, }; + if (plotRow.customValues !== undefined) { + data.customValues = plotRow.customValues; + } + return data; } function barData(plotRow: BarPlotRow): BarData { @@ -129,6 +139,14 @@ function candlestickData(plotRow: CandlestickPlotRow): CandlestickData { return result; } +function customData(plotRow: CustomPlotRow): CustomData { + const time = plotRow.originalTime as unknown as Time; + return { + ...plotRow.data, + time, + }; +} + const seriesPlotRowToDataMap: SeriesPlotRowToDataMap = { Area: areaData, Line: lineData, @@ -136,6 +154,7 @@ const seriesPlotRowToDataMap: SeriesPlotRowToDataMap = { Histogram: lineData, Bar: barData, Candlestick: candlestickData, + Custom: customData, }; export function getSeriesDataCreator(seriesType: TSeriesType): (plotRow: SeriesPlotRow) => SeriesDataItemTypeMap[TSeriesType] { diff --git a/src/api/get-series-plot-row-creator.ts b/src/api/get-series-plot-row-creator.ts index accb37b002..6ccb4ecac8 100644 --- a/src/api/get-series-plot-row-creator.ts +++ b/src/api/get-series-plot-row-creator.ts @@ -1,9 +1,12 @@ -import { PlotRow } from '../model/plot-data'; +import { ensureDefined } from '../helpers/assertions'; + +import { CustomData } from '../model/icustom-series'; +import { PlotRow, PlotRowValue } from '../model/plot-data'; import { SeriesPlotRow } from '../model/series-data'; import { SeriesType } from '../model/series-options'; import { OriginalTime, TimePoint, TimePointIndex } from '../model/time-data'; -import { AreaData, BarData, BaselineData, CandlestickData, HistogramData, isWhitespaceData, LineData, SeriesDataItemTypeMap } from './data-consumer'; +import { AreaData, BarData, BaselineData, CandlestickData, HistogramData, isWhitespaceData, LineData, SeriesDataItemTypeMap, WhitespaceData } from './data-consumer'; function getColoredLineBasedSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: LineData | HistogramData, originalTime: OriginalTime): Mutable> { const val = item.value; @@ -96,6 +99,19 @@ function getCandlestickSeriesPlotRow(time: TimePoint, index: TimePointIndex, ite return res; } +// The returned data is used for scaling the series, and providing the current value for the price scale +export type CustomDataToPlotRowValueConverter = (item: CustomData | WhitespaceData) => number[]; + +function getCustomSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: CustomData | WhitespaceData, originalTime: OriginalTime, dataToPlotRow?: CustomDataToPlotRowValueConverter): Mutable> { + const values = ensureDefined(dataToPlotRow)(item); + const max = Math.max(...values); + const min = Math.min(...values); + const last = values[values.length - 1]; + const value: PlotRowValue = [last, max, min, last]; + const { time: excludedTime, color, ...data } = item as CustomData; + return { index, time, value, originalTime, data, color }; +} + export type WhitespacePlotRow = Omit; export function isSeriesPlotRow(row: SeriesPlotRow | WhitespacePlotRow): row is SeriesPlotRow { @@ -103,16 +119,31 @@ export function isSeriesPlotRow(row: SeriesPlotRow | WhitespacePlotRow): row is } type SeriesItemValueFnMap = { - [T in keyof SeriesDataItemTypeMap]: (time: TimePoint, index: TimePointIndex, item: SeriesDataItemTypeMap[T], originalTime: OriginalTime) => Mutable | WhitespacePlotRow>; + [T in keyof SeriesDataItemTypeMap]: (time: TimePoint, index: TimePointIndex, item: SeriesDataItemTypeMap[T], originalTime: OriginalTime, dataToPlotRow?: CustomDataToPlotRowValueConverter, customIsWhitespace?: WhitespaceCheck) => Mutable | WhitespacePlotRow>; }; -function wrapWhitespaceData(createPlotRowFn: (typeof getBaselineSeriesPlotRow) | (typeof getBarSeriesPlotRow) | (typeof getCandlestickSeriesPlotRow)): SeriesItemValueFnMap[TSeriesType] { - return (time: TimePoint, index: TimePointIndex, bar: SeriesDataItemTypeMap[SeriesType], originalTime: OriginalTime) => { - if (isWhitespaceData(bar)) { - return { time, index, originalTime }; +function wrapCustomValues(plotRow: Mutable, bar: SeriesDataItemTypeMap[SeriesType]): Mutable { + if (bar.customValues !== undefined) { + plotRow.customValues = bar.customValues; + } + return plotRow; +} + +export type WhitespaceCheck = (bar: SeriesDataItemTypeMap[SeriesType]) => bar is WhitespaceData; +function isWhitespaceDataWithCustomCheck(bar: SeriesDataItemTypeMap[SeriesType], customIsWhitespace?: WhitespaceCheck): bar is WhitespaceData { + if (customIsWhitespace) { + return customIsWhitespace(bar); + } + return isWhitespaceData(bar); +} + +function wrapWhitespaceData(createPlotRowFn: (typeof getBaselineSeriesPlotRow) | (typeof getBarSeriesPlotRow) | (typeof getCandlestickSeriesPlotRow) | (typeof getCustomSeriesPlotRow)): SeriesItemValueFnMap[TSeriesType] { + return (time: TimePoint, index: TimePointIndex, bar: SeriesDataItemTypeMap[SeriesType], originalTime: OriginalTime, dataToPlotRow?: CustomDataToPlotRowValueConverter, customIsWhitespace?: WhitespaceCheck) => { + if (isWhitespaceDataWithCustomCheck(bar, customIsWhitespace)) { + return wrapCustomValues({ time, index, originalTime }, bar); } - return createPlotRowFn(time, index, bar, originalTime); + return wrapCustomValues(createPlotRowFn(time, index, bar, originalTime, dataToPlotRow), bar); }; } @@ -123,6 +154,7 @@ const seriesPlotRowFnMap: SeriesItemValueFnMap = { Baseline: wrapWhitespaceData(getBaselineSeriesPlotRow), Histogram: wrapWhitespaceData(getColoredLineBasedSeriesPlotRow), Line: wrapWhitespaceData(getColoredLineBasedSeriesPlotRow), + Custom: wrapWhitespaceData(getCustomSeriesPlotRow), }; export function getSeriesPlotRowCreator(seriesType: TSeriesType): SeriesItemValueFnMap[TSeriesType] { diff --git a/src/api/ichart-api.ts b/src/api/ichart-api.ts index 54562a9358..a16385c1cc 100644 --- a/src/api/ichart-api.ts +++ b/src/api/ichart-api.ts @@ -1,20 +1,23 @@ import { DeepPartial } from '../helpers/strict-type-checks'; import { ChartOptions } from '../model/chart-model'; +import { CustomData, ICustomSeriesPaneView } from '../model/icustom-series'; import { Point } from '../model/point'; import { AreaSeriesPartialOptions, BarSeriesPartialOptions, BaselineSeriesPartialOptions, CandlestickSeriesPartialOptions, + CustomSeriesOptions, HistogramSeriesPartialOptions, LineSeriesPartialOptions, + SeriesPartialOptions, SeriesType, } from '../model/series-options'; import { Logical, Time } from '../model/time-data'; import { TouchMouseEventData } from '../model/touch-mouse-event-data'; -import { BarData, HistogramData, LineData } from './data-consumer'; +import { BarData, HistogramData, LineData, WhitespaceData } from './data-consumer'; import { IPriceScaleApi } from './iprice-scale-api'; import { ISeriesApi } from './iseries-api'; import { ITimeScaleApi } from './itime-scale-api'; @@ -45,7 +48,7 @@ export interface MouseEventParams { * Keys of the map are {@link ISeriesApi} instances. Values are prices. * Values of the map are original data items */ - seriesData: Map, BarData | LineData | HistogramData>; + seriesData: Map, BarData | LineData | HistogramData | CustomData>; /** * The {@link ISeriesApi} for the series at the point of the mouse event. */ @@ -86,6 +89,27 @@ export interface IChartApi { */ resize(width: number, height: number, forceRepaint?: boolean): void; + /** + * Creates a custom series with specified parameters. + * + * A custom series is a generic series which can be extended with a custom renderer to + * implement chart types which the library doesn't support by default. + * + * @param customPaneView - A custom series pane view which implements the custom renderer. + * @param customOptions - Customization parameters of the series being created. + * ```js + * const series = chart.addCustomSeries(myCustomPaneView); + * ``` + */ + addCustomSeries< + TData extends CustomData, + TOptions extends CustomSeriesOptions, + TPartialOptions extends SeriesPartialOptions = SeriesPartialOptions + >( + customPaneView: ICustomSeriesPaneView, + customOptions?: SeriesPartialOptions + ): ISeriesApi<'Custom', TData | WhitespaceData, TOptions, TPartialOptions>; + /** * Creates an area series with specified parameters. * @@ -271,4 +295,12 @@ export interface IChartApi { * @returns Whether the `autoSize` option is enabled and the active. */ autoSizeActive(): boolean; + + /** + * Returns the generated div element containing the chart. This can be used for adding your own additional event listeners, or for measuring the + * elements dimensions and position within the document. + * + * @returns generated div element containing the chart. + */ + chartElement(): HTMLDivElement; } diff --git a/src/api/iseries-api.ts b/src/api/iseries-api.ts index 4708e2454a..4dbe85b4c4 100644 --- a/src/api/iseries-api.ts +++ b/src/api/iseries-api.ts @@ -15,6 +15,17 @@ import { Range, Time } from '../model/time-data'; import { SeriesDataItemTypeMap } from './data-consumer'; import { IPriceLine } from './iprice-line'; import { IPriceScaleApi } from './iprice-scale-api'; +import { ISeriesPrimitive } from './iseries-primitive-api'; + +/** + * The extent of the data change. + */ +export type DataChangedScope = 'full' | 'update'; + +/** + * A custom function use to handle data changed events. + */ +export type DataChangedHandler = (scope: DataChangedScope) => void; /** * Represents a range of bars and the number of bars outside the range. @@ -40,7 +51,12 @@ export interface BarsInfo extends Partial> { /** * Represents the interface for interacting with series. */ -export interface ISeriesApi { +export interface ISeriesApi< + TSeriesType extends SeriesType, + TData = SeriesDataItemTypeMap[TSeriesType], + TOptions = SeriesOptionsMap[TSeriesType], + TPartialOptions = SeriesPartialOptionsMap[TSeriesType], + > { /** * Returns current price formatter * @@ -97,14 +113,14 @@ export interface ISeriesApi { * * @param options - Any subset of options. */ - applyOptions(options: SeriesPartialOptionsMap[TSeriesType]): void; + applyOptions(options: TPartialOptions): void; /** * Returns currently applied options * * @returns Full set of currently applied options, including defaults */ - options(): Readonly; + options(): Readonly; /** * Returns interface of the price scale the series is currently attached @@ -132,7 +148,7 @@ export interface ISeriesApi { * ]); * ``` */ - setData(data: SeriesDataItemTypeMap[TSeriesType][]): void; + setData(data: TData[]): void; /** * Adds new data item to the existing set (or updates the latest item if times of the passed/latest items are equal). @@ -157,7 +173,7 @@ export interface ISeriesApi { * }); * ``` */ - update(bar: SeriesDataItemTypeMap[TSeriesType]): void; + update(bar: TData): void; /** * Returns a bar data by provided logical index. @@ -170,7 +186,46 @@ export interface ISeriesApi { * const originalData = series.dataByIndex(10, LightweightCharts.MismatchDirection.NearestLeft); * ``` */ - dataByIndex(logicalIndex: number, mismatchDirection?: MismatchDirection): SeriesDataItemTypeMap[TSeriesType] | null; + dataByIndex(logicalIndex: number, mismatchDirection?: MismatchDirection): TData | null; + + /** + * Returns all the bar data for the series. + * + * @returns Original data items provided via setData or update methods. + * @example + * ```js + * const originalData = series.data(); + * ``` + */ + data(): readonly TData[]; + + /** + * Subscribe to the data changed event. This event is fired whenever the `update` or `setData` method is evoked + * on the series. + * + * @param handler - Handler to be called on a data changed event. + * @example + * ```js + * function myHandler() { + * const data = series.data(); + * console.log(`The data has changed. New Data length: ${data.length}`); + * } + * + * series.subscribeDataChanged(myHandler); + * ``` + */ + subscribeDataChanged(handler: DataChangedHandler): void; + + /** + * Unsubscribe a handler that was previously subscribed using {@link subscribeDataChanged}. + * + * @param handler - Previously subscribed handler + * @example + * ```js + * chart.unsubscribeDataChanged(myHandler); + * ``` + */ + unsubscribeDataChanged(handler: DataChangedHandler): void; /** * Allows to set/replace all existing series markers with new ones. @@ -263,4 +318,19 @@ export interface ISeriesApi { * ``` */ seriesType(): TSeriesType; + + /** + * Attaches additional drawing primitive to the series + * + * @param primitive - any implementation of ISeriesPrimitive interface + */ + attachPrimitive(primitive: ISeriesPrimitive): void; + + /** + * Detaches additional drawing primitive from the series + * + * @param primitive - implementation of ISeriesPrimitive interface attached before + * Does nothing if specified primitive was not attached + */ + detachPrimitive(primitive: ISeriesPrimitive): void; } diff --git a/src/api/iseries-primitive-api.ts b/src/api/iseries-primitive-api.ts new file mode 100644 index 0000000000..624fce0cb9 --- /dev/null +++ b/src/api/iseries-primitive-api.ts @@ -0,0 +1,33 @@ +import { ISeriesPrimitiveBase } from '../model/iseries-primitive'; +import { SeriesOptionsMap, SeriesType } from '../model/series-options'; + +import { IChartApi } from './ichart-api'; +import { ISeriesApi } from './iseries-api'; + +/** + * Object containing references to the chart and series instances, and a requestUpdate method for triggering + * a refresh of the chart. + */ +export interface SeriesAttachedParameter< + TSeriesType extends SeriesType = keyof SeriesOptionsMap +> { + /** + * Chart instance. + */ + chart: IChartApi; + /** + * Series to which the Primitive is attached. + */ + series: ISeriesApi; + /** + * Request an update (redraw the chart) + */ + requestUpdate: () => void; +} + +/** + * Interface for series primitives. It must be implemented to add some external graphics to series. + */ +export type ISeriesPrimitive = ISeriesPrimitiveBase< + SeriesAttachedParameter +>; diff --git a/src/api/options/series-options-defaults.ts b/src/api/options/series-options-defaults.ts index 36bfec967e..0b57034123 100644 --- a/src/api/options/series-options-defaults.ts +++ b/src/api/options/series-options-defaults.ts @@ -3,6 +3,7 @@ import { BarStyleOptions, BaselineStyleOptions, CandlestickStyleOptions, + CustomStyleOptions, HistogramStyleOptions, LastPriceAnimationMode, LineStyleOptions, @@ -98,6 +99,10 @@ export const histogramStyleDefaults: HistogramStyleOptions = { base: 0, }; +export const customStyleDefaults: CustomStyleOptions = { + color: '#2196f3', +}; + export const seriesOptionsDefaults: SeriesOptionsCommon = { title: '', visible: true, diff --git a/src/api/series-api.ts b/src/api/series-api.ts index 3118106e63..fda9b228b7 100644 --- a/src/api/series-api.ts +++ b/src/api/series-api.ts @@ -1,14 +1,18 @@ import { IPriceFormatter } from '../formatters/iprice-formatter'; import { ensureNotNull } from '../helpers/assertions'; +import { Delegate } from '../helpers/delegate'; +import { IDestroyable } from '../helpers/idestroyable'; import { clone, merge } from '../helpers/strict-type-checks'; import { BarPrice } from '../model/bar'; import { Coordinate } from '../model/coordinate'; +import { ISeriesPrimitiveBase } from '../model/iseries-primitive'; import { MismatchDirection } from '../model/plot-list'; import { CreatePriceLineOptions, PriceLineOptions } from '../model/price-line-options'; import { RangeImpl } from '../model/range-impl'; import { Series } from '../model/series'; +import { SeriesPlotRow } from '../model/series-data'; import { SeriesMarker } from '../model/series-markers'; import { SeriesOptionsMap, @@ -19,26 +23,42 @@ import { Logical, OriginalTime, Range, Time, TimePoint, TimePointIndex } from '. import { TimeScaleVisibleRange } from '../model/time-scale-visible-range'; import { IPriceScaleApiProvider } from './chart-api'; -import { DataUpdatesConsumer, SeriesDataItemTypeMap } from './data-consumer'; +import { DataUpdatesConsumer, SeriesDataItemTypeMap, WhitespaceData } from './data-consumer'; import { convertTime } from './data-layer'; import { checkItemsAreOrdered, checkPriceLineOptions, checkSeriesValuesType } from './data-validators'; import { getSeriesDataCreator } from './get-series-data-creator'; +import { type IChartApi } from './ichart-api'; import { IPriceLine } from './iprice-line'; import { IPriceScaleApi } from './iprice-scale-api'; -import { BarsInfo, ISeriesApi } from './iseries-api'; +import { BarsInfo, DataChangedHandler, DataChangedScope, ISeriesApi } from './iseries-api'; +import { ISeriesPrimitive } from './iseries-primitive-api'; import { priceLineOptionsDefaults } from './options/price-line-options-defaults'; import { PriceLine } from './price-line-api'; -export class SeriesApi implements ISeriesApi { +export class SeriesApi< + TSeriesType extends SeriesType, + TData extends WhitespaceData = SeriesDataItemTypeMap[TSeriesType], + TOptions extends SeriesOptionsMap[TSeriesType] = SeriesOptionsMap[TSeriesType], + TPartialOptions extends SeriesPartialOptionsMap[TSeriesType] = SeriesPartialOptionsMap[TSeriesType] +> implements + ISeriesApi, + IDestroyable { protected _series: Series; protected _dataUpdatesConsumer: DataUpdatesConsumer; + protected readonly _chartApi: IChartApi; private readonly _priceScaleApiProvider: IPriceScaleApiProvider; + private readonly _dataChangedDelegate: Delegate = new Delegate(); - public constructor(series: Series, dataUpdatesConsumer: DataUpdatesConsumer, priceScaleApiProvider: IPriceScaleApiProvider) { + public constructor(series: Series, dataUpdatesConsumer: DataUpdatesConsumer, priceScaleApiProvider: IPriceScaleApiProvider, chartApi: IChartApi) { this._series = series; this._dataUpdatesConsumer = dataUpdatesConsumer; this._priceScaleApiProvider = priceScaleApiProvider; + this._chartApi = chartApi; + } + + public destroy(): void { + this._dataChangedDelegate.destroy(); } public priceFormatter(): IPriceFormatter { @@ -114,27 +134,43 @@ export class SeriesApi implements ISeriesApi) => seriesCreator(row) as TData); + } + + public subscribeDataChanged(handler: DataChangedHandler): void { + this._dataChangedDelegate.subscribe(handler); + } + + public unsubscribeDataChanged(handler: DataChangedHandler): void { + this._dataChangedDelegate.unsubscribe(handler); } public setMarkers(data: SeriesMarker