From 5f89e78fe12e612ee775fa0b258e41b7e8aa9a43 Mon Sep 17 00:00:00 2001 From: Andrii Ovcharenko Date: Mon, 18 Nov 2024 11:35:54 +0100 Subject: [PATCH 1/3] add relative gradient option to the baseline series --- src/model/series-options.ts | 8 +++- src/model/series/baseline-pane-view.ts | 32 +++++++++++--- src/model/series/baseline-series.ts | 2 +- src/renderers/area-renderer.ts | 3 +- src/renderers/baseline-renderer-area.ts | 5 ++- src/renderers/baseline-renderer-line.ts | 5 ++- src/renderers/gradient-style-cache.ts | 27 ++++++++---- .../series/baseline-relative-gradient.js | 43 +++++++++++++++++++ 8 files changed, 104 insertions(+), 21 deletions(-) create mode 100644 tests/e2e/graphics/test-cases/series/baseline-relative-gradient.js diff --git a/src/model/series-options.ts b/src/model/series-options.ts index ea3fa9c211..09f75e6251 100644 --- a/src/model/series-options.ts +++ b/src/model/series-options.ts @@ -375,7 +375,13 @@ export interface BaselineStyleOptions { * @defaultValue `{ type: 'price', price: 0 }` */ baseValue: BaseValueType; - + /** + * Gradient is relative to the base value and the currently visible range. + * If it is false, the gradient is relative to the top and bottom of the chart. + * + * @defaultValue `false` + */ + relativeGradient: boolean; /** * The first color of the top area. * diff --git a/src/model/series/baseline-pane-view.ts b/src/model/series/baseline-pane-view.ts index b8b92ade17..4432e26969 100644 --- a/src/model/series/baseline-pane-view.ts +++ b/src/model/series/baseline-pane-view.ts @@ -33,34 +33,52 @@ export class SeriesBaselinePaneView extends LinePaneViewBase<'Baseline', Baselin } const options = this._series.options(); - const baseLevelCoordinate = this._series.priceScale().priceToCoordinate(options.baseValue.price, firstValue.value); const barWidth = this._model.timeScale().barSpacing(); + if (this._itemsVisibleRange === null || this._items.length === 0) { + return; + } + let topCoordinate; + let bottomCoordinate; + + if (options.relativeGradient) { + topCoordinate = this._items[this._itemsVisibleRange.from].y; + bottomCoordinate = this._items[this._itemsVisibleRange.from].y; + + for (let i = this._itemsVisibleRange.from; i < this._itemsVisibleRange.to; i++) { + const item = this._items[i]; + if (item.y < topCoordinate) { + topCoordinate = item.y; + } + if (item.y > bottomCoordinate) { + bottomCoordinate = item.y; + } + } + } + this._baselineAreaRenderer.setData({ items: this._items, - lineWidth: options.lineWidth, lineStyle: options.lineStyle, lineType: options.lineType, - baseLevelCoordinate, + topCoordinate, + bottomCoordinate, invertFilledArea: false, - visibleRange: this._itemsVisibleRange, barWidth, }); this._baselineLineRenderer.setData({ items: this._items, - lineWidth: options.lineWidth, lineStyle: options.lineStyle, lineType: options.lineVisible ? options.lineType : undefined, pointMarkersRadius: options.pointMarkersVisible ? (options.pointMarkersRadius || options.lineWidth / 2 + 2) : undefined, - baseLevelCoordinate, - + topCoordinate, + bottomCoordinate, visibleRange: this._itemsVisibleRange, barWidth, }); diff --git a/src/model/series/baseline-series.ts b/src/model/series/baseline-series.ts index 430e5cbf78..1b0dc05e14 100644 --- a/src/model/series/baseline-series.ts +++ b/src/model/series/baseline-series.ts @@ -12,7 +12,7 @@ export const baselineStyleDefaults: BaselineStyleOptions = { type: 'price', price: 0, }, - + relativeGradient: false, topFillColor1: 'rgba(38, 166, 154, 0.28)', topFillColor2: 'rgba(38, 166, 154, 0.05)', topLineColor: 'rgba(38, 166, 154, 1)', diff --git a/src/renderers/area-renderer.ts b/src/renderers/area-renderer.ts index 62f82eeda4..69e10c1975 100644 --- a/src/renderers/area-renderer.ts +++ b/src/renderers/area-renderer.ts @@ -21,7 +21,8 @@ export class PaneRendererArea extends PaneRendererAreaBase topColor2: '', bottomColor1: '', bottomColor2: item.bottomColor, - bottom: renderingScope.bitmapSize.height as Coordinate, + topCoordinate: 0 as Coordinate, + bottomCoordinate: renderingScope.bitmapSize.height as Coordinate, } ); } diff --git a/src/renderers/baseline-renderer-area.ts b/src/renderers/baseline-renderer-area.ts index 369bbc8ca0..84ca398123 100644 --- a/src/renderers/baseline-renderer-area.ts +++ b/src/renderers/baseline-renderer-area.ts @@ -8,6 +8,8 @@ import { GradientStyleCache } from './gradient-style-cache'; export type BaselineFillItem = AreaFillItemBase & BaselineFillColorerStyle; export interface PaneRendererBaselineData extends PaneRendererAreaDataBase { + topCoordinate?: Coordinate; + bottomCoordinate?: Coordinate; } export class PaneRendererBaselineArea extends PaneRendererAreaBase { private readonly _fillCache: GradientStyleCache = new GradientStyleCache(); @@ -23,8 +25,9 @@ export class PaneRendererBaselineArea extends PaneRendererAreaBase { baseLevelCoordinate: Coordinate; + topCoordinate?: Coordinate; + bottomCoordinate?: Coordinate; } export class PaneRendererBaselineLine extends PaneRendererLineBase { @@ -25,8 +27,9 @@ export class PaneRendererBaselineLine extends PaneRendererLineBase 4) { + target.topFillColor1 = 'red'; + target.topFillColor2 = 'rgba(255, 0, 0, 0)'; + } + + if ((i % 10) > 6) { + target.bottomFillColor1 = 'yellow'; + target.bottomFillColor2 = 'rgba(255, 255, 0, 0)'; + } + + if ((i % 10) > 5) { + target.topLineColor = 'blue'; + target.bottomLineColor = 'green'; + } +} + +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 500; ++i) { + const item = { + time: time.getTime() / 1000, + }; + time.setUTCDate(time.getUTCDate() + 1); + + generateBar(i, item); + res.push(item); + } + return res; +} + +function runTestCase(container) { + const chart = window.chart = LightweightCharts.createChart(container, { layout: { attributionLogo: false } }); + + const mainSeries = chart.addSeries(LightweightCharts.BaselineSeries, { baseValue: { type: 'price', price: 88 }, relativeGradient: true }); + + mainSeries.setData(generateData()); +} From 178d9a2271ec3f07108142713c0c7f8eb9e40bf3 Mon Sep 17 00:00:00 2001 From: Andrii Ovcharenko Date: Mon, 18 Nov 2024 13:01:00 +0100 Subject: [PATCH 2/3] update size-limit --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index 29f97caba3..239e6da1f8 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -79,7 +79,7 @@ export default [ path: 'dist/lightweight-charts.production.mjs', import: '{ BaselineSeries }', ignore: ['fancy-canvas'], - limit: '3.2 KB', + limit: '3.3 KB', brotli: true, }, { From b5d78213a54e2cd6b64beb432c7d3fb25a7a9765 Mon Sep 17 00:00:00 2001 From: Andrii Ovcharenko Date: Thu, 21 Nov 2024 10:54:59 +0100 Subject: [PATCH 3/3] update Area series --- .size-limit.js | 4 ++-- src/model/series-options.ts | 8 +++++++ src/model/series/area-pane-view.ts | 15 ++++++++++++ src/model/series/area-series.ts | 1 + src/renderers/area-renderer.ts | 3 ++- .../series/area-relative-gradient-inverted.js | 24 +++++++++++++++++++ .../series/area-relative-gradient.js | 23 ++++++++++++++++++ 7 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 tests/e2e/graphics/test-cases/series/area-relative-gradient-inverted.js create mode 100644 tests/e2e/graphics/test-cases/series/area-relative-gradient.js diff --git a/.size-limit.js b/.size-limit.js index 239e6da1f8..1cb59e3cfc 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -79,7 +79,7 @@ export default [ path: 'dist/lightweight-charts.production.mjs', import: '{ BaselineSeries }', ignore: ['fancy-canvas'], - limit: '3.3 KB', + limit: '4.00 KB', brotli: true, }, { @@ -87,7 +87,7 @@ export default [ path: 'dist/lightweight-charts.production.mjs', import: '{ AreaSeries }', ignore: ['fancy-canvas'], - limit: '3.2 KB', + limit: '4.00 KB', brotli: true, }, { diff --git a/src/model/series-options.ts b/src/model/series-options.ts index 09f75e6251..ccb809a413 100644 --- a/src/model/series-options.ts +++ b/src/model/series-options.ts @@ -251,6 +251,14 @@ export interface AreaStyleOptions { */ bottomColor: string; + /** + * Gradient is relative to the base value and the currently visible range. + * If it is false, the gradient is relative to the top and bottom of the chart. + * + * @defaultValue `false` + */ + relativeGradient: boolean; + /** * Invert the filled area. Fills the area above the line if set to true. * diff --git a/src/model/series/area-pane-view.ts b/src/model/series/area-pane-view.ts index 8a4e8a8552..edeacd9483 100644 --- a/src/model/series/area-pane-view.ts +++ b/src/model/series/area-pane-view.ts @@ -28,13 +28,28 @@ export class SeriesAreaPaneView extends LinePaneViewBase<'Area', AreaFillItem & protected _prepareRendererData(): void { const options = this._series.options(); + if (this._itemsVisibleRange === null || this._items.length === 0) { + return; + } + let topCoordinate; + if (options.relativeGradient) { + topCoordinate = this._items[this._itemsVisibleRange.from].y; + + for (let i = this._itemsVisibleRange.from; i < this._itemsVisibleRange.to; i++) { + const item = this._items[i]; + if (item.y < topCoordinate) { + topCoordinate = item.y; + } + } + } this._areaRenderer.setData({ lineType: options.lineType, items: this._items, lineStyle: options.lineStyle, lineWidth: options.lineWidth, baseLevelCoordinate: null, + topCoordinate, invertFilledArea: options.invertFilledArea, visibleRange: this._itemsVisibleRange, barWidth: this._model.timeScale().barSpacing(), diff --git a/src/model/series/area-series.ts b/src/model/series/area-series.ts index 7cf8d96587..f6dd72005e 100644 --- a/src/model/series/area-series.ts +++ b/src/model/series/area-series.ts @@ -11,6 +11,7 @@ export const areaStyleDefaults: AreaStyleOptions = { topColor: 'rgba( 46, 220, 135, 0.4)', bottomColor: 'rgba( 40, 221, 100, 0)', invertFilledArea: false, + relativeGradient: false, lineColor: '#33D778', lineStyle: LineStyle.Solid, lineWidth: 3, diff --git a/src/renderers/area-renderer.ts b/src/renderers/area-renderer.ts index 69e10c1975..8382963d71 100644 --- a/src/renderers/area-renderer.ts +++ b/src/renderers/area-renderer.ts @@ -8,6 +8,7 @@ import { GradientStyleCache } from './gradient-style-cache'; export type AreaFillItem = AreaFillItemBase & AreaFillColorerStyle; export interface PaneRendererAreaData extends PaneRendererAreaDataBase { + topCoordinate?: Coordinate; } export class PaneRendererArea extends PaneRendererAreaBase { @@ -21,7 +22,7 @@ export class PaneRendererArea extends PaneRendererAreaBase topColor2: '', bottomColor1: '', bottomColor2: item.bottomColor, - topCoordinate: 0 as Coordinate, + topCoordinate: this._data?.topCoordinate ?? 0 as Coordinate, bottomCoordinate: renderingScope.bitmapSize.height as Coordinate, } ); diff --git a/tests/e2e/graphics/test-cases/series/area-relative-gradient-inverted.js b/tests/e2e/graphics/test-cases/series/area-relative-gradient-inverted.js new file mode 100644 index 0000000000..65916c1f01 --- /dev/null +++ b/tests/e2e/graphics/test-cases/series/area-relative-gradient-inverted.js @@ -0,0 +1,24 @@ +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 500; ++i) { + res.push({ + time: time.getTime() / 1000, + value: i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function runTestCase(container) { + const chart = window.chart = LightweightCharts.createChart(container, { layout: { attributionLogo: false } }); + + const mainSeries = chart.addSeries(LightweightCharts.AreaSeries, { + relativeGradient: true, + invertFilledArea: true, + }); + + mainSeries.setData(generateData()); +} diff --git a/tests/e2e/graphics/test-cases/series/area-relative-gradient.js b/tests/e2e/graphics/test-cases/series/area-relative-gradient.js new file mode 100644 index 0000000000..1c7e01e026 --- /dev/null +++ b/tests/e2e/graphics/test-cases/series/area-relative-gradient.js @@ -0,0 +1,23 @@ +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 500; ++i) { + res.push({ + time: time.getTime() / 1000, + value: i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function runTestCase(container) { + const chart = window.chart = LightweightCharts.createChart(container, { layout: { attributionLogo: false } }); + + const mainSeries = chart.addSeries(LightweightCharts.AreaSeries, { + relativeGradient: true, + }); + + mainSeries.setData(generateData()); +}