From 221360e9d28fd50afcb6f2b6b9c615c27e6e6723 Mon Sep 17 00:00:00 2001 From: Andrii Ovcharenko Date: Wed, 4 Sep 2024 15:03:22 +0200 Subject: [PATCH] Extract markers into plugin (#1687) * remove series markers * move markers into plugin * update sizelimit * add missing ts docs * fix coverage tests * fix hit test id * fix interaction test * remove primitive constructor * export createPrimitive functions instead of primitive class * fix coverage tests * update migration doc * apply suggestion to generate only public plugin's api * fix types * change wording * fix margins * change method visibility --- .size-limit.js | 12 +- package.json | 2 +- src/api/iseries-api.ts | 48 ---- src/api/itime-scale-api.ts | 10 +- src/api/series-api.ts | 18 +- src/api/time-scale-api.ts | 8 +- src/index.ts | 5 +- src/model/autoscale-info-impl.ts | 7 +- src/model/data-validators.ts | 3 +- src/model/series-options.ts | 2 +- src/model/series.ts | 81 +----- src/plugins/image-watermark/primitive.ts | 51 ++-- src/plugins/pane-primitive-wrapper.ts | 60 +++++ src/plugins/primitive-wrapper-base.ts | 25 ++ src/plugins/primitive-wrapper.ts | 27 ++ src/plugins/series-markers/pane-view.ts | 232 ++++++++++++++++ src/plugins/series-markers/primitive.ts | 189 +++++++++++++ .../series-markers/renderer.ts} | 43 +-- .../series-markers}/series-markers-arrow.ts | 6 +- .../series-markers}/series-markers-circle.ts | 4 +- .../series-markers}/series-markers-square.ts | 4 +- .../series-markers}/series-markers-text.ts | 2 +- .../series-markers/types.ts} | 21 +- .../series-markers/utils.ts} | 4 +- src/plugins/series-markers/wrapper.ts | 82 ++++++ src/plugins/series-primitive-adapter.ts | 63 +++++ src/plugins/text-watermark/primitive.ts | 77 ++++-- src/plugins/types.ts | 12 + src/views/pane/series-markers-pane-view.ts | 248 ------------------ .../test-cases/plugins/custom-series.js | 1 - .../test-cases/plugins/text-watermark.js | 9 +- .../coverage/test-cases/series/area-series.js | 1 - .../coverage/test-cases/series/bar-series.js | 1 - .../test-cases/series/baseline-series.js | 1 - .../test-cases/series/candlestick-series.js | 1 - .../test-cases/series/histogram-series.js | 1 - .../coverage/test-cases/series/line-series.js | 1 - .../e2e/coverage/test-cases/series/markers.js | 30 ++- .../add-markers-with-autosize-enabled.js | 5 +- .../graphics/test-cases/api/series-markers.js | 14 +- .../api/subscribe-crosshair-move.js | 4 +- .../applying-options/make-series-hidden.js | 23 +- .../applying-options/make-series-visible.js | 23 +- .../test-cases/applying-options/watermark.js | 4 +- .../graphics/test-cases/data-validation.js | 42 ++- .../test-cases/initial-options/watermark.js | 5 +- .../test-cases/plugins/image-watermark.js | 5 +- .../series-markers/marker-in-gap-from-left.js | 20 +- .../series-markers/marker-in-gap.js | 19 +- .../series-markers/series-arrow-markers.js | 23 +- .../series-markers/series-circle-markers.js | 24 +- .../series-markers/series-markers-aligned.js | 17 +- .../series-markers-all-above.js | 14 +- .../series-markers-all-below.js | 13 +- .../series-markers-all-inbar.js | 14 +- .../series-markers-max-bar-spacing.js | 17 +- .../series-markers-min-bar-spacing.js | 5 +- .../series-markers-object-business-day.js | 17 +- .../series-markers-out-of-visible-range.js | 18 +- .../series-markers-re-aligned.js | 17 +- .../series-markers/series-markers-update.js | 8 +- .../series-markers-with-text.js | 48 ++-- .../series-markers/series-square-markers.js | 24 +- .../set-markers-before-series-data.js | 47 ++-- .../test-cases/series/series-visibility.js | 23 +- .../test-cases/markers/text-hit-test.js | 24 +- tests/e2e/runner.ts | 1 + .../non-time-based-custom-series.ts | 13 +- tests/type-checks/series-markers.ts | 122 +++++++++ tests/type-checks/watermarks.ts | 8 +- website/docs/migrations/from-v4-to-v5.md | 17 +- website/tutorials/how_to/.eslintrc.js | 5 +- website/tutorials/how_to/series-markers.js | 3 +- website/tutorials/how_to/series-markers.mdx | 6 +- .../tutorials/how_to/watermark-advanced.js | 5 +- website/tutorials/how_to/watermark-simple.js | 6 +- website/tutorials/how_to/watermark.mdx | 23 +- 77 files changed, 1306 insertions(+), 812 deletions(-) create mode 100644 src/plugins/pane-primitive-wrapper.ts create mode 100644 src/plugins/primitive-wrapper-base.ts create mode 100644 src/plugins/primitive-wrapper.ts create mode 100644 src/plugins/series-markers/pane-view.ts create mode 100644 src/plugins/series-markers/primitive.ts rename src/{renderers/series-markers-renderer.ts => plugins/series-markers/renderer.ts} (77%) rename src/{renderers => plugins/series-markers}/series-markers-arrow.ts (89%) rename src/{renderers => plugins/series-markers}/series-markers-circle.ts (84%) rename src/{renderers => plugins/series-markers}/series-markers-square.ts (85%) rename src/{renderers => plugins/series-markers}/series-markers-text.ts (91%) rename src/{model/series-markers.ts => plugins/series-markers/types.ts} (60%) rename src/{renderers/series-markers-utils.ts => plugins/series-markers/utils.ts} (90%) create mode 100644 src/plugins/series-markers/wrapper.ts create mode 100644 src/plugins/series-primitive-adapter.ts delete mode 100644 src/views/pane/series-markers-pane-view.ts create mode 100644 tests/type-checks/series-markers.ts diff --git a/.size-limit.js b/.size-limit.js index 328ae38200..f5c70f02f7 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -45,7 +45,7 @@ export default [ { name: 'Plugin: Text Watermark', path: 'dist/lightweight-charts.production.mjs', - import: '{ TextWatermark }', + import: '{ createTextWatermark }', ignore: ['fancy-canvas'], limit: '2.00 KB', brotli: true, @@ -53,9 +53,17 @@ export default [ { name: 'Plugin: Image Watermark', path: 'dist/lightweight-charts.production.mjs', - import: '{ ImageWatermark }', + import: '{ createImageWatermark }', ignore: ['fancy-canvas'], limit: '2.00 KB', brotli: true, }, + { + name: 'Plugin: Series Markers', + path: 'dist/lightweight-charts.production.mjs', + import: '{ createSeriesMarkers }', + ignore: ['fancy-canvas'], + limit: '4.08 KB', + brotli: true, + }, ]; diff --git a/package.json b/package.json index e63f6da591..fa6b1e5ff8 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "tslint-eslint-rules": "~5.4.0", "tslint-microsoft-contrib": "~6.2.0", "tsx": "~4.16.2", - "typescript": "~5.4.5", + "typescript": "~5.5.4", "yargs": "~17.7.2" }, "scripts": { diff --git a/src/api/iseries-api.ts b/src/api/iseries-api.ts index 28521cc824..665d218b94 100644 --- a/src/api/iseries-api.ts +++ b/src/api/iseries-api.ts @@ -6,7 +6,6 @@ import { SeriesDataItemTypeMap } from '../model/data-consumer'; import { Time } from '../model/horz-scale-behavior-time/types'; import { MismatchDirection } from '../model/plot-list'; import { CreatePriceLineOptions } from '../model/price-line-options'; -import { SeriesMarker } from '../model/series-markers'; import { SeriesOptionsMap, SeriesPartialOptionsMap, @@ -230,53 +229,6 @@ export interface ISeriesApi< */ unsubscribeDataChanged(handler: DataChangedHandler): void; - /** - * Allows to set/replace all existing series markers with new ones. - * - * @param data - An array of series markers. This array should be sorted by time. Several markers with same time are allowed. - * @example - * ```js - * series.setMarkers([ - * { - * time: '2019-04-09', - * position: 'aboveBar', - * color: 'black', - * shape: 'arrowDown', - * }, - * { - * time: '2019-05-31', - * position: 'belowBar', - * color: 'red', - * shape: 'arrowUp', - * id: 'id3', - * }, - * { - * time: '2019-05-31', - * position: 'belowBar', - * color: 'orange', - * shape: 'arrowUp', - * id: 'id4', - * text: 'example', - * size: 2, - * }, - * ]); - * - * chart.subscribeCrosshairMove(param => { - * console.log(param.hoveredObjectId); - * }); - * - * chart.subscribeClick(param => { - * console.log(param.hoveredObjectId); - * }); - * ``` - */ - setMarkers(data: SeriesMarker[]): void; - - /** - * Returns an array of series markers. - */ - markers(): SeriesMarker[]; - /** * Creates a new price line * diff --git a/src/api/itime-scale-api.ts b/src/api/itime-scale-api.ts index 0fa9b6f10c..4ad1d3833d 100644 --- a/src/api/itime-scale-api.ts +++ b/src/api/itime-scale-api.ts @@ -1,7 +1,7 @@ import { DeepPartial } from '../helpers/strict-type-checks'; import { Coordinate } from '../model/coordinate'; -import { IRange, Logical, LogicalRange } from '../model/time-data'; +import { IRange, Logical, LogicalRange, TimePointIndex } from '../model/time-data'; import { HorzScaleOptions } from '../model/time-scale'; /** @@ -110,6 +110,14 @@ export interface ITimeScaleApi { */ coordinateToLogical(x: number): Logical | null; + /** + * Converts a time to local x coordinate. + * + * @param time - Time needs to be converted + * @returns X coordinate of that time or `null` if no time found on time scale + */ + timeToIndex(time: HorzScaleItem, findNearest?: boolean): TimePointIndex | null; + /** * Converts a time to local x coordinate. * diff --git a/src/api/series-api.ts b/src/api/series-api.ts index 608f88b89f..64db5e5b1d 100644 --- a/src/api/series-api.ts +++ b/src/api/series-api.ts @@ -9,7 +9,7 @@ import { BarPrice } from '../model/bar'; import { Coordinate } from '../model/coordinate'; import { DataUpdatesConsumer, SeriesDataItemTypeMap, WhitespaceData } from '../model/data-consumer'; import { checkItemsAreOrdered, checkPriceLineOptions, checkSeriesValuesType } from '../model/data-validators'; -import { IHorzScaleBehavior, InternalHorzScaleItem } from '../model/ihorz-scale-behavior'; +import { IHorzScaleBehavior } from '../model/ihorz-scale-behavior'; import { ISeriesPrimitiveBase } from '../model/iseries-primitive'; import { Pane } from '../model/pane'; import { MismatchDirection } from '../model/plot-list'; @@ -17,7 +17,6 @@ import { CreatePriceLineOptions, PriceLineOptions } from '../model/price-line-op import { RangeImpl } from '../model/range-impl'; import { Series } from '../model/series'; import { SeriesPlotRow } from '../model/series-data'; -import { convertSeriesMarker, SeriesMarker } from '../model/series-markers'; import { SeriesOptionsMap, SeriesPartialOptionsMap, @@ -187,21 +186,6 @@ export class SeriesApi< this._dataChangedDelegate.unsubscribe(handler); } - public setMarkers(data: SeriesMarker[]): void { - checkItemsAreOrdered(data, this._horzScaleBehavior, true); - - const convertedMarkers = data.map((marker: SeriesMarker) => - convertSeriesMarker(marker, this._horzScaleBehavior.convertHorzItemToInternal(marker.time), marker.time) - ); - this._series.setMarkers(convertedMarkers); - } - - public markers(): SeriesMarker[] { - return this._series.markers().map>((internalItem: SeriesMarker) => { - return convertSeriesMarker(internalItem, internalItem.originalTime as HorzScaleItem, undefined); - }); - } - public applyOptions(options: TPartialOptions): void { this._series.applyOptions(options); } diff --git a/src/api/time-scale-api.ts b/src/api/time-scale-api.ts index 1f842cdcbc..d1ebc4d2af 100644 --- a/src/api/time-scale-api.ts +++ b/src/api/time-scale-api.ts @@ -136,9 +136,13 @@ export class TimeScaleApi implements ITimeScaleApi } } - public timeToCoordinate(time: HorzScaleItem): Coordinate | null { + public timeToIndex(time: HorzScaleItem, findNearest: boolean): TimePointIndex | null { const timePoint = this._horzScaleBehavior.convertHorzItemToInternal(time); - const timePointIndex = this._timeScale.timeToIndex(timePoint, false); + return this._timeScale.timeToIndex(timePoint, findNearest); + } + + public timeToCoordinate(time: HorzScaleItem): Coordinate | null { + const timePointIndex = this.timeToIndex(time, false); if (timePointIndex === null) { return null; } diff --git a/src/index.ts b/src/index.ts index 14c3675742..df8518928a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,8 +24,9 @@ export { createChart, createChartEx, defaultHorzScaleBehavior } from './api/crea /* Plugins */ -export { TextWatermark } from './plugins/text-watermark/primitive'; -export { ImageWatermark } from './plugins/image-watermark/primitive'; +export { createTextWatermark } from './plugins/text-watermark/primitive'; +export { createImageWatermark } from './plugins/image-watermark/primitive'; +export { createSeriesMarkers } from './plugins/series-markers/wrapper'; /** * Returns the current version as a string. For example `'3.3.0'`. diff --git a/src/model/autoscale-info-impl.ts b/src/model/autoscale-info-impl.ts index 5174b74f56..058f0b2f93 100644 --- a/src/model/autoscale-info-impl.ts +++ b/src/model/autoscale-info-impl.ts @@ -28,12 +28,9 @@ export class AutoscaleInfoImpl { return this._margins; } - public toRaw(): AutoscaleInfo | null { - if (this._priceRange === null) { - return null; - } + public toRaw(): AutoscaleInfo { return { - priceRange: this._priceRange.toRaw(), + priceRange: this._priceRange === null ? null : this._priceRange.toRaw(), margins: this._margins || undefined, }; } diff --git a/src/model/data-validators.ts b/src/model/data-validators.ts index da6320e931..2babb62149 100644 --- a/src/model/data-validators.ts +++ b/src/model/data-validators.ts @@ -5,7 +5,6 @@ import { assert } from '../helpers/assertions'; import { isFulfilledData, SeriesDataItemTypeMap } from './data-consumer'; import { IHorzScaleBehavior } from './ihorz-scale-behavior'; import { CreatePriceLineOptions } from './price-line-options'; -import { SeriesMarker } from './series-markers'; import { SeriesType } from './series-options'; export function checkPriceLineOptions(options: CreatePriceLineOptions): void { @@ -16,7 +15,7 @@ export function checkPriceLineOptions(options: CreatePriceLineOptions): void { assert(typeof options.price === 'number', `the type of 'price' price line's property must be a number, got '${typeof options.price}'`); } -export function checkItemsAreOrdered(data: readonly (SeriesMarker | SeriesDataItemTypeMap[SeriesType])[], bh: IHorzScaleBehavior, allowDuplicates: boolean = false): void { +export function checkItemsAreOrdered(data: readonly (SeriesDataItemTypeMap[SeriesType])[], bh: IHorzScaleBehavior, allowDuplicates: boolean = false): void { if (process.env.NODE_ENV === 'production') { return; } diff --git a/src/model/series-options.ts b/src/model/series-options.ts index 2bb0a2b1c6..ea3fa9c211 100644 --- a/src/model/series-options.ts +++ b/src/model/series-options.ts @@ -641,7 +641,7 @@ export interface AutoscaleInfo { /** * Price range. */ - priceRange: PriceRange; + priceRange: PriceRange | null; /** * Scale margins. diff --git a/src/model/series.ts b/src/model/series.ts index dcf1c1a9fd..b2697464a8 100644 --- a/src/model/series.ts +++ b/src/model/series.ts @@ -19,20 +19,18 @@ import { SeriesLinePaneView } from '../views/pane/line-pane-view'; import { PanePriceAxisView } from '../views/pane/pane-price-axis-view'; import { SeriesHorizontalBaseLinePaneView } from '../views/pane/series-horizontal-base-line-pane-view'; import { SeriesLastPriceAnimationPaneView } from '../views/pane/series-last-price-animation-pane-view'; -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, AutoScaleMargins } from './autoscale-info-impl'; +import { AutoscaleInfoImpl } from './autoscale-info-impl'; import { BarPrice, BarPrices } from './bar'; import { IChartModelBase } from './chart-model'; import { Coordinate } from './coordinate'; import { CustomPriceLine } from './custom-price-line'; import { isDefaultPriceScale } from './default-price-scale'; import { CustomData, CustomSeriesWhitespaceData, ICustomSeriesPaneView, WhitespaceCheck } from './icustom-series'; -import { InternalHorzScaleItem } from './ihorz-scale-behavior'; import { PrimitiveHoveredItem, PrimitivePaneViewZOrder } from './ipane-primitive'; import { FirstValue, IPriceDataSource } from './iprice-data-source'; import { ISeriesPrimitiveBase } from './iseries-primitive'; @@ -45,7 +43,6 @@ import { PriceRangeImpl } from './price-range-impl'; import { PriceScale } from './price-scale'; import { ISeriesBarColorer, SeriesBarColorer } from './series-bar-colorer'; import { createSeriesPlotList, SeriesPlotList, SeriesPlotRow } from './series-data'; -import { InternalSeriesMarker, SeriesMarker } from './series-markers'; import { AreaStyleOptions, BaselineStyleOptions, @@ -138,7 +135,6 @@ export interface ISeries extends IPriceDataSource { title(): string; priceScale(): PriceScale; lastValueData(globalLast: boolean): LastValueDataResult; - indexedMarkers(): InternalSeriesMarker[]; barColorer(): ISeriesBarColorer; markerDataAtIndex(index: TimePointIndex): MarkerData | null; dataAt(time: TimePointIndex): SeriesDataAtTypeMap[SeriesType] | null; @@ -157,9 +153,6 @@ export class Series extends PriceDataSource implements IDe private readonly _lastPriceAnimationPaneView: SeriesLastPriceAnimationPaneView | null = null; private _barColorerCache: SeriesBarColorer | null = null; private readonly _options: SeriesOptionsInternal; - private _markers: readonly SeriesMarker[] = []; - private _indexedMarkers: InternalSeriesMarker[] = []; - private _markersPaneView!: SeriesMarkersPaneView; private _animationTimeoutId: TimerId | null = null; private _primitives: SeriesPrimitiveWrapper[] = []; @@ -293,10 +286,7 @@ export class Series extends PriceDataSource implements IDe public setData(data: readonly SeriesPlotRow[], updateInfo?: SeriesUpdateInfo): void { this._data.setData(data); - this._recalculateMarkers(); - this._paneView.update('data'); - this._markersPaneView.update('data'); if (this._lastPriceAnimationPaneView !== null) { if (updateInfo && updateInfo.lastBarUpdatedOrNewBarsAddedToTheRight) { @@ -313,25 +303,6 @@ export class Series extends PriceDataSource implements IDe this.model().lightUpdate(); } - public setMarkers(data: readonly SeriesMarker[]): void { - this._markers = data; - this._recalculateMarkers(); - const sourcePane = this.model().paneForSource(this); - this._markersPaneView.update('data'); - this.model().recalculatePane(sourcePane); - this.model().updateSource(this); - this.model().updateCrosshair(); - this.model().lightUpdate(); - } - - public markers(): readonly SeriesMarker[] { - return this._markers; - } - - public indexedMarkers(): InternalSeriesMarker[] { - return this._indexedMarkers; - } - public createPriceLine(options: PriceLineOptions): CustomPriceLine { const result = new CustomPriceLine(this, options); this._customPriceLines.push(result); @@ -426,8 +397,7 @@ export class Series extends PriceDataSource implements IDe res.push( this._paneView, - this._priceLineView, - this._markersPaneView + this._priceLineView ); const priceLineViews = this._customPriceLines.map((line: CustomPriceLine) => line.paneView()); @@ -509,7 +479,6 @@ export class Series extends PriceDataSource implements IDe public updateAllViews(): void { this._paneView.update(); - this._markersPaneView.update(); for (const priceAxisView of this._priceAxisViews) { priceAxisView.update(); @@ -602,14 +571,13 @@ export class Series extends PriceDataSource implements IDe const barsMinMax = this._data.minMaxOnRangeCached(startTimePoint, endTimePoint, plots); let range = barsMinMax !== null ? new PriceRangeImpl(barsMinMax.min, barsMinMax.max) : null; - + let margins = null; if (this.seriesType() === 'Histogram') { const base = (this._options as HistogramStyleOptions).base; const rangeWithBase = new PriceRangeImpl(base, base); range = range !== null ? range.merge(rangeWithBase) : rangeWithBase; } - let margins = this._markersPaneView.autoScaleMargins(); this._primitives.forEach((primitive: SeriesPrimitiveWrapper) => { const primitiveAutoscale = primitive.autoscaleInfo( startTimePoint, @@ -624,11 +592,11 @@ export class Series extends PriceDataSource implements IDe range = range !== null ? range.merge(primitiveRange) : primitiveRange; } if (primitiveAutoscale?.margins) { - margins = mergeMargins(margins, primitiveAutoscale.margins); + margins = primitiveAutoscale.margins; } }); - return new AutoscaleInfoImpl(range, margins); + return new AutoscaleInfoImpl(range, margins); } private _markerRadius(): number { @@ -711,39 +679,7 @@ export class Series extends PriceDataSource implements IDe } } - private _recalculateMarkers(): void { - const timeScale = this.model().timeScale(); - if (!timeScale.hasPoints() || this._data.isEmpty()) { - this._indexedMarkers = []; - return; - } - - const firstDataIndex = ensureNotNull(this._data.firstIndex()); - - this._indexedMarkers = this._markers.map>((marker: SeriesMarker, index: number) => { - // the first find index on the time scale (across all series) - const timePointIndex = ensureNotNull(timeScale.timeToIndex(marker.time, true)); - - // and then search that index inside the series data - const searchMode = timePointIndex < firstDataIndex ? MismatchDirection.NearestRight : MismatchDirection.NearestLeft; - const seriesDataIndex = ensureNotNull(this._data.search(timePointIndex, searchMode)).index; - return { - time: seriesDataIndex, - position: marker.position, - shape: marker.shape, - color: marker.color, - id: marker.id, - internalId: index, - text: marker.text, - size: marker.size, - originalTime: marker.originalTime, - }; - }); - } - private _recreatePaneViews(customPaneView?: ICustomSeriesPaneView): void { - this._markersPaneView = new SeriesMarkersPaneView(this, this.model()); - switch (this._seriesType) { case 'Bar': { this._paneView = new SeriesBarsPaneView(this as Series<'Bar'>, this.model()); @@ -790,10 +726,3 @@ export class Series extends PriceDataSource implements IDe return res; } } - -function mergeMargins(source: AutoScaleMargins | null, additionalMargin: AutoScaleMargins): AutoScaleMargins { - return { - above: Math.max(source?.above ?? 0, additionalMargin.above), - below: Math.max(source?.below ?? 0, additionalMargin.below), - }; -} diff --git a/src/plugins/image-watermark/primitive.ts b/src/plugins/image-watermark/primitive.ts index ed84aacd9c..f95ece4995 100644 --- a/src/plugins/image-watermark/primitive.ts +++ b/src/plugins/image-watermark/primitive.ts @@ -1,3 +1,4 @@ +import { IPaneApi } from '../../api/ipane-api'; import { IPanePrimitive, PaneAttachedParameter, @@ -5,9 +6,9 @@ import { import { DeepPartial } from '../../helpers/strict-type-checks'; -import { Time } from '../../model/horz-scale-behavior-time/types'; import { IPanePrimitivePaneView } from '../../model/ipane-primitive'; +import { IPanePrimitiveWithOptions, IPanePrimitiveWrapper, PanePrimitiveWrapper } from '../pane-primitive-wrapper'; import { ImageWatermarkOptions, imageWatermarkOptionsDefaults, @@ -23,23 +24,7 @@ function mergeOptionsWithDefaults( }; } -/** - * A pane primitive for rendering a image watermark. - * - * @example - * ```js - * import { ImageWatermark } from 'lightweight-charts'; - * - * const imageWatermark = new ImageWatermark('/images/my-image.png', { - * alpha: 0.5, - * padding: 20, - * }); - * - * const firstPane = chart.panes()[0]; - * firstPane.attachPrimitive(imageWatermark); - * ``` - */ -export class ImageWatermark implements IPanePrimitive { +class ImageWatermark implements IPanePrimitive { private _requestUpdate?: () => void; private _paneViews: ImageWatermarkPaneView[]; private _options: ImageWatermarkOptions; @@ -109,3 +94,33 @@ export class ImageWatermark implements IPanePrimitive { ); } } + +export type IImageWatermarkPluginApi = IPanePrimitiveWrapper; + +/** + * Creates an image watermark. + * + * @param pane - Target pane. + * @param imageUrl - Image URL. + * @param options - Watermark options. + * + * @returns Image watermark wrapper. + * + * @example + * ```js + * import { createImageWatermark } from 'lightweight-charts'; + * + * const firstPane = chart.panes()[0]; + * const imageWatermark = createImageWatermark(firstPane, '/images/my-image.png', { + * alpha: 0.5, + * padding: 20, + * }); + * // to change options + * imageWatermark.applyOptions({ padding: 10 }); + * // to remove watermark from the pane + * imageWatermark.detach(); + * ``` + */ +export function createImageWatermark(pane: IPaneApi, imageUrl: string, options: DeepPartial): IImageWatermarkPluginApi { + return new PanePrimitiveWrapper>(pane, new ImageWatermark(imageUrl, options)); +} diff --git a/src/plugins/pane-primitive-wrapper.ts b/src/plugins/pane-primitive-wrapper.ts new file mode 100644 index 0000000000..e1a6b1dc0a --- /dev/null +++ b/src/plugins/pane-primitive-wrapper.ts @@ -0,0 +1,60 @@ +import { IPaneApi } from '../api/ipane-api'; +import { IPanePrimitive } from '../api/ipane-primitive-api'; + +import { DeepPartial } from '../helpers/strict-type-checks'; + +/** + * Interface for a primitive with options. + */ +export interface IPanePrimitiveWithOptions extends IPanePrimitive { + /** + * @param options - Options to apply. The options are deeply merged with the current options. + */ + applyOptions?: (options: DeepPartial) => void; +} + +/** + * Interface for a pane primitive. + */ +export interface IPanePrimitiveWrapper { + /** + * Detaches the plugin from the pane. + */ + detach: () => void; + /** + * Returns the current pane. + */ + getPane: () => IPaneApi; + /** + * Applies options to the primitive. + * @param options - Options to apply. The options are deeply merged with the current options. + */ + applyOptions?: (options: DeepPartial) => void; +} + +export class PanePrimitiveWrapper = IPanePrimitive> implements IPanePrimitiveWrapper { + private _primitive: TPrimitive; + private _pane: IPaneApi; + + public constructor(pane: IPaneApi, primitive: TPrimitive) { + this._pane = pane; + this._primitive = primitive; + this._attach(); + } + + public detach(): void { + this._pane.detachPrimitive(this._primitive); + } + + public getPane(): IPaneApi { + return this._pane; + } + + public applyOptions(options: DeepPartial): void { + this._primitive.applyOptions?.(options); + } + + private _attach(): void { + this._pane.attachPrimitive(this._primitive); + } +} diff --git a/src/plugins/primitive-wrapper-base.ts b/src/plugins/primitive-wrapper-base.ts new file mode 100644 index 0000000000..474d638ec2 --- /dev/null +++ b/src/plugins/primitive-wrapper-base.ts @@ -0,0 +1,25 @@ +import { DeepPartial } from '../helpers/strict-type-checks'; + +/** + * Interface for a primitive wrapper. It must be implemented to add some plugin to the chart. + */ +export interface IPrimitiveWrapper { + /** + * @param options - Options to apply. The options are deeply merged with the current options. + */ + applyOptions(options: DeepPartial): void; + /** + * Detaches the plugin from the pane/series. + */ + detach(): void; +} + +/** + * Interface for a plugin that adds primitive with options. + */ +export interface IPrimitiveWithOptions { + /** + * @param options - Options to apply. The options are deeply merged with the current options. + */ + applyOptions(options: DeepPartial): void; +} diff --git a/src/plugins/primitive-wrapper.ts b/src/plugins/primitive-wrapper.ts new file mode 100644 index 0000000000..cbcc41a7c0 --- /dev/null +++ b/src/plugins/primitive-wrapper.ts @@ -0,0 +1,27 @@ +import { IPanePrimitive, PaneAttachedParameter } from '../api/ipane-primitive-api'; + +import { DeepPartial } from '../helpers/strict-type-checks'; + +export abstract class PrimitiveWrapper { + protected _primitive: IPanePrimitive; + protected _options: Options; + + public constructor(primitive: IPanePrimitive, options: Options) { + this._primitive = primitive; + this._options = options; + } + + public detach(): void { + this._primitive.detached?.(); + } + + public abstract applyOptions(options: DeepPartial): void; + + protected _attachToPrimitive(params: PaneAttachedParameter): void { + this._primitive.attached?.(params); + } + + protected _requestUpdate(): void { + this._primitive.updateAllViews?.(); + } +} diff --git a/src/plugins/series-markers/pane-view.ts b/src/plugins/series-markers/pane-view.ts new file mode 100644 index 0000000000..314cfa4591 --- /dev/null +++ b/src/plugins/series-markers/pane-view.ts @@ -0,0 +1,232 @@ +import { IChartApiBase } from '../../api/ichart-api'; +import { ISeriesApi } from '../../api/iseries-api'; + +import { ensureNever, ensureNotNull } from '../../helpers/assertions'; +import { isNumber } from '../../helpers/strict-type-checks'; + +import { Coordinate } from '../../model/coordinate'; +import { AreaData, BarData, BaselineData, CandlestickData, HistogramData, LineData, SeriesDataItemTypeMap, SingleValueData } from '../../model/data-consumer'; +import { IPrimitivePaneView } from '../../model/ipane-primitive'; +import { RangeImpl } from '../../model/range-impl'; +import { SeriesType } from '../../model/series-options'; +import { Logical, TimePointIndex, visibleTimedValues } from '../../model/time-data'; +import { UpdateType } from '../../views/pane/iupdatable-pane-view'; + +import { + SeriesMarkerRendererData, + SeriesMarkerRendererDataItem, + SeriesMarkersRenderer, +} from './renderer'; +import { InternalSeriesMarker } from './types'; +import { + calculateShapeHeight, + shapeMargin as calculateShapeMargin, +} from './utils'; + +const enum Constants { + TextMargin = 0.1, +} + +interface Offsets { + aboveBar: number; + belowBar: number; +} + +// eslint-disable-next-line max-params +function fillSizeAndY( + rendererItem: SeriesMarkerRendererDataItem, + marker: InternalSeriesMarker, + seriesData: SeriesDataItemTypeMap[SeriesType], + offsets: Offsets, + textHeight: number, + shapeMargin: number, + series: ISeriesApi, + chart: IChartApiBase +): void { + const timeScale = chart.timeScale(); + let inBarPrice: number; + let highPrice: number; + let lowPrice: number; + + if (isValueData(seriesData)) { + inBarPrice = seriesData.value; + highPrice = seriesData.value; + lowPrice = seriesData.value; + } else if (isOhlcData(seriesData)) { + inBarPrice = seriesData.close; + highPrice = seriesData.high; + lowPrice = seriesData.low; + } else { + return; + } + + const sizeMultiplier = isNumber(marker.size) ? Math.max(marker.size, 0) : 1; + const shapeSize = calculateShapeHeight(timeScale.options().barSpacing) * sizeMultiplier; + const halfSize = shapeSize / 2; + rendererItem.size = shapeSize; + switch (marker.position) { + case 'inBar': { + rendererItem.y = ensureNotNull(series.priceToCoordinate(inBarPrice)); + if (rendererItem.text !== undefined) { + rendererItem.text.y = rendererItem.y + halfSize + shapeMargin + textHeight * (0.5 + Constants.TextMargin) as Coordinate; + } + return; + } + case 'aboveBar': { + rendererItem.y = (ensureNotNull(series.priceToCoordinate(highPrice)) - halfSize - offsets.aboveBar) as Coordinate; + if (rendererItem.text !== undefined) { + rendererItem.text.y = rendererItem.y - halfSize - textHeight * (0.5 + Constants.TextMargin) as Coordinate; + offsets.aboveBar += textHeight * (1 + 2 * Constants.TextMargin); + } + offsets.aboveBar += shapeSize + shapeMargin; + return; + } + case 'belowBar': { + rendererItem.y = (ensureNotNull(series.priceToCoordinate(lowPrice)) + halfSize + offsets.belowBar) as Coordinate; + if (rendererItem.text !== undefined) { + rendererItem.text.y = rendererItem.y + halfSize + shapeMargin + textHeight * (0.5 + Constants.TextMargin) as Coordinate; + offsets.belowBar += textHeight * (1 + 2 * Constants.TextMargin); + } + offsets.belowBar += shapeSize + shapeMargin; + return; + } + } + + ensureNever(marker.position); +} + +function isValueData( + data: SeriesDataItemTypeMap[SeriesType] +): data is LineData | HistogramData | AreaData | BaselineData { + // eslint-disable-next-line no-restricted-syntax + return 'value' in data && typeof (data as unknown as SingleValueData).value === 'number'; +} + +function isOhlcData( + data: SeriesDataItemTypeMap[SeriesType] +): data is BarData | CandlestickData { + // eslint-disable-next-line no-restricted-syntax + return 'open' in data && 'high' in data && 'low' in data && 'close' in data; +} + +export class SeriesMarkersPaneView implements IPrimitivePaneView { + private readonly _series: ISeriesApi; + private readonly _chart: IChartApiBase; + private _data: SeriesMarkerRendererData; + private _markers: InternalSeriesMarker[] = []; + + private _invalidated: boolean = true; + private _dataInvalidated: boolean = true; + + private _renderer: SeriesMarkersRenderer = new SeriesMarkersRenderer(); + + public constructor(series: ISeriesApi, chart: IChartApiBase) { + this._series = series; + this._chart = chart; + this._data = { + items: [], + visibleRange: null, + }; + } + + public renderer(): SeriesMarkersRenderer | null { + if (!this._series.options().visible) { + return null; + } + + if (this._invalidated) { + this._makeValid(); + } + + const layout = this._chart.options()['layout']; + this._renderer.setParams(layout.fontSize, layout.fontFamily); + this._renderer.setData(this._data); + + return this._renderer; + } + + public setMarkers(markers: InternalSeriesMarker[]): void { + this._markers = markers; + this.update('data'); + } + + public update(updateType?: UpdateType): void { + this._invalidated = true; + if (updateType === 'data') { + this._dataInvalidated = true; + } + } + + protected _makeValid(): void { + const timeScale = this._chart.timeScale(); + const seriesMarkers = this._markers; + if (this._dataInvalidated) { + this._data.items = seriesMarkers.map((marker: InternalSeriesMarker) => ({ + time: marker.time, + x: 0 as Coordinate, + y: 0 as Coordinate, + size: 0, + shape: marker.shape, + color: marker.color, + externalId: marker.id, + internalId: marker.internalId, + text: undefined, + })); + this._dataInvalidated = false; + } + + const layoutOptions = this._chart.options()['layout']; + + this._data.visibleRange = null; + const visibleBars = timeScale.getVisibleLogicalRange(); + + if (visibleBars === null) { + return; + } + const visibleBarsRange = new RangeImpl(Math.floor(visibleBars.from) as TimePointIndex, Math.ceil(visibleBars.to) as TimePointIndex); + const firstValue = this._series.data()[0]; + if (firstValue === null) { + return; + } + if (this._data.items.length === 0) { + return; + } + let prevTimeIndex = NaN; + const shapeMargin = calculateShapeMargin(timeScale.options().barSpacing); + const offsets: Offsets = { + aboveBar: shapeMargin, + belowBar: shapeMargin, + }; + + this._data.visibleRange = visibleTimedValues(this._data.items, visibleBarsRange, true); + for (let index = this._data.visibleRange.from; index < this._data.visibleRange.to; index++) { + const marker = seriesMarkers[index]; + if (marker.time !== prevTimeIndex) { + // new bar, reset stack counter + offsets.aboveBar = shapeMargin; + offsets.belowBar = shapeMargin; + prevTimeIndex = marker.time; + } + + const rendererItem = this._data.items[index]; + rendererItem.x = ensureNotNull(timeScale.logicalToCoordinate(marker.time as unknown as Logical)); + if (marker.text !== undefined && marker.text.length > 0) { + rendererItem.text = { + content: marker.text, + x: 0 as Coordinate, + y: 0 as Coordinate, + width: 0, + height: 0, + }; + } + + const dataAt = ensureNotNull(this._series.dataByIndex(marker.time, -1)); + if (dataAt === null) { + continue; + } + fillSizeAndY(rendererItem, marker, dataAt, offsets, layoutOptions.fontSize, shapeMargin, this._series, this._chart); + } + + this._invalidated = false; + } +} diff --git a/src/plugins/series-markers/primitive.ts b/src/plugins/series-markers/primitive.ts new file mode 100644 index 0000000000..e792785a7b --- /dev/null +++ b/src/plugins/series-markers/primitive.ts @@ -0,0 +1,189 @@ +import { IChartApiBase } from '../../api/ichart-api'; +import { DataChangedHandler, DataChangedScope, ISeriesApi } from '../../api/iseries-api'; +import { ISeriesPrimitive, SeriesAttachedParameter } from '../../api/iseries-primitive-api'; + +import { ensureNotNull } from '../../helpers/assertions'; + +import { AutoScaleMargins } from '../../model/autoscale-info-impl'; +import { IPrimitivePaneView, PrimitiveHoveredItem } from '../../model/ipane-primitive'; +import { MismatchDirection } from '../../model/plot-list'; +import { AutoscaleInfo, SeriesType } from '../../model/series-options'; +import { Logical, TimePointIndex } from '../../model/time-data'; +import { UpdateType } from '../../views/pane/iupdatable-pane-view'; + +import { SeriesMarkersPaneView } from './pane-view'; +import { InternalSeriesMarker, MarkerPositions, SeriesMarker } from './types'; +import { + calculateAdjustedMargin, + calculateShapeHeight, + shapeMargin as calculateShapeMargin, +} from './utils'; + +export class SeriesMarkersPrimitive implements ISeriesPrimitive { + private _paneView: SeriesMarkersPaneView | null = null; + private _markers: SeriesMarker[] = []; + private _indexedMarkers: InternalSeriesMarker[] = []; + private _dataChangedHandler: DataChangedHandler | null = null; + private _series: ISeriesApi | null = null; + private _chart: IChartApiBase | null = null; + private _requestUpdate?: () => void; + private _autoScaleMarginsInvalidated: boolean = true; + private _autoScaleMargins: AutoScaleMargins | null = null; + private _markersPositions: MarkerPositions | null = null; + private _cachedBarSpacing: number | null = null; + + public attached(param: SeriesAttachedParameter): void { + this._recalculateMarkers(); + this._chart = param.chart; + this._series = param.series; + this._paneView = new SeriesMarkersPaneView(this._series, ensureNotNull(this._chart)); + this._requestUpdate = param.requestUpdate; + this._series.subscribeDataChanged((scope: DataChangedScope) => this._onDataChanged(scope)); + this.requestUpdate(); + } + + public requestUpdate(): void { + if (this._requestUpdate) { + this._requestUpdate(); + } + } + + public detached(): void { + if (this._series && this._dataChangedHandler) { + this._series.unsubscribeDataChanged(this._dataChangedHandler); + } + this._chart = null; + this._series = null; + this._paneView = null; + this._dataChangedHandler = null; + } + + public setMarkers(markers: SeriesMarker[]): void { + this._markers = markers; + this._recalculateMarkers(); + this._autoScaleMarginsInvalidated = true; + this._markersPositions = null; + this.requestUpdate(); + } + + public markers(): readonly SeriesMarker[] { + return this._markers; + } + + public paneViews(): readonly IPrimitivePaneView[] { + return this._paneView ? [this._paneView] : []; + } + + public updateAllViews(): void { + this._updateAllViews(); + } + + public hitTest(x: number, y: number): PrimitiveHoveredItem | null { + if (this._paneView) { + return this._paneView.renderer()?.hitTest(x, y) ?? null; + } + return null; + } + + public autoscaleInfo(startTimePoint: Logical, endTimePoint: Logical): AutoscaleInfo | null { + if (this._paneView) { + const margins = this._getAutoScaleMargins(); + if (margins) { + return { + priceRange: null, + margins: margins, + }; + } + } + return null; + } + + private _getAutoScaleMargins(): AutoScaleMargins | null { + const chart = ensureNotNull(this._chart); + const barSpacing = chart.timeScale().options().barSpacing; + if (this._autoScaleMarginsInvalidated || barSpacing !== this._cachedBarSpacing) { + this._cachedBarSpacing = barSpacing; + if (this._markers.length > 0) { + const shapeMargin = calculateShapeMargin(barSpacing); + const marginValue = calculateShapeHeight(barSpacing) * 1.5 + shapeMargin * 2; + const positions = this._getMarkerPositions(); + + this._autoScaleMargins = { + above: calculateAdjustedMargin(marginValue, positions.aboveBar, positions.inBar), + below: calculateAdjustedMargin(marginValue, positions.belowBar, positions.inBar), + }; + } else { + this._autoScaleMargins = null; + } + + this._autoScaleMarginsInvalidated = false; + } + + return this._autoScaleMargins; + } + + private _getMarkerPositions(): MarkerPositions { + if (this._markersPositions === null) { + this._markersPositions = this._markers.reduce( + (acc: MarkerPositions, marker: SeriesMarker) => { + if (!acc[marker.position]) { + acc[marker.position] = true; + } + return acc; + }, + { + inBar: false, + aboveBar: false, + belowBar: false, + } + ); + } + return this._markersPositions; + } + + private _recalculateMarkers(): void { + if (!this._chart || !this._series) { + return; + } + const timeScale = this._chart.timeScale(); + if (timeScale.getVisibleLogicalRange() == null || !this._series || this._series?.data().length === 0) { + this._indexedMarkers = []; + return; + } + + const seriesData = this._series?.data(); + const firstDataIndex = timeScale.timeToIndex(ensureNotNull(seriesData[0].time), true) as unknown as Logical; + this._indexedMarkers = this._markers.map>((marker: SeriesMarker, index: number) => { + const timePointIndex = timeScale.timeToIndex(marker.time, true) as unknown as Logical; + const searchMode = timePointIndex < firstDataIndex ? MismatchDirection.NearestRight : MismatchDirection.NearestLeft; + const seriesDataByIndex = ensureNotNull(this._series).dataByIndex(timePointIndex, searchMode); + // @TODO think about should we expose the series' `.search()` method + const finalIndex = timeScale.timeToIndex(ensureNotNull(seriesDataByIndex).time, false) as unknown as TimePointIndex; + + return { + time: finalIndex, + position: marker.position, + shape: marker.shape, + color: marker.color, + id: marker.id, + internalId: index, + text: marker.text, + size: marker.size, + originalTime: marker.time, + }; + }); + } + + private _updateAllViews(updateType?: UpdateType): void { + if (this._paneView) { + this._recalculateMarkers(); + this._paneView.setMarkers(this._indexedMarkers); + this._paneView.update(updateType); + } + } + + private _onDataChanged(scope: DataChangedScope): void { + this.requestUpdate(); + } +} + diff --git a/src/renderers/series-markers-renderer.ts b/src/plugins/series-markers/renderer.ts similarity index 77% rename from src/renderers/series-markers-renderer.ts rename to src/plugins/series-markers/renderer.ts index 9eb5833dc2..524b8b1274 100644 --- a/src/renderers/series-markers-renderer.ts +++ b/src/plugins/series-markers/renderer.ts @@ -1,20 +1,19 @@ -import { BitmapCoordinatesRenderingScope } from 'fancy-canvas'; +import { BitmapCoordinatesRenderingScope, CanvasRenderingTarget2D } from 'fancy-canvas'; -import { ensureNever } from '../helpers/assertions'; -import { makeFont } from '../helpers/make-font'; +import { ensureNever } from '../../helpers/assertions'; +import { makeFont } from '../../helpers/make-font'; -import { HoveredObject } from '../model/chart-model'; -import { Coordinate } from '../model/coordinate'; -import { SeriesMarkerShape } from '../model/series-markers'; -import { TextWidthCache } from '../model/text-width-cache'; -import { SeriesItemsIndexesRange, TimedValue } from '../model/time-data'; +import { Coordinate } from '../../model/coordinate'; +import { IPrimitivePaneRenderer, PrimitiveHoveredItem } from '../../model/ipane-primitive'; +import { TextWidthCache } from '../../model/text-width-cache'; +import { SeriesItemsIndexesRange, TimedValue } from '../../model/time-data'; -import { BitmapCoordinatesPaneRenderer } from './bitmap-coordinates-pane-renderer'; import { drawArrow, hitTestArrow } from './series-markers-arrow'; import { drawCircle, hitTestCircle } from './series-markers-circle'; import { drawSquare, hitTestSquare } from './series-markers-square'; import { drawText, hitTestText } from './series-markers-text'; -import { BitmapShapeItemCoordinates } from './series-markers-utils'; +import { SeriesMarkerShape } from './types'; +import { BitmapShapeItemCoordinates } from './utils'; export interface SeriesMarkerText { content: string; @@ -39,7 +38,7 @@ export interface SeriesMarkerRendererData { visibleRange: SeriesItemsIndexesRange | null; } -export class SeriesMarkersRenderer extends BitmapCoordinatesPaneRenderer { +export class SeriesMarkersRenderer implements IPrimitivePaneRenderer { private _data: SeriesMarkerRendererData | null = null; private _textWidthCache: TextWidthCache = new TextWidthCache(); private _fontSize: number = -1; @@ -59,17 +58,17 @@ export class SeriesMarkersRenderer extends BitmapCoordinatesPaneRenderer { } } - public hitTest(x: Coordinate, y: Coordinate): HoveredObject | null { + public hitTest(x: number, y: number): PrimitiveHoveredItem | null { if (this._data === null || this._data.visibleRange === null) { return null; } for (let i = this._data.visibleRange.from; i < this._data.visibleRange.to; i++) { const item = this._data.items[i]; - if (hitTestItem(item, x, y)) { + if (item && hitTestItem(item, x as Coordinate, y as Coordinate)) { return { - hitTestData: item.internalId, - externalId: item.externalId, + zOrder: 'normal', + externalId: item.externalId ?? '', }; } } @@ -77,16 +76,21 @@ export class SeriesMarkersRenderer extends BitmapCoordinatesPaneRenderer { return null; } - protected _drawImpl({ context: ctx, horizontalPixelRatio, verticalPixelRatio }: BitmapCoordinatesRenderingScope, isHovered: boolean, hitTestData?: unknown): void { + public draw(target: CanvasRenderingTarget2D): void { + target.useBitmapCoordinateSpace((scope: BitmapCoordinatesRenderingScope) => { + this._drawImpl(scope); + }); + } + + protected _drawImpl({ context: ctx, horizontalPixelRatio, verticalPixelRatio }: BitmapCoordinatesRenderingScope): void { if (this._data === null || this._data.visibleRange === null) { return; } ctx.textBaseline = 'middle'; ctx.font = this._font; - - for (let i = this._data.visibleRange.from; i < this._data.visibleRange.to; i++) { - const item = this._data.items[i]; + for (let index = this._data.visibleRange.from; index < this._data.visibleRange.to; index++) { + const item = this._data.items[index]; if (item.text !== undefined) { item.text.width = this._textWidthCache.measureText(ctx, item.text.content); item.text.height = this._fontSize; @@ -109,7 +113,6 @@ function bitmapShapeItemCoordinates(item: SeriesMarkerRendererDataItem, horizont function drawItem(item: SeriesMarkerRendererDataItem, ctx: CanvasRenderingContext2D, horizontalPixelRatio: number, verticalPixelRatio: number): void { ctx.fillStyle = item.color; - if (item.text !== undefined) { drawText(ctx, item.text.content, item.text.x, item.text.y, horizontalPixelRatio, verticalPixelRatio); } diff --git a/src/renderers/series-markers-arrow.ts b/src/plugins/series-markers/series-markers-arrow.ts similarity index 89% rename from src/renderers/series-markers-arrow.ts rename to src/plugins/series-markers/series-markers-arrow.ts index a4cfb6bf48..d0995cbba6 100644 --- a/src/renderers/series-markers-arrow.ts +++ b/src/plugins/series-markers/series-markers-arrow.ts @@ -1,9 +1,9 @@ -import { ceiledOdd } from '../helpers/mathex'; +import { ceiledOdd } from '../../helpers/mathex'; -import { Coordinate } from '../model/coordinate'; +import { Coordinate } from '../../model/coordinate'; import { hitTestSquare } from './series-markers-square'; -import { BitmapShapeItemCoordinates, shapeSize } from './series-markers-utils'; +import { BitmapShapeItemCoordinates, shapeSize } from './utils'; export function drawArrow( up: boolean, diff --git a/src/renderers/series-markers-circle.ts b/src/plugins/series-markers/series-markers-circle.ts similarity index 84% rename from src/renderers/series-markers-circle.ts rename to src/plugins/series-markers/series-markers-circle.ts index 667da73669..592c9a0eb2 100644 --- a/src/renderers/series-markers-circle.ts +++ b/src/plugins/series-markers/series-markers-circle.ts @@ -1,6 +1,6 @@ -import { Coordinate } from '../model/coordinate'; +import { Coordinate } from '../../model/coordinate'; -import { BitmapShapeItemCoordinates, shapeSize } from './series-markers-utils'; +import { BitmapShapeItemCoordinates, shapeSize } from './utils'; export function drawCircle( ctx: CanvasRenderingContext2D, diff --git a/src/renderers/series-markers-square.ts b/src/plugins/series-markers/series-markers-square.ts similarity index 85% rename from src/renderers/series-markers-square.ts rename to src/plugins/series-markers/series-markers-square.ts index a60409e3c0..f8ab178e1a 100644 --- a/src/renderers/series-markers-square.ts +++ b/src/plugins/series-markers/series-markers-square.ts @@ -1,6 +1,6 @@ -import { Coordinate } from '../model/coordinate'; +import { Coordinate } from '../../model/coordinate'; -import { BitmapShapeItemCoordinates, shapeSize } from './series-markers-utils'; +import { BitmapShapeItemCoordinates, shapeSize } from './utils'; export function drawSquare( ctx: CanvasRenderingContext2D, diff --git a/src/renderers/series-markers-text.ts b/src/plugins/series-markers/series-markers-text.ts similarity index 91% rename from src/renderers/series-markers-text.ts rename to src/plugins/series-markers/series-markers-text.ts index 3a87448594..18894030b5 100644 --- a/src/renderers/series-markers-text.ts +++ b/src/plugins/series-markers/series-markers-text.ts @@ -1,4 +1,4 @@ -import { Coordinate } from '../model/coordinate'; +import { Coordinate } from '../../model/coordinate'; export function drawText( ctx: CanvasRenderingContext2D, diff --git a/src/model/series-markers.ts b/src/plugins/series-markers/types.ts similarity index 60% rename from src/model/series-markers.ts rename to src/plugins/series-markers/types.ts index eb334a5ca9..cecae515d4 100644 --- a/src/model/series-markers.ts +++ b/src/plugins/series-markers/types.ts @@ -42,27 +42,10 @@ export interface SeriesMarker { * @defaultValue `1` */ size?: number; - - /** - * @internal - */ - originalTime: unknown; } +export type MarkerPositions = Record; + export interface InternalSeriesMarker extends SeriesMarker { internalId: number; } - -export function convertSeriesMarker(sm: SeriesMarker, newTime: OutTimeType, originalTime?: unknown): SeriesMarker { - const { time: inTime, originalTime: inOriginalTime, ...values } = sm; - /* eslint-disable @typescript-eslint/consistent-type-assertions */ - const res = { - time: newTime, - ...values, - } as SeriesMarker; - /* eslint-enable @typescript-eslint/consistent-type-assertions */ - if (originalTime !== undefined) { - res.originalTime = originalTime; - } - return res; -} diff --git a/src/renderers/series-markers-utils.ts b/src/plugins/series-markers/utils.ts similarity index 90% rename from src/renderers/series-markers-utils.ts rename to src/plugins/series-markers/utils.ts index 97e4cc682a..89e884ceaf 100644 --- a/src/renderers/series-markers-utils.ts +++ b/src/plugins/series-markers/utils.ts @@ -1,6 +1,6 @@ -import { ceiledEven, ceiledOdd } from '../helpers/mathex'; +import { ceiledEven, ceiledOdd } from '../../helpers/mathex'; -import { SeriesMarkerShape } from '../model/series-markers'; +import { SeriesMarkerShape } from './types'; const enum Constants { MinShapeSize = 12, diff --git a/src/plugins/series-markers/wrapper.ts b/src/plugins/series-markers/wrapper.ts new file mode 100644 index 0000000000..7f533389c0 --- /dev/null +++ b/src/plugins/series-markers/wrapper.ts @@ -0,0 +1,82 @@ +import { ISeriesApi } from '../../api/iseries-api'; + +import { SeriesType } from '../../model/series-options'; + +import { ISeriesPrimitiveWrapper, SeriesPrimitiveAdapter } from '../series-primitive-adapter'; +import { SeriesMarkersPrimitive } from './primitive'; +import { SeriesMarker } from './types'; + +/** + * Interface for a series markers plugin + */ +export interface ISeriesMarkersPluginApi extends ISeriesPrimitiveWrapper { + /** + * Set markers to the series. + * @param markers - An array of markers to be displayed on the series. + */ + setMarkers: (markers: SeriesMarker[]) => void; + /** + * Returns current markers. + */ + markers: () => readonly SeriesMarker[]; + /** + * Detaches the plugin from the series. + */ + detach: () => void; +} + +class SeriesMarkersPrimitiveWrapper extends SeriesPrimitiveAdapter> implements ISeriesPrimitiveWrapper, ISeriesMarkersPluginApi { + public constructor(series: ISeriesApi, primitive: SeriesMarkersPrimitive, markers?: SeriesMarker[]) { + super(series, primitive); + if (markers) { + this.setMarkers(markers); + } + } + public setMarkers(markers: SeriesMarker[]): void { + this._primitive.setMarkers(markers); + } + + public markers(): readonly SeriesMarker[] { + return this._primitive.markers(); + } +} + +/** + * A function to create a series markers primitive. + * + * @param series - The series to which the primitive will be attached. + * + * @param markers - An array of markers to be displayed on the series. + * + * @example + * ```js + * import { createSeriesMarkers } from 'lightweight-charts'; + * + * const seriesMarkers = createSeriesMarkers( + * series, + * [ + * { + * color: 'green', + * position: 'inBar', + * shape: 'arrowDown', + * time: 1556880900, + * }, + * ] + * ); + * // and then you can modify the markers + * // set it to empty array to remove all markers + * seriesMarkers.setMarkers([]); + * + * // `seriesMarkers.markers()` returns current markers + * ``` + */ +export function createSeriesMarkers( + series: ISeriesApi, + markers?: SeriesMarker[] +): ISeriesMarkersPluginApi { + const wrapper = new SeriesMarkersPrimitiveWrapper(series, new SeriesMarkersPrimitive()); + if (markers) { + wrapper.setMarkers(markers); + } + return wrapper; +} diff --git a/src/plugins/series-primitive-adapter.ts b/src/plugins/series-primitive-adapter.ts new file mode 100644 index 0000000000..21caf80e5e --- /dev/null +++ b/src/plugins/series-primitive-adapter.ts @@ -0,0 +1,63 @@ +import { ISeriesApi } from '../api/iseries-api'; +import { ISeriesPrimitive } from '../api/iseries-primitive-api'; + +import { DeepPartial } from '../helpers/strict-type-checks'; + +import { SeriesType } from '../model/series-options'; +/** + * Interface for a primitive wrapper. It must be implemented to add some plugin to the chart. + */ +interface ISeriesPrimitiveWithOptions extends ISeriesPrimitive { + /** + * @param options - Options to apply. The options are deeply merged with the current options. + */ + applyOptions?: (options: DeepPartial) => void; +} + +/** + * Interface for a series primitive. + */ +export interface ISeriesPrimitiveWrapper { + /** + * Detaches the plugin from the series. + */ + detach: () => void; + /** + * Returns the current series. + */ + getSeries: () => ISeriesApi; + /** + * Applies options to the primitive. + * @param options - Options to apply. The options are deeply merged with the current options. + */ + applyOptions?: (options: DeepPartial) => void; +} + +export class SeriesPrimitiveAdapter = ISeriesPrimitive, TSeriesType extends SeriesType = SeriesType> implements ISeriesPrimitiveWrapper { + protected _primitive: IPrimitive; + protected _series: ISeriesApi; + + public constructor(series: ISeriesApi, primitive: IPrimitive) { + this._series = series; + this._primitive = primitive; + this._attach(); + } + + public detach(): void { + this._series.detachPrimitive(this._primitive); + } + + public getSeries(): ISeriesApi { + return this._series; + } + + public applyOptions(options: DeepPartial): void { + if (this._primitive && this._primitive.applyOptions) { + this._primitive.applyOptions(options); + } + } + + private _attach(): void { + this._series.attachPrimitive(this._primitive); + } +} diff --git a/src/plugins/text-watermark/primitive.ts b/src/plugins/text-watermark/primitive.ts index 3892b5107f..62f40d1cc2 100644 --- a/src/plugins/text-watermark/primitive.ts +++ b/src/plugins/text-watermark/primitive.ts @@ -1,3 +1,4 @@ +import { IPaneApi } from '../../api/ipane-api'; import { IPanePrimitive, PaneAttachedParameter, @@ -5,9 +6,10 @@ import { import { DeepPartial } from '../../helpers/strict-type-checks'; -import { Time } from '../../model/horz-scale-behavior-time/types'; import { IPanePrimitivePaneView } from '../../model/ipane-primitive'; +import { IPanePrimitiveWithOptions, IPanePrimitiveWrapper, PanePrimitiveWrapper } from '../pane-primitive-wrapper'; +import { IPrimitiveWithOptions } from '../primitive-wrapper-base'; import { TextWatermarkLineOptions, textWatermarkLineOptionsDefaults, @@ -35,34 +37,7 @@ function mergeOptionsWithDefaults( }; } -/** - * A pane primitive for rendering a text watermark. - * - * @example - * ```js - * const textWatermark = new TextWatermark({ - * horzAlign: 'center', - * vertAlign: 'center', - * lines: [ - * { - * text: 'Hello', - * color: 'rgba(255,0,0,0.5)', - * fontSize: 100, - * fontStyle: 'bold', - * }, - * { - * text: 'This is a text watermark', - * color: 'rgba(0,0,255,0.5)', - * fontSize: 50, - * fontStyle: 'italic', - * fontFamily: 'monospace', - * }, - * ], - * }); - * chart.panes()[0].attachPrimitive(textWatermark); - * ``` - */ -export class TextWatermark implements IPanePrimitive { +class TextWatermark implements IPanePrimitive, IPrimitiveWithOptions { public requestUpdate?: () => void; private _paneViews: TextWatermarkPaneView[]; private _options: TextWatermarkOptions; @@ -97,3 +72,47 @@ export class TextWatermark implements IPanePrimitive { } } } + +export type ITextWatermarkPluginApi = IPanePrimitiveWrapper; + +/** + * Creates an image watermark. + * + * @param pane - Target pane. + * @param options - Watermark options. + * + * @returns Image watermark wrapper. + * + * @example + * ```js + * import { createTextWatermark } from 'lightweight-charts'; + * + * const firstPane = chart.panes()[0]; + * const textWatermark = createTextWatermark(firstPane, { + * horzAlign: 'center', + * vertAlign: 'center', + * lines: [ + * { + * text: 'Hello', + * color: 'rgba(255,0,0,0.5)', + * fontSize: 100, + * fontStyle: 'bold', + * }, + * { + * text: 'This is a text watermark', + * color: 'rgba(0,0,255,0.5)', + * fontSize: 50, + * fontStyle: 'italic', + * fontFamily: 'monospace', + * }, + * ], + * }); + * // to change options + * textWatermark.applyOptions({ horzAlign: 'left' }); + * // to remove watermark from the pane + * textWatermark.detach(); + * ``` + */ +export function createTextWatermark(pane: IPaneApi, options: DeepPartial): ITextWatermarkPluginApi { + return new PanePrimitiveWrapper>(pane, new TextWatermark(options)); +} diff --git a/src/plugins/types.ts b/src/plugins/types.ts index b6f88ff426..143a49f69a 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -1,3 +1,7 @@ +import { ISeriesApi } from '../api/iseries-api'; + +import { SeriesType } from '../model/series-options'; + /** * Represents a horizontal alignment. */ @@ -6,3 +10,11 @@ export type HorzAlign = 'left' | 'center' | 'right'; * Represents a vertical alignment. */ export type VertAlign = 'top' | 'center' | 'bottom'; + +type DefaultOptionsType = Record; +export interface IPluginApiBase> { + series(): ISeriesApi; + detach(): void; + options(): Options; + applyOptions(options: Options | Partial): void; +} diff --git a/src/views/pane/series-markers-pane-view.ts b/src/views/pane/series-markers-pane-view.ts deleted file mode 100644 index e170cf6664..0000000000 --- a/src/views/pane/series-markers-pane-view.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { ensureNever } from '../../helpers/assertions'; -import { isNumber } from '../../helpers/strict-type-checks'; - -import { AutoScaleMargins } from '../../model/autoscale-info-impl'; -import { BarPrice, BarPrices } from '../../model/bar'; -import { IChartModelBase } from '../../model/chart-model'; -import { Coordinate } from '../../model/coordinate'; -import { Pane } from '../../model/pane'; -import { PriceScale } from '../../model/price-scale'; -import { ISeries } from '../../model/series'; -import { InternalSeriesMarker, SeriesMarkerPosition } from '../../model/series-markers'; -import { SeriesType } from '../../model/series-options'; -import { TimePointIndex, visibleTimedValues } from '../../model/time-data'; -import { ITimeScale } from '../../model/time-scale'; -import { IPaneRenderer } from '../../renderers/ipane-renderer'; -import { - SeriesMarkerRendererData, - SeriesMarkerRendererDataItem, - SeriesMarkersRenderer, -} from '../../renderers/series-markers-renderer'; -import { - calculateAdjustedMargin, - calculateShapeHeight, - shapeMargin as calculateShapeMargin, -} from '../../renderers/series-markers-utils'; - -import { IUpdatablePaneView, UpdateType } from './iupdatable-pane-view'; - -const enum Constants { - TextMargin = 0.1, -} - -interface Offsets { - aboveBar: number; - belowBar: number; -} - -type MarkerPositions = Record; - -// eslint-disable-next-line max-params -function fillSizeAndY( - rendererItem: SeriesMarkerRendererDataItem, - marker: InternalSeriesMarker, - seriesData: BarPrices | BarPrice, - offsets: Offsets, - textHeight: number, - shapeMargin: number, - priceScale: PriceScale, - timeScale: ITimeScale, - firstValue: number -): void { - const inBarPrice = isNumber(seriesData) ? seriesData : seriesData.close; - const highPrice = isNumber(seriesData) ? seriesData : seriesData.high; - const lowPrice = isNumber(seriesData) ? seriesData : seriesData.low; - const sizeMultiplier = isNumber(marker.size) ? Math.max(marker.size, 0) : 1; - const shapeSize = calculateShapeHeight(timeScale.barSpacing()) * sizeMultiplier; - const halfSize = shapeSize / 2; - rendererItem.size = shapeSize; - - switch (marker.position) { - case 'inBar': { - rendererItem.y = priceScale.priceToCoordinate(inBarPrice, firstValue); - if (rendererItem.text !== undefined) { - rendererItem.text.y = rendererItem.y + halfSize + shapeMargin + textHeight * (0.5 + Constants.TextMargin) as Coordinate; - } - return; - } - case 'aboveBar': { - rendererItem.y = (priceScale.priceToCoordinate(highPrice, firstValue) - halfSize - offsets.aboveBar) as Coordinate; - if (rendererItem.text !== undefined) { - rendererItem.text.y = rendererItem.y - halfSize - textHeight * (0.5 + Constants.TextMargin) as Coordinate; - offsets.aboveBar += textHeight * (1 + 2 * Constants.TextMargin); - } - offsets.aboveBar += shapeSize + shapeMargin; - return; - } - case 'belowBar': { - rendererItem.y = (priceScale.priceToCoordinate(lowPrice, firstValue) + halfSize + offsets.belowBar) as Coordinate; - if (rendererItem.text !== undefined) { - rendererItem.text.y = rendererItem.y + halfSize + shapeMargin + textHeight * (0.5 + Constants.TextMargin) as Coordinate; - offsets.belowBar += textHeight * (1 + 2 * Constants.TextMargin); - } - offsets.belowBar += shapeSize + shapeMargin; - return; - } - } - - ensureNever(marker.position); -} - -export class SeriesMarkersPaneView implements IUpdatablePaneView { - private readonly _series: ISeries; - private readonly _model: IChartModelBase; - private _data: SeriesMarkerRendererData; - - private _invalidated: boolean = true; - private _dataInvalidated: boolean = true; - private _autoScaleMarginsInvalidated: boolean = true; - - private _autoScaleMargins: AutoScaleMargins | null = null; - private _markersPositions: MarkerPositions | null = null; - private _renderer: SeriesMarkersRenderer = new SeriesMarkersRenderer(); - - public constructor(series: ISeries, model: IChartModelBase) { - this._series = series; - this._model = model; - this._data = { - items: [], - visibleRange: null, - }; - } - - public update(updateType?: UpdateType): void { - this._invalidated = true; - this._autoScaleMarginsInvalidated = true; - if (updateType === 'data') { - this._dataInvalidated = true; - this._markersPositions = null; - } - } - - public renderer(pane: Pane, addAnchors?: boolean): IPaneRenderer | null { - if (!this._series.visible()) { - return null; - } - - if (this._invalidated) { - this._makeValid(); - } - - const layout = this._model.options()['layout']; - this._renderer.setParams(layout.fontSize, layout.fontFamily); - this._renderer.setData(this._data); - - return this._renderer; - } - - public autoScaleMargins(): AutoScaleMargins | null { - if (this._autoScaleMarginsInvalidated) { - if (this._series.indexedMarkers().length > 0) { - const barSpacing = this._model.timeScale().barSpacing(); - const shapeMargin = calculateShapeMargin(barSpacing); - const marginValue = calculateShapeHeight(barSpacing) * 1.5 + shapeMargin * 2; - const positions = this._getMarkerPositions(); - - this._autoScaleMargins = { - above: calculateAdjustedMargin(marginValue, positions.aboveBar, positions.inBar), - below: calculateAdjustedMargin(marginValue, positions.belowBar, positions.inBar), - }; - } else { - this._autoScaleMargins = null; - } - - this._autoScaleMarginsInvalidated = false; - } - - return this._autoScaleMargins; - } - - protected _getMarkerPositions(): MarkerPositions { - if (this._markersPositions === null) { - this._markersPositions = this._series.indexedMarkers().reduce( - (acc: MarkerPositions, marker: InternalSeriesMarker) => { - if (!acc[marker.position]) { - acc[marker.position] = true; - } - return acc; - }, - { - inBar: false, - aboveBar: false, - belowBar: false, - } - ); - } - return this._markersPositions; - } - - protected _makeValid(): void { - const priceScale = this._series.priceScale(); - const timeScale = this._model.timeScale(); - const seriesMarkers = this._series.indexedMarkers(); - if (this._dataInvalidated) { - this._data.items = seriesMarkers.map((marker: InternalSeriesMarker) => ({ - time: marker.time, - x: 0 as Coordinate, - y: 0 as Coordinate, - size: 0, - shape: marker.shape, - color: marker.color, - internalId: marker.internalId, - externalId: marker.id, - text: undefined, - })); - this._dataInvalidated = false; - } - - const layoutOptions = this._model.options()['layout']; - - this._data.visibleRange = null; - const visibleBars = timeScale.visibleStrictRange(); - if (visibleBars === null) { - return; - } - - const firstValue = this._series.firstValue(); - if (firstValue === null) { - return; - } - if (this._data.items.length === 0) { - return; - } - let prevTimeIndex = NaN; - const shapeMargin = calculateShapeMargin(timeScale.barSpacing()); - const offsets: Offsets = { - aboveBar: shapeMargin, - belowBar: shapeMargin, - }; - this._data.visibleRange = visibleTimedValues(this._data.items, visibleBars, true); - for (let index = this._data.visibleRange.from; index < this._data.visibleRange.to; index++) { - const marker = seriesMarkers[index]; - if (marker.time !== prevTimeIndex) { - // new bar, reset stack counter - offsets.aboveBar = shapeMargin; - offsets.belowBar = shapeMargin; - prevTimeIndex = marker.time; - } - - const rendererItem = this._data.items[index]; - rendererItem.x = timeScale.indexToCoordinate(marker.time); - if (marker.text !== undefined && marker.text.length > 0) { - rendererItem.text = { - content: marker.text, - x: 0 as Coordinate, - y: 0 as Coordinate, - width: 0, - height: 0, - }; - } - const dataAt = this._series.dataAt(marker.time); - if (dataAt === null) { - continue; - } - fillSizeAndY(rendererItem, marker, dataAt, offsets, layoutOptions.fontSize, shapeMargin, priceScale, timeScale, firstValue.value); - } - this._invalidated = false; - } -} diff --git a/tests/e2e/coverage/test-cases/plugins/custom-series.js b/tests/e2e/coverage/test-cases/plugins/custom-series.js index b4f05f264a..5efb8e4a45 100644 --- a/tests/e2e/coverage/test-cases/plugins/custom-series.js +++ b/tests/e2e/coverage/test-cases/plugins/custom-series.js @@ -312,7 +312,6 @@ function testSeriesApi(series) { }); series.priceFormatter(); series.seriesType(); - series.markers(); series.dataByIndex(10); series.dataByIndex(-5); series.dataByIndex(-5, LightweightCharts.MismatchDirection.NearestRight); diff --git a/tests/e2e/coverage/test-cases/plugins/text-watermark.js b/tests/e2e/coverage/test-cases/plugins/text-watermark.js index 4c16bb78d5..cb76c1b7aa 100644 --- a/tests/e2e/coverage/test-cases/plugins/text-watermark.js +++ b/tests/e2e/coverage/test-cases/plugins/text-watermark.js @@ -19,8 +19,8 @@ function beforeInteractions(container) { const mainSeries = chart.addLineSeries(); mainSeries.setData(simpleData()); - - textWatermark = new LightweightCharts.TextWatermark({ + const pane = chart.panes()[0]; + textWatermark = LightweightCharts.createTextWatermark(pane, { horzAlign: 'center', vertAlign: 'center', lines: [ @@ -39,9 +39,7 @@ function beforeInteractions(container) { }, ], }); - - const pane = chart.panes()[0]; - pane.attachPrimitive(textWatermark); + textWatermark.getPane(); return Promise.resolve(); } @@ -58,6 +56,7 @@ function afterInteractions() { horzAlign: 'right', vertAlign: 'bottom', }); + textWatermark.detach(); }); requestAnimationFrame(resolve); }); diff --git a/tests/e2e/coverage/test-cases/series/area-series.js b/tests/e2e/coverage/test-cases/series/area-series.js index ee8b25033d..3013443ecd 100644 --- a/tests/e2e/coverage/test-cases/series/area-series.js +++ b/tests/e2e/coverage/test-cases/series/area-series.js @@ -12,7 +12,6 @@ function testSeriesApi(series) { }); series.priceFormatter(); series.seriesType(); - series.markers(); series.dataByIndex(10); series.dataByIndex(-5); series.dataByIndex(-5, LightweightCharts.MismatchDirection.NearestRight); diff --git a/tests/e2e/coverage/test-cases/series/bar-series.js b/tests/e2e/coverage/test-cases/series/bar-series.js index 34b35319f6..a97cea17fd 100644 --- a/tests/e2e/coverage/test-cases/series/bar-series.js +++ b/tests/e2e/coverage/test-cases/series/bar-series.js @@ -12,7 +12,6 @@ function testSeriesApi(series) { }); series.priceFormatter(); series.seriesType(); - series.markers(); series.dataByIndex(10); series.dataByIndex(-5); series.dataByIndex(-5, LightweightCharts.MismatchDirection.NearestRight); diff --git a/tests/e2e/coverage/test-cases/series/baseline-series.js b/tests/e2e/coverage/test-cases/series/baseline-series.js index e2ae086da9..82daf1a8b0 100644 --- a/tests/e2e/coverage/test-cases/series/baseline-series.js +++ b/tests/e2e/coverage/test-cases/series/baseline-series.js @@ -12,7 +12,6 @@ function testSeriesApi(series) { }); series.priceFormatter(); series.seriesType(); - series.markers(); series.dataByIndex(10); series.dataByIndex(-5); series.dataByIndex(-5, LightweightCharts.MismatchDirection.NearestRight); diff --git a/tests/e2e/coverage/test-cases/series/candlestick-series.js b/tests/e2e/coverage/test-cases/series/candlestick-series.js index d80b7cddef..9740aa8734 100644 --- a/tests/e2e/coverage/test-cases/series/candlestick-series.js +++ b/tests/e2e/coverage/test-cases/series/candlestick-series.js @@ -12,7 +12,6 @@ function testSeriesApi(series) { }); series.priceFormatter(); series.seriesType(); - series.markers(); series.dataByIndex(10); series.dataByIndex(-5); series.dataByIndex(-5, LightweightCharts.MismatchDirection.NearestRight); diff --git a/tests/e2e/coverage/test-cases/series/histogram-series.js b/tests/e2e/coverage/test-cases/series/histogram-series.js index 4b87c7d37a..e0af2effdb 100644 --- a/tests/e2e/coverage/test-cases/series/histogram-series.js +++ b/tests/e2e/coverage/test-cases/series/histogram-series.js @@ -12,7 +12,6 @@ function testSeriesApi(series) { }); series.priceFormatter(); series.seriesType(); - series.markers(); series.dataByIndex(10); series.dataByIndex(-5); series.dataByIndex(-5, LightweightCharts.MismatchDirection.NearestRight); diff --git a/tests/e2e/coverage/test-cases/series/line-series.js b/tests/e2e/coverage/test-cases/series/line-series.js index cd23cb32d3..84d4544437 100644 --- a/tests/e2e/coverage/test-cases/series/line-series.js +++ b/tests/e2e/coverage/test-cases/series/line-series.js @@ -12,7 +12,6 @@ function testSeriesApi(series) { }); series.priceFormatter(); series.seriesType(); - series.markers(); series.dataByIndex(10); series.dataByIndex(-5); series.dataByIndex(-5, LightweightCharts.MismatchDirection.NearestRight); diff --git a/tests/e2e/coverage/test-cases/series/markers.js b/tests/e2e/coverage/test-cases/series/markers.js index 533c988510..3ab48aafe8 100644 --- a/tests/e2e/coverage/test-cases/series/markers.js +++ b/tests/e2e/coverage/test-cases/series/markers.js @@ -6,29 +6,33 @@ function interactionsToPerform() { ]; } -let mainSeries; +let seriesMarkerPrimitives; function beforeInteractions(container) { const chart = LightweightCharts.createChart(container); - mainSeries = chart.addBaselineSeries(); + const mainSeries = chart.addBaselineSeries(); const data = generateLineData(); mainSeries.setData(data); - - mainSeries.setMarkers([ - { time: data[data.length - 7].time, position: 'belowBar', color: 'rgb(255, 0, 0)', shape: 'arrowUp', text: 'test' }, - { time: data[data.length - 5].time, position: 'aboveBar', color: 'rgba(255, 255, 0, 1)', shape: 'arrowDown', text: 'test' }, - { time: data[data.length - 3].time, position: 'inBar', color: '#f0f', shape: 'circle', text: 'test' }, - { time: data[data.length - 1].time, position: 'belowBar', color: '#fff00a', shape: 'square', text: 'test', size: 2 }, - ]); - - mainSeries.markers(); - + seriesMarkerPrimitives = LightweightCharts.createSeriesMarkers( + mainSeries, + [ + { time: data[data.length - 7].time, position: 'belowBar', color: 'rgb(255, 0, 0)', shape: 'arrowUp', text: 'test' }, + { time: data[data.length - 5].time, position: 'aboveBar', color: 'rgba(255, 255, 0, 1)', shape: 'arrowDown', text: 'test' }, + { time: data[data.length - 3].time, position: 'inBar', color: '#f0f', shape: 'circle', text: 'test' }, + { time: data[data.length - 1].time, position: 'belowBar', color: '#fff00a', shape: 'square', text: 'test', size: 2 }, + ] + ); + + seriesMarkerPrimitives.markers(); + seriesMarkerPrimitives.getSeries(); + seriesMarkerPrimitives.applyOptions({}); return Promise.resolve(); } function afterInteractions() { - mainSeries.setMarkers([]); + seriesMarkerPrimitives.setMarkers([]); + seriesMarkerPrimitives.detach(); return Promise.resolve(); } diff --git a/tests/e2e/graphics/test-cases/add-markers-with-autosize-enabled.js b/tests/e2e/graphics/test-cases/add-markers-with-autosize-enabled.js index 7c9f8b9752..2de17a770b 100644 --- a/tests/e2e/graphics/test-cases/add-markers-with-autosize-enabled.js +++ b/tests/e2e/graphics/test-cases/add-markers-with-autosize-enabled.js @@ -70,5 +70,8 @@ function runTestCase(container) { }); } } - series.setMarkers(markers); + LightweightCharts.createSeriesMarkers( + series, + markers + ); } diff --git a/tests/e2e/graphics/test-cases/api/series-markers.js b/tests/e2e/graphics/test-cases/api/series-markers.js index 6880723234..45d41d831c 100644 --- a/tests/e2e/graphics/test-cases/api/series-markers.js +++ b/tests/e2e/graphics/test-cases/api/series-markers.js @@ -37,10 +37,14 @@ function runTestCase(container) { { time: '1990-04-30', position: 'aboveBar', color: 'red', shape: 'arrowDown' }, ]; - series.setMarkers(markers); - const seriesApiMarkers = series.markers(); + const markersPrimitive = LightweightCharts.createSeriesMarkers( + series, + markers + ); + const seriesApiMarkers = markersPrimitive.markers(); - const textWatermark = new LightweightCharts.TextWatermark({ + const pane = chart.panes()[0]; + LightweightCharts.createTextWatermark(pane, { lines: [ { text: JSON.stringify(seriesApiMarkers[0]), @@ -48,8 +52,6 @@ function runTestCase(container) { }, ], }); - const pane = chart.panes()[0]; - pane.attachPrimitive(textWatermark); - console.assert(compare(markers, seriesApiMarkers), `series.markers() should return exactly the same that was provided to series.setMarkers()\n${JSON.stringify(seriesApiMarkers)}\n${JSON.stringify(markers)}`); + console.assert(compare(markers, seriesApiMarkers), `seriesMarkersPrimitive.markers() should return exactly the same that was provided to series.setMarkers()\n${JSON.stringify(seriesApiMarkers)}\n${JSON.stringify(markers)}`); } diff --git a/tests/e2e/graphics/test-cases/api/subscribe-crosshair-move.js b/tests/e2e/graphics/test-cases/api/subscribe-crosshair-move.js index 9e03dda8a5..e5f6c773b7 100644 --- a/tests/e2e/graphics/test-cases/api/subscribe-crosshair-move.js +++ b/tests/e2e/graphics/test-cases/api/subscribe-crosshair-move.js @@ -14,7 +14,7 @@ function runTestCase(container) { chart.timeScale().fitContent(); - const textWatermark = new LightweightCharts.TextWatermark({ + const textWatermark = LightweightCharts.createTextWatermark(chart.panes()[0], { lines: [ { text: '', @@ -23,8 +23,6 @@ function runTestCase(container) { }, ], }); - const pane = chart.panes()[0]; - pane.attachPrimitive(textWatermark); chart.subscribeCrosshairMove(param => { if (param.time) { diff --git a/tests/e2e/graphics/test-cases/applying-options/make-series-hidden.js b/tests/e2e/graphics/test-cases/applying-options/make-series-hidden.js index ad6b5ae1ca..ea5eba465d 100644 --- a/tests/e2e/graphics/test-cases/applying-options/make-series-hidden.js +++ b/tests/e2e/graphics/test-cases/applying-options/make-series-hidden.js @@ -25,16 +25,19 @@ function runTestCase(container) { }); const data = generateData(); lineSeries.setData(data); - lineSeries.setMarkers([ - { time: data[data.length - 30].time, position: 'belowBar', color: 'orange', shape: 'arrowUp' }, - { time: data[data.length - 30].time, position: 'belowBar', color: 'yellow', shape: 'arrowUp' }, - { time: data[data.length - 30].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'orange', shape: 'arrowDown' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'yellow', shape: 'arrowDown' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'red', shape: 'arrowDown' }, - { time: data[data.length - 10].time, position: 'inBar', color: 'orange', shape: 'arrowUp' }, - { time: data[data.length - 10].time, position: 'inBar', color: 'red', shape: 'arrowDown' }, - ]); + LightweightCharts.createSeriesMarkers( + lineSeries, + [ + { time: data[data.length - 30].time, position: 'belowBar', color: 'orange', shape: 'arrowUp' }, + { time: data[data.length - 30].time, position: 'belowBar', color: 'yellow', shape: 'arrowUp' }, + { time: data[data.length - 30].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'orange', shape: 'arrowDown' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'yellow', shape: 'arrowDown' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'red', shape: 'arrowDown' }, + { time: data[data.length - 10].time, position: 'inBar', color: 'orange', shape: 'arrowUp' }, + { time: data[data.length - 10].time, position: 'inBar', color: 'red', shape: 'arrowDown' }, + ] + ); return new Promise(resolve => { setTimeout(() => { diff --git a/tests/e2e/graphics/test-cases/applying-options/make-series-visible.js b/tests/e2e/graphics/test-cases/applying-options/make-series-visible.js index a5c33c0aee..ef91fa1b9c 100644 --- a/tests/e2e/graphics/test-cases/applying-options/make-series-visible.js +++ b/tests/e2e/graphics/test-cases/applying-options/make-series-visible.js @@ -26,16 +26,19 @@ function runTestCase(container) { }); const data = generateData(); lineSeries.setData(data); - lineSeries.setMarkers([ - { time: data[data.length - 30].time, position: 'belowBar', color: 'orange', shape: 'arrowUp' }, - { time: data[data.length - 30].time, position: 'belowBar', color: 'yellow', shape: 'arrowUp' }, - { time: data[data.length - 30].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'orange', shape: 'arrowDown' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'yellow', shape: 'arrowDown' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'red', shape: 'arrowDown' }, - { time: data[data.length - 10].time, position: 'inBar', color: 'orange', shape: 'arrowUp' }, - { time: data[data.length - 10].time, position: 'inBar', color: 'red', shape: 'arrowDown' }, - ]); + LightweightCharts.createSeriesMarkers( + lineSeries, + [ + { time: data[data.length - 30].time, position: 'belowBar', color: 'orange', shape: 'arrowUp' }, + { time: data[data.length - 30].time, position: 'belowBar', color: 'yellow', shape: 'arrowUp' }, + { time: data[data.length - 30].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'orange', shape: 'arrowDown' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'yellow', shape: 'arrowDown' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'red', shape: 'arrowDown' }, + { time: data[data.length - 10].time, position: 'inBar', color: 'orange', shape: 'arrowUp' }, + { time: data[data.length - 10].time, position: 'inBar', color: 'red', shape: 'arrowDown' }, + ] + ); return new Promise(resolve => { setTimeout(() => { diff --git a/tests/e2e/graphics/test-cases/applying-options/watermark.js b/tests/e2e/graphics/test-cases/applying-options/watermark.js index eddc3b25c5..1d1008ea58 100644 --- a/tests/e2e/graphics/test-cases/applying-options/watermark.js +++ b/tests/e2e/graphics/test-cases/applying-options/watermark.js @@ -33,7 +33,7 @@ function runTestCase(container) { mainSeries.setData(generateData()); - textWatermark = new LightweightCharts.TextWatermark({ + textWatermark = LightweightCharts.createTextWatermark(chart.panes()[0], { horzAlign: 'left', vertAlign: 'bottom', lines: [ @@ -44,8 +44,6 @@ function runTestCase(container) { }, ], }); - const pane = chart.panes()[0]; - pane.attachPrimitive(textWatermark); return new Promise(resolve => { setTimeout(() => { diff --git a/tests/e2e/graphics/test-cases/data-validation.js b/tests/e2e/graphics/test-cases/data-validation.js index 5cde6d3e4f..3ad780dc70 100644 --- a/tests/e2e/graphics/test-cases/data-validation.js +++ b/tests/e2e/graphics/test-cases/data-validation.js @@ -29,17 +29,6 @@ function runTestCase(container) { // passed } - try { - lineSeries.setMarkers([ - { time: 1 }, - { time: 0, value: 0 }, - ]); - - console.assert(false, 'should fail if series markers is not ordered'); - } catch (e) { - // passed - } - try { lineSeries.setData([ { time: 0 }, @@ -93,18 +82,21 @@ function runTestCase(container) { } // should pass - several markers could be on the same bar - lineSeries.setMarkers([ - { - color: 'green', - position: 'belowBar', - shape: 'arrowDown', - time: 0, - }, - { - color: 'green', - position: 'aboveBar', - shape: 'arrowUp', - time: 0, - }, - ]); + LightweightCharts.createSeriesMarkers( + lineSeries, + [ + { + color: 'green', + position: 'belowBar', + shape: 'arrowDown', + time: 0, + }, + { + color: 'green', + position: 'aboveBar', + shape: 'arrowUp', + time: 0, + }, + ] + ); } diff --git a/tests/e2e/graphics/test-cases/initial-options/watermark.js b/tests/e2e/graphics/test-cases/initial-options/watermark.js index e48f1f7b7d..7442ed56f8 100644 --- a/tests/e2e/graphics/test-cases/initial-options/watermark.js +++ b/tests/e2e/graphics/test-cases/initial-options/watermark.js @@ -20,7 +20,7 @@ function runTestCase(container) { const mainSeries = chart.addAreaSeries(); mainSeries.setData(generateData()); - const textWatermark = new LightweightCharts.TextWatermark({ + LightweightCharts.createTextWatermark(chart.panes()[0], { visible: true, lines: [ { @@ -32,7 +32,4 @@ function runTestCase(container) { }, ], }); - - const pane = chart.panes()[0]; - pane.attachPrimitive(textWatermark); } diff --git a/tests/e2e/graphics/test-cases/plugins/image-watermark.js b/tests/e2e/graphics/test-cases/plugins/image-watermark.js index 0e2fd8a905..d2ed2d56c3 100644 --- a/tests/e2e/graphics/test-cases/plugins/image-watermark.js +++ b/tests/e2e/graphics/test-cases/plugins/image-watermark.js @@ -71,11 +71,8 @@ function runTestCase(container) { const mainSeries = chart.addAreaSeries(); mainSeries.setData(generateData()); - const imageWatermark = new LightweightCharts.ImageWatermark(imageDataUrl, { + LightweightCharts.createImageWatermark(chart.panes()[0], imageDataUrl, { alpha: 0.5, padding: 20, }); - - const pane = chart.panes()[0]; - pane.attachPrimitive(imageWatermark); } diff --git a/tests/e2e/graphics/test-cases/series-markers/marker-in-gap-from-left.js b/tests/e2e/graphics/test-cases/series-markers/marker-in-gap-from-left.js index e62d6978e3..713af824b9 100644 --- a/tests/e2e/graphics/test-cases/series-markers/marker-in-gap-from-left.js +++ b/tests/e2e/graphics/test-cases/series-markers/marker-in-gap-from-left.js @@ -25,15 +25,17 @@ function runTestCase(container) { { time: 1556892000, value: 132.24 }, { time: 1556895600, value: 132.52 }, ]); - - lineSeries.setMarkers([ - { - color: 'green', - position: 'inBar', - shape: 'arrowDown', - time: 1556870600, - }, - ]); + LightweightCharts.createSeriesMarkers( + lineSeries, + [ + { + color: 'green', + position: 'inBar', + shape: 'arrowDown', + time: 1556870600, + }, + ] + ); chart.timeScale().fitContent(); } diff --git a/tests/e2e/graphics/test-cases/series-markers/marker-in-gap.js b/tests/e2e/graphics/test-cases/series-markers/marker-in-gap.js index 38549f3be7..1434a7a471 100644 --- a/tests/e2e/graphics/test-cases/series-markers/marker-in-gap.js +++ b/tests/e2e/graphics/test-cases/series-markers/marker-in-gap.js @@ -25,14 +25,17 @@ function runTestCase(container) { { time: 1556895600, value: 132.52 }, ]); - lineSeries.setMarkers([ - { - color: 'green', - position: 'inBar', - shape: 'arrowDown', - time: 1556880900, - }, - ]); + LightweightCharts.createSeriesMarkers( + lineSeries, + [ + { + color: 'green', + position: 'inBar', + shape: 'arrowDown', + time: 1556880900, + }, + ] + ); chart.timeScale().fitContent(); } diff --git a/tests/e2e/graphics/test-cases/series-markers/series-arrow-markers.js b/tests/e2e/graphics/test-cases/series-markers/series-arrow-markers.js index 7025fa5c16..f66e6213ed 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-arrow-markers.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-arrow-markers.js @@ -30,14 +30,17 @@ function runTestCase(container) { const data = generateData(); mainSeries.setData(data); - mainSeries.setMarkers([ - { time: data[data.length - 30].time, position: 'belowBar', color: 'orange', shape: 'arrowUp' }, - { time: data[data.length - 30].time, position: 'belowBar', color: 'yellow', shape: 'arrowUp' }, - { time: data[data.length - 30].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'orange', shape: 'arrowDown' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'yellow', shape: 'arrowDown' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'red', shape: 'arrowDown' }, - { time: data[data.length - 10].time, position: 'inBar', color: 'orange', shape: 'arrowUp' }, - { time: data[data.length - 10].time, position: 'inBar', color: 'red', shape: 'arrowDown' }, - ]); + LightweightCharts.createSeriesMarkers( + mainSeries, + [ + { time: data[data.length - 30].time, position: 'belowBar', color: 'orange', shape: 'arrowUp' }, + { time: data[data.length - 30].time, position: 'belowBar', color: 'yellow', shape: 'arrowUp' }, + { time: data[data.length - 30].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'orange', shape: 'arrowDown' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'yellow', shape: 'arrowDown' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'red', shape: 'arrowDown' }, + { time: data[data.length - 10].time, position: 'inBar', color: 'orange', shape: 'arrowUp' }, + { time: data[data.length - 10].time, position: 'inBar', color: 'red', shape: 'arrowDown' }, + ] + ); } diff --git a/tests/e2e/graphics/test-cases/series-markers/series-circle-markers.js b/tests/e2e/graphics/test-cases/series-markers/series-circle-markers.js index 0aced988b6..fb3e7ce8b9 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-circle-markers.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-circle-markers.js @@ -29,15 +29,17 @@ function runTestCase(container) { const data = generateData(); mainSeries.setData(data); - - mainSeries.setMarkers([ - { time: data[data.length - 30].time, position: 'belowBar', color: 'orange', shape: 'circle' }, - { time: data[data.length - 30].time, position: 'belowBar', color: 'yellow', shape: 'circle' }, - { time: data[data.length - 30].time, position: 'belowBar', color: 'red', shape: 'circle' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'orange', shape: 'circle' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'yellow', shape: 'circle' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'red', shape: 'circle' }, - { time: data[data.length - 10].time, position: 'inBar', color: 'orange', shape: 'circle' }, - { time: data[data.length - 10].time, position: 'inBar', color: 'red', shape: 'circle' }, - ]); + LightweightCharts.createSeriesMarkers( + mainSeries, + [ + { time: data[data.length - 30].time, position: 'belowBar', color: 'orange', shape: 'circle' }, + { time: data[data.length - 30].time, position: 'belowBar', color: 'yellow', shape: 'circle' }, + { time: data[data.length - 30].time, position: 'belowBar', color: 'red', shape: 'circle' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'orange', shape: 'circle' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'yellow', shape: 'circle' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'red', shape: 'circle' }, + { time: data[data.length - 10].time, position: 'inBar', color: 'orange', shape: 'circle' }, + { time: data[data.length - 10].time, position: 'inBar', color: 'red', shape: 'circle' }, + ] + ); } diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-aligned.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-aligned.js index 23101b0d68..4beb5411f8 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-aligned.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-aligned.js @@ -15,13 +15,16 @@ function runTestCase(container) { { time: '2017-04-23', value: 81.89 }, ]); - line.setMarkers([ - { time: '2017-04-10', position: 'inBar', color: 'orange', shape: 'circle' }, - { time: '2017-04-16', position: 'inBar', color: 'orange', shape: 'circle' }, - { time: '2017-04-17', position: 'inBar', color: 'orange', shape: 'circle' }, - { time: '2017-04-18', position: 'inBar', color: 'orange', shape: 'circle' }, - { time: '2017-04-24', position: 'inBar', color: 'orange', shape: 'circle' }, - ]); + LightweightCharts.createSeriesMarkers( + line, + [ + { time: '2017-04-10', position: 'inBar', color: 'orange', shape: 'circle' }, + { time: '2017-04-16', position: 'inBar', color: 'orange', shape: 'circle' }, + { time: '2017-04-17', position: 'inBar', color: 'orange', shape: 'circle' }, + { time: '2017-04-18', position: 'inBar', color: 'orange', shape: 'circle' }, + { time: '2017-04-24', position: 'inBar', color: 'orange', shape: 'circle' }, + ] + ); chart.timeScale().fitContent(); } diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-all-above.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-all-above.js index 980a40c98e..fe618756b6 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-all-above.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-all-above.js @@ -20,11 +20,11 @@ function runTestCase(container) { const data = generateData(); mainSeries.setData(data); - const markers = [ - { time: data[0].time, position: 'aboveBar', color: 'red', shape: 'arrowUp' }, - { time: data[1].time, position: 'aboveBar', color: 'red', shape: 'arrowUp' }, - - ]; - - mainSeries.setMarkers(markers); + LightweightCharts.createSeriesMarkers( + mainSeries, + [ + { time: data[0].time, position: 'aboveBar', color: 'red', shape: 'arrowUp' }, + { time: data[1].time, position: 'aboveBar', color: 'red', shape: 'arrowUp' }, + ] + ); } diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-all-below.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-all-below.js index 949978319b..287a0b6d26 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-all-below.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-all-below.js @@ -20,10 +20,11 @@ function runTestCase(container) { const data = generateData(); mainSeries.setData(data); - const markers = [ - { time: data[data.length - 3].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, - { time: data[data.length - 2].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, - ]; - - mainSeries.setMarkers(markers); + LightweightCharts.createSeriesMarkers( + mainSeries, + [ + { time: data[data.length - 3].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, + { time: data[data.length - 2].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, + ] + ); } diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-all-inbar.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-all-inbar.js index 31855e49be..33cc25c8f9 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-all-inbar.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-all-inbar.js @@ -19,11 +19,11 @@ function runTestCase(container) { const data = generateData(); mainSeries.setData(data); - - const markers = [ - { time: data[data.length - 3].time, position: 'inBar', color: 'red', shape: 'arrowUp' }, - { time: data[data.length - 2].time, position: 'inBar', color: 'red', shape: 'arrowUp' }, - ]; - - mainSeries.setMarkers(markers); + LightweightCharts.createSeriesMarkers( + mainSeries, + [ + { time: data[data.length - 3].time, position: 'inBar', color: 'red', shape: 'arrowUp' }, + { time: data[data.length - 2].time, position: 'inBar', color: 'red', shape: 'arrowUp' }, + ] + ); } diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-max-bar-spacing.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-max-bar-spacing.js index 167c205a2c..4e7803c7f6 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-max-bar-spacing.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-max-bar-spacing.js @@ -20,14 +20,15 @@ function runTestCase(container) { const data = generateData(); mainSeries.setData(data); - const markers = [ - { time: data[data.length - 4].time, position: 'inBar', color: 'red', shape: 'square' }, - { time: data[data.length - 3].time, position: 'inBar', color: 'red', shape: 'circle' }, - { time: data[data.length - 2].time, position: 'inBar', color: 'red', shape: 'arrowDown' }, - { time: data[data.length - 1].time, position: 'inBar', color: 'red', shape: 'arrowUp' }, - ]; - - mainSeries.setMarkers(markers); + LightweightCharts.createSeriesMarkers( + mainSeries, + [ + { time: data[data.length - 4].time, position: 'inBar', color: 'red', shape: 'square' }, + { time: data[data.length - 3].time, position: 'inBar', color: 'red', shape: 'circle' }, + { time: data[data.length - 2].time, position: 'inBar', color: 'red', shape: 'arrowDown' }, + { time: data[data.length - 1].time, position: 'inBar', color: 'red', shape: 'arrowUp' }, + ] + ); chart.applyOptions({ timeScale: { barSpacing: 1000, // will be corrected to max available bar spacing diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-min-bar-spacing.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-min-bar-spacing.js index 6b253fc623..5ee848a928 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-min-bar-spacing.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-min-bar-spacing.js @@ -27,7 +27,10 @@ function runTestCase(container) { { time: data[data.length - 10].time, position: 'inBar', color: 'red', shape: 'arrowUp' }, ]; - mainSeries.setMarkers(markers); + LightweightCharts.createSeriesMarkers( + mainSeries, + markers + ); chart.applyOptions({ timeScale: { barSpacing: 0.01, // will be corrected to min available bar spacing diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-object-business-day.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-object-business-day.js index faf7f97577..c2758d2d6f 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-object-business-day.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-object-business-day.js @@ -15,13 +15,16 @@ function runTestCase(container) { { time: '2017-04-23', value: 81.89 }, ]); - line.setMarkers([ - { time: { year: 2017, month: 4, day: 11 }, position: 'inBar', color: 'orange', shape: 'circle' }, - { time: { year: 2017, month: 4, day: 14 }, position: 'inBar', color: 'orange', shape: 'circle' }, - { time: { year: 2017, month: 4, day: 15 }, position: 'inBar', color: 'orange', shape: 'circle' }, - { time: { year: 2017, month: 4, day: 19 }, position: 'inBar', color: 'orange', shape: 'circle' }, - { time: { year: 2017, month: 4, day: 23 }, position: 'inBar', color: 'orange', shape: 'circle' }, - ]); + LightweightCharts.createSeriesMarkers( + line, + [ + { time: { year: 2017, month: 4, day: 11 }, position: 'inBar', color: 'orange', shape: 'circle' }, + { time: { year: 2017, month: 4, day: 14 }, position: 'inBar', color: 'orange', shape: 'circle' }, + { time: { year: 2017, month: 4, day: 15 }, position: 'inBar', color: 'orange', shape: 'circle' }, + { time: { year: 2017, month: 4, day: 19 }, position: 'inBar', color: 'orange', shape: 'circle' }, + { time: { year: 2017, month: 4, day: 23 }, position: 'inBar', color: 'orange', shape: 'circle' }, + ] + ); chart.timeScale().fitContent(); } diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-out-of-visible-range.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-out-of-visible-range.js index 6e467f43c2..25bc105757 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-out-of-visible-range.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-out-of-visible-range.js @@ -20,13 +20,15 @@ function runTestCase(container) { const data = generateData(); mainSeries.setData(data); - mainSeries.setMarkers([ - { time: data[0].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, - { time: data[data.length - 4].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, - { time: data[data.length - 3].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, - { time: data[data.length - 2].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, - { time: data[data.length - 1].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, - ]); - + LightweightCharts.createSeriesMarkers( + mainSeries, + [ + { time: data[0].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, + { time: data[data.length - 4].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, + { time: data[data.length - 3].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, + { time: data[data.length - 2].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, + { time: data[data.length - 1].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, + ] + ); chart.timeScale().scrollToPosition(-4); } diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-re-aligned.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-re-aligned.js index c731e063e2..a3f0801818 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-re-aligned.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-re-aligned.js @@ -15,13 +15,16 @@ function runTestCase(container) { { time: '2017-04-23', value: 91.89 }, ]); - line.setMarkers([ - { time: '2017-04-10', position: 'inBar', color: 'orange', shape: 'circle' }, - { time: '2017-04-16', position: 'inBar', color: 'orange', shape: 'circle' }, - { time: '2017-04-17', position: 'inBar', color: 'orange', shape: 'circle' }, - { time: '2017-04-18', position: 'inBar', color: 'orange', shape: 'circle' }, - { time: '2017-04-24', position: 'inBar', color: 'orange', shape: 'circle' }, - ]); + LightweightCharts.createSeriesMarkers( + line, + [ + { time: '2017-04-10', position: 'inBar', color: 'orange', shape: 'circle' }, + { time: '2017-04-16', position: 'inBar', color: 'orange', shape: 'circle' }, + { time: '2017-04-17', position: 'inBar', color: 'orange', shape: 'circle' }, + { time: '2017-04-18', position: 'inBar', color: 'orange', shape: 'circle' }, + { time: '2017-04-24', position: 'inBar', color: 'orange', shape: 'circle' }, + ] + ); line.setData([ { time: '2017-04-10', value: 85.01 }, diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-update.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-update.js index 8be06811ff..f26c279c2a 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-update.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-update.js @@ -26,10 +26,12 @@ function runTestCase(container) { { time: data[data.length - 20].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, { time: data[data.length - 10].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, ]; - - mainSeries.setMarkers(markers); + const markerPrimitive = LightweightCharts.createSeriesMarkers( + mainSeries, + markers + ); markers.push({ time: data[data.length - 1].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }); - mainSeries.setMarkers(markers); + markerPrimitive.setMarkers(markers); } diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-with-text.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-with-text.js index f8a099becf..77b03da123 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-with-text.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-with-text.js @@ -37,27 +37,29 @@ function runTestCase(container) { const data = generateData(); mainSeries.setData(data); - - mainSeries.setMarkers([ - { time: data[data.length - 50].time, position: 'belowBar', color: 'red', shape: 'arrowUp', text: 'test' }, - { time: data[data.length - 50].time, position: 'belowBar', color: 'red', shape: 'arrowDown', text: 'test' }, - { time: data[data.length - 50].time, position: 'belowBar', color: 'red', shape: 'circle', text: 'test' }, - { time: data[data.length - 50].time, position: 'belowBar', color: 'red', shape: 'square', text: 'test' }, - { time: data[data.length - 40].time, position: 'aboveBar', color: 'red', shape: 'arrowUp', text: 'test' }, - { time: data[data.length - 40].time, position: 'aboveBar', color: 'red', shape: 'arrowDown', text: 'test' }, - { time: data[data.length - 40].time, position: 'aboveBar', color: 'red', shape: 'circle', text: 'test' }, - { time: data[data.length - 40].time, position: 'aboveBar', color: 'red', shape: 'square', text: 'test' }, - { time: data[data.length - 30].time, position: 'inBar', color: 'blue', shape: 'arrowUp', text: 'test' }, - { time: data[data.length - 30].time, position: 'inBar', color: 'blue', shape: 'arrowDown', text: 'test' }, - { time: data[data.length - 30].time, position: 'inBar', color: 'blue', shape: 'circle', text: 'test' }, - { time: data[data.length - 30].time, position: 'inBar', color: 'blue', shape: 'square', text: 'test' }, - { time: data[data.length - 20].time, position: 'belowBar', color: 'aqua', shape: 'square', text: 'test', size: 0 }, - { time: data[data.length - 20].time, position: 'belowBar', color: 'aqua', shape: 'square', text: 'test', size: 1 }, - { time: data[data.length - 20].time, position: 'belowBar', color: 'aqua', shape: 'square', text: 'test', size: 2 }, - { time: data[data.length - 20].time, position: 'belowBar', color: 'aqua', shape: 'square', text: 'test', size: 3 }, - { time: data[data.length - 10].time, position: 'aboveBar', color: 'aqua', shape: 'cricle', text: '', size: 0 }, - { time: data[data.length - 10].time, position: 'aboveBar', color: 'aqua', shape: 'cricle', text: '', size: 1 }, - { time: data[data.length - 10].time, position: 'aboveBar', color: 'aqua', shape: 'cricle', text: '', size: 2 }, - { time: data[data.length - 10].time, position: 'aboveBar', color: 'aqua', shape: 'cricle', text: '', size: 3 }, - ]); + LightweightCharts.createSeriesMarkers( + mainSeries, + [ + { time: data[data.length - 50].time, position: 'belowBar', color: 'red', shape: 'arrowUp', text: 'test' }, + { time: data[data.length - 50].time, position: 'belowBar', color: 'red', shape: 'arrowDown', text: 'test' }, + { time: data[data.length - 50].time, position: 'belowBar', color: 'red', shape: 'circle', text: 'test' }, + { time: data[data.length - 50].time, position: 'belowBar', color: 'red', shape: 'square', text: 'test' }, + { time: data[data.length - 40].time, position: 'aboveBar', color: 'red', shape: 'arrowUp', text: 'test' }, + { time: data[data.length - 40].time, position: 'aboveBar', color: 'red', shape: 'arrowDown', text: 'test' }, + { time: data[data.length - 40].time, position: 'aboveBar', color: 'red', shape: 'circle', text: 'test' }, + { time: data[data.length - 40].time, position: 'aboveBar', color: 'red', shape: 'square', text: 'test' }, + { time: data[data.length - 30].time, position: 'inBar', color: 'blue', shape: 'arrowUp', text: 'test' }, + { time: data[data.length - 30].time, position: 'inBar', color: 'blue', shape: 'arrowDown', text: 'test' }, + { time: data[data.length - 30].time, position: 'inBar', color: 'blue', shape: 'circle', text: 'test' }, + { time: data[data.length - 30].time, position: 'inBar', color: 'blue', shape: 'square', text: 'test' }, + { time: data[data.length - 20].time, position: 'belowBar', color: 'aqua', shape: 'square', text: 'test', size: 0 }, + { time: data[data.length - 20].time, position: 'belowBar', color: 'aqua', shape: 'square', text: 'test', size: 1 }, + { time: data[data.length - 20].time, position: 'belowBar', color: 'aqua', shape: 'square', text: 'test', size: 2 }, + { time: data[data.length - 20].time, position: 'belowBar', color: 'aqua', shape: 'square', text: 'test', size: 3 }, + { time: data[data.length - 10].time, position: 'aboveBar', color: 'aqua', shape: 'cricle', text: '', size: 0 }, + { time: data[data.length - 10].time, position: 'aboveBar', color: 'aqua', shape: 'cricle', text: '', size: 1 }, + { time: data[data.length - 10].time, position: 'aboveBar', color: 'aqua', shape: 'cricle', text: '', size: 2 }, + { time: data[data.length - 10].time, position: 'aboveBar', color: 'aqua', shape: 'cricle', text: '', size: 3 }, + ] + ); } diff --git a/tests/e2e/graphics/test-cases/series-markers/series-square-markers.js b/tests/e2e/graphics/test-cases/series-markers/series-square-markers.js index 8585b7bd89..c9616d2c53 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-square-markers.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-square-markers.js @@ -29,15 +29,17 @@ function runTestCase(container) { const data = generateData(); mainSeries.setData(data); - - mainSeries.setMarkers([ - { time: data[data.length - 30].time, position: 'belowBar', color: 'orange', shape: 'square' }, - { time: data[data.length - 30].time, position: 'belowBar', color: 'yellow', shape: 'square' }, - { time: data[data.length - 30].time, position: 'belowBar', color: 'red', shape: 'square' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'orange', shape: 'square' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'yellow', shape: 'square' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'red', shape: 'square' }, - { time: data[data.length - 10].time, position: 'inBar', color: 'orange', shape: 'square' }, - { time: data[data.length - 10].time, position: 'inBar', color: 'red', shape: 'square' }, - ]); + LightweightCharts.createSeriesMarkers( + mainSeries, + [ + { time: data[data.length - 30].time, position: 'belowBar', color: 'orange', shape: 'square' }, + { time: data[data.length - 30].time, position: 'belowBar', color: 'yellow', shape: 'square' }, + { time: data[data.length - 30].time, position: 'belowBar', color: 'red', shape: 'square' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'orange', shape: 'square' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'yellow', shape: 'square' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'red', shape: 'square' }, + { time: data[data.length - 10].time, position: 'inBar', color: 'orange', shape: 'square' }, + { time: data[data.length - 10].time, position: 'inBar', color: 'red', shape: 'square' }, + ] + ); } diff --git a/tests/e2e/graphics/test-cases/series-markers/set-markers-before-series-data.js b/tests/e2e/graphics/test-cases/series-markers/set-markers-before-series-data.js index 18f4a7a1ef..5aa41db417 100644 --- a/tests/e2e/graphics/test-cases/series-markers/set-markers-before-series-data.js +++ b/tests/e2e/graphics/test-cases/series-markers/set-markers-before-series-data.js @@ -3,28 +3,31 @@ function runTestCase(container) { const mainSeries = chart.addLineSeries(); - mainSeries.setMarkers([ - { - time: '2019-04-09', - position: 'aboveBar', - color: 'black', - shape: 'arrowDown', - }, - { - time: '2019-05-31', - position: 'belowBar', - color: 'red', - shape: 'arrowUp', - id: 'id3', - }, - { - time: '2019-05-31', - position: 'belowBar', - color: 'orange', - shape: 'arrowUp', - id: 'id4', - }, - ]); + LightweightCharts.createSeriesMarkers( + mainSeries, + [ + { + time: '2019-04-09', + position: 'aboveBar', + color: 'black', + shape: 'arrowDown', + }, + { + time: '2019-05-31', + position: 'belowBar', + color: 'red', + shape: 'arrowUp', + id: 'id3', + }, + { + time: '2019-05-31', + position: 'belowBar', + color: 'orange', + shape: 'arrowUp', + id: 'id4', + }, + ] + ); mainSeries.setData([{ time: '2018-12-12', value: 24.11 }]); } diff --git a/tests/e2e/graphics/test-cases/series/series-visibility.js b/tests/e2e/graphics/test-cases/series/series-visibility.js index fe80a04e60..6eefd599a1 100644 --- a/tests/e2e/graphics/test-cases/series/series-visibility.js +++ b/tests/e2e/graphics/test-cases/series/series-visibility.js @@ -45,16 +45,19 @@ function runTestCase(container) { }); const data = generateData(); lineSeries.setData(data); - lineSeries.setMarkers([ - { time: data[data.length - 30].time, position: 'belowBar', color: 'orange', shape: 'arrowUp' }, - { time: data[data.length - 30].time, position: 'belowBar', color: 'yellow', shape: 'arrowUp' }, - { time: data[data.length - 30].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'orange', shape: 'arrowDown' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'yellow', shape: 'arrowDown' }, - { time: data[data.length - 20].time, position: 'aboveBar', color: 'red', shape: 'arrowDown' }, - { time: data[data.length - 10].time, position: 'inBar', color: 'orange', shape: 'arrowUp' }, - { time: data[data.length - 10].time, position: 'inBar', color: 'red', shape: 'arrowDown' }, - ]); + LightweightCharts.createSeriesMarkers( + lineSeries, + [ + { time: data[data.length - 30].time, position: 'belowBar', color: 'orange', shape: 'arrowUp' }, + { time: data[data.length - 30].time, position: 'belowBar', color: 'yellow', shape: 'arrowUp' }, + { time: data[data.length - 30].time, position: 'belowBar', color: 'red', shape: 'arrowUp' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'orange', shape: 'arrowDown' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'yellow', shape: 'arrowDown' }, + { time: data[data.length - 20].time, position: 'aboveBar', color: 'red', shape: 'arrowDown' }, + { time: data[data.length - 10].time, position: 'inBar', color: 'orange', shape: 'arrowUp' }, + { time: data[data.length - 10].time, position: 'inBar', color: 'red', shape: 'arrowDown' }, + ] + ); const candleSeries = chart.addCandlestickSeries({ visible: false, diff --git a/tests/e2e/interactions/test-cases/markers/text-hit-test.js b/tests/e2e/interactions/test-cases/markers/text-hit-test.js index ba30f7cc87..1a7a4cc43f 100644 --- a/tests/e2e/interactions/test-cases/markers/text-hit-test.js +++ b/tests/e2e/interactions/test-cases/markers/text-hit-test.js @@ -47,17 +47,21 @@ function beforeInteractions(container) { const mainSeriesData = generateData(); const markerTime = mainSeriesData[450].time; const price = mainSeriesData[450].value; + mainSeries.setData(mainSeriesData); - mainSeries.setMarkers([ - { - time: markerTime, - position: 'belowBar', - color: '#2196F3', - shape: 'arrowUp', - text: 'This is a Marker', - id: 'TEST', - }, - ]); + LightweightCharts.createSeriesMarkers( + mainSeries, + [ + { + time: markerTime, + position: 'belowBar', + color: '#2196F3', + shape: 'arrowUp', + text: 'This is a Marker', + id: 'TEST', + }, + ] + ); chart.subscribeClick(mouseParams => { if (!mouseParams) { diff --git a/tests/e2e/runner.ts b/tests/e2e/runner.ts index a165018791..45bafc47d5 100644 --- a/tests/e2e/runner.ts +++ b/tests/e2e/runner.ts @@ -66,6 +66,7 @@ export async function runTests( summary.push( `${data.file} - "${data.name}" (${Math.round( data.details.duration_ms + // eslint-disable-next-line @typescript-eslint/no-base-to-string )}ms)\n${error.toString()} ` ); }); diff --git a/tests/type-checks/non-time-based-custom-series.ts b/tests/type-checks/non-time-based-custom-series.ts index a30145a361..2dabbd97d2 100644 --- a/tests/type-checks/non-time-based-custom-series.ts +++ b/tests/type-checks/non-time-based-custom-series.ts @@ -1,9 +1,10 @@ -import { createChartEx, customSeriesDefaultOptions, TextWatermark } from '../../src'; +import { createChartEx, createTextWatermark, customSeriesDefaultOptions } from '../../src'; import { CandlestickData, WhitespaceData } from '../../src/model/data-consumer'; import { Time } from '../../src/model/horz-scale-behavior-time/types'; import { CustomData, CustomSeriesPricePlotValues, ICustomSeriesPaneRenderer, ICustomSeriesPaneView, PaneRendererCustomData } from '../../src/model/icustom-series'; import { IHorzScaleBehavior } from '../../src/model/ihorz-scale-behavior'; import { CustomSeriesOptions } from '../../src/model/series-options'; +import { ITextWatermarkPluginApi } from '../../src/plugins/text-watermark/primitive'; type HorizontalScaleType = number; @@ -107,7 +108,11 @@ if (dataSet) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call dataSet.push({ time: 12 }); -const textWatermark = new TextWatermark({ +createTextWatermark(chart.panes()[1], { lines: [], -}); -chart.panes()[1].attachPrimitive(textWatermark); +}) satisfies ITextWatermarkPluginApi; + +createTextWatermark(chart.panes()[1], { + lines: [], +// @ts-expect-error Time is not the expected Generic here. +}) satisfies ITextWatermarkPluginApi