From d36211dafbb672cd465ecd90387d02d9d21f63ec Mon Sep 17 00:00:00 2001 From: Anton Vorobev Date: Thu, 30 Nov 2023 16:45:42 +0300 Subject: [PATCH 1/8] fix: fix background drawer optimization --- src/chart/components/chart/chart.component.ts | 2 +- src/chart/drawers/chart-background.drawer.ts | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/chart/components/chart/chart.component.ts b/src/chart/components/chart/chart.component.ts index ca41a0f7..1e41a989 100644 --- a/src/chart/components/chart/chart.component.ts +++ b/src/chart/components/chart/chart.component.ts @@ -117,7 +117,7 @@ export class ChartComponent extends ChartBaseElement { //#region data series drawers this.registerDefaultDataSeriesDrawers(); //#endregion - this.backgroundDrawer = new BackgroundDrawer(backgroundCanvasModel, this.config); + this.backgroundDrawer = new BackgroundDrawer(backgroundCanvasModel, this.config, this.canvasBoundsContainer); drawingManager.addDrawer(this.backgroundDrawer, 'MAIN_BACKGROUND'); cursorHandler.setCursorForCanvasEl(CanvasElement.PANE_UUID(CHART_UUID), config.components.chart.cursor); diff --git a/src/chart/drawers/chart-background.drawer.ts b/src/chart/drawers/chart-background.drawer.ts index dbc66d93..edc27ff8 100644 --- a/src/chart/drawers/chart-background.drawer.ts +++ b/src/chart/drawers/chart-background.drawer.ts @@ -9,20 +9,23 @@ import { ChartAreaTheme, FullChartConfig } from '../chart.config'; import { getDPR } from '../utils/device/device-pixel-ratio.utils'; import { deepEqual } from '../utils/object.utils'; import { floor } from '../utils/math.utils'; +import { CanvasBoundsContainer } from '../canvas/canvas-bounds-container'; export class BackgroundDrawer implements Drawer { - constructor(private canvasModel: CanvasModel, private config: FullChartConfig) {} + constructor( + private canvasModel: CanvasModel, + private config: FullChartConfig, + private canvasBoundsContainer: CanvasBoundsContainer, + ) {} // we need to save previous state to avoid unnecessary redraws private prevState: Partial = {}; - private prevWidth = 0; - private prevHeight = 0; + private prevCanvasLocation = {}; draw(): void { if ( deepEqual(this.config.colors.chartAreaTheme, this.prevState) && - this.prevHeight === this.canvasModel.height && - this.prevWidth === this.canvasModel.width + this.prevCanvasLocation === this.canvasBoundsContainer.canvasOnPageLocation ) { return; } @@ -39,8 +42,7 @@ export class BackgroundDrawer implements Drawer { ctx.fillRect(0, 0, this.canvasModel.width, this.canvasModel.height); // save prev state this.prevState = { ...this.config.colors.chartAreaTheme }; - this.prevWidth = this.canvasModel.width; - this.prevHeight = this.canvasModel.height; + this.prevCanvasLocation = this.canvasBoundsContainer.canvasOnPageLocation; } getCanvasIds(): Array { From de496b0f948fbf568980e22c82efdd80fad5e41b Mon Sep 17 00:00:00 2001 From: Anton Vorobev Date: Tue, 5 Dec 2023 10:55:42 +0300 Subject: [PATCH 2/8] fix: fix background drawer optimization --- src/chart/components/chart/chart.component.ts | 2 +- src/chart/drawers/chart-background.drawer.ts | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/chart/components/chart/chart.component.ts b/src/chart/components/chart/chart.component.ts index 1e41a989..ca41a0f7 100644 --- a/src/chart/components/chart/chart.component.ts +++ b/src/chart/components/chart/chart.component.ts @@ -117,7 +117,7 @@ export class ChartComponent extends ChartBaseElement { //#region data series drawers this.registerDefaultDataSeriesDrawers(); //#endregion - this.backgroundDrawer = new BackgroundDrawer(backgroundCanvasModel, this.config, this.canvasBoundsContainer); + this.backgroundDrawer = new BackgroundDrawer(backgroundCanvasModel, this.config); drawingManager.addDrawer(this.backgroundDrawer, 'MAIN_BACKGROUND'); cursorHandler.setCursorForCanvasEl(CanvasElement.PANE_UUID(CHART_UUID), config.components.chart.cursor); diff --git a/src/chart/drawers/chart-background.drawer.ts b/src/chart/drawers/chart-background.drawer.ts index edc27ff8..1a5d3182 100644 --- a/src/chart/drawers/chart-background.drawer.ts +++ b/src/chart/drawers/chart-background.drawer.ts @@ -9,23 +9,18 @@ import { ChartAreaTheme, FullChartConfig } from '../chart.config'; import { getDPR } from '../utils/device/device-pixel-ratio.utils'; import { deepEqual } from '../utils/object.utils'; import { floor } from '../utils/math.utils'; -import { CanvasBoundsContainer } from '../canvas/canvas-bounds-container'; export class BackgroundDrawer implements Drawer { - constructor( - private canvasModel: CanvasModel, - private config: FullChartConfig, - private canvasBoundsContainer: CanvasBoundsContainer, - ) {} + constructor(private canvasModel: CanvasModel, private config: FullChartConfig) {} // we need to save previous state to avoid unnecessary redraws private prevState: Partial = {}; - private prevCanvasLocation = {}; draw(): void { if ( deepEqual(this.config.colors.chartAreaTheme, this.prevState) && - this.prevCanvasLocation === this.canvasBoundsContainer.canvasOnPageLocation + this.canvasModel.height === this.canvasModel.prevHeight && + this.canvasModel.width === this.canvasModel.prevWidth ) { return; } @@ -40,9 +35,8 @@ export class BackgroundDrawer implements Drawer { ctx.fillStyle = this.config.colors.chartAreaTheme.backgroundColor; } ctx.fillRect(0, 0, this.canvasModel.width, this.canvasModel.height); - // save prev state + // save prev color state this.prevState = { ...this.config.colors.chartAreaTheme }; - this.prevCanvasLocation = this.canvasBoundsContainer.canvasOnPageLocation; } getCanvasIds(): Array { From 2e1fc04a8615c762803d043556ccfb39c0cc32a3 Mon Sep 17 00:00:00 2001 From: Anton Vorobev Date: Fri, 8 Dec 2023 18:27:04 +0300 Subject: [PATCH 3/8] fix: fix background drawer optimization --- src/chart/drawers/chart-background.drawer.ts | 74 ++++++++++++++------ 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/src/chart/drawers/chart-background.drawer.ts b/src/chart/drawers/chart-background.drawer.ts index 1a5d3182..4e573421 100644 --- a/src/chart/drawers/chart-background.drawer.ts +++ b/src/chart/drawers/chart-background.drawer.ts @@ -5,38 +5,68 @@ */ import { Drawer } from './drawing-manager'; import { CanvasModel } from '../model/canvas.model'; -import { ChartAreaTheme, FullChartConfig } from '../chart.config'; +import { FullChartConfig } from '../chart.config'; import { getDPR } from '../utils/device/device-pixel-ratio.utils'; -import { deepEqual } from '../utils/object.utils'; import { floor } from '../utils/math.utils'; +import Color from 'color'; export class BackgroundDrawer implements Drawer { constructor(private canvasModel: CanvasModel, private config: FullChartConfig) {} - // we need to save previous state to avoid unnecessary redraws - private prevState: Partial = {}; - draw(): void { - if ( - deepEqual(this.config.colors.chartAreaTheme, this.prevState) && - this.canvasModel.height === this.canvasModel.prevHeight && - this.canvasModel.width === this.canvasModel.prevWidth - ) { - return; - } - this.canvasModel.clear(); const ctx = this.canvasModel.ctx; + const shouldRedraw = this.shouldRedrawBackground(ctx); + + if (shouldRedraw) { + this.canvasModel.clear(); + if (this.config.colors.chartAreaTheme.backgroundMode === 'gradient') { + const grd = ctx.createLinearGradient(0, 0, this.canvasModel.width, this.canvasModel.height); + grd.addColorStop(0, this.config.colors.chartAreaTheme.backgroundGradientTopColor); + grd.addColorStop(1, this.config.colors.chartAreaTheme.backgroundGradientBottomColor); + ctx.fillStyle = grd; + } else { + ctx.fillStyle = this.config.colors.chartAreaTheme.backgroundColor; + } + ctx.fillRect(0, 0, this.canvasModel.width, this.canvasModel.height); + } + } + + shouldRedrawBackground(ctx: CanvasRenderingContext2D): boolean { + // checking the gradient background + // it takes two edge pixels (far left and far right) and compares them with the config values if (this.config.colors.chartAreaTheme.backgroundMode === 'gradient') { - const grd = ctx.createLinearGradient(0, 0, this.canvasModel.width, this.canvasModel.height); - grd.addColorStop(0, this.config.colors.chartAreaTheme.backgroundGradientTopColor); - grd.addColorStop(1, this.config.colors.chartAreaTheme.backgroundGradientBottomColor); - ctx.fillStyle = grd; - } else { - ctx.fillStyle = this.config.colors.chartAreaTheme.backgroundColor; + const dpr = getDPR(); + + const imageDataLeft = ctx.getImageData(1, 1, 1, 1).data; + const rgbaLeft = `rgba(${imageDataLeft[0]}, ${imageDataLeft[1]}, ${imageDataLeft[2]}, ${ + imageDataLeft[3] / 255 + })`; + + const imageDataRight = ctx.getImageData( + this.canvasModel.width * dpr - 1, + this.canvasModel.height * dpr - 1, + 1, + 1, + ).data; + const rgbaRight = `rgba(${imageDataRight[0]}, ${imageDataRight[1]}, ${imageDataRight[2]}, ${ + imageDataRight[3] / 255 + })`; + + if ( + rgbaLeft === this.config.colors.chartAreaTheme.backgroundGradientTopColor && + rgbaRight === this.config.colors.chartAreaTheme.backgroundGradientBottomColor + ) { + return false; + } + } + // checking the regular background when one color is used + if ( + this.config.colors.chartAreaTheme.backgroundMode === 'regular' && + this.canvasModel.ctx.fillStyle === Color(this.config.colors.chartAreaTheme.backgroundColor).hex() + ) { + return false; } - ctx.fillRect(0, 0, this.canvasModel.width, this.canvasModel.height); - // save prev color state - this.prevState = { ...this.config.colors.chartAreaTheme }; + return true; } getCanvasIds(): Array { From f6dd213229c12096ab45a6b6b99f231a2bf60fe6 Mon Sep 17 00:00:00 2001 From: Anton Vorobev Date: Mon, 11 Dec 2023 15:21:18 +0300 Subject: [PATCH 4/8] fix: fix background drawer optimization --- src/chart/bootstrap.ts | 1 + src/chart/components/chart/chart.component.ts | 22 ++++++- src/chart/drawers/chart-background.drawer.ts | 59 +++++-------------- 3 files changed, 35 insertions(+), 47 deletions(-) diff --git a/src/chart/bootstrap.ts b/src/chart/bootstrap.ts index bce2fa93..224f0f02 100755 --- a/src/chart/bootstrap.ts +++ b/src/chart/bootstrap.ts @@ -363,6 +363,7 @@ export default class ChartBootstrap { paneManager, this.cursorHandler, this.dynamicObjects, + this.chartResizeHandler, ); this.chartComponents.push(chartComponent); this.chartComponent = chartComponent; diff --git a/src/chart/components/chart/chart.component.ts b/src/chart/components/chart/chart.component.ts index ca41a0f7..bf5d14fb 100644 --- a/src/chart/components/chart/chart.component.ts +++ b/src/chart/components/chart/chart.component.ts @@ -3,7 +3,7 @@ * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. * If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Observable } from 'rxjs'; +import { Observable, BehaviorSubject } from 'rxjs'; import { CHART_UUID, CanvasBoundsContainer, CanvasElement } from '../../canvas/canvas-bounds-container'; import { CursorHandler } from '../../canvas/cursor.handler'; import { ChartBaseElement } from '../../model/chart-base-element'; @@ -51,6 +51,7 @@ import { CandleWidthCalculator, ChartModel, LastCandleLabelHandler, VisualCandle import { PrependedCandlesData } from './chart-base.model'; import { TrendHistogramDrawer } from '../../drawers/data-series-drawers/trend-histogram.drawer'; import { DynamicObjectsComponent } from '../dynamic-objects/dynamic-objects.component'; +import { ChartResizeHandler } from '../../inputhandlers/chart-resize.handler'; /** * Represents a financial instrument to be displayed on a chart @@ -81,6 +82,7 @@ export class ChartComponent extends ChartBaseElement { private readonly backgroundDrawer: BackgroundDrawer; private readonly _dataSeriesDrawers: Record = {}; private readonly dataSeriesDrawer: DataSeriesDrawer; + private backgroundDrawPredicateSubj = new BehaviorSubject(true); constructor( public readonly chartModel: ChartModel, @@ -96,6 +98,7 @@ export class ChartComponent extends ChartBaseElement { private paneManager: PaneManager, cursorHandler: CursorHandler, private dynamicObjects: DynamicObjectsComponent, + private chartResizeHandler: ChartResizeHandler, ) { super(); this.addChildEntity(this.chartModel); @@ -117,7 +120,11 @@ export class ChartComponent extends ChartBaseElement { //#region data series drawers this.registerDefaultDataSeriesDrawers(); //#endregion - this.backgroundDrawer = new BackgroundDrawer(backgroundCanvasModel, this.config); + this.backgroundDrawer = new BackgroundDrawer( + backgroundCanvasModel, + this.config, + () => this.backgroundDrawPredicateSubj.getValue(), + ); drawingManager.addDrawer(this.backgroundDrawer, 'MAIN_BACKGROUND'); cursorHandler.setCursorForCanvasEl(CanvasElement.PANE_UUID(CHART_UUID), config.components.chart.cursor); @@ -154,6 +161,17 @@ export class ChartComponent extends ChartBaseElement { this.dynamicObjects.model.removeObject(series.id); }), ); + // redraw background only when chart is resized + this.addRxSubscription( + this.canvasBoundsContainer.observeAnyBoundsChanged().subscribe(() => { + this.backgroundDrawPredicateSubj.next(false); + }), + ); + this.addRxSubscription( + this.chartResizeHandler.canvasResized.subscribe(() => { + this.backgroundDrawPredicateSubj.next(true); + }), + ); } /** diff --git a/src/chart/drawers/chart-background.drawer.ts b/src/chart/drawers/chart-background.drawer.ts index 4e573421..309c2db9 100644 --- a/src/chart/drawers/chart-background.drawer.ts +++ b/src/chart/drawers/chart-background.drawer.ts @@ -5,20 +5,25 @@ */ import { Drawer } from './drawing-manager'; import { CanvasModel } from '../model/canvas.model'; -import { FullChartConfig } from '../chart.config'; +import { ChartAreaTheme, FullChartConfig } from '../chart.config'; import { getDPR } from '../utils/device/device-pixel-ratio.utils'; import { floor } from '../utils/math.utils'; -import Color from 'color'; +import { deepEqual } from '../utils/object.utils'; export class BackgroundDrawer implements Drawer { - constructor(private canvasModel: CanvasModel, private config: FullChartConfig) {} + constructor( + private canvasModel: CanvasModel, + private config: FullChartConfig, + private backgroundDrawPredicate: () => boolean = () => true, + ) {} - draw(): void { - const ctx = this.canvasModel.ctx; - const shouldRedraw = this.shouldRedrawBackground(ctx); + // we need to save previous state to avoid unnecessary redraws + private prevState: Partial = {}; - if (shouldRedraw) { + draw(): void { + if (this.backgroundDrawPredicate() || !deepEqual(this.config.colors.chartAreaTheme, this.prevState)) { this.canvasModel.clear(); + const ctx = this.canvasModel.ctx; if (this.config.colors.chartAreaTheme.backgroundMode === 'gradient') { const grd = ctx.createLinearGradient(0, 0, this.canvasModel.width, this.canvasModel.height); grd.addColorStop(0, this.config.colors.chartAreaTheme.backgroundGradientTopColor); @@ -29,44 +34,8 @@ export class BackgroundDrawer implements Drawer { } ctx.fillRect(0, 0, this.canvasModel.width, this.canvasModel.height); } - } - - shouldRedrawBackground(ctx: CanvasRenderingContext2D): boolean { - // checking the gradient background - // it takes two edge pixels (far left and far right) and compares them with the config values - if (this.config.colors.chartAreaTheme.backgroundMode === 'gradient') { - const dpr = getDPR(); - - const imageDataLeft = ctx.getImageData(1, 1, 1, 1).data; - const rgbaLeft = `rgba(${imageDataLeft[0]}, ${imageDataLeft[1]}, ${imageDataLeft[2]}, ${ - imageDataLeft[3] / 255 - })`; - - const imageDataRight = ctx.getImageData( - this.canvasModel.width * dpr - 1, - this.canvasModel.height * dpr - 1, - 1, - 1, - ).data; - const rgbaRight = `rgba(${imageDataRight[0]}, ${imageDataRight[1]}, ${imageDataRight[2]}, ${ - imageDataRight[3] / 255 - })`; - - if ( - rgbaLeft === this.config.colors.chartAreaTheme.backgroundGradientTopColor && - rgbaRight === this.config.colors.chartAreaTheme.backgroundGradientBottomColor - ) { - return false; - } - } - // checking the regular background when one color is used - if ( - this.config.colors.chartAreaTheme.backgroundMode === 'regular' && - this.canvasModel.ctx.fillStyle === Color(this.config.colors.chartAreaTheme.backgroundColor).hex() - ) { - return false; - } - return true; + // save prev state + this.prevState = { ...this.config.colors.chartAreaTheme }; } getCanvasIds(): Array { From 223f5bfb7f157475780ea2b6a1276024c70b1398 Mon Sep 17 00:00:00 2001 From: Sergey Vlasov Date: Thu, 30 Nov 2023 12:56:41 +0200 Subject: [PATCH 5/8] Low label doesn't show up --- .../components/high_low/high-low.drawer.ts | 10 +++- .../y_axis/price_labels/price-label.drawer.ts | 52 +++++++++---------- .../components/y_axis/y-axis-labels.drawer.ts | 45 +++++++++------- .../canvas/canvas-text-functions.utils.ts | 12 +++-- 4 files changed, 70 insertions(+), 49 deletions(-) diff --git a/src/chart/components/high_low/high-low.drawer.ts b/src/chart/components/high_low/high-low.drawer.ts index 9eded9ec..ef7e5620 100644 --- a/src/chart/components/high_low/high-low.drawer.ts +++ b/src/chart/components/high_low/high-low.drawer.ts @@ -9,6 +9,7 @@ import { FullChartConfig } from '../../chart.config'; import { ChartModel } from '../chart/chart.model'; import { CanvasModel } from '../../model/canvas.model'; import { getTextLineHeight } from '../../utils/canvas/canvas-text-functions.utils'; +import { clipToBounds } from '../../drawers/data-series.drawer'; type MarkerType = 'high' | 'low'; @@ -41,7 +42,10 @@ export class HighLowDrawer implements Drawer { ctx.font = this.config.components.highLow.font; this.drawMarkerLabel(ctx, finalHighIdx, high, 'high'); this.drawMarkerLabel(ctx, finalLowIdx, low, 'low'); + const chartBounds = this.canvasBoundsContainer.getBounds('PANE_CHART'); ctx.restore(); + // We need clip here so lowLabel won't overlap other panes + clipToBounds(ctx, chartBounds); } } @@ -55,7 +59,9 @@ export class HighLowDrawer implements Drawer { */ private drawMarkerLabel(ctx: CanvasRenderingContext2D, candleIdx: number, yValue: number, type: MarkerType): void { const y = this.getMarkerY(ctx, yValue, type === 'low'); - if (!this.checkMarkerInBounds(y)) { + const fontSize = getTextLineHeight(ctx, false); + const yForBoundsTrack = type === 'low' ? y - fontSize : y; + if (!this.checkMarkerInBounds(yForBoundsTrack)) { return; } const text = this.getMarkerText(yValue, type); @@ -89,7 +95,7 @@ export class HighLowDrawer implements Drawer { private getMarkerY(ctx: CanvasRenderingContext2D, yValue: number, offset: boolean = false): number { const y = this.chartModel.toY(yValue); if (offset) { - const fontSize = getTextLineHeight(ctx); + const fontSize = getTextLineHeight(ctx, false); return y + fontSize; } return y; diff --git a/src/chart/components/y_axis/price_labels/price-label.drawer.ts b/src/chart/components/y_axis/price_labels/price-label.drawer.ts index 0c2fadb1..4ceebf61 100644 --- a/src/chart/components/y_axis/price_labels/price-label.drawer.ts +++ b/src/chart/components/y_axis/price_labels/price-label.drawer.ts @@ -9,14 +9,14 @@ import { FullChartColors, getFontFromConfig, YAxisAlign, - YAxisLabelAppearanceType, + YAxisLabelAppearanceType, YAxisLabelMode, } from '../../../chart.config'; import { redrawBackgroundArea } from '../../../drawers/chart-background.drawer'; import { Bounds } from '../../../model/bounds.model'; import { avoidAntialiasing, drawLine } from '../../../utils/canvas/canvas-drawing-functions.utils'; import { calculateSymbolHeight, calculateTextWidth } from '../../../utils/canvas/canvas-font-measure-tool.utils'; import { floor } from '../../../utils/math.utils'; -import { drawBadgeLabel, drawPlainLabel, drawRectLabel } from '../y-axis-labels.drawer'; +import { drawBadgeLabel, drawPlainLabel, drawRectLabel, checkLabelInBoundaries } from '../y-axis-labels.drawer'; import { VisualYAxisLabel, YAxisVisualLabelType } from './y-axis-labels.model'; type LabelDrawer = typeof drawBadgeLabel | typeof drawRectLabel | typeof drawPlainLabel; @@ -96,22 +96,30 @@ export function drawLabel( showLine && avoidAntialiasing(ctx, () => drawLine(ctx, lineXStart, lineY, lineXEnd, lineY, 1)); const _drawLabel = () => drawLabel(ctx, bounds, text, centralY, visualLabel, config, colors, false, backgroundCtx); - switch (mode) { - case 'line': - _drawLine(); - _drawDescription(); - break; - case 'line-label': - _drawLine(); - _drawDescription(); - _drawLabel(); - break; - case 'label': - _drawDescription(); - _drawLabel(); - break; - case 'none': - break; + const drawLineLabel = () => { + _drawLine(); + _drawDescription(); + } + + const drawLineLabelLabel = () => { + _drawLine(); + _drawLabel(); + _drawDescription(); + } + + const drawLabelLabel = () => { + _drawDescription(); + _drawLabel(); + } + + const labelDrawerByMode: Record, () => void> = { + 'line': drawLineLabel, + 'line-label': drawLineLabelLabel, + 'label': drawLabelLabel, + } + + if (mode !== 'none' && checkLabelInBoundaries(centralY, bounds, labelBoxHeight)) { + labelDrawerByMode[mode](); } ctx.restore(); @@ -143,14 +151,6 @@ function drawDescription( const labelBoxBottom = centralY + fontHeight / 2 + paddingBottom; const labelBoxHeight = labelBoxBottom - labelBoxY; - // do not draw, if description is out of bounds - if ( - centralY < labelBounds.y + labelBoxHeight / 2 || - centralY > labelBounds.y + labelBounds.height - labelBoxHeight / 2 - ) { - return; - } - ctx.save(); // overlay rect diff --git a/src/chart/components/y_axis/y-axis-labels.drawer.ts b/src/chart/components/y_axis/y-axis-labels.drawer.ts index 1ce95d9f..c9e6490f 100644 --- a/src/chart/components/y_axis/y-axis-labels.drawer.ts +++ b/src/chart/components/y_axis/y-axis-labels.drawer.ts @@ -43,8 +43,9 @@ export const DEFAULT_PRICE_LABEL_PADDING = 4; * @param text - text to draw * @param centralY - y * @param config - label styles config - * @param align * @param yAxisState + * @param yAxisColors + * @param checkBoundaries */ export function drawBadgeLabel( ctx: CanvasRenderingContext2D, @@ -54,7 +55,7 @@ export function drawBadgeLabel( config: YAxisLabelDrawConfig, yAxisState: YAxisConfig, yAxisColors: FullChartColors['yAxis'], - drawOutside: boolean = false, + checkBoundaries: boolean = true, ): void { const align = yAxisState.align; const textFont = config.textFont ?? getFontFromConfig(yAxisState); @@ -71,12 +72,10 @@ export function drawBadgeLabel( const labelBoxHeight = labelBoxBottomY - labelBoxTopY; // do not draw, if label is out of bounds - if ( - (centralY < bounds.y + labelBoxHeight / 2 || centralY > bounds.y + bounds.height - labelBoxHeight / 2) && - !drawOutside - ) { + if (checkBoundaries && !checkLabelInBoundaries(centralY, bounds, labelBoxHeight)) { return; } + ctx.save(); ctx.fillStyle = bgColor; ctx.strokeStyle = bgColor; @@ -119,8 +118,9 @@ export function drawBadgeLabel( * @param text - text to draw * @param centralY - y * @param config - label styles config - * @param align * @param yAxisState + * @param yAxisColors + * @param checkBoundaries */ export function drawRectLabel( ctx: CanvasRenderingContext2D, @@ -130,7 +130,7 @@ export function drawRectLabel( config: YAxisLabelDrawConfig, yAxisState: YAxisConfig, yAxisColors: FullChartColors['yAxis'], - drawOutside: boolean = false, + checkBoundaries: boolean = true, ) { const align = yAxisState.align; const textFont = config.textFont ?? getFontFromConfig(yAxisState); @@ -150,12 +150,10 @@ export function drawRectLabel( const rounded = config.rounded ?? yAxisState.typeConfig.rectangle?.rounded; // do not draw, if label is out of bounds - if ( - (centralY < bounds.y + labelBoxHeight / 2 || centralY > bounds.y + bounds.height - labelBoxHeight / 2) && - !drawOutside - ) { + if (checkBoundaries && !checkLabelInBoundaries(centralY, bounds, labelBoxHeight)) { return; } + ctx.save(); ctx.fillStyle = bgColor; ctx.strokeStyle = bgColor; @@ -188,8 +186,10 @@ export function drawRectLabel( * @param text - text to draw * @param centralY - y * @param config - label styles config - * @param align * @param yAxisState + * @param yAxisColors + * @param checkBoundaries + * @param backgroundCtx */ export function drawPlainLabel( ctx: CanvasRenderingContext2D, @@ -199,7 +199,7 @@ export function drawPlainLabel( config: YAxisLabelDrawConfig, yAxisState: YAxisConfig, yAxisColors: FullChartColors['yAxis'], - drawOutside: boolean = false, + checkBoundaries: boolean = true, backgroundCtx?: CanvasRenderingContext2D, ) { const align = yAxisState.align; @@ -219,12 +219,10 @@ export function drawPlainLabel( const labelBoxHeight = labelBoxBottomY - labelBoxTopY; // do not draw, if label is out of bounds - if ( - (centralY < bounds.y + labelBoxHeight / 2 || centralY > bounds.y + bounds.height - labelBoxHeight / 2) && - !drawOutside - ) { + if (checkBoundaries && !checkLabelInBoundaries(centralY, bounds, labelBoxHeight)) { return; } + ctx.save(); ctx.fillStyle = bgColor; ctx.strokeStyle = bgColor; @@ -261,3 +259,14 @@ export function getLabelYOffset( const fontHeight = calculateSymbolHeight(font, ctx); return fontHeight / 2 + paddingTop; } + +/** + * Checks if label fits in chart scale boundaries + * @param centralY + * @param bounds + * @param labelBoxHeight + * returns true if label fits + */ +export function checkLabelInBoundaries(centralY: number, bounds: Bounds, labelBoxHeight: number) { + return !(centralY < bounds.y + labelBoxHeight / 2 || centralY > bounds.y + bounds.height - labelBoxHeight / 2); +} \ No newline at end of file diff --git a/src/chart/utils/canvas/canvas-text-functions.utils.ts b/src/chart/utils/canvas/canvas-text-functions.utils.ts index ad6038c3..5b5ee627 100644 --- a/src/chart/utils/canvas/canvas-text-functions.utils.ts +++ b/src/chart/utils/canvas/canvas-text-functions.utils.ts @@ -19,6 +19,11 @@ export interface CanvasTextProperties { rtl?: boolean; } +/** + * Baseline Height in Project + */ +const __BASELINE__ = 1.33; + /** * Sets the font, fill style and text alignment of a canvas context based on the provided properties. * @param {CanvasRenderingContext2D} ctx - The canvas context to modify. @@ -47,9 +52,10 @@ export function prepareTextForFill(ctx: CanvasRenderingContext2D, properties: Ca /** * Calculates the line height of a text based on the font size of the provided CanvasRenderingContext2D. * @param {CanvasRenderingContext2D} ctx - The CanvasRenderingContext2D object used to draw the text. + * @param includeBaseLine * @returns {number} The calculated line height of the text. */ -export function getTextLineHeight(ctx: CanvasRenderingContext2D): number { +export function getTextLineHeight(ctx: CanvasRenderingContext2D, includeBaseLine: boolean = true): number { const textSizeMatch = ctx.font.match(/(\d+.)?\d+(px|pt)/gi); let textSize = '10px'; if (textSizeMatch && textSizeMatch.length) { @@ -60,7 +66,7 @@ export function getTextLineHeight(ctx: CanvasRenderingContext2D): number { textSize = textSizeMatch[0]; } } - return parseInt(textSize, 10) * 1.33; // Base Line Height in Project + return includeBaseLine ? parseInt(textSize, 10) * __BASELINE__ : parseInt(textSize, 10); } /** @@ -106,7 +112,7 @@ export function getTextLines(text: string): string[] { * @param {number} y - The y-coordinate of the starting position of the text. * @param {CanvasTextProperties} properties - An object containing properties for the text, such as font size, style, and alignment. * @returns {void} - + */ export function drawText( ctx: CanvasRenderingContext2D, From 935c2e0f462b9d0655dba568c09d43b45ef3b4af Mon Sep 17 00:00:00 2001 From: Sergey Vlasov Date: Mon, 11 Dec 2023 15:38:53 +0200 Subject: [PATCH 6/8] fix: low label display --- .../components/high_low/high-low.drawer.ts | 3 ++- .../highlights/highlights.drawer.ts | 2 +- .../components/volumes/volumes.drawer.ts | 2 +- src/chart/components/y_axis/y-axis.drawer.ts | 2 +- .../candle-series-wrapper.ts | 3 ++- src/chart/drawers/data-series.drawer.ts | 9 +-------- src/chart/drawers/ht-data-series.drawer.ts | 3 ++- .../canvas/canvas-drawing-functions.utils.ts | 19 ++++++++++++++++++- .../canvas/canvas-text-functions.utils.ts | 4 ++-- 9 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/chart/components/high_low/high-low.drawer.ts b/src/chart/components/high_low/high-low.drawer.ts index ef7e5620..88b3b53d 100644 --- a/src/chart/components/high_low/high-low.drawer.ts +++ b/src/chart/components/high_low/high-low.drawer.ts @@ -9,7 +9,7 @@ import { FullChartConfig } from '../../chart.config'; import { ChartModel } from '../chart/chart.model'; import { CanvasModel } from '../../model/canvas.model'; import { getTextLineHeight } from '../../utils/canvas/canvas-text-functions.utils'; -import { clipToBounds } from '../../drawers/data-series.drawer'; +import { clipToBounds } from '../../utils/canvas/canvas-drawing-functions.utils'; type MarkerType = 'high' | 'low'; @@ -60,6 +60,7 @@ export class HighLowDrawer implements Drawer { private drawMarkerLabel(ctx: CanvasRenderingContext2D, candleIdx: number, yValue: number, type: MarkerType): void { const y = this.getMarkerY(ctx, yValue, type === 'low'); const fontSize = getTextLineHeight(ctx, false); + // we need to measure fit into the bounds for low label by its top point const yForBoundsTrack = type === 'low' ? y - fontSize : y; if (!this.checkMarkerInBounds(yForBoundsTrack)) { return; diff --git a/src/chart/components/highlights/highlights.drawer.ts b/src/chart/components/highlights/highlights.drawer.ts index 5706d7f9..f159f316 100644 --- a/src/chart/components/highlights/highlights.drawer.ts +++ b/src/chart/components/highlights/highlights.drawer.ts @@ -12,7 +12,7 @@ import { CanvasBoundsContainer, CanvasElement } from '../../canvas/canvas-bounds import { Drawer } from '../../drawers/drawing-manager'; import { ChartModel } from '../chart/chart.model'; import { unitToPixels } from '../../model/scaling/viewport.model'; -import { clipToBounds } from '../../drawers/data-series.drawer'; +import { clipToBounds } from '../../utils/canvas/canvas-drawing-functions.utils'; const LABEL_PADDINGS = [20, 10]; diff --git a/src/chart/components/volumes/volumes.drawer.ts b/src/chart/components/volumes/volumes.drawer.ts index 4d9e011a..f79d00f3 100644 --- a/src/chart/components/volumes/volumes.drawer.ts +++ b/src/chart/components/volumes/volumes.drawer.ts @@ -4,7 +4,7 @@ * If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { BarType, FullChartConfig } from '../../chart.config'; -import { clipToBounds } from '../../drawers/data-series.drawer'; +import { clipToBounds } from '../../utils/canvas/canvas-drawing-functions.utils'; import { PriceMovement } from '../../model/candle-series.model'; import { CanvasModel } from '../../model/canvas.model'; import { Pixel, ViewportModel, unitToPixels } from '../../model/scaling/viewport.model'; diff --git a/src/chart/components/y_axis/y-axis.drawer.ts b/src/chart/components/y_axis/y-axis.drawer.ts index c72ba426..a5e8554d 100644 --- a/src/chart/components/y_axis/y-axis.drawer.ts +++ b/src/chart/components/y_axis/y-axis.drawer.ts @@ -4,7 +4,7 @@ * If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { FullChartConfig, YAxisAlign, getFontFromConfig } from '../../chart.config'; -import { clipToBounds } from '../../drawers/data-series.drawer'; +import { clipToBounds } from '../../utils/canvas/canvas-drawing-functions.utils'; import { Drawer } from '../../drawers/drawing-manager'; import { Bounds } from '../../model/bounds.model'; import { CanvasModel } from '../../model/canvas.model'; diff --git a/src/chart/drawers/data-series-drawers/candle-series-wrapper.ts b/src/chart/drawers/data-series-drawers/candle-series-wrapper.ts index 5a4b2feb..4ae59599 100644 --- a/src/chart/drawers/data-series-drawers/candle-series-wrapper.ts +++ b/src/chart/drawers/data-series-drawers/candle-series-wrapper.ts @@ -6,7 +6,8 @@ import { BarType, FullChartConfig } from '../../chart.config'; import { BoundsProvider } from '../../model/bounds.model'; import { DataSeriesModel, VisualSeriesPoint } from '../../model/data-series.model'; -import { ChartDrawerConfig, clipToBounds, SeriesDrawer } from '../data-series.drawer'; +import { ChartDrawerConfig, SeriesDrawer } from '../data-series.drawer'; +import { clipToBounds } from '../../utils/canvas/canvas-drawing-functions.utils'; export const candleTypesList: BarType[] = [ 'candle', diff --git a/src/chart/drawers/data-series.drawer.ts b/src/chart/drawers/data-series.drawer.ts index 1118137c..54c0b868 100644 --- a/src/chart/drawers/data-series.drawer.ts +++ b/src/chart/drawers/data-series.drawer.ts @@ -5,9 +5,9 @@ */ import { DynamicModelDrawer } from '../components/dynamic-objects/dynamic-objects.drawer'; import { PaneManager } from '../components/pane/pane-manager.component'; -import { Bounds } from '../model/bounds.model'; import { CanvasModel } from '../model/canvas.model'; import { DataSeriesModel, VisualSeriesPoint } from '../model/data-series.model'; +import { clipToBounds } from '../utils/canvas/canvas-drawing-functions.utils'; export interface ChartDrawerConfig { singleColor?: string; @@ -69,13 +69,6 @@ export class DataSeriesDrawer implements DynamicModelDrawer { } } -export const clipToBounds = (ctx: CanvasRenderingContext2D, bounds: Bounds) => { - ctx.beginPath(); - ctx.rect(bounds.x, bounds.y, bounds.width, bounds.height); - ctx.clip(); - ctx.closePath(); -}; - export const setLineWidth = ( ctx: CanvasRenderingContext2D, lineWidth: number, diff --git a/src/chart/drawers/ht-data-series.drawer.ts b/src/chart/drawers/ht-data-series.drawer.ts index 0fa11d9a..898c5883 100644 --- a/src/chart/drawers/ht-data-series.drawer.ts +++ b/src/chart/drawers/ht-data-series.drawer.ts @@ -6,7 +6,8 @@ import { PaneManager } from '../components/pane/pane-manager.component'; import { DataSeriesModel } from '../model/data-series.model'; import { HitTestCanvasModel } from '../model/hit-test-canvas.model'; -import { ChartDrawerConfig, clipToBounds, SeriesDrawer } from './data-series.drawer'; +import { ChartDrawerConfig, SeriesDrawer } from './data-series.drawer'; +import { clipToBounds } from '../utils/canvas/canvas-drawing-functions.utils'; import { Drawer } from './drawing-manager'; /*** diff --git a/src/chart/utils/canvas/canvas-drawing-functions.utils.ts b/src/chart/utils/canvas/canvas-drawing-functions.utils.ts index b24fb424..0f95f5b2 100644 --- a/src/chart/utils/canvas/canvas-drawing-functions.utils.ts +++ b/src/chart/utils/canvas/canvas-drawing-functions.utils.ts @@ -4,6 +4,7 @@ * If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { YAxisAlign } from '../../chart.config'; +import { Bounds } from '../../model/bounds.model'; /** * Draws a rounded rectangle on a canvas context @@ -171,7 +172,7 @@ export function avoidAntialiasing(ctx: CanvasRenderingContext2D, draw: () => voi * @param {{x: number, y: number}} a - The first point of the rectangle. * @param {{x: number, y: number}} b - The second point of the rectangle. * @returns {void} - + */ export function fillRect( ctx: CanvasRenderingContext2D, @@ -190,3 +191,19 @@ export function fillRect( const h = Math.abs(a.y - b.y); ctx.fillRect(x, y, w, h); } + +/** + * Sets clipping region for a Canvas 2D context according to the provided bounds. + * + * @param {CanvasRenderingContext2D} ctx - The canvas 2D context which will get the new clip region. + * @param {object} bounds - The bounds of the clipping region. Object containing x, y, width and height properties. + * + * @example + * clipToBounds(ctx, {x: 50, y: 50, width: 100, height: 100}); + */ +export const clipToBounds = (ctx: CanvasRenderingContext2D, bounds: Bounds) => { + ctx.beginPath(); + ctx.rect(bounds.x, bounds.y, bounds.width, bounds.height); + ctx.clip(); + ctx.closePath(); +}; diff --git a/src/chart/utils/canvas/canvas-text-functions.utils.ts b/src/chart/utils/canvas/canvas-text-functions.utils.ts index 5b5ee627..1085d460 100644 --- a/src/chart/utils/canvas/canvas-text-functions.utils.ts +++ b/src/chart/utils/canvas/canvas-text-functions.utils.ts @@ -22,7 +22,7 @@ export interface CanvasTextProperties { /** * Baseline Height in Project */ -const __BASELINE__ = 1.33; +const BASELINE = 1.33; /** * Sets the font, fill style and text alignment of a canvas context based on the provided properties. @@ -66,7 +66,7 @@ export function getTextLineHeight(ctx: CanvasRenderingContext2D, includeBaseLine textSize = textSizeMatch[0]; } } - return includeBaseLine ? parseInt(textSize, 10) * __BASELINE__ : parseInt(textSize, 10); + return includeBaseLine ? parseInt(textSize, 10) * BASELINE : parseInt(textSize, 10); } /** From 76142c85c1bdf9bf8be9eafacde57ff9ab900ba1 Mon Sep 17 00:00:00 2001 From: Kirill Bobkov Date: Thu, 30 Nov 2023 04:15:24 +0400 Subject: [PATCH 7/8] feat: Improve X-labels generation time --- package.json | 1 - src/chart/components/chart/chart.component.ts | 8 +- src/chart/components/chart/chart.model.ts | 5 +- .../parser/time-formats-matchers.functions.ts | 2 +- .../x_axis/time/x-axis-weights.functions.ts | 4 +- .../x_axis/time/x-axis-weights.generator.ts | 14 ++-- .../x_axis/x-axis-labels.generator.ts | 80 ++++++++++++++----- .../x_axis/x-axis-time-labels.drawer.ts | 4 - .../components/x_axis/x-axis.component.ts | 17 ++-- .../components/y_axis/y-axis-scale.handler.ts | 2 + src/chart/model/data-series.model.ts | 2 +- src/chart/model/scale.model.ts | 22 +++-- src/chart/model/time-zone.model.ts | 54 ++++--------- src/chart/utils/timezone.utils.ts | 45 +++++++++++ yarn.lock | 10 --- 15 files changed, 161 insertions(+), 109 deletions(-) create mode 100644 src/chart/utils/timezone.utils.ts diff --git a/package.json b/package.json index 5b0dd73f..5bb55fd9 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,6 @@ "dependencies": { "color": "^4.2.3", "date-fns": "^2.30.0", - "date-fns-tz": "^1.3.8", "rxjs": "^7.5.7" }, "peerDependencies": { diff --git a/src/chart/components/chart/chart.component.ts b/src/chart/components/chart/chart.component.ts index bf5d14fb..880fe749 100644 --- a/src/chart/components/chart/chart.component.ts +++ b/src/chart/components/chart/chart.component.ts @@ -289,8 +289,8 @@ export class ChartComponent extends ChartBaseElement { * @param {Timestamp} end - The end timestamp of the range. * @returns {void} */ - public setTimestampRange(start: Timestamp, end: Timestamp): void { - return this.chartModel.setTimestampRange(start, end); + public setTimestampRange(start: Timestamp, end: Timestamp, forceNoAnimation: boolean = true): void { + return this.chartModel.setTimestampRange(start, end, forceNoAnimation); } /** @@ -298,8 +298,8 @@ export class ChartComponent extends ChartBaseElement { * @param xStart - viewport start in units * @param xEnd - viewport end in units */ - public setXScale(xStart: Unit, xEnd: Unit) { - return this.scale.setXScale(xStart, xEnd); + public setXScale(xStart: Unit, xEnd: Unit, forceNoAnimation: boolean = true) { + return this.scale.setXScale(xStart, xEnd, forceNoAnimation); } /** diff --git a/src/chart/components/chart/chart.model.ts b/src/chart/components/chart/chart.model.ts index 74fc8a0f..11c3162c 100755 --- a/src/chart/components/chart/chart.model.ts +++ b/src/chart/components/chart/chart.model.ts @@ -64,7 +64,6 @@ export class ChartModel extends ChartBaseElement { public readonly nextCandleTimeStampSubject: Subject = new Subject(); public readonly chartTypeChanged: Subject = new Subject(); public readonly mainInstrumentChangedSubject: Subject = new Subject(); - public readonly scaleInversedSubject: Subject = new Subject(); public readonly offsetsChanged = new Subject(); private candlesTransformersByChartType: Partial> = {}; lastCandleLabelsByChartType: Partial> = {}; @@ -802,11 +801,11 @@ export class ChartModel extends ChartBaseElement { * @param {Timestamp} end - The end timestamp of the range. * @returns {void} */ - setTimestampRange(start: Timestamp, end: Timestamp): void { + setTimestampRange(start: Timestamp, end: Timestamp, forceNoAnimation: boolean = true): void { const startUnit = this.candleFromTimestamp(start).startUnit; const endCandle = this.candleFromTimestamp(end); const endUnit = endCandle.startUnit + endCandle.width; - return this.scale.setXScale(startUnit, endUnit); + return this.scale.setXScale(startUnit, endUnit, forceNoAnimation); } /** diff --git a/src/chart/components/x_axis/time/parser/time-formats-matchers.functions.ts b/src/chart/components/x_axis/time/parser/time-formats-matchers.functions.ts index bfde0208..24bdfdaa 100644 --- a/src/chart/components/x_axis/time/parser/time-formats-matchers.functions.ts +++ b/src/chart/components/x_axis/time/parser/time-formats-matchers.functions.ts @@ -16,7 +16,7 @@ import { isSunday, startOfMonth, endOfMonth, -} from 'date-fns/esm'; +} from 'date-fns'; import { ParsedCTimeFormat, ParsedNCTimeFormat, diff --git a/src/chart/components/x_axis/time/x-axis-weights.functions.ts b/src/chart/components/x_axis/time/x-axis-weights.functions.ts index 9f2e9eb8..0b7f9a49 100644 --- a/src/chart/components/x_axis/time/x-axis-weights.functions.ts +++ b/src/chart/components/x_axis/time/x-axis-weights.functions.ts @@ -60,14 +60,14 @@ export const getWeightFromTimeFormat = (format: ParsedTimeFormat) => { export const groupLabelsByWeight = (weightedLabels: XAxisLabelWeighted[]): Record => { const labelsGroupedByWeight: Record = {}; - weightedLabels.forEach(weightedLabel => { + for (const weightedLabel of weightedLabels) { const labelsByWeight = labelsGroupedByWeight[weightedLabel.weight]; if (labelsByWeight === undefined) { labelsGroupedByWeight[weightedLabel.weight] = [weightedLabel]; } else { labelsByWeight.push(weightedLabel); } - }); + } return labelsGroupedByWeight; }; diff --git a/src/chart/components/x_axis/time/x-axis-weights.generator.ts b/src/chart/components/x_axis/time/x-axis-weights.generator.ts index 3cb7c8f6..f2278665 100644 --- a/src/chart/components/x_axis/time/x-axis-weights.generator.ts +++ b/src/chart/components/x_axis/time/x-axis-weights.generator.ts @@ -30,12 +30,9 @@ const getWeightByDate = ( currentDate: Date, previousDate: Date, sortedWeights: [number, TimeFormatMatcher][], - tzOffset: (time: number) => Date, ): number => { - const offsetCurrentDate = tzOffset(currentDate.getTime()); - const offsetPrevDate = tzOffset(previousDate.getTime()); for (const [weight, timeMatcher] of sortedWeights) { - if (timeMatcher(offsetCurrentDate, offsetPrevDate)) { + if (timeMatcher(currentDate, previousDate)) { return weight; } } @@ -52,16 +49,15 @@ export function mapCandlesToWeightedPoints( tzOffset: (time: number) => Date, ): WeightedPoint[] { const result: WeightedPoint[] = new Array(visualCandles.length); - // assume that candles` timestamp before the first visual candle is unreachable - let prevDate = new Date(0); + let prevDate = tzOffset(0); for (let i = 0; i < visualCandles.length; i++) { const currentCandle = visualCandles[i]; - const currentDate = new Date(currentCandle.candle.timestamp); - + // calculate timstamp to Date in acordance with provided timezone and time + const currentDate = tzOffset(currentCandle.candle.timestamp); const currentWeightedPoint: WeightedPoint = { - weight: getWeightByDate(currentDate, prevDate, sortedWeights, tzOffset), + weight: getWeightByDate(currentDate, prevDate, sortedWeights), }; result[i] = currentWeightedPoint; prevDate = currentDate; diff --git a/src/chart/components/x_axis/x-axis-labels.generator.ts b/src/chart/components/x_axis/x-axis-labels.generator.ts index ebdb30e7..14defb01 100644 --- a/src/chart/components/x_axis/x-axis-labels.generator.ts +++ b/src/chart/components/x_axis/x-axis-labels.generator.ts @@ -3,6 +3,7 @@ * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. * If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { CanvasBoundsContainer, CanvasElement } from '../../canvas/canvas-bounds-container'; import { FullChartConfig } from '../../chart.config'; import EventBus from '../../events/event-bus'; import { CanvasModel } from '../../model/canvas.model'; @@ -68,9 +69,10 @@ export class XAxisTimeLabelsGenerator implements XAxisLabelsGenerator { private labelsGroupedByWeight: Record = {}; private weightedCache?: { labels: XAxisLabelWeighted[]; coverUpLevel: number }; private levelsCache: Record = {}; + private prevAnimationId = ''; get labels(): XAxisLabelWeighted[] { - return this.getLabelsFromChartType(); + return this.filterLabelsInViewport(this.getLabelsFromChartType()); } private formatsByWeightMap: Record; @@ -89,6 +91,7 @@ export class XAxisTimeLabelsGenerator implements XAxisLabelsGenerator { private scale: ScaleModel, private timeZoneModel: TimeZoneModel, private canvasModel: CanvasModel, + private canvasBoundsContainer: CanvasBoundsContainer, ) { this.formatsByWeightMap = config.components.xAxis.formatsForLabelsConfig; const { weightToTimeFormatsDict, weightToTimeFormatMatcherDict } = generateWeightsMapForConfig( @@ -100,6 +103,21 @@ export class XAxisTimeLabelsGenerator implements XAxisLabelsGenerator { this.weightToTimeFormatsDict = weightToTimeFormatsDict; } + private filterLabelsInViewport(labels: XAxisLabelWeighted[]) { + const bounds = this.canvasBoundsContainer.getBounds(CanvasElement.X_AXIS); + const filteredLabels = []; + + for (const label of labels) { + const x = this.scale.toX(label.value); + // skip labels outside viewport + if (x < 0 || x > bounds.width) { + continue; + } + filteredLabels.push(label); + } + return filteredLabels; + } + private getLabelsFromChartType() { const labels = this.weightedCache?.labels ?? []; //@ts-ignore @@ -143,7 +161,8 @@ export class XAxisTimeLabelsGenerator implements XAxisLabelsGenerator { allCandlesWithFake: VisualCandle[], ): XAxisLabelWeighted[] { const arr = new Array(weightedPoints.length); - weightedPoints.forEach((point, index) => { + for (let index = 0; index < weightedPoints.length; ++index) { + const point = weightedPoints[index]; const visualCandle = allCandlesWithFake[index]; const labelFormat = this.weightToTimeFormatsDict[point.weight]; const formattedLabel = this.timeZoneModel.getDateTimeFormatter(labelFormat)(visualCandle.candle.timestamp); @@ -155,7 +174,7 @@ export class XAxisTimeLabelsGenerator implements XAxisLabelsGenerator { time: visualCandle.candle.timestamp, text: formattedLabel, }; - }); + } return arr; } @@ -334,35 +353,54 @@ export class XAxisTimeLabelsGenerator implements XAxisLabelsGenerator { } /** - * Recalculates cached labels based on the current configuration and zoom level. - * If there are no grouped labels, the cache is not set. - * Calculates the maximum label width based on the font size and the maximum format length. * Calculates the cover up level based on the maximum label width and the mean candle width. - * If the cover up level is negative, the cache is not updated. - * If the cover up level has not changed, the cached labels are returned. - * Otherwise, the labels are filtered by extended rules and grouped by cover up level. - * The filtered labels are then cached and returned. + * Cover up level is based on maximum label width, which is based on the font size and the maximum format length. + * Used to group labels. */ - private recalculateCachedLabels() { - // skip cache setting if we don't have grouped labels + private calculateCoverUpLevel = () => { + const animation = this.scale.currentAnimation; + const meanCandleWidthInUnits = this.chartModel.mainCandleSeries.meanCandleWidth; + let meanCandleWidthInPixels; if (Object.getOwnPropertyNames(this.labelsGroupedByWeight).length === 0) { - return; + return -1; } + // calculate coverUpLevel for target zoomX to prevent extra labels calculations on every animation tick + if (animation?.animationInProgress) { + if (animation.id !== this.prevAnimationId) { + this.prevAnimationId = animation.id; + } + meanCandleWidthInPixels = unitToPixels(meanCandleWidthInUnits, animation.animationConfig.targetZoomX); + } else { + meanCandleWidthInPixels = unitToPixels(meanCandleWidthInUnits, this.scale.zoomX); + } + if (!isFinite(meanCandleWidthInPixels)) { + return -1; + } const fontSize = this.config.components.xAxis.fontSize; - const maxFormatLength = Object.values(this.formatsByWeightMap).reduce((max, item) => { - return Math.max(item.length, max); - }, 1); + const maxFormatLength = Object.values(this.formatsByWeightMap).reduce( + (max, item) => Math.max(item.length, max), + 1, + ); const maxLabelWidth = fontSize * maxFormatLength; - const meanCandleWidthPx = unitToPixels(this.chartModel.mainCandleSeries.meanCandleWidth, this.scale.zoomX); - - const coverUpLevel = Math.round(maxLabelWidth / meanCandleWidthPx); + return Math.round(maxLabelWidth / meanCandleWidthInPixels); + }; + /** + * Recalculates cached labels based on the current configuration and zoom level. + * If there are no grouped labels, the cache is not set. + * Recalculating depends on cover up level. + * If the cover up level is negative, the cache is not updated. + * If the cover up level has not changed, the cached labels are returned. + * Otherwise, the labels are filtered by extended rules and grouped by cover up level. + * The filtered labels are then cached and returned. + */ + private recalculateCachedLabels() { + const coverUpLevel = this.calculateCoverUpLevel(); // for some reason sometimes `this.scale.zoomX` is negative so we dont want to update labels - if (coverUpLevel < 0 && !isFinite(meanCandleWidthPx)) { + if (coverUpLevel < 0) { return; } - if (this.weightedCache === undefined || coverUpLevel !== this.weightedCache.coverUpLevel) { const labelsFromCache = this.getLabelsFromCache(coverUpLevel); if (labelsFromCache) { diff --git a/src/chart/components/x_axis/x-axis-time-labels.drawer.ts b/src/chart/components/x_axis/x-axis-time-labels.drawer.ts index 5acefb91..ac2ffc6b 100644 --- a/src/chart/components/x_axis/x-axis-time-labels.drawer.ts +++ b/src/chart/components/x_axis/x-axis-time-labels.drawer.ts @@ -75,10 +75,6 @@ export class XAxisTimeLabelsDrawer implements Drawer { ctx.fillStyle = color; for (const label of labels) { const x = this.viewportModel.toX(label.value) - calculateTextWidth(label.text, ctx, font) / 2; - // skip labels outside viewport - if (x < 0 || x > bounds.width) { - continue; - } const y = bounds.y + fontHeight - 1 + offsetTop; // -1 for font drawing inconsistency const labelText = label.text; ctx.font = font; diff --git a/src/chart/components/x_axis/x-axis.component.ts b/src/chart/components/x_axis/x-axis.component.ts index 076dd1b7..777dbbfe 100644 --- a/src/chart/components/x_axis/x-axis.component.ts +++ b/src/chart/components/x_axis/x-axis.component.ts @@ -61,6 +61,7 @@ export class XAxisComponent extends ChartBaseElement { scale, timeZoneModel, this.canvasModel, + canvasBoundsContainer, ); this.xAxisLabelsGenerator = xAxisLabelsGenerator; @@ -121,18 +122,14 @@ export class XAxisComponent extends ChartBaseElement { this.chartComponent.chartModel.candlesPrependSubject .pipe( filter(({ prependedCandles }) => prependedCandles.length !== 0), - map(({ prependedCandles }) => { - return this.chartComponent.chartModel.mainCandleSeries.visualPoints.slice( - 0, - prependedCandles.length, - ); - }), + map(({ prependedCandles }) => + this.chartComponent.chartModel.mainCandleSeries.visualPoints.slice(0, prependedCandles.length), + ), ) .subscribe(newCandles => { //@ts-ignore if (availableBarTypes.includes(this.config.components.chart.type)) { - this.xAxisLabelsGenerator.updateHistoryLabels && - this.xAxisLabelsGenerator.updateHistoryLabels(newCandles); + this.xAxisLabelsGenerator.updateHistoryLabels?.(newCandles); } }), ); @@ -152,9 +149,7 @@ export class XAxisComponent extends ChartBaseElement { distinctUntilChanged((a, b) => a?.candle?.timestamp === b?.candle?.timestamp), filter(notEmpty), ) - .subscribe(x => { - this.xAxisLabelsGenerator.updateLastLabel && this.xAxisLabelsGenerator.updateLastLabel(x); - }), + .subscribe(x => this.xAxisLabelsGenerator?.updateLastLabel?.(x)), ); } diff --git a/src/chart/components/y_axis/y-axis-scale.handler.ts b/src/chart/components/y_axis/y-axis-scale.handler.ts index d4d5fc57..7188cd47 100644 --- a/src/chart/components/y_axis/y-axis-scale.handler.ts +++ b/src/chart/components/y_axis/y-axis-scale.handler.ts @@ -69,6 +69,8 @@ export class YAxisScaleHandler extends ChartBaseElement { } private onYDragStart = () => { + // halt previous scale animation if drag is started + this.scale.haltAnimation(); this.lastYStart = this.scale.yStart; this.lastYEnd = this.scale.yEnd; this.lastYHeight = this.scale.yEnd - this.scale.yStart; diff --git a/src/chart/model/data-series.model.ts b/src/chart/model/data-series.model.ts index 2c451fd3..c0eaf592 100644 --- a/src/chart/model/data-series.model.ts +++ b/src/chart/model/data-series.model.ts @@ -147,7 +147,7 @@ export class DataSeriesModel< protected doActivate(): void { this.addRxSubscription(this.scale.xChanged.subscribe(() => this.recalculateDataViewportIndexes())); - this.addRxSubscription(this.scale.scaleInversedSubject.subscribe(() => this.recalculateVisualPoints())); + this.addRxSubscription(this.scale.scaleInversedSubject.subscribe(() => { this.recalculateVisualPoints(); })); } /** diff --git a/src/chart/model/scale.model.ts b/src/chart/model/scale.model.ts index ad5d2fe9..0a40d4fc 100644 --- a/src/chart/model/scale.model.ts +++ b/src/chart/model/scale.model.ts @@ -83,6 +83,14 @@ export class ScaleModel extends ViewportModel { ); } + protected doActivate(): void { + this.addRxSubscription( + this.scaleInversedSubject.subscribe(() => { + this.fireChanged(); + }), + ); + } + /** * The method adds a new "constraint" to the existing list of x-axis constraints for charting. * The "constraint" is expected to be an object containing information about the constraints, such as the minimum and maximum values for the x-axis. @@ -181,10 +189,10 @@ export class ScaleModel extends ViewportModel { * @param fireChanged * @param forceNoAutoScale - force NOT apply auto-scaling (for lazy loading) */ - public setXScale(xStart: Unit, xEnd: Unit) { + public setXScale(xStart: Unit, xEnd: Unit, forceNoAnimation: boolean = true) { const initialState = this.export(); - super.setXScale(xStart, xEnd, false); - const state = this.export(); + const zoomX = this.calculateZoomX(xStart, xEnd); + const state = { ...initialState, zoomX, xStart, xEnd }; const constrainedState = this.scalePostProcessor(initialState, state); if (this.state.lockPriceToBarRatio) { changeYToKeepRatio(constrainedState, this.zoomXYRatio); @@ -192,8 +200,12 @@ export class ScaleModel extends ViewportModel { if (this.state.auto) { this.autoScaleModel.doAutoYScale(constrainedState); } - - this.apply(constrainedState); + if (forceNoAnimation || this.config.scale.disableAnimations) { + this.haltAnimation(); + this.apply(constrainedState); + } else { + startViewportModelAnimation(this.canvasAnimation, this, constrainedState); + } } public setYScale(yStart: Unit, yEnd: Unit, fire = false) { diff --git a/src/chart/model/time-zone.model.ts b/src/chart/model/time-zone.model.ts index 8c4d9147..cedf8a5c 100644 --- a/src/chart/model/time-zone.model.ts +++ b/src/chart/model/time-zone.model.ts @@ -3,12 +3,11 @@ * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. * If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { getTimezoneOffset as getTimezoneOffsetDateFnsTz } from 'date-fns-tz'; import { Observable, ReplaySubject, Subject } from 'rxjs'; import { DateFormatter, FullChartConfig } from '../chart.config'; -import { memoize } from '../utils/performance/memoize.utils'; import { DateTimeFormatter, DateTimeFormatterFactory, dateTimeFormatterFactory } from './date-time.formatter'; import { Timestamp } from './scaling/viewport.model'; +import { getTimezoneOffset } from '../utils/timezone.utils'; export interface TimeZone { readonly timeZone: string; @@ -16,18 +15,17 @@ export interface TimeZone { readonly utcOffset: string; } -export const memoizedTZOffset = memoize(getTimezoneOffsetDateFnsTz); - -export const getTimezoneOffset = (timezone: string, time: Timestamp) => { - // we must pass time here, because we can have daylight saving time, otherwise we will get wrong offset - const localOffset = new Date(time).getTimezoneOffset() * 60 * 1000; - return localOffset + memoizedTZOffset(timezone, time); -}; +// 1 hour in milliseconds +const timeMultiplier = 60 * 1000; export class TimeZoneModel { private timeZoneChangedSubject: Subject = new ReplaySubject(); private dateTimeFormatterFactory: DateTimeFormatterFactory; - public currentTzOffset = (time: Timestamp): number => this.getOffset(this.config.timezone, time); + + public currentTzOffset = (time: Timestamp): number => { + // we must pass time here, because we can have daylight saving time, otherwise we will get wrong offset + return getTimezoneOffset(this.config.timezone, time) + new Date(time).getTimezoneOffset() * timeMultiplier; + }; constructor(private config: FullChartConfig) { this.dateTimeFormatterFactory = this.initFormatterFactory(this.config.dateFormatter); @@ -100,35 +98,17 @@ export class TimeZoneModel { return this.formatterCache[format]; } - /** - * Calculates the offset of a given timezone from the local timezone. - * @private - * @param {string} timezone - The timezone to calculate the offset for. - */ - private getOffset = (timezone: string, time: Timestamp) => getTimezoneOffset(timezone, time); - - /** - * Gets the timezone offset value in milliseconds - * @param {string} timezone name - * @returns {function(time:Number):Date} - */ public tzOffset(timezone: string): (time: number) => Date { - // we must pass time here, because we can have daylight saving time, otherwise we will get wrong offset - const localOffset = (time: Timestamp) => -new Date(time).getTimezoneOffset() * timeMultiplier; - let offset: (time: Timestamp) => number; if (!timezone) { - offset = localOffset; + return time => new Date(time); } else { - // we must pass time here, because we can have daylight saving time, otherwise we will get wrong offset - offset = (time: Timestamp) => memoizedTZOffset(timezone, time); + // In JS Date object is created with local tz offset, + // so we have to subtract localOffset from current time + return time => { + return new Date( + time + getTimezoneOffset(timezone, time) + new Date(time).getTimezoneOffset() * timeMultiplier, + ); + }; } - // In JS Date object is created with local tz offset, - // so we have to subtract localOffset from current time - return time => { - return new Date(+time + offset(time) - localOffset(time)); - }; } -} - -// 1 hour in milliseconds -const timeMultiplier = 60 * 1000; +} \ No newline at end of file diff --git a/src/chart/utils/timezone.utils.ts b/src/chart/utils/timezone.utils.ts new file mode 100644 index 00000000..557b9e0e --- /dev/null +++ b/src/chart/utils/timezone.utils.ts @@ -0,0 +1,45 @@ +/** + * Get a cached Intl.DateTimeFormat instance for the IANA `timeZone`. This can be used + * to get deterministic local date/time output according to the `en-US` locale which + * can be used to extract local time parts as necessary. + * Returns the [year, month, day, hour, minute] tokens of the provided timezone + */ +const dateTimeFormatCache: Record = {}; +export function getDateTimeFormat(timeZone: string) { + if (!dateTimeFormatCache[timeZone]) { + // New browsers use `hourCycle`, IE and Chrome <73 does not support it and uses `hour12` + dateTimeFormatCache[timeZone] = new Intl.DateTimeFormat('en-US', { + hourCycle: 'h23', + timeZone, + year: 'numeric', + month: 'numeric', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); + } + return dateTimeFormatCache[timeZone]; +} + +/** + * Calulates provided timezone offset. + * @param timeZone - timezone as IANA string ('Asia/Amman', 'Europe/Amsterdam') + * @param timestamp - timestamp as number (1701251319717) + * Returns offset from UTC + */ +export const getTimezoneOffset = (timeZone: string, timestamp: number = Date.now()) => { + const formatter = getDateTimeFormat(timeZone); + const dateString = formatter.format(timestamp); + // exec() returns [, fMonth, fDay, fYear, fHour, fMinute] + const tokens = /(\d+)\/(\d+)\/(\d+),? (\d+):(\d+)/.exec(dateString); + + if (tokens) { + const asUTC = Date.UTC(+tokens[3], +tokens[1] - 1, +tokens[2], +tokens[4] % 24, +tokens[5], 0); + let asTimezone = timestamp; + const over = asTimezone % 1000; + asTimezone -= over >= 0 ? over : 1000 + over; + return asUTC - asTimezone; + } + return 0; +}; diff --git a/yarn.lock b/yarn.lock index d5f79ffe..d67fc61e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -648,7 +648,6 @@ __metadata: color: ^4.2.3 commander: ^10.0.1 date-fns: ^2.30.0 - date-fns-tz: ^1.3.8 esbuild: ^0.17.19 esbuild-jest: ^0.5.0 esbuild-loader: ^3.0.1 @@ -3731,15 +3730,6 @@ __metadata: languageName: node linkType: hard -"date-fns-tz@npm:^1.3.8": - version: 1.3.8 - resolution: "date-fns-tz@npm:1.3.8" - peerDependencies: - date-fns: ">=2.0.0" - checksum: dbf17cb88de7c7b71a9f86b815ac62ebdbf725c030e78d2421309456b5001a85284a5b6763f4af0ba660183f12e876c165e725a0723f642edb7846407db03133 - languageName: node - linkType: hard - "date-fns@npm:^2.30.0": version: 2.30.0 resolution: "date-fns@npm:2.30.0" From 056c23397e95acb45f77681595eb00eb1032daad Mon Sep 17 00:00:00 2001 From: Kirill Bobkov Date: Thu, 7 Dec 2023 20:26:15 +0400 Subject: [PATCH 8/8] feat: Improve X-labels generation time // PR fix --- .../components/x_axis/x-axis-labels.generator.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/chart/components/x_axis/x-axis-labels.generator.ts b/src/chart/components/x_axis/x-axis-labels.generator.ts index 14defb01..e2033251 100644 --- a/src/chart/components/x_axis/x-axis-labels.generator.ts +++ b/src/chart/components/x_axis/x-axis-labels.generator.ts @@ -69,7 +69,6 @@ export class XAxisTimeLabelsGenerator implements XAxisLabelsGenerator { private labelsGroupedByWeight: Record = {}; private weightedCache?: { labels: XAxisLabelWeighted[]; coverUpLevel: number }; private levelsCache: Record = {}; - private prevAnimationId = ''; get labels(): XAxisLabelWeighted[] { return this.filterLabelsInViewport(this.getLabelsFromChartType()); @@ -360,20 +359,14 @@ export class XAxisTimeLabelsGenerator implements XAxisLabelsGenerator { private calculateCoverUpLevel = () => { const animation = this.scale.currentAnimation; const meanCandleWidthInUnits = this.chartModel.mainCandleSeries.meanCandleWidth; - let meanCandleWidthInPixels; if (Object.getOwnPropertyNames(this.labelsGroupedByWeight).length === 0) { return -1; } - // calculate coverUpLevel for target zoomX to prevent extra labels calculations on every animation tick - if (animation?.animationInProgress) { - if (animation.id !== this.prevAnimationId) { - this.prevAnimationId = animation.id; - } - meanCandleWidthInPixels = unitToPixels(meanCandleWidthInUnits, animation.animationConfig.targetZoomX); - } else { - meanCandleWidthInPixels = unitToPixels(meanCandleWidthInUnits, this.scale.zoomX); - } + const meanCandleWidthInPixels = animation?.animationInProgress + ? unitToPixels(meanCandleWidthInUnits, animation.animationConfig.targetZoomX) + : unitToPixels(meanCandleWidthInUnits, this.scale.zoomX); + if (!isFinite(meanCandleWidthInPixels)) { return -1; }