From c5a84095f838f697ed45c381b58821bf96d61304 Mon Sep 17 00:00:00 2001 From: Mark Silverwood <3482679+SlicedSilver@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:36:38 +0100 Subject: [PATCH] extract watermark into TextWatermark primitive plugin --- .size-limit.js | 10 +- src/api/options/chart-options-defaults.ts | 2 - src/api/options/watermark-options-defaults.ts | 14 -- src/gui/pane-widget.ts | 7 - src/index.ts | 5 + src/model/chart-model.ts | 19 --- src/model/watermark.ts | 93 ------------ src/plugins/text-watermark/options.ts | 93 ++++++++++++ src/plugins/text-watermark/pane-renderer.ts | 118 ++++++++++++++++ src/plugins/text-watermark/pane-view.ts | 54 +++++++ src/plugins/text-watermark/primitive.ts | 98 +++++++++++++ src/plugins/types.ts | 8 ++ src/renderers/watermark-renderer.ts | 133 ------------------ src/tsconfig.composite.json | 1 + src/views/pane/watermark-pane-view.ts | 62 -------- .../coverage/test-cases/chart/watermark.js | 53 ------- .../test-cases/plugins/text-watermark.js | 64 +++++++++ .../graphics/test-cases/api/series-markers.js | 15 +- .../api/subscribe-crosshair-move.js | 30 +++- .../test-cases/applying-options/watermark.js | 44 ++++-- .../test-cases/initial-options/watermark.js | 21 ++- website/docs/migrations/from-v4-to-v5.md | 72 +++++++++- website/tutorials/how_to/.eslintrc.js | 1 + website/tutorials/how_to/watermark-simple.js | 23 +-- website/tutorials/how_to/watermark.mdx | 35 +++-- 25 files changed, 637 insertions(+), 438 deletions(-) delete mode 100644 src/api/options/watermark-options-defaults.ts delete mode 100644 src/model/watermark.ts create mode 100644 src/plugins/text-watermark/options.ts create mode 100644 src/plugins/text-watermark/pane-renderer.ts create mode 100644 src/plugins/text-watermark/pane-view.ts create mode 100644 src/plugins/text-watermark/primitive.ts create mode 100644 src/plugins/types.ts delete mode 100644 src/renderers/watermark-renderer.ts delete mode 100644 src/views/pane/watermark-pane-view.ts delete mode 100644 tests/e2e/coverage/test-cases/chart/watermark.js create mode 100644 tests/e2e/coverage/test-cases/plugins/text-watermark.js diff --git a/.size-limit.js b/.size-limit.js index 7e4e3f3c4a..ac4b0e917e 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -30,7 +30,7 @@ export default [ brotli: true, }, { - name: 'Standalone-ESM', + name: 'ESM Standalone', path: 'dist/lightweight-charts.standalone.production.mjs', limit: '50.00 KB', import: '*', @@ -42,4 +42,12 @@ export default [ limit: '50.00 KB', brotli: true, }, + { + name: 'Plugin: Text Watermark', + path: 'dist/lightweight-charts.production.mjs', + import: '{ TextWatermark }', + ignore: ['fancy-canvas'], + limit: '2.00 KB', + brotli: true, + }, ]; diff --git a/src/api/options/chart-options-defaults.ts b/src/api/options/chart-options-defaults.ts index 10c003e58f..3ef44f5464 100644 --- a/src/api/options/chart-options-defaults.ts +++ b/src/api/options/chart-options-defaults.ts @@ -7,7 +7,6 @@ import { gridOptionsDefaults } from './grid-options-defaults'; import { layoutOptionsDefaults } from './layout-options-defaults'; import { priceScaleOptionsDefaults } from './price-scale-options-defaults'; import { timeScaleOptionsDefaults } from './time-scale-options-defaults'; -import { watermarkOptionsDefaults } from './watermark-options-defaults'; export function chartOptionsDefaults(): ChartOptionsInternal { return { @@ -29,7 +28,6 @@ export function chartOptionsDefaults(): ChartOptionsInternal implements IDestroyable, IChartModelBase private readonly _panes: Pane[] = []; private readonly _crosshair: Crosshair; private readonly _magnet: Magnet; - private readonly _watermark: Watermark; private _serieses: Series[] = []; @@ -486,7 +473,6 @@ export class ChartModel implements IDestroyable, IChartModelBase this._timeScale = new TimeScale(this, options.timeScale, this._options.localization, horzScaleBehavior); this._crosshair = new Crosshair(this, options.crosshair); this._magnet = new Magnet(options.crosshair); - this._watermark = new Watermark(this, options.watermark); this._getOrCreatePane(0); this._panes[0].setStretchFactor(DEFAULT_STRETCH_FACTOR * 2); @@ -604,10 +590,6 @@ export class ChartModel implements IDestroyable, IChartModelBase return this._panes; } - public watermarkSource(): Watermark { - return this._watermark; - } - public crosshairSource(): Crosshair { return this._crosshair; } @@ -887,7 +869,6 @@ export class ChartModel implements IDestroyable, IChartModelBase } public recalculateAllPanes(): void { - this._watermark.updateAllViews(); this._panes.forEach((p: Pane) => p.recalculate()); this.updateCrosshair(); } diff --git a/src/model/watermark.ts b/src/model/watermark.ts deleted file mode 100644 index 0cb88ca98e..0000000000 --- a/src/model/watermark.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { HorzAlign, VertAlign } from '../renderers/watermark-renderer'; -import { IPaneView } from '../views/pane/ipane-view'; -import { WatermarkPaneView } from '../views/pane/watermark-pane-view'; -import { IPriceAxisView } from '../views/price-axis/iprice-axis-view'; - -import { IChartModelBase } from './chart-model'; -import { DataSource } from './data-source'; - -/** Watermark options. */ -export interface WatermarkOptions { - /** - * Watermark color. - * - * @defaultValue `'rgba(0, 0, 0, 0)'` - */ - color: string; - - /** - * Display the watermark. - * - * @defaultValue `false` - */ - visible: boolean; - - /** - * Text of the watermark. Word wrapping is not supported. - * - * @defaultValue `''` - */ - text: string; - - /** - * Font size in pixels. - * - * @defaultValue `48` - */ - fontSize: number; - - /** - * Font family. - * - * @defaultValue `-apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif` - */ - fontFamily: string; - - /** - * Font style. - * - * @defaultValue `''` - */ - fontStyle: string; - - /** - * Horizontal alignment inside the chart area. - * - * @defaultValue `'center'` - */ - horzAlign: HorzAlign; - - /** - * Vertical alignment inside the chart area. - * - * @defaultValue `'center'` - */ - vertAlign: VertAlign; -} - -export class Watermark extends DataSource { - private readonly _paneView: WatermarkPaneView; - private readonly _options: WatermarkOptions; - - public constructor(model: IChartModelBase, options: WatermarkOptions) { - super(); - this._options = options; - this._paneView = new WatermarkPaneView(this); - } - - public override priceAxisViews(): readonly IPriceAxisView[] { - return []; - } - - public paneViews(): readonly IPaneView[] { - return [this._paneView]; - } - - public options(): Readonly { - return this._options; - } - - public updateAllViews(): void { - this._paneView.update(); - } -} diff --git a/src/plugins/text-watermark/options.ts b/src/plugins/text-watermark/options.ts new file mode 100644 index 0000000000..d35cc26d63 --- /dev/null +++ b/src/plugins/text-watermark/options.ts @@ -0,0 +1,93 @@ +import { defaultFontFamily } from '../../helpers/make-font'; + +import { HorzAlign, VertAlign } from '../types'; + +export interface TextWatermarkOptions { + /** + * Display the watermark. + * + * @defaultValue `true` + */ + visible: boolean; + + /** + * Horizontal alignment inside the chart area. + * + * @defaultValue `'center'` + */ + horzAlign: HorzAlign; + + /** + * Vertical alignment inside the chart area. + * + * @defaultValue `'center'` + */ + vertAlign: VertAlign; + + /** + * Text to be displayed within the watermark. Each item + * in the array is treated as new line. + * + * @defaultValue `[]` + */ + lines: TextWatermarkLineOptions[]; +} + +export interface TextWatermarkLineOptions { + /** + * Watermark color. + * + * @defaultValue `'rgba(0, 0, 0, 0.5)'` + */ + + color: string; + /** + * Text of the watermark. Word wrapping is not supported. + * + * @defaultValue `''` + */ + text: string; + + /** + * Font size in pixels. + * + * @defaultValue `48` + */ + fontSize: number; + + /** + * Line height in pixels. + * + * @defaultValue `1.2 * fontSize` + */ + lineHeight?: number; + + /** + * Font family. + * + * @defaultValue `-apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif` + */ + fontFamily: string; + + /** + * Font style. + * + * @defaultValue `''` + */ + fontStyle: string; +} + +export const textWatermarkOptionsDefaults: TextWatermarkOptions = { + visible: true, + horzAlign: 'center', + vertAlign: 'center', + lines: [], +}; + +export const textWatermarkLineOptionsDefaults: TextWatermarkLineOptions = { + color: 'rgba(0, 0, 0, 0.5)', + fontSize: 48, + fontFamily: defaultFontFamily, + fontStyle: '', + text: '', +}; diff --git a/src/plugins/text-watermark/pane-renderer.ts b/src/plugins/text-watermark/pane-renderer.ts new file mode 100644 index 0000000000..186089f5a7 --- /dev/null +++ b/src/plugins/text-watermark/pane-renderer.ts @@ -0,0 +1,118 @@ +import { + CanvasRenderingTarget2D, + MediaCoordinatesRenderingScope, +} from 'fancy-canvas'; + +import { IPrimitivePaneRenderer } from '../../model/ipane-primitive'; + +import { TextWatermarkLineOptions, TextWatermarkOptions } from './options'; + +export interface TextWatermarkLineRendererOptions + extends TextWatermarkLineOptions { + zoom: number; + vertOffset: number; + font: string; + lineHeight: number; +} + +export interface TextWatermarkRendererOptions extends TextWatermarkOptions { + lines: TextWatermarkLineRendererOptions[]; +} + +export class TextWatermarkRenderer implements IPrimitivePaneRenderer { + private _data: TextWatermarkRendererOptions; + private _metricsCache: Map> = new Map(); + + public constructor(options: TextWatermarkRendererOptions) { + this._data = options; + } + + public draw(target: CanvasRenderingTarget2D): void { + target.useMediaCoordinateSpace((scope: MediaCoordinatesRenderingScope) => { + const { context: ctx, mediaSize } = scope; + + let textHeight = 0; + for (const line of this._data.lines) { + if (line.text.length === 0) { + continue; + } + + ctx.font = line.font; + const textWidth = this._metrics(ctx, line.text); + if (textWidth > mediaSize.width) { + line.zoom = mediaSize.width / textWidth; + } else { + line.zoom = 1; + } + + textHeight += line.lineHeight * line.zoom; + } + + let vertOffset = 0; + switch (this._data.vertAlign) { + case 'top': + vertOffset = 0; + break; + + case 'center': + vertOffset = Math.max((mediaSize.height - textHeight) / 2, 0); + break; + + case 'bottom': + vertOffset = Math.max(mediaSize.height - textHeight, 0); + break; + } + + for (const line of this._data.lines) { + ctx.save(); + ctx.fillStyle = line.color; + let horzOffset = 0; + switch (this._data.horzAlign) { + case 'left': + ctx.textAlign = 'left'; + horzOffset = line.lineHeight / 2; + break; + + case 'center': + ctx.textAlign = 'center'; + horzOffset = mediaSize.width / 2; + break; + + case 'right': + ctx.textAlign = 'right'; + horzOffset = mediaSize.width - 1 - line.lineHeight / 2; + break; + } + + ctx.translate(horzOffset, vertOffset); + ctx.textBaseline = 'top'; + ctx.font = line.font; + ctx.scale(line.zoom, line.zoom); + ctx.fillText(line.text, 0, line.vertOffset); + ctx.restore(); + vertOffset += line.lineHeight * line.zoom; + } + }); + } + + private _metrics(ctx: CanvasRenderingContext2D, text: string): number { + const fontCache = this._fontCache(ctx.font); + let result = fontCache.get(text); + if (result === undefined) { + result = ctx.measureText(text).width; + fontCache.set(text, result); + } + + return result; + } + + private _fontCache(font: string): Map { + let fontCache = this._metricsCache.get(font); + if (fontCache === undefined) { + fontCache = new Map(); + this._metricsCache.set(font, fontCache); + } + + return fontCache; + } +} diff --git a/src/plugins/text-watermark/pane-view.ts b/src/plugins/text-watermark/pane-view.ts new file mode 100644 index 0000000000..46ec02e96d --- /dev/null +++ b/src/plugins/text-watermark/pane-view.ts @@ -0,0 +1,54 @@ +import { makeFont } from '../../helpers/make-font'; + +import { + IPrimitivePaneRenderer, + IPrimitivePaneView, +} from '../../model/ipane-primitive'; + +import { TextWatermarkLineOptions, TextWatermarkOptions } from './options'; +import { + TextWatermarkLineRendererOptions, + TextWatermarkRenderer, + TextWatermarkRendererOptions, +} from './pane-renderer'; + +export class TextWatermarkPaneView implements IPrimitivePaneView { + private _options: TextWatermarkRendererOptions; + + public constructor(options: TextWatermarkOptions) { + this._options = buildRendererOptions(options); + } + + public update(options: TextWatermarkOptions): void { + this._options = buildRendererOptions(options); + } + + public renderer(): IPrimitivePaneRenderer { + return new TextWatermarkRenderer(this._options); + } +} + +function buildRendererLineOptions( + lineOption: TextWatermarkLineOptions +): TextWatermarkLineRendererOptions { + return { + ...lineOption, + font: makeFont( + lineOption.fontSize, + lineOption.fontFamily, + lineOption.fontStyle + ), + lineHeight: lineOption.lineHeight || lineOption.fontSize * 1.2, + vertOffset: 0, + zoom: 0, + }; +} + +function buildRendererOptions( + options: TextWatermarkOptions +): TextWatermarkRendererOptions { + return { + ...options, + lines: options.lines.map(buildRendererLineOptions), + }; +} diff --git a/src/plugins/text-watermark/primitive.ts b/src/plugins/text-watermark/primitive.ts new file mode 100644 index 0000000000..6a8e0e17c6 --- /dev/null +++ b/src/plugins/text-watermark/primitive.ts @@ -0,0 +1,98 @@ +import { + IPanePrimitive, + PaneAttachedParameter, +} from '../../api/ipane-primitive-api'; + +import { DeepPartial } from '../../helpers/strict-type-checks'; + +import { IPanePrimitivePaneView } from '../../model/ipane-primitive'; + +import { + TextWatermarkLineOptions, + textWatermarkLineOptionsDefaults, + TextWatermarkOptions, + textWatermarkOptionsDefaults, +} from './options'; +import { TextWatermarkPaneView } from './pane-view'; + +function mergeLineOptionsWithDefaults( + options: Partial +): TextWatermarkLineOptions { + return { + ...textWatermarkLineOptionsDefaults, + ...options, + }; +} + +function mergeOptionsWithDefaults( + options: DeepPartial +): TextWatermarkOptions { + return { + ...textWatermarkOptionsDefaults, + ...options, + lines: options.lines?.map(mergeLineOptionsWithDefaults) ?? [], + }; +} + +/** + * 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 { + public requestUpdate?: () => void; + private _paneViews: TextWatermarkPaneView[]; + private _options: TextWatermarkOptions; + + public constructor(options: DeepPartial) { + this._options = mergeOptionsWithDefaults(options); + this._paneViews = [new TextWatermarkPaneView(this._options)]; + } + + public updateAllViews(): void { + this._paneViews.forEach((pw: TextWatermarkPaneView) => + pw.update(this._options) + ); + } + + public paneViews(): readonly IPanePrimitivePaneView[] { + return this._paneViews; + } + + public attached({ requestUpdate }: PaneAttachedParameter): void { + this.requestUpdate = requestUpdate; + } + + public detached(): void { + this.requestUpdate = undefined; + } + + public applyOptions(options: DeepPartial): void { + this._options = mergeOptionsWithDefaults({ ...this._options, ...options }); + if (this.requestUpdate) { + this.requestUpdate(); + } + } +} diff --git a/src/plugins/types.ts b/src/plugins/types.ts new file mode 100644 index 0000000000..b6f88ff426 --- /dev/null +++ b/src/plugins/types.ts @@ -0,0 +1,8 @@ +/** + * Represents a horizontal alignment. + */ +export type HorzAlign = 'left' | 'center' | 'right'; +/** + * Represents a vertical alignment. + */ +export type VertAlign = 'top' | 'center' | 'bottom'; diff --git a/src/renderers/watermark-renderer.ts b/src/renderers/watermark-renderer.ts deleted file mode 100644 index 3e43a35259..0000000000 --- a/src/renderers/watermark-renderer.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { MediaCoordinatesRenderingScope } from 'fancy-canvas'; - -import { MediaCoordinatesPaneRenderer } from './media-coordinates-pane-renderer'; - -export interface WatermarkRendererLineData { - text: string; - font: string; - lineHeight: number; - vertOffset: number; - zoom: number; -} - -/** - * Represents a horizontal alignment. - */ -export type HorzAlign = 'left' | 'center' | 'right'; -/** - * Represents a vertical alignment. - */ -export type VertAlign = 'top' | 'center' | 'bottom'; - -export interface WatermarkRendererData { - lines: WatermarkRendererLineData[]; - color: string; - visible: boolean; - horzAlign: HorzAlign; - vertAlign: VertAlign; -} - -export class WatermarkRenderer extends MediaCoordinatesPaneRenderer { - private readonly _data: WatermarkRendererData; - private _metricsCache: Map> = new Map(); - - public constructor(data: WatermarkRendererData) { - super(); - this._data = data; - } - - protected _drawImpl(renderingScope: MediaCoordinatesRenderingScope): void {} - - protected override _drawBackgroundImpl(renderingScope: MediaCoordinatesRenderingScope): void { - if (!this._data.visible) { - return; - } - - const { context: ctx, mediaSize } = renderingScope; - - let textHeight = 0; - for (const line of this._data.lines) { - if (line.text.length === 0) { - continue; - } - - ctx.font = line.font; - const textWidth = this._metrics(ctx, line.text); - if (textWidth > mediaSize.width) { - line.zoom = mediaSize.width / textWidth; - } else { - line.zoom = 1; - } - - textHeight += line.lineHeight * line.zoom; - } - - let vertOffset = 0; - switch (this._data.vertAlign) { - case 'top': - vertOffset = 0; - break; - - case 'center': - vertOffset = Math.max((mediaSize.height - textHeight) / 2, 0); - break; - - case 'bottom': - vertOffset = Math.max((mediaSize.height - textHeight), 0); - break; - } - - ctx.fillStyle = this._data.color; - - for (const line of this._data.lines) { - ctx.save(); - - let horzOffset = 0; - switch (this._data.horzAlign) { - case 'left': - ctx.textAlign = 'left'; - horzOffset = line.lineHeight / 2; - break; - - case 'center': - ctx.textAlign = 'center'; - horzOffset = mediaSize.width / 2; - break; - - case 'right': - ctx.textAlign = 'right'; - horzOffset = mediaSize.width - 1 - line.lineHeight / 2; - break; - } - - ctx.translate(horzOffset, vertOffset); - ctx.textBaseline = 'top'; - ctx.font = line.font; - ctx.scale(line.zoom, line.zoom); - ctx.fillText(line.text, 0, line.vertOffset); - ctx.restore(); - vertOffset += line.lineHeight * line.zoom; - } - } - - private _metrics(ctx: CanvasRenderingContext2D, text: string): number { - const fontCache = this._fontCache(ctx.font); - let result = fontCache.get(text); - if (result === undefined) { - result = ctx.measureText(text).width; - fontCache.set(text, result); - } - - return result; - } - - private _fontCache(font: string): Map { - let fontCache = this._metricsCache.get(font); - if (fontCache === undefined) { - fontCache = new Map(); - this._metricsCache.set(font, fontCache); - } - - return fontCache; - } -} diff --git a/src/tsconfig.composite.json b/src/tsconfig.composite.json index 8ca4c042d1..4906c1a091 100644 --- a/src/tsconfig.composite.json +++ b/src/tsconfig.composite.json @@ -8,6 +8,7 @@ { "path": "./tsconfig.model.json" } ], "include": [ + "./plugins/**/*.ts", "./index.ts", "./standalone.ts" ] diff --git a/src/views/pane/watermark-pane-view.ts b/src/views/pane/watermark-pane-view.ts deleted file mode 100644 index d3d94d1ddd..0000000000 --- a/src/views/pane/watermark-pane-view.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { makeFont } from '../../helpers/make-font'; - -import { Watermark } from '../../model/watermark'; -import { IPaneRenderer } from '../../renderers/ipane-renderer'; -import { WatermarkRenderer, WatermarkRendererData } from '../../renderers/watermark-renderer'; - -import { IUpdatablePaneView } from './iupdatable-pane-view'; - -export class WatermarkPaneView implements IUpdatablePaneView { - private _source: Watermark; - private _invalidated: boolean = true; - - private readonly _rendererData: WatermarkRendererData = { - visible: false, - color: '', - lines: [], - vertAlign: 'center', - horzAlign: 'center', - }; - private readonly _renderer: WatermarkRenderer = new WatermarkRenderer(this._rendererData); - - public constructor(source: Watermark) { - this._source = source; - } - - public update(): void { - this._invalidated = true; - } - - public renderer(): IPaneRenderer { - if (this._invalidated) { - this._updateImpl(); - this._invalidated = false; - } - - return this._renderer; - } - - private _updateImpl(): void { - const options = this._source.options(); - const data = this._rendererData; - data.visible = options.visible; - - if (!data.visible) { - return; - } - - data.color = options.color; - data.horzAlign = options.horzAlign; - data.vertAlign = options.vertAlign; - - data.lines = [ - { - text: options.text, - font: makeFont(options.fontSize, options.fontFamily, options.fontStyle), - lineHeight: options.fontSize * 1.2, - vertOffset: 0, - zoom: 0, - }, - ]; - } -} diff --git a/tests/e2e/coverage/test-cases/chart/watermark.js b/tests/e2e/coverage/test-cases/chart/watermark.js deleted file mode 100644 index 17f5285bf0..0000000000 --- a/tests/e2e/coverage/test-cases/chart/watermark.js +++ /dev/null @@ -1,53 +0,0 @@ -function simpleData() { - return [ - { time: 1663740000, value: 10 }, - { time: 1663750000, value: 20 }, - { time: 1663760000, value: 30 }, - ]; -} - -function interactionsToPerform() { - return []; -} - -let chart; - -function beforeInteractions(container) { - chart = LightweightCharts.createChart(container, { - watermark: { - visible: true, - color: 'red', - text: 'Watermark', - fontSize: 24, - fontStyle: 'italic', - }, - }); - - const mainSeries = chart.addLineSeries(); - - mainSeries.setData(simpleData()); - - return Promise.resolve(); -} - -function afterInteractions() { - chart.applyOptions({ - watermark: { - fontFamily: 'Roboto', - horzAlign: 'left', - vertAlign: 'top', - }, - }); - - return new Promise(resolve => { - requestAnimationFrame(() => { - chart.applyOptions({ - watermark: { - horzAlign: 'right', - vertAlign: 'bottom', - }, - }); - }); - requestAnimationFrame(resolve); - }); -} diff --git a/tests/e2e/coverage/test-cases/plugins/text-watermark.js b/tests/e2e/coverage/test-cases/plugins/text-watermark.js new file mode 100644 index 0000000000..4c16bb78d5 --- /dev/null +++ b/tests/e2e/coverage/test-cases/plugins/text-watermark.js @@ -0,0 +1,64 @@ +function simpleData() { + return [ + { time: 1663740000, value: 10 }, + { time: 1663750000, value: 20 }, + { time: 1663760000, value: 30 }, + ]; +} + +function interactionsToPerform() { + return []; +} + +let chart; +let textWatermark; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addLineSeries(); + + mainSeries.setData(simpleData()); + + textWatermark = new LightweightCharts.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', + }, + ], + }); + + const pane = chart.panes()[0]; + pane.attachPrimitive(textWatermark); + + return Promise.resolve(); +} + +function afterInteractions() { + textWatermark.applyOptions({ + horzAlign: 'left', + vertAlign: 'top', + }); + + return new Promise(resolve => { + requestAnimationFrame(() => { + textWatermark.applyOptions({ + horzAlign: 'right', + vertAlign: 'bottom', + }); + }); + requestAnimationFrame(resolve); + }); +} diff --git a/tests/e2e/graphics/test-cases/api/series-markers.js b/tests/e2e/graphics/test-cases/api/series-markers.js index b06dfe62ab..6880723234 100644 --- a/tests/e2e/graphics/test-cases/api/series-markers.js +++ b/tests/e2e/graphics/test-cases/api/series-markers.js @@ -40,13 +40,16 @@ function runTestCase(container) { series.setMarkers(markers); const seriesApiMarkers = series.markers(); - chart.applyOptions({ - watermark: { - color: 'red', - visible: true, - text: JSON.stringify(seriesApiMarkers[0]), - }, + const textWatermark = new LightweightCharts.TextWatermark({ + lines: [ + { + text: JSON.stringify(seriesApiMarkers[0]), + color: 'red', + }, + ], }); + 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)}`); } 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 0592dd6e64..9e03dda8a5 100644 --- a/tests/e2e/graphics/test-cases/api/subscribe-crosshair-move.js +++ b/tests/e2e/graphics/test-cases/api/subscribe-crosshair-move.js @@ -1,5 +1,7 @@ function runTestCase(container) { - const chart = window.chart = LightweightCharts.createChart(container, { layout: { attributionLogo: false } }); + const chart = (window.chart = LightweightCharts.createChart(container, { + layout: { attributionLogo: false }, + })); const series = chart.addAreaSeries(); series.setData([ { time: '1990-04-24', value: 0 }, @@ -12,15 +14,29 @@ function runTestCase(container) { chart.timeScale().fitContent(); + const textWatermark = new LightweightCharts.TextWatermark({ + lines: [ + { + text: '', + color: 'red', + fontSize: 12, + }, + ], + }); + const pane = chart.panes()[0]; + pane.attachPrimitive(textWatermark); + chart.subscribeCrosshairMove(param => { if (param.time) { const seriesData = param.seriesData.get(series) || {}; - chart.applyOptions({ - watermark: { - color: 'red', - visible: true, - text: `${param.time} - ${seriesData.time}`, - }, + textWatermark.applyOptions({ + lines: [ + { + text: `${param.time} - ${seriesData.time}`, + color: 'red', + fontSize: 12, + }, + ], }); } }); diff --git a/tests/e2e/graphics/test-cases/applying-options/watermark.js b/tests/e2e/graphics/test-cases/applying-options/watermark.js index 4f4dacb688..eddc3b25c5 100644 --- a/tests/e2e/graphics/test-cases/applying-options/watermark.js +++ b/tests/e2e/graphics/test-cases/applying-options/watermark.js @@ -22,26 +22,46 @@ function generateData() { return res; } +let textWatermark; + function runTestCase(container) { - const chart = window.chart = LightweightCharts.createChart(container, { layout: { attributionLogo: false } }); + const chart = (window.chart = LightweightCharts.createChart(container, { + layout: { attributionLogo: false }, + })); const mainSeries = chart.addCandlestickSeries(); mainSeries.setData(generateData()); + textWatermark = new LightweightCharts.TextWatermark({ + horzAlign: 'left', + vertAlign: 'bottom', + lines: [ + { + text: 'Watermark Before', + color: 'rgba(0, 0, 0, 0.5)', + fontSize: 12, + }, + ], + }); + const pane = chart.panes()[0]; + pane.attachPrimitive(textWatermark); + return new Promise(resolve => { setTimeout(() => { - chart.applyOptions({ - watermark: { - visible: true, - fontSize: 24, - horzAlign: 'center', - vertAlign: 'center', - color: 'rgba(171, 71, 188, 0.5)', - text: 'Watermark', - fontFamily: 'Roboto', - fontStyle: 'bold', - }, + textWatermark.applyOptions({ + visible: true, + horzAlign: 'center', + vertAlign: 'center', + lines: [ + { + text: 'Watermark', + color: 'rgba(171, 71, 188, 0.5)', + fontSize: 24, + fontFamily: 'Roboto', + fontStyle: 'bold', + }, + ], }); resolve(); }, 300); diff --git a/tests/e2e/graphics/test-cases/initial-options/watermark.js b/tests/e2e/graphics/test-cases/initial-options/watermark.js index 19c135d485..2cbe45b2a6 100644 --- a/tests/e2e/graphics/test-cases/initial-options/watermark.js +++ b/tests/e2e/graphics/test-cases/initial-options/watermark.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = window.chart = LightweightCharts.createChart(container, { + const chart = (window.chart = LightweightCharts.createChart(container, { watermark: { visible: true, color: 'red', @@ -23,9 +23,24 @@ function runTestCase(container) { fontStyle: 'italic', }, layout: { attributionLogo: false }, - }); + })); const mainSeries = chart.addAreaSeries(); - mainSeries.setData(generateData()); + + const textWatermark = new LightweightCharts.TextWatermark({ + visible: true, + lines: [ + { + color: 'red', + text: 'TradingView Watermark Example', + fontSize: 24, + fontFamily: 'Roboto', + fontStyle: 'italic', + }, + ], + }); + + const pane = chart.panes()[0]; + pane.attachPrimitive(textWatermark); } diff --git a/website/docs/migrations/from-v4-to-v5.md b/website/docs/migrations/from-v4-to-v5.md index 8b887a153b..e2598a53f5 100644 --- a/website/docs/migrations/from-v4-to-v5.md +++ b/website/docs/migrations/from-v4-to-v5.md @@ -1,10 +1,78 @@ # From v4 to v5 -In this document you can find the migration guide from the previous version v4 to v5. +In this document you can find the migration guide from the previous version v4 +to v5. + +## Watermarks + +### Overview of Changes + +In the new version of Lightweight Charts, the watermark feature has undergone significant changes: + +1. **Extraction from Core**: The watermark functionality has been extracted from the core library. +2. **Re-implementation**: It's now re-implemented as a Pane Primitive (plugin) included within the library. +3. **Improved Tree-shaking**: This change makes the feature more tree-shakeable, potentially reducing bundle sizes for users who don't need watermarks. + +If you're currently using the watermark feature, you'll need to make a few adjustments to your code. + +### Accessing the New TextWatermark + +The TextWatermark primitive is now available as follows: + +- **ESM builds**: Import [`TextWatermark`](/api/classes/TextWatermark.md) directly. +- **Standalone script build**: Access via `LightweightCharts.TextWatermark`. + +### Changes in Options + +The options structure for watermarks has been revised: + +1. **Multiple Lines**: The plugin now supports multiple lines of text. +2. **Text Options**: Text-related options are now defined per line within the `lines` property of the options object. + +### Attaching the Watermark + +To use the Primitive, you need to attach it to a Pane: + +1. **Single Pane**: If you're using only one pane, you can easily fetch it using `chart.panes()[0]`. +2. **Multiple Panes**: For charts with multiple panes, you'll need to specify which pane to attach the watermark to. + +### Example: Implementing a Text Watermark + +Here's a comprehensive example demonstrating how to implement a text watermark in the new version: + +```js +const chart = createChart(container, options); +const mainSeries = chart.addLineSeries(); +mainSeries.setData(generateData()); + +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', + }, + ], +}); + +const firstPane = chart.panes()[0]; +firstPane.attachPrimitive(textWatermark); +``` ## Plugin Typings -Some of the plugin types and interfaces have been renamed due to the additional of Pane Primitives. +Some of the plugin types and interfaces have been renamed due to the additional +of Pane Primitives. - `ISeriesPrimitivePaneView` -> `IPrimitivePaneView` - `ISeriesPrimitivePaneRenderer` -> `IPrimitivePaneRenderer` diff --git a/website/tutorials/how_to/.eslintrc.js b/website/tutorials/how_to/.eslintrc.js index e5bae9c40c..ab4f85f00a 100644 --- a/website/tutorials/how_to/.eslintrc.js +++ b/website/tutorials/how_to/.eslintrc.js @@ -3,5 +3,6 @@ module.exports = { document: false, createChart: false, createChartEx: false, + TextWatermark: false, }, }; diff --git a/website/tutorials/how_to/watermark-simple.js b/website/tutorials/how_to/watermark-simple.js index a8bbd80fe7..73e09bfded 100644 --- a/website/tutorials/how_to/watermark-simple.js +++ b/website/tutorials/how_to/watermark-simple.js @@ -13,17 +13,22 @@ const chartOptions = { /** @type {import('lightweight-charts').IChartApi} */ const chart = createChart(document.getElementById('container'), chartOptions); +// remove-line +/** @type {import('lightweight-charts').TextWatermark} */ // highlight-start -chart.applyOptions({ - watermark: { - visible: true, - fontSize: 24, - horzAlign: 'center', - vertAlign: 'center', - color: 'rgba(171, 71, 188, 0.5)', - text: 'Watermark Example', - }, +const textWatermark = new TextWatermark({ + horzAlign: 'center', + vertAlign: 'center', + lines: [ + { + text: 'Watermark Example', + color: 'rgba(171, 71, 188, 0.5)', + fontSize: 24, + }, + ], }); +const firstPane = chart.panes()[0]; +firstPane.attachPrimitive(textWatermark); // highlight-end const lineSeries = chart.addAreaSeries({ diff --git a/website/tutorials/how_to/watermark.mdx b/website/tutorials/how_to/watermark.mdx index cda9d4f57d..a3a1fbb484 100644 --- a/website/tutorials/how_to/watermark.mdx +++ b/website/tutorials/how_to/watermark.mdx @@ -16,31 +16,36 @@ included below. ## Short answer -A simple text watermark can be configured and added by specifying the [`watermark`](/docs/api/interfaces/ChartOptionsBase#watermark) property within -the chart options as follows: +A simple text watermark can be configured and added by using the [`TextWatermark`](/docs/next/api/classes/TextWatermark) pane primitive exported +from the library as follows: ```js -chart.applyOptions({ - watermark: { - visible: true, - fontSize: 24, - horzAlign: 'center', - vertAlign: 'center', - color: 'rgba(171, 71, 188, 0.5)', - text: 'Watermark Example', - }, +import { TextWatermark } from 'lightweight-charts'; + +const textWatermark = new TextWatermark({ + horzAlign: 'center', + vertAlign: 'center', + lines: [ + { + text: 'Watermark Example', + color: 'rgba(171, 71, 188, 0.5)', + fontSize: 24, + }, + ], }); -``` -The options available for the watermark are: [Watermark Options](/docs/api/interfaces/WatermarkOptions). +const firstPane = chart.panes()[0]; +firstPane.attachPrimitive(textWatermark); +``` -To have the watermark appear, you need to set `visible` to `true` and ensure that the `text` property isn't empty. +The options available for the watermark are: [TextWatermark Options](/docs/next/api/interfaces/TextWatermarkOptions). You can see full [working examples](#examples) below. ## Resources -- [Watermark Options](/docs/api/interfaces/WatermarkOptions) +- [`TextWatermark` pane primitive](/docs/next/api/classes/TextWatermark). +- [TextWatermark Options](/docs/next/api/interfaces/TextWatermarkOptions) ## Examples