From 9d4956fad528de50c2fc038962f924cb8e807110 Mon Sep 17 00:00:00 2001 From: Eugene Korobko Date: Thu, 12 Jan 2023 13:14:44 +0300 Subject: [PATCH 01/65] Plugins subsystem MVP --- .size-limit.js | 4 +- src/api/iseries-api.ts | 16 +++ src/api/iseries-primitive.ts | 106 ++++++++++++++++ src/api/series-api.ts | 9 ++ src/api/series-primitive-wrapper.ts | 186 ++++++++++++++++++++++++++++ src/api/time-scale-api.ts | 7 +- src/gui/time-axis-widget.ts | 2 +- src/model/series.ts | 30 +++++ src/tsconfig.model.json | 4 +- 9 files changed, 358 insertions(+), 6 deletions(-) create mode 100644 src/api/iseries-primitive.ts create mode 100644 src/api/series-primitive-wrapper.ts diff --git a/.size-limit.js b/.size-limit.js index d8dad352cb..09d11d69ef 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -4,11 +4,11 @@ module.exports = [ { name: 'ESM', path: 'dist/lightweight-charts.esm.production.js', - limit: '43.9 KB', + limit: '44.36 KB', }, { name: 'Standalone', path: 'dist/lightweight-charts.standalone.production.js', - limit: '44.4 KB', + limit: '44.95 KB', }, ]; diff --git a/src/api/iseries-api.ts b/src/api/iseries-api.ts index dbe2d1d70a..d867eec248 100644 --- a/src/api/iseries-api.ts +++ b/src/api/iseries-api.ts @@ -15,6 +15,7 @@ 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'; /** * Represents a range of bars and the number of bars outside the range. @@ -263,4 +264,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.ts b/src/api/iseries-primitive.ts new file mode 100644 index 0000000000..d68301e72c --- /dev/null +++ b/src/api/iseries-primitive.ts @@ -0,0 +1,106 @@ +/** + * This interface represents a label on the price or time axis + */ +export interface ISeriesPrimitiveAxisView { + /** + * coordiate of the label, vertical for price axis and horizontal for time axis + * + * @returns coordinate. 0 means left and top + */ + coordinate(): number; + /** + * @returns text of the label + */ + text(): string; + + /** + * @returns text color of the label + */ + textColor(): string; + + /** + * @returns background color of the label + */ + backColor(): string; +} + +/** + * This interface represents rendering some element on the canvas + */ +export interface ISeriesPrimitivePaneRenderer { + /** + * Method to draw main content of the element + * + * @param ctx - cavnas context to draw on + * @param pixelRatio - DPR of the canvas. The library used phisical pixels coordinate spaces to support pixel-perfect rendering + * + * Usually renderer uses someting like this + * ```js + * const scaledX = Math.round(this._x * pixelRatio); + * const scaledY = Math.round(this._y * pixelRatio); + * ctx.moveTo + * ``` + */ + draw(ctx: CanvasRenderingContext2D, pixelRatio: number): void; + + /** + * Optional method to draw the background. + * Some elements could implement this method to draw on the background of the chart + * Usually this is some kind of watermarks or time areas highlighting + * + * @param ctx - cavnas context to draw on + * @param pixelRatio - DPR of the canvas + */ + drawBackground?(ctx: CanvasRenderingContext2D, pixelRatio: number): void; +} + +/** + * This interface represents the primitive in the main area of the chart + */ +export interface ISeriesPrimitivePaneView { + /** + * This method returns a renderer - special object to draw data + * + * @param height - height of the area to draw on + * @param width - width of the area to draw on + * @returns an renderer object to be used for drawing or null if we have nothin to draw + */ + renderer(height: number, width: number): ISeriesPrimitivePaneRenderer | null; +} + +/** + * Base interface for series primitives. It must be implemented to add some external graphics to series + */ +export interface ISeriesPrimitive { + /** + * This method is called when viewport has been changed, so primitive have to reacalculate/invaildate its data + */ + updateAllViews(): void; + + /** + * Returns array of labels to be drawn on the price axis used by the series + * + * @returns array of objects; each of then must impement ISeriesPrimitiveAxisView interface + * + * Try to implement this method returning the same array if nothing changed, this would help the library to save memory and CPU + */ + priceAxisViews(): readonly ISeriesPrimitiveAxisView[]; + + /** + * Returns array of labels to be drawn on the time axis + * + * @returns array of objects; each of then must impement ISeriesPrimitiveAxisView interface + * + * Try to implement this method returning the same array if nothing changed, this would help the library to save memory and CPU + */ + timeAxisViews(): readonly ISeriesPrimitiveAxisView[]; + + /** + * Returns array of objects representing primitive in the main area of the chart + * + * @returns array of objects; each of then must impement ISeriesPrimitivePaneView interface + * + * Try to implement this method returning the same array if nothing changed, this would help the library to save memory and CPU + */ + paneViews(): readonly ISeriesPrimitivePaneView[]; +} diff --git a/src/api/series-api.ts b/src/api/series-api.ts index 3118106e63..c2f494e3a8 100644 --- a/src/api/series-api.ts +++ b/src/api/series-api.ts @@ -26,6 +26,7 @@ import { getSeriesDataCreator } from './get-series-data-creator'; import { IPriceLine } from './iprice-line'; import { IPriceScaleApi } from './iprice-scale-api'; import { BarsInfo, ISeriesApi } from './iseries-api'; +import { ISeriesPrimitive } from './iseries-primitive'; import { priceLineOptionsDefaults } from './options/price-line-options-defaults'; import { PriceLine } from './price-line-api'; @@ -185,4 +186,12 @@ export class SeriesApi implements ISeriesApi { + base: Base; + wrapper: Wrapper; +} + +class SeriesPrimitivePaneViewWrapper implements IPaneView { + private readonly _paneView: ISeriesPrimitivePaneView; + private _cache: RendererCache | null = null; + + public constructor(paneView: ISeriesPrimitivePaneView) { + this._paneView = paneView; + } + + public renderer(height: number, width: number, addAnchors?: boolean): IPaneRenderer | null { + const baseRenderer = this._paneView.renderer(height, width); + if (baseRenderer === null) { + return null; + } + if (this._cache?.base === baseRenderer) { + return this._cache.wrapper; + } + const wrapper = new SeriesPrimitiveRendererWrapper(baseRenderer); + this._cache = { + base: baseRenderer, + wrapper, + }; + return wrapper; + } +} + +class SeriesPrimitiveTimeAxisViewWrapper implements ITimeAxisView { + private readonly _baseView: ISeriesPrimitiveAxisView; + private readonly _timeScale: TimeScale; + private readonly _renderer: TimeAxisViewRenderer = new TimeAxisViewRenderer(); + + public constructor(baseView: ISeriesPrimitiveAxisView, timeScale: TimeScale) { + this._baseView = baseView; + this._timeScale = timeScale; + } + + public renderer(): TimeAxisViewRenderer { + this._renderer.setData({ + width: this._timeScale.width(), + text: this._baseView.text(), + coordinate: this._baseView.coordinate(), + color: this._baseView.textColor(), + background: this._baseView.backColor(), + visible: true, + tickVisible: true, + }); + return this._renderer; + } +} + +class SeriesPrimitivePriceAxisViewWrapper extends PriceAxisView { + private readonly _baseView: ISeriesPrimitiveAxisView; + private readonly _priceScale: PriceScale; + + public constructor(baseView: ISeriesPrimitiveAxisView, priceScale: PriceScale) { + super(); + this._baseView = baseView; + this._priceScale = priceScale; + } + + protected override _updateRendererData( + axisRendererData: PriceAxisViewRendererData, + paneRendererData: PriceAxisViewRendererData, + commonRendererData: PriceAxisViewRendererCommonData + ): void { + axisRendererData.visible = false; + + commonRendererData.background = this._baseView.backColor(); + commonRendererData.color = this._baseView.textColor(); + + const additionalPadding = 2 / 12 * this._priceScale.fontSize(); + + commonRendererData.additionalPaddingTop = additionalPadding; + commonRendererData.additionalPaddingBottom = additionalPadding; + + commonRendererData.coordinate = this._baseView.coordinate(); + axisRendererData.text = this._baseView.text(); + axisRendererData.visible = true; + } +} + +export class SeriesPrimitiveWrapper { + private readonly _primitive: ISeriesPrimitive; + private readonly _series: Series; + private _paneViewsCache: RendererCache | null = null; + private _timeAxisViewsCache: RendererCache | null = null; + private _priceAxisViewsCache: RendererCache | null = null; + + public constructor(primitive: ISeriesPrimitive, series: Series) { + this._primitive = primitive; + this._series = series; + } + + public primitive(): ISeriesPrimitive { + return this._primitive; + } + + public updateAllViews(): void { + this._primitive.updateAllViews(); + } + + public paneViews(): readonly IPaneView[] { + const base = this._primitive.paneViews(); + if (this._paneViewsCache?.base === base) { + return this._paneViewsCache.wrapper; + } + const wrapper = base.map((pw: ISeriesPrimitivePaneView) => new SeriesPrimitivePaneViewWrapper(pw)); + this._paneViewsCache = { + base, + wrapper, + }; + return wrapper; + } + + public timeAxisViews(): readonly ITimeAxisView[] { + const base = this._primitive.timeAxisViews(); + if (this._timeAxisViewsCache?.base === base) { + return this._timeAxisViewsCache.wrapper; + } + const timeScale = this._series.model().timeScale(); + const wrapper = base.map((aw: ISeriesPrimitiveAxisView) => new SeriesPrimitiveTimeAxisViewWrapper(aw, timeScale)); + this._timeAxisViewsCache = { + base, + wrapper, + }; + return wrapper; + } + + public priceAxisViews(): readonly IPriceAxisView[] { + const base = this._primitive.priceAxisViews(); + if (this._priceAxisViewsCache?.base === base) { + return this._priceAxisViewsCache.wrapper; + } + const priceScale = this._series.priceScale(); + const wrapper = base.map((aw: ISeriesPrimitiveAxisView) => new SeriesPrimitivePriceAxisViewWrapper(aw, priceScale)); + this._priceAxisViewsCache = { + base, + wrapper, + }; + return wrapper; + } +} diff --git a/src/api/time-scale-api.ts b/src/api/time-scale-api.ts index c35c334a2d..fd7c8243fe 100644 --- a/src/api/time-scale-api.ts +++ b/src/api/time-scale-api.ts @@ -4,7 +4,7 @@ import { TimeAxisWidget } from '../gui/time-axis-widget'; import { assert } from '../helpers/assertions'; import { Delegate } from '../helpers/delegate'; import { IDestroyable } from '../helpers/idestroyable'; -import { clone, DeepPartial } from '../helpers/strict-type-checks'; +import { DeepPartial } from '../helpers/strict-type-checks'; import { ChartModel } from '../model/chart-model'; import { Coordinate } from '../model/coordinate'; @@ -190,7 +190,10 @@ export class TimeScaleApi implements ITimeScaleApi, IDestroyable { } public options(): Readonly { - return clone(this._timeScale.options()); + return { + ...this._timeScale.options(), + barSpacing: this._timeScale.barSpacing(), + }; } private _onVisibleBarsChanged(): void { diff --git a/src/gui/time-axis-widget.ts b/src/gui/time-axis-widget.ts index 70c9140650..83d16ad478 100644 --- a/src/gui/time-axis-widget.ts +++ b/src/gui/time-axis-widget.ts @@ -292,7 +292,7 @@ export class TimeAxisWidget implements MouseEventHandlers, IDestroyable { const pixelRatio = this._topCanvasBinding.pixelRatio; topCtx.clearRect(0, 0, Math.ceil(this._size.w * pixelRatio), Math.ceil(this._size.h * pixelRatio)); - this._drawLabels([this._chart.model().crosshairSource()], topCtx, pixelRatio); + this._drawLabels([...this._chart.model().serieses(), this._chart.model().crosshairSource()], topCtx, pixelRatio); } private _drawBackground(ctx: CanvasRenderingContext2D, pixelRatio: number): void { diff --git a/src/model/series.ts b/src/model/series.ts index 29a5002e12..c116ec1f43 100644 --- a/src/model/series.ts +++ b/src/model/series.ts @@ -1,3 +1,6 @@ +import { ISeriesPrimitive } from '../api/iseries-primitive'; +import { SeriesPrimitiveWrapper } from '../api/series-primitive-wrapper'; + import { IPriceFormatter } from '../formatters/iprice-formatter'; import { PercentageFormatter } from '../formatters/percentage-formatter'; import { PriceFormatter } from '../formatters/price-formatter'; @@ -22,6 +25,7 @@ import { SeriesMarkersPaneView } from '../views/pane/series-markers-pane-view'; import { SeriesPriceLinePaneView } from '../views/pane/series-price-line-pane-view'; import { IPriceAxisView } from '../views/price-axis/iprice-axis-view'; import { SeriesPriceAxisView } from '../views/price-axis/series-price-axis-view'; +import { ITimeAxisView } from '../views/time-axis/itime-axis-view'; import { AutoscaleInfoImpl } from './autoscale-info-impl'; import { BarPrice, BarPrices } from './bar'; @@ -111,6 +115,7 @@ export class Series extends PriceDataSource i private _indexedMarkers: InternalSeriesMarker[] = []; private _markersPaneView!: SeriesMarkersPaneView; private _animationTimeoutId: TimerId | null = null; + private _primitives: SeriesPrimitiveWrapper[] = []; public constructor(model: ChartModel, options: SeriesOptionsInternal, seriesType: T) { super(model); @@ -378,6 +383,10 @@ export class Series extends PriceDataSource i const priceLineViews = this._customPriceLines.map((line: CustomPriceLine) => line.paneView()); res.push(...priceLineViews); + this._primitives.forEach((wrapper: SeriesPrimitiveWrapper) => { + res.push(...wrapper.paneViews()); + }); + return res; } @@ -396,9 +405,20 @@ export class Series extends PriceDataSource i for (const customPriceLine of this._customPriceLines) { result.push(customPriceLine.priceAxisView()); } + this._primitives.forEach((wrapper: SeriesPrimitiveWrapper) => { + result.push(...wrapper.priceAxisViews()); + }); return result; } + public override timeAxisViews(): readonly ITimeAxisView[] { + const res: ITimeAxisView[] = []; + this._primitives.forEach((wrapper: SeriesPrimitiveWrapper) => { + res.push(...wrapper.timeAxisViews()); + }); + return res; + } + public autoscaleInfo(startTimePoint: TimePointIndex, endTimePoint: TimePointIndex): AutoscaleInfoImpl | null { if (this._options.autoscaleInfoProvider !== undefined) { const autoscaleInfo = this._options.autoscaleInfoProvider(() => { @@ -434,6 +454,8 @@ export class Series extends PriceDataSource i this._priceLineView.update(); this._baseHorizontalLineView.update(); this._lastPriceAnimationPaneView?.update(); + + this._primitives.forEach((wrapper: SeriesPrimitiveWrapper) => wrapper.updateAllViews()); } public override priceScale(): PriceScale { @@ -467,6 +489,14 @@ export class Series extends PriceDataSource i return this._options.visible; } + public attachPrimitive(primitive: ISeriesPrimitive): void { + this._primitives.push(new SeriesPrimitiveWrapper(primitive, this)); + } + + public detachPrimitive(source: ISeriesPrimitive): void { + this._primitives = this._primitives.filter((wrapper: SeriesPrimitiveWrapper) => wrapper.primitive() !== source); + } + private _isOverlay(): boolean { const priceScale = this.priceScale(); return !isDefaultPriceScale(priceScale.id()); diff --git a/src/tsconfig.model.json b/src/tsconfig.model.json index aac88ae534..2c57b765f4 100644 --- a/src/tsconfig.model.json +++ b/src/tsconfig.model.json @@ -7,6 +7,8 @@ "include": [ "./model/**/*.ts", "./renderers/**/*.ts", - "./views/**/*.ts" + "./views/**/*.ts", + "./api/iseries-primitive.ts", + "./api/series-primitive-wrapper.ts", ] } From e7b64508b950264304f28b1706e5be1c864da5be Mon Sep 17 00:00:00 2001 From: Eugene Korobko Date: Thu, 12 Jan 2023 13:58:53 +0300 Subject: [PATCH 02/65] Fixed after merge fancy canvas --- src/api/iseries-primitive.ts | 22 +++++++--------------- src/api/series-primitive-wrapper.ts | 14 ++++++++------ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/api/iseries-primitive.ts b/src/api/iseries-primitive.ts index d68301e72c..0f6dde6664 100644 --- a/src/api/iseries-primitive.ts +++ b/src/api/iseries-primitive.ts @@ -1,3 +1,5 @@ +import { CanvasRenderingTarget2D } from "fancy-canvas"; + /** * This interface represents a label on the price or time axis */ @@ -31,27 +33,19 @@ export interface ISeriesPrimitivePaneRenderer { /** * Method to draw main content of the element * - * @param ctx - cavnas context to draw on - * @param pixelRatio - DPR of the canvas. The library used phisical pixels coordinate spaces to support pixel-perfect rendering + * @param target - cavnas context to draw on, refer FancyCanvas library for more details about this class * - * Usually renderer uses someting like this - * ```js - * const scaledX = Math.round(this._x * pixelRatio); - * const scaledY = Math.round(this._y * pixelRatio); - * ctx.moveTo - * ``` */ - draw(ctx: CanvasRenderingContext2D, pixelRatio: number): void; + draw(target: CanvasRenderingTarget2D): void; /** * Optional method to draw the background. * Some elements could implement this method to draw on the background of the chart * Usually this is some kind of watermarks or time areas highlighting * - * @param ctx - cavnas context to draw on - * @param pixelRatio - DPR of the canvas + * @param target - cavnas context to draw on, refer FancyCanvas library for more details about this class */ - drawBackground?(ctx: CanvasRenderingContext2D, pixelRatio: number): void; + drawBackground?(target: CanvasRenderingTarget2D): void; } /** @@ -61,11 +55,9 @@ export interface ISeriesPrimitivePaneView { /** * This method returns a renderer - special object to draw data * - * @param height - height of the area to draw on - * @param width - width of the area to draw on * @returns an renderer object to be used for drawing or null if we have nothin to draw */ - renderer(height: number, width: number): ISeriesPrimitivePaneRenderer | null; + renderer(): ISeriesPrimitivePaneRenderer | null; } /** diff --git a/src/api/series-primitive-wrapper.ts b/src/api/series-primitive-wrapper.ts index c39938321f..b62e177df9 100644 --- a/src/api/series-primitive-wrapper.ts +++ b/src/api/series-primitive-wrapper.ts @@ -1,3 +1,5 @@ +import { CanvasRenderingTarget2D } from 'fancy-canvas'; + import { HoveredObject } from '../model/chart-model'; import { Coordinate } from '../model/coordinate'; import { PriceScale } from '../model/price-scale'; @@ -25,12 +27,12 @@ class SeriesPrimitiveRendererWrapper implements IPaneRenderer { this._baseRenderer = baseRenderer; } - public draw(ctx: CanvasRenderingContext2D, pixelRatio: number, isHovered: boolean): void { - this._baseRenderer.draw(ctx, pixelRatio); + public draw(target: CanvasRenderingTarget2D, isHovered: boolean, hitTestData?: unknown): void { + this._baseRenderer.draw(target); } - public drawBackground(ctx: CanvasRenderingContext2D, pixelRatio: number, isHovered: boolean): void { - this._baseRenderer.drawBackground?.(ctx, pixelRatio); + public drawBackground?(target: CanvasRenderingTarget2D, isHovered: boolean, hitTestData?: unknown): void { + this._baseRenderer.drawBackground?.(target); } public hitTest(x: Coordinate, y: Coordinate): HoveredObject | null { @@ -51,8 +53,8 @@ class SeriesPrimitivePaneViewWrapper implements IPaneView { this._paneView = paneView; } - public renderer(height: number, width: number, addAnchors?: boolean): IPaneRenderer | null { - const baseRenderer = this._paneView.renderer(height, width); + public renderer(addAnchors?: boolean): IPaneRenderer | null { + const baseRenderer = this._paneView.renderer(); if (baseRenderer === null) { return null; } From fe96a04de32532658f2cfdfc885fa605c994aa65 Mon Sep 17 00:00:00 2001 From: Eugene Korobko Date: Thu, 12 Jan 2023 15:02:51 +0300 Subject: [PATCH 03/65] Fixed linters and sizes --- .size-limit.js | 4 ++-- src/api/iseries-primitive.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index f7bbb515bf..21ae646cf9 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -4,11 +4,11 @@ module.exports = [ { name: 'ESM', path: 'dist/lightweight-charts.esm.production.js', - limit: '44.36 KB', + limit: '44.51 KB', }, { name: 'Standalone', path: 'dist/lightweight-charts.standalone.production.js', - limit: '45.8 KB', + limit: '46.30 KB', }, ]; diff --git a/src/api/iseries-primitive.ts b/src/api/iseries-primitive.ts index 0f6dde6664..51f1d781cc 100644 --- a/src/api/iseries-primitive.ts +++ b/src/api/iseries-primitive.ts @@ -1,4 +1,4 @@ -import { CanvasRenderingTarget2D } from "fancy-canvas"; +import { CanvasRenderingTarget2D } from 'fancy-canvas'; /** * This interface represents a label on the price or time axis From e4f6090d29a6dc5d574393ec7a9006809924053d Mon Sep 17 00:00:00 2001 From: Mark Silverwood Date: Thu, 12 Jan 2023 12:43:31 +0000 Subject: [PATCH 04/65] fix a few small typos --- src/api/iseries-primitive.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/api/iseries-primitive.ts b/src/api/iseries-primitive.ts index 51f1d781cc..a7ee887938 100644 --- a/src/api/iseries-primitive.ts +++ b/src/api/iseries-primitive.ts @@ -5,7 +5,7 @@ import { CanvasRenderingTarget2D } from 'fancy-canvas'; */ export interface ISeriesPrimitiveAxisView { /** - * coordiate of the label, vertical for price axis and horizontal for time axis + * coordinate of the label, vertical for price axis and horizontal for time axis * * @returns coordinate. 0 means left and top */ @@ -33,17 +33,17 @@ export interface ISeriesPrimitivePaneRenderer { /** * Method to draw main content of the element * - * @param target - cavnas context to draw on, refer FancyCanvas library for more details about this class + * @param target - canvas context to draw on, refer to FancyCanvas library for more details about this class * */ draw(target: CanvasRenderingTarget2D): void; /** * Optional method to draw the background. - * Some elements could implement this method to draw on the background of the chart - * Usually this is some kind of watermarks or time areas highlighting + * Some elements could implement this method to draw on the background of the chart. + * Usually this is some kind of watermarks or time areas highlighting. * - * @param target - cavnas context to draw on, refer FancyCanvas library for more details about this class + * @param target - canvas context to draw on, refer FancyCanvas library for more details about this class */ drawBackground?(target: CanvasRenderingTarget2D): void; } @@ -55,7 +55,7 @@ export interface ISeriesPrimitivePaneView { /** * This method returns a renderer - special object to draw data * - * @returns an renderer object to be used for drawing or null if we have nothin to draw + * @returns an renderer object to be used for drawing, or `null` if we have nothing to draw. */ renderer(): ISeriesPrimitivePaneRenderer | null; } @@ -65,34 +65,34 @@ export interface ISeriesPrimitivePaneView { */ export interface ISeriesPrimitive { /** - * This method is called when viewport has been changed, so primitive have to reacalculate/invaildate its data + * This method is called when viewport has been changed, so primitive have to recalculate / invalidate its data */ updateAllViews(): void; /** * Returns array of labels to be drawn on the price axis used by the series * - * @returns array of objects; each of then must impement ISeriesPrimitiveAxisView interface + * @returns array of objects; each of then must implement ISeriesPrimitiveAxisView interface * - * Try to implement this method returning the same array if nothing changed, this would help the library to save memory and CPU + * Try to implement this method such that the same array is returned if nothing changed, this would help the library to save memory and CPU. */ priceAxisViews(): readonly ISeriesPrimitiveAxisView[]; /** * Returns array of labels to be drawn on the time axis * - * @returns array of objects; each of then must impement ISeriesPrimitiveAxisView interface + * @returns array of objects; each of then must implement ISeriesPrimitiveAxisView interface * - * Try to implement this method returning the same array if nothing changed, this would help the library to save memory and CPU + * Try to implement this method such that the same array is returned if nothing changed, this would help the library to save memory and CPU. */ timeAxisViews(): readonly ISeriesPrimitiveAxisView[]; /** * Returns array of objects representing primitive in the main area of the chart * - * @returns array of objects; each of then must impement ISeriesPrimitivePaneView interface + * @returns array of objects; each of then must implement ISeriesPrimitivePaneView interface * - * Try to implement this method returning the same array if nothing changed, this would help the library to save memory and CPU + * Try to implement this method such that the same array is returned if nothing changed, this would help the library to save memory and CPU. */ paneViews(): readonly ISeriesPrimitivePaneView[]; } From 2bf8859528c7ff237e96466d59e086eaaef724b1 Mon Sep 17 00:00:00 2001 From: Eugene Korobko Date: Fri, 13 Jan 2023 18:42:40 +0300 Subject: [PATCH 05/65] Plugins graphics test --- .../e2e/graphics/test-cases/plugins/basic.js | 506 ++++++++++++++++++ 1 file changed, 506 insertions(+) create mode 100644 tests/e2e/graphics/test-cases/plugins/basic.js diff --git a/tests/e2e/graphics/test-cases/plugins/basic.js b/tests/e2e/graphics/test-cases/plugins/basic.js new file mode 100644 index 0000000000..29fe5793ea --- /dev/null +++ b/tests/e2e/graphics/test-cases/plugins/basic.js @@ -0,0 +1,506 @@ +class VolumeProfileRenderer { + constructor(data) { + this._data = data; + } + draw(target) { + target.useBitmapCoordinateSpace(scope => { + const ctx = scope.context; + const scaledX = Math.round(this._data.x * scope.horizontalPixelRatio); + const scaledTop = Math.round(this._data.top * scope.verticalPixelRatio); + const scaledWidth = Math.round(this._data.width * scope.horizontalPixelRatio); + const scaledHeight = Math.round(this._data.columnHeight * this._data.items.length * scope.verticalPixelRatio); + + const scaledRowHeight = Math.round(this._data.columnHeight * scope.verticalPixelRatio); + + ctx.fillStyle = 'rgba(0, 0, 255, 0.2)'; + ctx.fillRect(scaledX, scaledTop, scaledWidth, -scaledHeight); + + ctx.fillStyle = 'rgba(80, 80, 255, 0.8)'; + this._data.items.forEach(row => { + ctx.fillRect(scaledX, Math.round(row.y * scope.verticalPixelRatio), Math.round(row.width * scope.horizontalPixelRatio), 1 - scaledRowHeight); + }); + }); + } +} + +class VolumeProfilePaneView { + constructor(source) { + this._source = source; + } + update() { + const data = this._source._vpData; + const series = this._source._series; + const timeScale = this._source._chart.timeScale(); + this._x = timeScale.timeToCoordinate(data.time); + this._width = timeScale.options().barSpacing; + + const y1 = series.priceToCoordinate(data.profile[0].price); + const y2 = series.priceToCoordinate(data.profile[1].price); + this._columnHeight = Math.max(1, y1 - y2); + const maxVolume = data.profile.reduce((acc, item) => Math.max(acc, item.vol), 0); + + this._top = y1; + + this._items = data.profile.map(row => ({ + y: series.priceToCoordinate(row.price), + width: this._width * row.vol / maxVolume, + })); + } + + renderer() { + return new VolumeProfileRenderer({ + x: this._x, + top: this._top, + columnHeight: this._columnHeight, + width: this._width, + items: this._items, + }); + } + +} + +class VolumeProfile { + // points - {date, price} + constructor(chart, series, vpData) { + this._chart = chart; + this._series = series; + this._vpData = vpData; + this._paneViews = [new VolumeProfilePaneView(this)]; + } + updateAllViews() { + this._paneViews.forEach(pw => pw.update()); + } + priceAxisViews() { + return []; + } + timeAxisViews() { + return []; + } + paneViews() { + return this._paneViews; + } +} + +class VertLinePaneRenderer { + constructor(x) { + this._x = x; + } + draw(target) { + target.useBitmapCoordinateSpace(scope => { + const ctx = scope.context; + const xScaled = Math.round(this._x * scope.horizontalPixelRatio) + 0.5; + ctx.strokeStyle = "red"; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(xScaled, 0); + ctx.lineTo(xScaled, scope.bitmapSize.height); + ctx.stroke(); + }); + } +} + +class VertLinePaneView { + constructor(source) { + this._source = source; + } + update() { + const series = this._source._series; + const timeScale = this._source._chart.timeScale(); + this._x = timeScale.timeToCoordinate(this._source._time); + } + renderer() { + return new VertLinePaneRenderer(this._x); + } +} + +class VertLineTimeAxisView { + constructor(source) { + this._source = source; + } + update() { + const series = this._source._series; + const timeScale = this._source._chart.timeScale(); + this._x = timeScale.timeToCoordinate(this._source._time); + } + coordinate() { + return this._x; + } + text() { + return this._source._time; + } + textColor() { + return 'white'; + } + backColor() { + return 'green'; + } +} + +class VertLine { + // points - {date, price} + constructor(chart, series, time) { + this._chart = chart; + this._series = series; + this._time = time; + this._paneViews = [new VertLinePaneView(this)]; + this._timeAxisViews = [new VertLineTimeAxisView(this)] + } + updateAllViews() { + this._paneViews.forEach(pw => pw.update()); + this._timeAxisViews.forEach(tw => tw.update()) + } + priceAxisViews() { + return []; + } + timeAxisViews() { + return this._timeAxisViews; + } + paneViews() { + return this._paneViews; + } +} + +class TrendLinePaneRenderer { + constructor(p1, p2, text) { + this._p1 = p1; + this._p2 = p2; + this._text = text; + } + draw(target) { + target.useBitmapCoordinateSpace(scope => { + const ctx = scope.context; + const x1Scaled = Math.round(this._p1.x * scope.horizontalPixelRatio); + const y1Scaled = Math.round(this._p1.y * scope.verticalPixelRatio); + const x2Scaled = Math.round(this._p2.x * scope.horizontalPixelRatio); + const y2Scaled = Math.round(this._p2.y * scope.verticalPixelRatio); + ctx.lineWidth = 2; + ctx.strokeStyle = "blue"; + ctx.beginPath(); + ctx.moveTo(x1Scaled, y1Scaled); + ctx.lineTo(x2Scaled, y2Scaled); + ctx.stroke(); + ctx.font = '24px Arial' + ctx.fillStyle = 'green'; + ctx.fillText(this._text, x2Scaled, y2Scaled) + }); + } +} +class TrendLinePaneView { + constructor(source) { + this._source = source; + } + update() { + const series = this._source._series; + const y1 = series.priceToCoordinate(this._source._p1.price); + const y2 = series.priceToCoordinate(this._source._p2.price); + const timeScale = this._source._chart.timeScale(); + const x1 = timeScale.timeToCoordinate(this._source._p1.time); + const x2 = timeScale.timeToCoordinate(this._source._p2.time); + this._p1 = {x: x1, y: y1}; + this._p2 = {x: x2, y: y2}; + } + renderer() { + return new TrendLinePaneRenderer(this._p1, this._p2, '' + this._source._p2.price); + } +} +class TrendLine { + // points - {date, price} + constructor(chart, series, p1, p2) { + this._chart = chart; + this._series = series; + this._p1 = p1; + this._p2 = p2; + this._paneViews = [new TrendLinePaneView(this)]; + } + updateAllViews() { + this._paneViews.forEach(pw => pw.update()); + } + priceAxisViews() { + return []; + } + timeAxisViews() { + return []; + } + paneViews() { + return this._paneViews; + } +} + +class RectanglePaneRenderer { + constructor(p1, p2) { + this._p1 = p1; + this._p2 = p2; + } + draw(target) { + target.useBitmapCoordinateSpace(scope => { + const ctx = scope.context; + const x1Scaled = Math.round(this._p1.x * scope.horizontalPixelRatio); + const y1Scaled = Math.round(this._p1.y * scope.verticalPixelRatio); + const x2Scaled = Math.round(this._p2.x * scope.horizontalPixelRatio); + const y2Scaled = Math.round(this._p2.y * scope.verticalPixelRatio); + const width = x2Scaled - x1Scaled + 1; + const height = y2Scaled - y1Scaled + 1; + ctx.fillStyle = "rgba(0, 0, 255, 0.5)"; + ctx.fillRect(x1Scaled, y1Scaled, width, height); + }); + } +} + +class RectanglePaneView { + constructor(source) { + this._source = source; + } + update() { + const series = this._source._series; + const y1 = series.priceToCoordinate(this._source._p1.price); + const y2 = series.priceToCoordinate(this._source._p2.price); + const timeScale = this._source._chart.timeScale(); + const x1 = timeScale.timeToCoordinate(this._source._p1.time); + const x2 = timeScale.timeToCoordinate(this._source._p2.time); + this._p1 = {x: x1, y: y1}; + this._p2 = {x: x2, y: y2}; + } + renderer(height, width) { + return new RectanglePaneRenderer(this._p1, this._p2, this._source._color); + } +} + +class RectangleTimeAxisView { + constructor(source, p) { + this._source = source; + this._p = p; + } + update() { + const series = this._source._series; + const timeScale = this._source._chart.timeScale(); + this._x = timeScale.timeToCoordinate(this._p.time); + } + coordinate() { + return this._x; + } + text() { + return this._p.time; + } + textColor() { + return 'white'; + } + backColor() { + return 'blue'; + } +} + +class RectanglePriceAxisView { + constructor(source, p) { + this._source = source; + this._p = p; + } + update() { + const series = this._source._series; + this._y = series.priceToCoordinate(this._p.price); + } + coordinate() { + return this._y; + } + text() { + return '' + this._p.price; + } + textColor() { + return 'white'; + } + backColor() { + return 'blue'; + } +} + +class Rectangle { + // points - {date, price} + constructor(chart, series, p1, p2, color) { + this._chart = chart; + this._series = series; + this._p1 = p1; + this._p2 = p2; + this._color = color; + this._paneViews = [new RectanglePaneView(this)]; + this._timeAxisViews = [new RectangleTimeAxisView(this, p1), new RectangleTimeAxisView(this, p2)]; + this._priceAxisViews = [new RectanglePriceAxisView(this, p1), new RectanglePriceAxisView(this, p2)]; + } + updateAllViews() { + this._paneViews.forEach(pw => pw.update()); + this._timeAxisViews.forEach(pw => pw.update()); + this._priceAxisViews.forEach(pw => pw.update()); + } + priceAxisViews() { + return this._priceAxisViews; + } + timeAxisViews() { + return this._timeAxisViews; + } + paneViews() { + return this._paneViews; + } +} + + +class AnchoredTextRenderer { + constructor(data) { + this._data = data; + } + draw(target) { + target.useBitmapCoordinateSpace(scope => { + const ctx = scope.context; + ctx.save(); + ctx.font = this._data.font; + const textWidth = ctx.measureText(this._data.text).width; + ctx.scale(scope.horizontalPixelRatio, scope.verticalPixelRatio); + const horzMargin = 20; + let x = horzMargin; + const width = scope.mediaSize.width; + const height = scope.mediaSize.height; + switch (this._data.horzAlign) { + case 'right': { + x = width - horzMargin - textWidth; + break; + } + case 'middle': { + x = (width - textWidth / 2); + break; + } + } + const vertMargin = 10; + const y = vertMargin + this._data.lineHeight; + switch (this._data.vertAlign) { + case 'middle': { + y = (height - this._data.lineHeight) / 2; + break; + } + case 'bottom': { + y = height - vertMargin; + break; + } + } + ctx.fillStyle = this._data.color; + ctx.fillText(this._data.text, x, y); + ctx.restore(); + }); + } +} + +class AnchoredTextPaneView { + constructor(source) { + this._source = source; + } + update() { + } + renderer() { + return new AnchoredTextRenderer(this._source._data); + } +} + +class AnchoredText { + constructor(chart, series, data) { + this._chart = chart; + this._series = series; + this._data = data; + this._paneViews = [new AnchoredTextPaneView(this)]; + } + updateAllViews() { + this._paneViews.forEach(pw => pw.update()); + } + priceAxisViews() { + return []; + } + timeAxisViews() { + return []; + } + paneViews() { + return this._paneViews; + } +} + + +function runTestCase(container) { + const chart = window.chart = LightweightCharts.createChart(container); + + chart.timeScale().applyOptions({ + barSpacing: 50, + rightOffset: 5 + }); + const s1 = chart.addLineSeries({ + color: 'red' + }); + s1.setData([ + { 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 }, + ]); + + const rect = new Rectangle(chart, s1, { time: '2019-04-11', price: 70.01 }, { time: '2019-04-16', price: 90.01 }); + s1.attachPrimitive(rect); + + const trend = new TrendLine(chart, s1, { time: '2019-04-11', price: 70.01 }, { time: '2019-04-16', price: 90.01 }, 'trend'); + s1.attachPrimitive(trend); + + const vert = new VertLine(chart, s1, '2019-04-14'); + s1.attachPrimitive(vert); + + const vpData = { + time: '2019-04-12', + profile: [ + { + price: 90, + vol: 4 + }, + { + price: 91, + vol: 7 + }, + { + price: 92, + vol: 7 + }, + { + price: 93, + vol: 11 + }, + { + price: 94, + vol: 17 + }, + { + price: 95, + vol: 15 + }, + { + price: 96, + vol: 10 + }, + { + price: 97, + vol: 13 + }, + { + price: 98, + vol: 1 + }, + { + price: 99, + vol: 6 + } + ] + }; + const vp = new VolumeProfile(chart, s1, vpData); + s1.attachPrimitive(vp); + + const anchoredText = new AnchoredText(chart, s1, { + vertAlign: 'top', + horzAlign: 'right', + text: 'My Text', + lineHeight: 54, + font: 'italic bold 54px Arial', + color: 'red' + }); + s1.attachPrimitive(anchoredText); +} From e91065bcbcceb993fd967a7ebc25772ef31a83b2 Mon Sep 17 00:00:00 2001 From: Eugene Korobko Date: Fri, 13 Jan 2023 18:58:03 +0300 Subject: [PATCH 06/65] Fixed linter for test --- .../e2e/graphics/test-cases/plugins/basic.js | 90 +++++++++---------- 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/tests/e2e/graphics/test-cases/plugins/basic.js b/tests/e2e/graphics/test-cases/plugins/basic.js index 29fe5793ea..3674eff79f 100644 --- a/tests/e2e/graphics/test-cases/plugins/basic.js +++ b/tests/e2e/graphics/test-cases/plugins/basic.js @@ -1,3 +1,4 @@ +/* eslint-disable max-classes-per-file */ class VolumeProfileRenderer { constructor(data) { this._data = data; @@ -20,7 +21,7 @@ class VolumeProfileRenderer { ctx.fillRect(scaledX, Math.round(row.y * scope.verticalPixelRatio), Math.round(row.width * scope.horizontalPixelRatio), 1 - scaledRowHeight); }); }); - } + } } class VolumeProfilePaneView { @@ -40,7 +41,7 @@ class VolumeProfilePaneView { const maxVolume = data.profile.reduce((acc, item) => Math.max(acc, item.vol), 0); this._top = y1; - + this._items = data.profile.map(row => ({ y: series.priceToCoordinate(row.price), width: this._width * row.vol / maxVolume, @@ -56,7 +57,6 @@ class VolumeProfilePaneView { items: this._items, }); } - } class VolumeProfile { @@ -84,19 +84,19 @@ class VolumeProfile { class VertLinePaneRenderer { constructor(x) { this._x = x; - } + } draw(target) { target.useBitmapCoordinateSpace(scope => { const ctx = scope.context; const xScaled = Math.round(this._x * scope.horizontalPixelRatio) + 0.5; - ctx.strokeStyle = "red"; + ctx.strokeStyle = 'red'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(xScaled, 0); ctx.lineTo(xScaled, scope.bitmapSize.height); ctx.stroke(); }); - } + } } class VertLinePaneView { @@ -104,7 +104,6 @@ class VertLinePaneView { this._source = source; } update() { - const series = this._source._series; const timeScale = this._source._chart.timeScale(); this._x = timeScale.timeToCoordinate(this._source._time); } @@ -118,7 +117,6 @@ class VertLineTimeAxisView { this._source = source; } update() { - const series = this._source._series; const timeScale = this._source._chart.timeScale(); this._x = timeScale.timeToCoordinate(this._source._time); } @@ -143,11 +141,11 @@ class VertLine { this._series = series; this._time = time; this._paneViews = [new VertLinePaneView(this)]; - this._timeAxisViews = [new VertLineTimeAxisView(this)] + this._timeAxisViews = [new VertLineTimeAxisView(this)]; } updateAllViews() { this._paneViews.forEach(pw => pw.update()); - this._timeAxisViews.forEach(tw => tw.update()) + this._timeAxisViews.forEach(tw => tw.update()); } priceAxisViews() { return []; @@ -174,14 +172,14 @@ class TrendLinePaneRenderer { const x2Scaled = Math.round(this._p2.x * scope.horizontalPixelRatio); const y2Scaled = Math.round(this._p2.y * scope.verticalPixelRatio); ctx.lineWidth = 2; - ctx.strokeStyle = "blue"; + ctx.strokeStyle = 'blue'; ctx.beginPath(); ctx.moveTo(x1Scaled, y1Scaled); ctx.lineTo(x2Scaled, y2Scaled); ctx.stroke(); - ctx.font = '24px Arial' + ctx.font = '24px Arial'; ctx.fillStyle = 'green'; - ctx.fillText(this._text, x2Scaled, y2Scaled) + ctx.fillText(this._text, x2Scaled, y2Scaled); }); } } @@ -196,8 +194,8 @@ class TrendLinePaneView { const timeScale = this._source._chart.timeScale(); const x1 = timeScale.timeToCoordinate(this._source._p1.time); const x2 = timeScale.timeToCoordinate(this._source._p2.time); - this._p1 = {x: x1, y: y1}; - this._p2 = {x: x2, y: y2}; + this._p1 = { x: x1, y: y1 }; + this._p2 = { x: x2, y: y2 }; } renderer() { return new TrendLinePaneRenderer(this._p1, this._p2, '' + this._source._p2.price); @@ -240,9 +238,9 @@ class RectanglePaneRenderer { const y2Scaled = Math.round(this._p2.y * scope.verticalPixelRatio); const width = x2Scaled - x1Scaled + 1; const height = y2Scaled - y1Scaled + 1; - ctx.fillStyle = "rgba(0, 0, 255, 0.5)"; + ctx.fillStyle = 'rgba(0, 0, 255, 0.5)'; ctx.fillRect(x1Scaled, y1Scaled, width, height); - }); + }); } } @@ -257,8 +255,8 @@ class RectanglePaneView { const timeScale = this._source._chart.timeScale(); const x1 = timeScale.timeToCoordinate(this._source._p1.time); const x2 = timeScale.timeToCoordinate(this._source._p2.time); - this._p1 = {x: x1, y: y1}; - this._p2 = {x: x2, y: y2}; + this._p1 = { x: x1, y: y1 }; + this._p2 = { x: x2, y: y2 }; } renderer(height, width) { return new RectanglePaneRenderer(this._p1, this._p2, this._source._color); @@ -271,7 +269,6 @@ class RectangleTimeAxisView { this._p = p; } update() { - const series = this._source._series; const timeScale = this._source._chart.timeScale(); this._x = timeScale.timeToCoordinate(this._p.time); } @@ -340,7 +337,6 @@ class Rectangle { } } - class AnchoredTextRenderer { constructor(data) { this._data = data; @@ -367,7 +363,7 @@ class AnchoredTextRenderer { } } const vertMargin = 10; - const y = vertMargin + this._data.lineHeight; + let y = vertMargin + this._data.lineHeight; switch (this._data.vertAlign) { case 'middle': { y = (height - this._data.lineHeight) / 2; @@ -382,15 +378,14 @@ class AnchoredTextRenderer { ctx.fillText(this._data.text, x, y); ctx.restore(); }); - } + } } class AnchoredTextPaneView { constructor(source) { this._source = source; } - update() { - } + update() {} renderer() { return new AnchoredTextRenderer(this._source._data); } @@ -402,7 +397,7 @@ class AnchoredText { this._series = series; this._data = data; this._paneViews = [new AnchoredTextPaneView(this)]; - } + } updateAllViews() { this._paneViews.forEach(pw => pw.update()); } @@ -417,24 +412,23 @@ class AnchoredText { } } - function runTestCase(container) { const chart = window.chart = LightweightCharts.createChart(container); chart.timeScale().applyOptions({ barSpacing: 50, - rightOffset: 5 + rightOffset: 5, }); const s1 = chart.addLineSeries({ - color: 'red' + color: 'red', }); s1.setData([ { 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-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 }, ]); const rect = new Rectangle(chart, s1, { time: '2019-04-11', price: 70.01 }, { time: '2019-04-16', price: 90.01 }); @@ -451,45 +445,45 @@ function runTestCase(container) { profile: [ { price: 90, - vol: 4 + vol: 4, }, { price: 91, - vol: 7 + vol: 7, }, { price: 92, - vol: 7 + vol: 7, }, { price: 93, - vol: 11 + vol: 11, }, { price: 94, - vol: 17 + vol: 17, }, { price: 95, - vol: 15 + vol: 15, }, { price: 96, - vol: 10 + vol: 10, }, { price: 97, - vol: 13 + vol: 13, }, { price: 98, - vol: 1 + vol: 1, }, { price: 99, - vol: 6 - } - ] + vol: 6, + }, + ], }; const vp = new VolumeProfile(chart, s1, vpData); s1.attachPrimitive(vp); @@ -498,9 +492,9 @@ function runTestCase(container) { vertAlign: 'top', horzAlign: 'right', text: 'My Text', - lineHeight: 54, + lineHeight: 54, font: 'italic bold 54px Arial', - color: 'red' + color: 'red', }); s1.attachPrimitive(anchoredText); } From 3be80d6a44986e044194cb0c0d3c0bd33a3935cf Mon Sep 17 00:00:00 2001 From: Eugene Korobko Date: Mon, 16 Jan 2023 09:31:26 +0300 Subject: [PATCH 07/65] Fixed comment with API description --- src/api/iseries-primitive.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/api/iseries-primitive.ts b/src/api/iseries-primitive.ts index a7ee887938..1363fd9663 100644 --- a/src/api/iseries-primitive.ts +++ b/src/api/iseries-primitive.ts @@ -5,9 +5,9 @@ import { CanvasRenderingTarget2D } from 'fancy-canvas'; */ export interface ISeriesPrimitiveAxisView { /** - * coordinate of the label, vertical for price axis and horizontal for time axis + *coordinate of the label. For a price axis the value returned will represent the vertical distance (pixels) from the top. For a time axis the value will represent the horizontal distance from the left. * - * @returns coordinate. 0 means left and top + * @returns coordinate. distance from top for price axis, or distance from left for time axis. * */ coordinate(): number; /** @@ -74,7 +74,8 @@ export interface ISeriesPrimitive { * * @returns array of objects; each of then must implement ISeriesPrimitiveAxisView interface * - * Try to implement this method such that the same array is returned if nothing changed, this would help the library to save memory and CPU. + * For performance reasons, the lightweight library uses internal caches based on references to arrays + * So, this method must return new array if set of views has changed and should try to return the same array if nothing changed */ priceAxisViews(): readonly ISeriesPrimitiveAxisView[]; @@ -83,7 +84,8 @@ export interface ISeriesPrimitive { * * @returns array of objects; each of then must implement ISeriesPrimitiveAxisView interface * - * Try to implement this method such that the same array is returned if nothing changed, this would help the library to save memory and CPU. + * For performance reasons, the lightweight library uses internal caches based on references to arrays + * So, this method must return new array if set of views has changed and should try to return the same array if nothing changed */ timeAxisViews(): readonly ISeriesPrimitiveAxisView[]; @@ -92,7 +94,8 @@ export interface ISeriesPrimitive { * * @returns array of objects; each of then must implement ISeriesPrimitivePaneView interface * - * Try to implement this method such that the same array is returned if nothing changed, this would help the library to save memory and CPU. + * For performance reasons, the lightweight library uses internal caches based on references to arrays + * So, this method must return new array if set of views has changed and should try to return the same array if nothing changed */ paneViews(): readonly ISeriesPrimitivePaneView[]; } From bc8242adf2114048724b948fe53d5aa6bfb9afff Mon Sep 17 00:00:00 2001 From: Eugene Korobko Date: Mon, 23 Jan 2023 09:13:32 +0300 Subject: [PATCH 08/65] Move files to another folder --- src/api/iseries-api.ts | 2 +- src/api/series-api.ts | 2 +- src/{api => model}/iseries-primitive.ts | 0 src/{api => model}/series-primitive-wrapper.ts | 10 +++++----- src/model/series.ts | 5 ++--- src/tsconfig.model.json | 2 -- 6 files changed, 9 insertions(+), 12 deletions(-) rename src/{api => model}/iseries-primitive.ts (100%) rename src/{api => model}/series-primitive-wrapper.ts (96%) diff --git a/src/api/iseries-api.ts b/src/api/iseries-api.ts index c68ce41144..60f27f3e17 100644 --- a/src/api/iseries-api.ts +++ b/src/api/iseries-api.ts @@ -2,6 +2,7 @@ import { IPriceFormatter } from '../formatters/iprice-formatter'; import { BarPrice } from '../model/bar'; import { Coordinate } from '../model/coordinate'; +import { ISeriesPrimitive } from '../model/iseries-primitive'; import { MismatchDirection } from '../model/plot-list'; import { CreatePriceLineOptions } from '../model/price-line-options'; import { SeriesMarker } from '../model/series-markers'; @@ -15,7 +16,6 @@ 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'; /** * Represents a range of bars and the number of bars outside the range. diff --git a/src/api/series-api.ts b/src/api/series-api.ts index c2f494e3a8..b8b2119683 100644 --- a/src/api/series-api.ts +++ b/src/api/series-api.ts @@ -5,6 +5,7 @@ import { clone, merge } from '../helpers/strict-type-checks'; import { BarPrice } from '../model/bar'; import { Coordinate } from '../model/coordinate'; +import { ISeriesPrimitive } from '../model/iseries-primitive'; import { MismatchDirection } from '../model/plot-list'; import { CreatePriceLineOptions, PriceLineOptions } from '../model/price-line-options'; import { RangeImpl } from '../model/range-impl'; @@ -26,7 +27,6 @@ import { getSeriesDataCreator } from './get-series-data-creator'; import { IPriceLine } from './iprice-line'; import { IPriceScaleApi } from './iprice-scale-api'; import { BarsInfo, ISeriesApi } from './iseries-api'; -import { ISeriesPrimitive } from './iseries-primitive'; import { priceLineOptionsDefaults } from './options/price-line-options-defaults'; import { PriceLine } from './price-line-api'; diff --git a/src/api/iseries-primitive.ts b/src/model/iseries-primitive.ts similarity index 100% rename from src/api/iseries-primitive.ts rename to src/model/iseries-primitive.ts diff --git a/src/api/series-primitive-wrapper.ts b/src/model/series-primitive-wrapper.ts similarity index 96% rename from src/api/series-primitive-wrapper.ts rename to src/model/series-primitive-wrapper.ts index b62e177df9..7b92cb4dbd 100644 --- a/src/api/series-primitive-wrapper.ts +++ b/src/model/series-primitive-wrapper.ts @@ -1,10 +1,10 @@ import { CanvasRenderingTarget2D } from 'fancy-canvas'; -import { HoveredObject } from '../model/chart-model'; -import { Coordinate } from '../model/coordinate'; -import { PriceScale } from '../model/price-scale'; -import { Series } from '../model/series'; -import { TimeScale } from '../model/time-scale'; +import { HoveredObject } from './chart-model'; +import { Coordinate } from './coordinate'; +import { PriceScale } from './price-scale'; +import { Series } from './series'; +import { TimeScale } from './time-scale'; import { IPaneRenderer } from '../renderers/ipane-renderer'; import { PriceAxisViewRendererCommonData, PriceAxisViewRendererData } from '../renderers/iprice-axis-view-renderer'; import { TimeAxisViewRenderer } from '../renderers/time-axis-view-renderer'; diff --git a/src/model/series.ts b/src/model/series.ts index c116ec1f43..e03d1da18c 100644 --- a/src/model/series.ts +++ b/src/model/series.ts @@ -1,6 +1,3 @@ -import { ISeriesPrimitive } from '../api/iseries-primitive'; -import { SeriesPrimitiveWrapper } from '../api/series-primitive-wrapper'; - import { IPriceFormatter } from '../formatters/iprice-formatter'; import { PercentageFormatter } from '../formatters/percentage-formatter'; import { PriceFormatter } from '../formatters/price-formatter'; @@ -34,6 +31,7 @@ import { Coordinate } from './coordinate'; import { CustomPriceLine } from './custom-price-line'; import { isDefaultPriceScale } from './default-price-scale'; import { FirstValue } from './iprice-data-source'; +import { ISeriesPrimitive } from './iseries-primitive'; import { Pane } from './pane'; import { PlotRowValueIndex } from './plot-data'; import { MismatchDirection } from './plot-list'; @@ -53,6 +51,7 @@ import { SeriesPartialOptionsMap, SeriesType, } from './series-options'; +import { SeriesPrimitiveWrapper } from './series-primitive-wrapper'; import { TimePoint, TimePointIndex } from './time-data'; export interface LastValueDataResultWithoutData { diff --git a/src/tsconfig.model.json b/src/tsconfig.model.json index 2c57b765f4..1ad184d815 100644 --- a/src/tsconfig.model.json +++ b/src/tsconfig.model.json @@ -8,7 +8,5 @@ "./model/**/*.ts", "./renderers/**/*.ts", "./views/**/*.ts", - "./api/iseries-primitive.ts", - "./api/series-primitive-wrapper.ts", ] } From c9a685b0c473bbc1c878d31dfa3695ca02c522bb Mon Sep 17 00:00:00 2001 From: Eugene Korobko Date: Mon, 23 Jan 2023 09:48:37 +0300 Subject: [PATCH 09/65] Fixed linter --- .size-limit.js | 4 ++-- src/model/series-primitive-wrapper.ts | 10 +++++----- src/tsconfig.model.json | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 21ae646cf9..fd3446bbff 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -4,11 +4,11 @@ module.exports = [ { name: 'ESM', path: 'dist/lightweight-charts.esm.production.js', - limit: '44.51 KB', + limit: '44.79 KB', }, { name: 'Standalone', path: 'dist/lightweight-charts.standalone.production.js', - limit: '46.30 KB', + limit: '46.55 KB', }, ]; diff --git a/src/model/series-primitive-wrapper.ts b/src/model/series-primitive-wrapper.ts index 7b92cb4dbd..7bb6b8200a 100644 --- a/src/model/series-primitive-wrapper.ts +++ b/src/model/series-primitive-wrapper.ts @@ -1,10 +1,5 @@ import { CanvasRenderingTarget2D } from 'fancy-canvas'; -import { HoveredObject } from './chart-model'; -import { Coordinate } from './coordinate'; -import { PriceScale } from './price-scale'; -import { Series } from './series'; -import { TimeScale } from './time-scale'; import { IPaneRenderer } from '../renderers/ipane-renderer'; import { PriceAxisViewRendererCommonData, PriceAxisViewRendererData } from '../renderers/iprice-axis-view-renderer'; import { TimeAxisViewRenderer } from '../renderers/time-axis-view-renderer'; @@ -13,12 +8,17 @@ import { IPriceAxisView } from '../views/price-axis/iprice-axis-view'; import { PriceAxisView } from '../views/price-axis/price-axis-view'; import { ITimeAxisView } from '../views/time-axis/itime-axis-view'; +import { HoveredObject } from './chart-model'; +import { Coordinate } from './coordinate'; import { ISeriesPrimitive, ISeriesPrimitiveAxisView, ISeriesPrimitivePaneRenderer, ISeriesPrimitivePaneView, } from './iseries-primitive'; +import { PriceScale } from './price-scale'; +import { Series } from './series'; +import { TimeScale } from './time-scale'; class SeriesPrimitiveRendererWrapper implements IPaneRenderer { private readonly _baseRenderer: ISeriesPrimitivePaneRenderer; diff --git a/src/tsconfig.model.json b/src/tsconfig.model.json index 1ad184d815..aac88ae534 100644 --- a/src/tsconfig.model.json +++ b/src/tsconfig.model.json @@ -7,6 +7,6 @@ "include": [ "./model/**/*.ts", "./renderers/**/*.ts", - "./views/**/*.ts", + "./views/**/*.ts" ] } From 10879e8b3546e70f019255c0290eee9fbe29e38c Mon Sep 17 00:00:00 2001 From: Mark Silverwood Date: Tue, 28 Mar 2023 16:08:25 +0100 Subject: [PATCH 10/65] fix JSDoc comment within ISeriesPrimitiveAxisView --- src/model/iseries-primitive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/iseries-primitive.ts b/src/model/iseries-primitive.ts index 1363fd9663..b63aa2fe63 100644 --- a/src/model/iseries-primitive.ts +++ b/src/model/iseries-primitive.ts @@ -5,7 +5,7 @@ import { CanvasRenderingTarget2D } from 'fancy-canvas'; */ export interface ISeriesPrimitiveAxisView { /** - *coordinate of the label. For a price axis the value returned will represent the vertical distance (pixels) from the top. For a time axis the value will represent the horizontal distance from the left. + * coordinate of the label. For a price axis the value returned will represent the vertical distance (pixels) from the top. For a time axis the value will represent the horizontal distance from the left. * * @returns coordinate. distance from top for price axis, or distance from left for time axis. * */ From 1a5dde1c5ca599af9dccbfaa727699b113a782f2 Mon Sep 17 00:00:00 2001 From: Mark Silverwood Date: Tue, 28 Mar 2023 16:16:03 +0100 Subject: [PATCH 11/65] update size-limit and primitive wrapper after master merge --- .size-limit.js | 8 ++++---- src/model/series-primitive-wrapper.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index cb3cb2eaa0..856978c98a 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -4,21 +4,21 @@ module.exports = [ { name: 'CJS', path: 'dist/lightweight-charts.production.cjs', - limit: '44.45 KB', + limit: '45.0 KB', }, { name: 'ESM', path: 'dist/lightweight-charts.production.mjs', - limit: '44.35 KB', + limit: '45.95 KB', }, { name: 'Standalone-ESM', path: 'dist/lightweight-charts.standalone.production.mjs', - limit: '46.05 KB', + limit: '46.65 KB', }, { name: 'Standalone', path: 'dist/lightweight-charts.standalone.production.js', - limit: '46.1 KB', + limit: '46.7 KB', }, ]; diff --git a/src/model/series-primitive-wrapper.ts b/src/model/series-primitive-wrapper.ts index 7bb6b8200a..06590e504b 100644 --- a/src/model/series-primitive-wrapper.ts +++ b/src/model/series-primitive-wrapper.ts @@ -112,7 +112,7 @@ class SeriesPrimitivePriceAxisViewWrapper extends PriceAxisView { axisRendererData.visible = false; commonRendererData.background = this._baseView.backColor(); - commonRendererData.color = this._baseView.textColor(); + axisRendererData.color = this._baseView.textColor(); const additionalPadding = 2 / 12 * this._priceScale.fontSize(); From 7dbd5aec4643c6a8b278feb284abbc3eafad2ea5 Mon Sep 17 00:00:00 2001 From: Mark Silverwood Date: Tue, 28 Mar 2023 16:35:46 +0100 Subject: [PATCH 12/65] add coverage test for plugins --- .../e2e/coverage/test-cases/chart/plugins.js | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 tests/e2e/coverage/test-cases/chart/plugins.js diff --git a/tests/e2e/coverage/test-cases/chart/plugins.js b/tests/e2e/coverage/test-cases/chart/plugins.js new file mode 100644 index 0000000000..0deffce5c5 --- /dev/null +++ b/tests/e2e/coverage/test-cases/chart/plugins.js @@ -0,0 +1,157 @@ +function interactionsToPerform() { + return []; +} + +function beforeInteractions(container) { + const chart = (window.chart = LightweightCharts.createChart(container)); + + chart.timeScale().applyOptions({ + barSpacing: 50, + rightOffset: 5, + }); + const s1 = chart.addLineSeries({ + color: 'red', + }); + s1.setData([ + { 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 }, + ]); + + const rect = new Rectangle( + chart, + s1, + { time: '2019-04-11', price: 70.01 }, + { time: '2019-04-16', price: 90.01 } + ); + s1.attachPrimitive(rect); + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +function afterInteractions() { + return Promise.resolve(); +} + +class RectanglePaneRenderer { + constructor(p1, p2) { + this._p1 = p1; + this._p2 = p2; + } + draw(target) { + target.useBitmapCoordinateSpace(scope => { + const ctx = scope.context; + const x1Scaled = Math.round(this._p1.x * scope.horizontalPixelRatio); + const y1Scaled = Math.round(this._p1.y * scope.verticalPixelRatio); + const x2Scaled = Math.round(this._p2.x * scope.horizontalPixelRatio); + const y2Scaled = Math.round(this._p2.y * scope.verticalPixelRatio); + const width = x2Scaled - x1Scaled + 1; + const height = y2Scaled - y1Scaled + 1; + ctx.fillStyle = 'rgba(0, 0, 255, 0.5)'; + ctx.fillRect(x1Scaled, y1Scaled, width, height); + }); + } +} + +class RectanglePaneView { + constructor(source) { + this._source = source; + } + update() { + const series = this._source._series; + const y1 = series.priceToCoordinate(this._source._p1.price); + const y2 = series.priceToCoordinate(this._source._p2.price); + const timeScale = this._source._chart.timeScale(); + const x1 = timeScale.timeToCoordinate(this._source._p1.time); + const x2 = timeScale.timeToCoordinate(this._source._p2.time); + this._p1 = { x: x1, y: y1 }; + this._p2 = { x: x2, y: y2 }; + } + renderer() { + return new RectanglePaneRenderer(this._p1, this._p2, this._source._color); + } +} + +class RectangleTimeAxisView { + constructor(source, p) { + this._source = source; + this._p = p; + } + update() { + const timeScale = this._source._chart.timeScale(); + this._x = timeScale.timeToCoordinate(this._p.time); + } + coordinate() { + return this._x; + } + text() { + return this._p.time; + } + textColor() { + return 'white'; + } + backColor() { + return 'blue'; + } +} + +class RectanglePriceAxisView { + constructor(source, p) { + this._source = source; + this._p = p; + } + update() { + const series = this._source._series; + this._y = series.priceToCoordinate(this._p.price); + } + coordinate() { + return this._y; + } + text() { + return '' + this._p.price; + } + textColor() { + return 'white'; + } + backColor() { + return 'blue'; + } +} + +class Rectangle { + constructor(chart, series, p1, p2, color) { + this._chart = chart; + this._series = series; + this._p1 = p1; + this._p2 = p2; + this._color = color; + this._paneViews = [new RectanglePaneView(this)]; + this._timeAxisViews = [ + new RectangleTimeAxisView(this, p1), + new RectangleTimeAxisView(this, p2), + ]; + this._priceAxisViews = [ + new RectanglePriceAxisView(this, p1), + new RectanglePriceAxisView(this, p2), + ]; + } + updateAllViews() { + this._paneViews.forEach(pw => pw.update()); + this._timeAxisViews.forEach(pw => pw.update()); + this._priceAxisViews.forEach(pw => pw.update()); + } + priceAxisViews() { + return this._priceAxisViews; + } + timeAxisViews() { + return this._timeAxisViews; + } + paneViews() { + return this._paneViews; + } +} From 039630f6a4466bff66c25eb26c9e0080730f64d1 Mon Sep 17 00:00:00 2001 From: Mark Silverwood Date: Tue, 28 Mar 2023 18:29:23 +0100 Subject: [PATCH 13/65] added attached and detached lifecycle hooks --- src/api/chart-api.ts | 2 +- src/api/series-api.ts | 11 ++++++++++- src/model/iseries-primitive.ts | 20 ++++++++++++++++++++ src/tsconfig.model.json | 4 +++- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/api/chart-api.ts b/src/api/chart-api.ts index 9c638641b3..0266f2a1ea 100644 --- a/src/api/chart-api.ts +++ b/src/api/chart-api.ts @@ -268,7 +268,7 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer { const strictOptions = merge(clone(seriesOptionsDefaults), clone(styleDefaults), options) as SeriesOptionsMap[TSeries]; const series = this._chartWidget.model().createSeries(type, strictOptions); - 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); diff --git a/src/api/series-api.ts b/src/api/series-api.ts index b8b2119683..1dd4d1cbb3 100644 --- a/src/api/series-api.ts +++ b/src/api/series-api.ts @@ -24,6 +24,7 @@ import { DataUpdatesConsumer, SeriesDataItemTypeMap } 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'; @@ -33,13 +34,15 @@ import { PriceLine } from './price-line-api'; export class SeriesApi implements ISeriesApi { protected _series: Series; protected _dataUpdatesConsumer: DataUpdatesConsumer; + protected readonly _chartApi: IChartApi; private readonly _priceScaleApiProvider: IPriceScaleApiProvider; - 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 priceFormatter(): IPriceFormatter { @@ -189,9 +192,15 @@ export class SeriesApi implements ISeriesApi) => void; + /** + * Detached Lifecycle hook. + * + * @returns void + */ + detached?: () => void; } diff --git a/src/tsconfig.model.json b/src/tsconfig.model.json index aac88ae534..5f00bf880a 100644 --- a/src/tsconfig.model.json +++ b/src/tsconfig.model.json @@ -7,6 +7,8 @@ "include": [ "./model/**/*.ts", "./renderers/**/*.ts", - "./views/**/*.ts" + "./views/**/*.ts", + "./api/**/*.ts", + "./gui/**/*.ts" ] } From 974d193d9befc4aa6c93e141e71d14a7dea34c9c Mon Sep 17 00:00:00 2001 From: Mark Silverwood Date: Wed, 29 Mar 2023 13:19:20 +0100 Subject: [PATCH 14/65] add data getter and subscriber --- .size-limit.js | 8 ++++---- src/api/iseries-api.ts | 33 +++++++++++++++++++++++++++++++++ src/api/series-api.ts | 30 ++++++++++++++++++++++++++++-- 3 files changed, 65 insertions(+), 6 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 856978c98a..8759995485 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -4,21 +4,21 @@ module.exports = [ { name: 'CJS', path: 'dist/lightweight-charts.production.cjs', - limit: '45.0 KB', + limit: '45.05 KB', }, { name: 'ESM', path: 'dist/lightweight-charts.production.mjs', - limit: '45.95 KB', + limit: '46.0 KB', }, { name: 'Standalone-ESM', path: 'dist/lightweight-charts.standalone.production.mjs', - limit: '46.65 KB', + limit: '46.70 KB', }, { name: 'Standalone', path: 'dist/lightweight-charts.standalone.production.js', - limit: '46.7 KB', + limit: '46.75 KB', }, ]; diff --git a/src/api/iseries-api.ts b/src/api/iseries-api.ts index 60f27f3e17..0e66c182e0 100644 --- a/src/api/iseries-api.ts +++ b/src/api/iseries-api.ts @@ -17,6 +17,11 @@ import { SeriesDataItemTypeMap } from './data-consumer'; import { IPriceLine } from './iprice-line'; import { IPriceScaleApi } from './iprice-scale-api'; +/** + * A custom function use to handle data changed events. + */ +export type DataChangedHandler = () => void; + /** * Represents a range of bars and the number of bars outside the range. */ @@ -173,6 +178,34 @@ export interface ISeriesApi { */ dataByIndex(logicalIndex: number, mismatchDirection?: MismatchDirection): SeriesDataItemTypeMap[TSeriesType] | 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 SeriesDataItemTypeMap[TSeriesType][]; + + /** + * 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; + /** * Allows to set/replace all existing series markers with new ones. * diff --git a/src/api/series-api.ts b/src/api/series-api.ts index 1dd4d1cbb3..fb96d84b2d 100644 --- a/src/api/series-api.ts +++ b/src/api/series-api.ts @@ -1,6 +1,8 @@ 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'; @@ -10,6 +12,7 @@ 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, @@ -27,16 +30,17 @@ 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, ISeriesApi } from './iseries-api'; import { priceLineOptionsDefaults } from './options/price-line-options-defaults'; import { PriceLine } from './price-line-api'; -export class SeriesApi implements ISeriesApi { +export class SeriesApi 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, chartApi: IChartApi) { this._series = series; @@ -45,6 +49,10 @@ export class SeriesApi implements ISeriesApi implements ISeriesApi implements ISeriesApi) => seriesCreator(row)); + } + + public subscribeDataChanged(handler: DataChangedHandler): void { + this._dataChangedDelegate.subscribe(handler); + } + public setMarkers(data: SeriesMarker