From 4e21d6fefcb019635311aa7a051eb968848a68dc Mon Sep 17 00:00:00 2001 From: Pavel Malyshev Date: Tue, 8 Oct 2024 20:10:11 +0300 Subject: [PATCH] feat: try to avoid removing github workflows during copybara sync process GitOrigin-RevId: 27eb53740385097c3b73ffde653b90710a6aa078 --- package.json | 2 +- src/chart/bootstrap.ts | 1 - src/chart/canvas/canvas-bounds-container.ts | 14 +- src/chart/chart.config.ts | 93 +++---------- .../chart/chart-area-pan.handler.ts | 9 +- .../cross_tool/cross-tool.component.ts | 8 +- .../cross_tool/cross-tool.drawer.ts | 8 +- .../components/cross_tool/cross-tool.model.ts | 63 ++------- .../types/cross-and-labels.drawer.ts | 6 +- .../components/events/events-custom-icons.ts | 77 ----------- src/chart/components/events/events.drawer.ts | 130 +++++++++++------- .../components/high_low/high-low.drawer.ts | 8 +- .../components/pane/pane-manager.component.ts | 44 +----- src/chart/components/pane/pane.component.ts | 44 +----- .../resizer/bar-resizer.component.ts | 2 - .../x_axis/x-axis-draw.functions.ts | 4 +- .../components/x_axis/x-axis-scale.handler.ts | 49 ++----- .../x_axis/x-axis-time-labels.drawer.ts | 4 +- .../price_labels/y-axis-labels.model.ts | 8 +- .../components/y_axis/y-axis-scale.handler.ts | 15 -- .../cross-event-producer.component.ts | 37 +---- .../inputhandlers/hover-producer.component.ts | 108 +++------------ .../main-canvas-touch.handler.ts | 29 ---- .../canvas-input-listener.component.ts | 11 +- src/chart/model/candle-series.model.ts | 2 +- src/chart/model/data-series.model.ts | 26 ---- src/chart/utils/math.utils.ts | 4 +- 27 files changed, 180 insertions(+), 626 deletions(-) delete mode 100644 src/chart/components/events/events-custom-icons.ts diff --git a/package.json b/package.json index 6c78fa2..4465419 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devexperts/dxcharts-lite", - "version": "2.6.2", + "version": "2.5.8", "description": "DXCharts Lite", "author": "Devexperts Solutions IE Limited", "license": "MPL 2.0", diff --git a/src/chart/bootstrap.ts b/src/chart/bootstrap.ts index 9b57194..a87b94e 100755 --- a/src/chart/bootstrap.ts +++ b/src/chart/bootstrap.ts @@ -506,7 +506,6 @@ export default class ChartBootstrap { this.canvasBoundsContainer, this.paneManager, timeZoneModel, - chartPanComponent.mainCanvasTouchHandler, formatterFactory, ); this.chartComponents.push(this.hoverProducer); diff --git a/src/chart/canvas/canvas-bounds-container.ts b/src/chart/canvas/canvas-bounds-container.ts index 8b20fd4..f68d13f 100644 --- a/src/chart/canvas/canvas-bounds-container.ts +++ b/src/chart/canvas/canvas-bounds-container.ts @@ -60,9 +60,6 @@ const N_MAP_BUTTON_W = 15; const KNOTS_W_MOBILE_MULTIPLIER = 1.5; const N_MAP_KNOT_W = isMobile() ? 8 * KNOTS_W_MOBILE_MULTIPLIER : 8; -// additional x axis height padding for mobiles, used for better usability on mobile devices -export const X_AXIS_MOBILE_PADDING = isMobile() ? 5 : 0; - /** * we need to check that: heightRatios - 1 < 0.000001 after calculations between decimals */ @@ -469,8 +466,7 @@ export class CanvasBoundsContainer { this.xAxisHeight = fontHeight + (this.config.components.xAxis.padding.top ?? 0) + - (this.config.components.xAxis.padding.bottom ?? 0) + - X_AXIS_MOBILE_PADDING; + (this.config.components.xAxis.padding.bottom ?? 0); } return this.xAxisHeight; } @@ -764,10 +760,10 @@ export class CanvasBoundsContainer { /** * Gets hit-test fn for canvas element. * @param {string} el - CanvasElement.ELEMENT_NAME - * @param {Object} options - An object containing options for the hit test. - * @param {number} [options.extensionX=0] - The amount of extension in the x-axis. - * @param {number} [options.extensionY=0] - The amount of extension in the y-axis. - * @param {boolean} [options.wholePage=false] - Whether to test against the whole page or just the bounds object. + * @param {boolean} reverse - reverses the hit test condition + * @param {number} extensionX - extended hitBoundsTest in horizontal direction + * @param {number} extensionY - extended hitBoundsTest in vertical direction + * @param wholePage * @return {HitBoundsTest} hit-test fn */ getBoundsHitTest(el: string, options: AtLeastOne = DEFAULT_HIT_TEST_OPTIONS): HitBoundsTest { diff --git a/src/chart/chart.config.ts b/src/chart/chart.config.ts index 56d2373..b2e311b 100644 --- a/src/chart/chart.config.ts +++ b/src/chart/chart.config.ts @@ -1,13 +1,3 @@ -/* - * Copyright (C) 2019 - 2024 Devexperts Solutions IE Limited - * 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/. - */ -/* - * Copyright (C) 2019 - 2024 Devexperts Solutions IE Limited - * 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/. - */ /* * Copyright (C) 2019 - 2024 Devexperts Solutions IE Limited * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. @@ -25,7 +15,6 @@ import { DateTimeFormatter, TimeFormatterConfig } from './model/date-time.format import { DEFAULT_MERGE_OPTIONS, merge, MergeOptions } from './utils/merge.utils'; import { DeepPartial } from './utils/object.utils'; import { Candle, defaultSortCandles } from './model/candle.model'; -import { CustomIcon } from './components/events/events-custom-icons'; export const MAIN_FONT = 'Open Sans Semibold, sans-serif'; @@ -121,7 +110,6 @@ export const getDefaultConfig = (): FullChartConfig => ({ histogram: { barCapSize: 1, }, - maxYAxisScalesAmount: 10, sortCandles: defaultSortCandles, }, yAxis: { @@ -243,7 +231,7 @@ export const getDefaultConfig = (): FullChartConfig => ({ logoWidth: 20, logoHeight: 20, }, - highLow: { visible: false, font: '12px sans-serif', prefix: { high: 'H: ', low: 'L: ' } }, + highLow: { visible: false, font: '12px sans-serif' }, highlights: { visible: false, fontFamily: 'Open Sans', @@ -437,35 +425,15 @@ export const getDefaultConfig = (): FullChartConfig => ({ highLowTheme: { highColor: 'rgba(223,222,223,1)', lowColor: 'rgba(223,222,223,1)' }, instrumentInfo: { textColor: '#aeb1b3' }, paneResizer: { - lineColor: 'rgba(61,61,61,1)', + lineColor: 'rgba(55,55,54,1)', bgColor: 'rgba(20,20,19,1)', bgHoverColor: 'rgba(55,55,54,0.6)', }, events: { - earnings: { - color: 'rgba(217,44,64,1)', - normal: 'rgba(217,44,64,1)', - hover: 'rgba(217,44,64,1)', - line: 'rgba(217,44,64,1)', - }, - dividends: { - color: 'rgba(169,38,251,1)', - normal: 'rgba(169,38,251,1)', - hover: 'rgba(169,38,251,1)', - line: 'rgba(169,38,251,1)', - }, - splits: { - color: 'rgba(244,187,63,1)', - normal: 'rgba(244,187,63,1)', - hover: 'rgba(244,187,63,1)', - line: 'rgba(244,187,63,1)', - }, - 'conference-calls': { - color: 'rgba(48,194,97,1)', - normal: 'rgba(48,194,97,1)', - hover: 'rgba(48,194,97,1)', - line: 'rgba(48,194,97,1)', - }, + earnings: { color: 'rgba(217,44,64,1)' }, + dividends: { color: 'rgba(169,38,251,1)' }, + splits: { color: 'rgba(244,187,63,1)' }, + 'conference-calls': { color: 'rgba(48,194,97,1)' }, }, secondaryChartTheme: [ { @@ -803,7 +771,12 @@ export interface FullChartColors { labelBoxColor: string; labelTextColor: string; }; - events: ChartConfigComponentsEventsColors; + events: { + earnings: EventColors; + dividends: EventColors; + splits: EventColors; + 'conference-calls': EventColors; + }; navigationMap: { buttonColor: string; knotColor: string; @@ -1006,10 +979,6 @@ export interface ChartConfigComponentsChart { selectedWidth: number; minCandlesOffset: number; histogram: ChartConfigComponentsHistogram; - /** - * The maximum amount of Y axis scales on a single chart - */ - maxYAxisScalesAmount: number; // optional because backward compability sortCandles?: (candles: Candle[]) => Candle[]; } @@ -1042,17 +1011,6 @@ export interface ChartConfigComponentsEvents { * ' */ icons?: ChartConfigComponentsEventsIcons; - /** - * Configure the event type vertical line appearance - */ - line?: ChartConfigComponentsEventsLine; -} - -export interface ChartConfigComponentsEventsColors { - earnings: EventColors; - dividends: EventColors; - splits: EventColors; - 'conference-calls': EventColors; } export interface DateTimeFormatConfig { @@ -1190,7 +1148,6 @@ export interface ChartConfigComponentsHighLow { * Font config of high/low labels. */ font: string; - prefix: { high: string; low: string }; } export interface ChartConfigComponentsCrossTool { /** @@ -1380,19 +1337,10 @@ export interface HighlightsColors { label: string; } -/** - * @deprecated use {normal}, {hover} instead, will be removed in v6 - */ -export interface EventColorsOld { +export interface EventColors { color: string; } -export interface EventColors extends EventColorsOld { - line?: string; - normal?: string; - hover?: string; -} - export interface HistogramColors { upCap: string; upBottom: string; @@ -1521,19 +1469,18 @@ export interface YAxisLabelsColors extends Record }; - dividends?: { width: number; dash: Array }; - splits?: { width: number; dash: Array }; - 'conference-calls'?: { width: number; dash: Array }; + conferenceCalls?: CustomIcon; } export type CursorType = string; diff --git a/src/chart/components/chart/chart-area-pan.handler.ts b/src/chart/components/chart/chart-area-pan.handler.ts index 310ca9e..f27b6fe 100644 --- a/src/chart/components/chart/chart-area-pan.handler.ts +++ b/src/chart/components/chart/chart-area-pan.handler.ts @@ -47,7 +47,7 @@ interface ChartPanningOptions { * @param {CanvasBoundsContainer} canvasBoundsContainer - An instance of the CanvasBoundsContainer class. * @param {CanvasAnimation} canvasAnimation - An instance of the CanvasAnimation class. * @param {ChartPanComponent} chartPanComponent - An instance of the ChartPanComponent class. - + */ export class ChartAreaPanHandler extends ChartBaseElement { private currentPoint: Point = { x: 0, y: 0 }; @@ -98,7 +98,6 @@ export class ChartAreaPanHandler extends ChartBaseElement { * If the zoomToCursor configuration is set to false, it calls the zoomXToEnd method of the scaleModel to zoom in or out based on the zoomIn parameter. * Finally, it fires the draw event of the bus to redraw the canvas. * @param {WheelEvent} e - Wheel event - * @param {number} zoomSensitivity - zoom sensitivity * @returns {void} */ private zoomXHandler = (e: WheelEvent, zoomSensitivity: number) => { @@ -138,9 +137,9 @@ export class ChartAreaPanHandler extends ChartBaseElement { ) .subscribe(e => { const device = deviceDetector(); - const direction = device === 'apple' || device === 'mobile' ? -1 : 1; + const direction = device === 'apple' || device === 'mobile' ? 1 : -1; const deltaX = 0 + e.deltaX * direction; - const deltaY = 0 + e.deltaY * direction; + const deltaY = 0 + e.deltaY * -direction; if (e.ctrlKey) { const zoomSensitivity = this.calculateDynamicSesitivity( @@ -193,7 +192,7 @@ export class ChartAreaPanHandler extends ChartBaseElement { /** * Registers a handler for panning the chart along the Y-axis. - * @param {ScaleModel} scale - The scale model of the extent. + * @param {ScaleModel} scaleModel - The scale model of the extent. * @param {HitBoundsTest} hitTest - The hit test of the pane. * @returns {DragNDropYComponent} - The drag and drop component for panning the chart along the Y-axis. */ diff --git a/src/chart/components/cross_tool/cross-tool.component.ts b/src/chart/components/cross_tool/cross-tool.component.ts index 9b2884b..7b75ed6 100644 --- a/src/chart/components/cross_tool/cross-tool.component.ts +++ b/src/chart/components/cross_tool/cross-tool.component.ts @@ -41,17 +41,11 @@ export class CrossToolComponent extends ChartBaseElement { this.crossToolCanvasModel, crossEventProducer, hoverProducer, - this.canvasBoundsContainer, ); this.addChildEntity(this.model); const clearCanvasDrawer = new ClearCanvasDrawer(this.crossToolCanvasModel); this.registerDefaultDrawerTypes(); - const crossToolDrawer = new CrossToolDrawer( - this.model, - this.config, - this.crossToolCanvasModel, - this.crossToolTypeDrawers, - ); + const crossToolDrawer = new CrossToolDrawer(this.model, this.crossToolCanvasModel, this.crossToolTypeDrawers); const compositeDrawer = new CompositeDrawer(); compositeDrawer.addDrawer(clearCanvasDrawer, 'CLEAR_CANVAS'); compositeDrawer.addDrawer(crossToolDrawer, 'CROSS_TOOL_DRAWER'); diff --git a/src/chart/components/cross_tool/cross-tool.drawer.ts b/src/chart/components/cross_tool/cross-tool.drawer.ts index 36b2cd7..ac8018c 100644 --- a/src/chart/components/cross_tool/cross-tool.drawer.ts +++ b/src/chart/components/cross_tool/cross-tool.drawer.ts @@ -6,7 +6,6 @@ import { CanvasModel } from '../../model/canvas.model'; import { Drawer } from '../../drawers/drawing-manager'; import { CrossToolHover, CrossToolModel, CrossToolType } from './cross-tool.model'; -import { FullChartConfig } from '../../chart.config'; export interface CrossToolTypeDrawer { draw: (ctx: CanvasRenderingContext2D, hover: CrossToolHover) => void; @@ -15,7 +14,6 @@ export interface CrossToolTypeDrawer { export class CrossToolDrawer implements Drawer { constructor( private model: CrossToolModel, - private config: FullChartConfig, private crossToolCanvasModel: CanvasModel, private readonly crossToolTypeDrawers: Record, ) {} @@ -29,13 +27,11 @@ export class CrossToolDrawer implements Drawer { * @returns {void} */ draw() { - const drawer = this.crossToolTypeDrawers[this.config.components.crossTool.type]; + const drawer = this.crossToolTypeDrawers[this.model.type]; if (drawer) { this.model.currentHover && drawer.draw(this.crossToolCanvasModel.ctx, this.model.currentHover); } else { - console.error( - `No cross tool drawer type registered for drawer type ${this.config.components.crossTool.type}`, - ); + console.error(`No cross tool drawer type registered for drawer type ${this.model.type}`); } } diff --git a/src/chart/components/cross_tool/cross-tool.model.ts b/src/chart/components/cross_tool/cross-tool.model.ts index adc2869..3206f6f 100644 --- a/src/chart/components/cross_tool/cross-tool.model.ts +++ b/src/chart/components/cross_tool/cross-tool.model.ts @@ -9,8 +9,7 @@ import { CrossEventProducerComponent } from '../../inputhandlers/cross-event-pro import { Hover, HoverProducerComponent } from '../../inputhandlers/hover-producer.component'; import { CanvasModel } from '../../model/canvas.model'; import { ChartBaseElement } from '../../model/chart-base-element'; -import { CanvasBoundsContainer, CanvasElement, CHART_UUID } from '../../canvas/canvas-bounds-container'; -import { isMobile } from '../../utils/device/browser.utils'; +import { CHART_UUID } from '../../canvas/canvas-bounds-container'; export type CrossToolType = 'cross-and-labels' | 'only-labels' | 'none' | string; @@ -30,15 +29,16 @@ export class CrossToolModel extends ChartBaseElement { set currentHover(value: CrossToolHover | null) { this.currentHoverSubject.next(value); } + type: CrossToolType = 'cross-and-labels'; constructor( private config: Required, private crossToolCanvasModel: CanvasModel, private crossEventProducer: CrossEventProducerComponent, private hoverProducer: HoverProducerComponent, - private canvasBoundsContainer: CanvasBoundsContainer, ) { super(); + this.type = config.type; } /** @@ -48,19 +48,21 @@ export class CrossToolModel extends ChartBaseElement { * @returns {void} */ public setType(type: CrossToolType) { - this.config.type = type; + this.type = type; } /** * Method to activate the cross tool. - * It subscribes to the hoverProducer's hover event and updates the crosstool. + * It subscribes to the canvasInputListener's mouse move event and fires the draw event. + * It also subscribes to the eventBus's hover and close hover events and updates the hover and fires the draw event accordingly. + * It also subscribes to the chartModel's candlesSetSubject and timeZoneModel's observeTimeZoneChanged events and recalculates the cross tool X formatter. */ protected doActivate() { super.doActivate(); this.addRxSubscription( this.hoverProducer.hoverSubject.subscribe(hover => { if (this.crossEventProducer.crossSubject.getValue() !== null && hover !== null) { - isMobile() ? this.updateCrossToolMobile(hover) : this.updateCrossTool(hover); + this.updateHover(hover); } else { this.currentHover = null; } @@ -74,7 +76,7 @@ export class CrossToolModel extends ChartBaseElement { * @private */ private fireDraw() { - if (this.config.type !== 'none') { + if (this.type !== 'none') { this.crossToolCanvasModel.fireDraw(); } } @@ -93,7 +95,7 @@ export class CrossToolModel extends ChartBaseElement { * @param {number} hover.candleHover.closestOHLCY - The y coordinate of the closest OHLC price of the candle. * @returns {void} */ - public updateCrossTool(hover: Hover, magnetTarget = this.config.magnetTarget) { + public updateHover(hover: Hover, magnetTarget = this.config.magnetTarget) { if (this.currentHover === null) { this.currentHover = { x: hover.x, y: 0, time: hover.timeFormatted, paneId: hover.paneId }; } else { @@ -127,49 +129,4 @@ export class CrossToolModel extends ChartBaseElement { this.currentHover.paneId = hover.paneId; this.currentHoverSubject.next(this.currentHover); } - - private updateCrossToolMobile(hover: Hover) { - // mobile crosstool works only after long touch event - if (!this.hoverProducer.longTouchActivatedSubject.getValue()) { - return; - } - - const { fixed, temp, isSet } = this.crossEventProducer.crossToolTouchInfo; - - // long touch makes crosstool fixed and the further moving will place crosstool under the current hover coordinates (ordinary logic) - // if crosstool is already set (long touch end event happened) the moving of chart will move crosstool according to it's initial (fixed position) - if (!isSet) { - this.updateCrossTool(hover); - return; - } - - // additional crosstool move logic - const paneBounds = this.canvasBoundsContainer.getBounds(CanvasElement.PANE_UUID(hover.paneId)); - const offset = 5; - - // take a difference inbetween current hover and temporary crosstool coordinates - const xDiff = hover.x - temp.x; - const yDiff = hover.y - temp.y; - - // apply the difference to the fixed coordinates - const rawX = fixed.x + xDiff; - const rawY = fixed.y + yDiff; - - const paneYStart = paneBounds.y + offset; - const paneYEnd = paneBounds.y + paneBounds.height - offset; - - // check for chart bounds and don't move crosstool outside of it - const x = rawX < offset ? offset : rawX > paneBounds.width - offset ? paneBounds.width - offset : rawX; - const y = rawY < paneYStart ? paneYStart : rawY > paneYEnd ? paneYEnd : rawY; - const crossToolHover = this.hoverProducer.createHover(x, y, hover.paneId) ?? hover; - - const updatedHover = { - ...crossToolHover, - x, - y, - }; - - this.crossEventProducer.crossToolHover = updatedHover; - this.updateCrossTool(updatedHover); - } } diff --git a/src/chart/components/cross_tool/types/cross-and-labels.drawer.ts b/src/chart/components/cross_tool/types/cross-and-labels.drawer.ts index e756f30..dc3639c 100644 --- a/src/chart/components/cross_tool/types/cross-and-labels.drawer.ts +++ b/src/chart/components/cross_tool/types/cross-and-labels.drawer.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 { CanvasBoundsContainer, CanvasElement, X_AXIS_MOBILE_PADDING } from '../../../canvas/canvas-bounds-container'; +import { CanvasBoundsContainer, CanvasElement } from '../../../canvas/canvas-bounds-container'; import { FullChartConfig } from '../../../chart.config'; import { avoidAntialiasing, drawRoundedRect } from '../../../utils/canvas/canvas-drawing-functions.utils'; import { PaneManager } from '../../pane/pane-manager.component'; @@ -112,12 +112,12 @@ export class CrossAndLabelsDrawerType implements CrossToolTypeDrawer { const boxWidth = width + xLabelPaddingLeft + xLabelPaddingRight; const boxHeight = fontHeight + xLabelPaddingTop + xLabelPaddingBottom; const xBoxPos = Math.max(x - boxWidth / 2, 0); - const yBoxPos = xAxis.y + xLabelMarginTop + X_AXIS_MOBILE_PADDING / 2; + const yBoxPos = xAxis.y + xLabelMarginTop; drawRoundedRect(ctx, xBoxPos, yBoxPos, boxWidth, boxHeight); // label ctx.fillStyle = crossToolColors.labelTextColor; const xTextPos = Math.max(x - width / 2, xLabelPaddingLeft); - const yTextPos = xAxis.y + xLabelMarginTop + fontHeight + X_AXIS_MOBILE_PADDING / 2 + xLabelPaddingTop - 1; // -1 for vertical adjustment + const yTextPos = xAxis.y + xLabelMarginTop + fontHeight + xLabelPaddingTop - 1; // -1 for vertical adjustment ctx.fillText(labelText, xTextPos, yTextPos); ctx.restore(); } diff --git a/src/chart/components/events/events-custom-icons.ts b/src/chart/components/events/events-custom-icons.ts deleted file mode 100644 index 3a7a0a3..0000000 --- a/src/chart/components/events/events-custom-icons.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2019 - 2024 Devexperts Solutions IE Limited - * 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/. - */ -/* - * Copyright (C) 2019 - 2024 Devexperts Solutions IE Limited - * 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 { Point } from '../../inputlisteners/canvas-input-listener.component'; -export interface CustomIcon { - normal: string; - hover: string; -} - -export interface CustomIconImage { - img: HTMLImageElement; - svgHeight: number; -} - -export const getIconHash = (type: string, state: keyof CustomIcon) => `${type}_${state}`; - -/** - * Creates a custom icon for a given event type. - * @param {string} type - The type of the event. - * @param {CustomIcon} [icon] - The custom icon object containing the normal and hover images. - * @returns {void} - */ -export const createCustomIcon = (type: string, icon?: CustomIcon) => { - if (icon) { - const normal = createIconImage(icon.normal); - const hover = createIconImage(icon.hover); - - return { type, normal, hover }; - } -}; - -/** - * Creates an icon image from a string containing SVG data. - * @param {string} iconString - The string containing SVG data. - * @returns {CustomIconImage} An object containing an Image object and the height of the SVG element. - */ -export const createIconImage = (iconString: string): CustomIconImage => { - const parser = new DOMParser(); - const svgSelector = parser.parseFromString(iconString, 'text/html').querySelector('svg'); - let svgHeight = 0; - if (svgSelector) { - svgHeight = parseInt(svgSelector.getAttribute('height') ?? '', 10); - } - const svg64 = btoa(iconString); - const b64Start = 'data:image/svg+xml;base64,'; - const image64 = b64Start + svg64; - const img = new Image(); - img.src = image64; - - return { - img, - svgHeight, - }; -}; - -export const drawCustomSvgIcon = ( - ctx: CanvasRenderingContext2D, - icons: Record, - point: Point, - type: string, - isHovered: boolean, -) => { - if (isHovered) { - const hover = icons[getIconHash(type, 'hover')]; - ctx.drawImage(hover.img, point.x - hover.svgHeight / 2, point.y - hover.svgHeight / 2); - } else { - const normal = icons[getIconHash(type, 'normal')]; - ctx.drawImage(normal.img, point.x - normal.svgHeight / 2, point.y - normal.svgHeight / 2); - } -}; diff --git a/src/chart/components/events/events.drawer.ts b/src/chart/components/events/events.drawer.ts index 600c92f..2c6cac0 100644 --- a/src/chart/components/events/events.drawer.ts +++ b/src/chart/components/events/events.drawer.ts @@ -1,13 +1,3 @@ -/* - * Copyright (C) 2019 - 2024 Devexperts Solutions IE Limited - * 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/. - */ -/* - * Copyright (C) 2019 - 2024 Devexperts Solutions IE Limited - * 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/. - */ /* * Copyright (C) 2019 - 2024 Devexperts Solutions IE Limited * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. @@ -15,14 +5,17 @@ */ import { Bounds } from '../../model/bounds.model'; import { CanvasBoundsContainer, CanvasElement } from '../../canvas/canvas-bounds-container'; -import { ChartConfigComponentsEventsIcons, FullChartConfig, EventColors } from '../../chart.config'; +import { CustomIcon, FullChartConfig } from '../../chart.config'; import { CanvasModel } from '../../model/canvas.model'; import { Drawer } from '../../drawers/drawing-manager'; import { DateTimeFormatter } from '../../model/date-time.formatter'; import { ChartModel } from '../chart/chart.model'; -import { EconomicEvent, EventsModel, EventWithId } from './events.model'; -import { createCustomIcon, CustomIconImage, drawCustomSvgIcon, getIconHash } from './events-custom-icons'; -import { Point } from '../../inputlisteners/canvas-input-listener.component'; +import { EconomicEvent, EventsModel, EventType, EventWithId } from './events.model'; + +interface CreatedCustomIcon { + img: HTMLImageElement; + svgHeight: number; +} const eventsSizesDict = { 'rhombus-small': 4, @@ -30,16 +23,11 @@ const eventsSizesDict = { 'rhombus-large': 8, }; -const iconTypes: Array = [ - 'earnings', - 'dividends', - 'splits', - 'conference-calls', -]; +const getIconHash = (type: EventType, state: keyof CustomIcon) => `${type}_${state}`; export class EventsDrawer implements Drawer { // cache of created icons - private customIcons: Record = {}; + private customIcons: Record = {}; constructor( private canvasModel: CanvasModel, @@ -51,16 +39,51 @@ export class EventsDrawer implements Drawer { ) { const iconsConfig = this.config.components.events.icons; if (iconsConfig) { - iconTypes.forEach(type => { - const customIcon = createCustomIcon(type, iconsConfig[type]); - if (customIcon) { - this.customIcons[getIconHash(customIcon.type, 'normal')] = customIcon.normal; - this.customIcons[getIconHash(customIcon.type, 'hover')] = customIcon.hover; - } - }); + this.createCustomIcon('earnings', iconsConfig.earnings); + this.createCustomIcon('dividends', iconsConfig.dividends); + this.createCustomIcon('splits', iconsConfig.splits); + this.createCustomIcon('conference-calls', iconsConfig.conferenceCalls); + } + } + + /** + * Creates a custom icon for a given event type. + * @param {EventType} type - The type of the event. + * @param {CustomIcon} [icon] - The custom icon object containing the normal and hover images. + * @returns {void} + */ + createCustomIcon(type: EventType, icon?: CustomIcon) { + if (icon) { + const normal = this.createIconImage(icon.normal); + const hover = this.createIconImage(icon.hover); + this.customIcons[getIconHash(type, 'normal')] = normal; + this.customIcons[getIconHash(type, 'hover')] = hover; } } + /** + * Creates an icon image from a string containing SVG data. + * @param {string} iconString - The string containing SVG data. + * @returns {Object} An object containing an Image object and the height of the SVG element. + */ + createIconImage(iconString: string) { + const parser = new DOMParser(); + const svgSelector = parser.parseFromString(iconString, 'text/html').querySelector('svg'); + let svgHeight = 0; + if (svgSelector) { + svgHeight = parseInt(svgSelector.getAttribute('height') ?? '', 10); + } + const svg64 = btoa(iconString); + const b64Start = 'data:image/svg+xml;base64,'; + const image64 = b64Start + svg64; + const img = new Image(); + img.src = image64; + return { + img, + svgHeight, + }; + } + /** * Draws events on the chart canvas if they are visible according to the configuration. * The events are filtered by type and then iterated over to draw them on the chart. @@ -84,26 +107,18 @@ export class EventsDrawer implements Drawer { .forEach(event => { const x = this.chartModel.candleFromTimestamp(event.timestamp).xCenter(this.chartModel.scale); if (x > chartBounds.x && x < chartBounds.x + chartBounds.width) { - const colors = this.config.colors.events[event.type]; - ctx.strokeStyle = colors.line ?? colors.normal ?? colors.color; + const color = this.config.colors.events[event.type].color; + ctx.strokeStyle = color; // check custom icon in cache if (this.customIcons[getIconHash(event.type, 'hover')] !== undefined) { - const point: Point = { x, y: bounds.y + bounds.height / 2 }; - const isHovered = this.model.hoveredEvent.getValue() === event; - drawCustomSvgIcon(ctx, this.customIcons, point, event.type, isHovered); + this.drawCustomSvgEvent(ctx, x, bounds, event); } else { - this.drawDefaultEvent(ctx, x, bounds, event, colors); + this.drawDefaultEvent(ctx, x, bounds, event, color); } // draw vertical line and label for the hovered event if (this.model.hoveredEvent.getValue() === event) { - const line = this.config.components.events.line; - const width = line && line[event.type] && line[event.type]?.width; - const dash = line && line[event.type] && line[event.type]?.dash; - - ctx.lineWidth = width ?? 1; ctx.beginPath(); - ctx.setLineDash(dash ?? []); ctx.moveTo(x, chartBounds.y); ctx.lineTo(x, bounds.y + bounds.height / 2); ctx.stroke(); @@ -118,24 +133,37 @@ export class EventsDrawer implements Drawer { ctx.restore(); } + /** + * Draws a custom SVG event on a canvas context. + * @param {CanvasRenderingContext2D} ctx - The canvas context to draw on. + * @param {number} x - The x coordinate of the event. + * @param {Bounds} bounds - The bounds of the event. + * @param {EventWithId} event - The event to draw. + * @returns {void} + */ + drawCustomSvgEvent(ctx: CanvasRenderingContext2D, x: number, bounds: Bounds, event: EventWithId) { + const y = bounds.y + bounds.height / 2; + const normal = this.customIcons[getIconHash(event.type, 'normal')]; + const hover = this.customIcons[getIconHash(event.type, 'hover')]; + if (this.model.hoveredEvent.getValue() === event) { + ctx.drawImage(hover.img, x - hover.svgHeight / 2, y - hover.svgHeight / 2); + } else { + ctx.drawImage(normal.img, x - normal.svgHeight / 2, y - normal.svgHeight / 2); + } + } + /** * Draws a default event on a canvas context. * @param {CanvasRenderingContext2D} ctx - The canvas context to draw on. * @param {number} x - The x coordinate of the event. * @param {Bounds} bounds - The bounds of the event. * @param {EventWithId} event - The event to draw. - * @param {EventColors} colors - The colors of the event. + * @param {string} color - The color of the event. * @returns {void} */ - drawDefaultEvent( - ctx: CanvasRenderingContext2D, - x: number, - bounds: Bounds, - event: EventWithId, - colors: EventColors, - ) { + drawDefaultEvent(ctx: CanvasRenderingContext2D, x: number, bounds: Bounds, event: EventWithId, color: string) { const y = bounds.y + bounds.height / 2; - ctx.fillStyle = colors.normal ?? colors.color; + ctx.fillStyle = color; // draw figure ctx.lineWidth = 1.5; // 1.5 pixels const size = getEventSize(event); @@ -146,10 +174,8 @@ export class EventsDrawer implements Drawer { ctx.lineTo(x, y + size); ctx.closePath(); if (this.model.hoveredEvent.getValue() === event) { - ctx.fillStyle = colors.hover ?? colors.color; ctx.fill(); } else { - ctx.strokeStyle = colors.normal ?? colors.color; ctx.stroke(); } } @@ -193,7 +219,7 @@ export class EventsDrawer implements Drawer { * Returns the size of an event based on its style * @param {EconomicEvent} event - The event to get the size of * @returns {number} - The size of the event - + */ export function getEventSize(event: EconomicEvent) { return eventsSizesDict[event.style]; diff --git a/src/chart/components/high_low/high-low.drawer.ts b/src/chart/components/high_low/high-low.drawer.ts index dfe8187..81a3a69 100644 --- a/src/chart/components/high_low/high-low.drawer.ts +++ b/src/chart/components/high_low/high-low.drawer.ts @@ -1,8 +1,3 @@ -/* - * Copyright (C) 2019 - 2024 Devexperts Solutions IE Limited - * 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/. - */ /* * Copyright (C) 2019 - 2024 Devexperts Solutions IE Limited * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. @@ -89,8 +84,7 @@ export class HighLowDrawer implements Drawer { */ private getMarkerText(yValue: number, type: MarkerType): string { const formattedValue = this.chartModel.pane.regularFormatter(yValue); - const prefix = - type === 'high' ? this.config.components.highLow.prefix.high : this.config.components.highLow.prefix.low; + const prefix = type === 'high' ? 'H:' : 'L:'; return `${prefix} ${formattedValue}`; } diff --git a/src/chart/components/pane/pane-manager.component.ts b/src/chart/components/pane/pane-manager.component.ts index 45953e5..a443f64 100644 --- a/src/chart/components/pane/pane-manager.component.ts +++ b/src/chart/components/pane/pane-manager.component.ts @@ -20,7 +20,7 @@ import { uuid as generateUuid } from '../../utils/uuid.utils'; import { ChartBaseModel } from '../chart/chart-base.model'; import { createHighLowOffsetCalculator } from '../chart/data-series.high-low-provider'; import { ChartPanComponent } from '../pan/chart-pan.component'; -import { BarResizerComponent, RESIZER_HIT_TEST_EXTENSION } from '../resizer/bar-resizer.component'; +import { BarResizerComponent } from '../resizer/bar-resizer.component'; import { YExtentComponent, YExtentCreationOptions, @@ -34,8 +34,6 @@ import { HitTestCanvasModel } from '../../model/hit-test-canvas.model'; import { firstOf, flatMap, lastOf } from '../../utils/array.utils'; import { ChartResizeHandler } from '../../inputhandlers/chart-resize.handler'; -export type MoveDataSeriesToPaneDirection = 'above' | 'below'; - export class PaneManager extends ChartBaseElement { public panes: Record = {}; public paneRemovedSubject: Subject = new Subject(); @@ -97,7 +95,7 @@ export class PaneManager extends ChartBaseElement { */ private addResizer(uuid: string) { const resizerHT = this.canvasBoundsContainer.getBoundsHitTest(CanvasElement.PANE_UUID_RESIZER(uuid), { - extensionY: this.config.components.paneResizer.dragZone + RESIZER_HIT_TEST_EXTENSION, + extensionY: this.config.components.paneResizer.dragZone, }); const dragPredicate = () => this.chartBaseModel.mainVisualPoints.length !== 0; const dragTick = () => { @@ -143,7 +141,6 @@ export class PaneManager extends ChartBaseElement { /** * Creates sub-plot on the chart with y-axis * @param uuid - * @param {AtLeastOne} options * @returns */ public createPane(uuid = generateUuid(), options?: AtLeastOne): PaneComponent { @@ -289,41 +286,6 @@ export class PaneManager extends ChartBaseElement { this.recalculateState(); } - /** - * Move data series to a certain pane, or create a new one if no pane is found - */ - public moveDataSeriesToPane( - dataSeries: DataSeriesModel[], - initialPane: PaneComponent, - initialExtent: YExtentComponent, - paneUUID?: string, - extent?: YExtentComponent, - direction?: MoveDataSeriesToPaneDirection, - index = 0, - ) { - const pane = paneUUID && this.panes[paneUUID]; - - if (!pane) { - const order = direction && direction === 'above' ? index : this.panesOrder.length + index; - const newPane = this.createPane(paneUUID, { order }); - newPane.moveDataSeriesToExistingExtentComponent(dataSeries, initialPane, initialExtent, newPane.mainExtent); - initialPane.yExtentComponents.length === 0 && this.removePane(initialPane.uuid); - return; - } - - if (extent) { - pane.moveDataSeriesToExistingExtentComponent(dataSeries, initialPane, initialExtent, extent); - } else { - pane.moveDataSeriesToNewExtentComponent( - dataSeries, - initialPane, - initialExtent, - initialExtent.yAxis.state.align, - ); - } - initialPane.yExtentComponents.length === 0 && this.removePane(initialPane.uuid); - } - /** * Adds cursors to the chart elements based on the provided uuid and cursor type. * @private @@ -340,7 +302,7 @@ export class PaneManager extends ChartBaseElement { this.cursorHandler.setCursorForCanvasEl( paneResizer, this.config.components.paneResizer.cursor, - this.config.components.paneResizer.dragZone + RESIZER_HIT_TEST_EXTENSION, + this.config.components.paneResizer.dragZone, ); return () => { diff --git a/src/chart/components/pane/pane.component.ts b/src/chart/components/pane/pane.component.ts index e4e9381..37a5dca 100644 --- a/src/chart/components/pane/pane.component.ts +++ b/src/chart/components/pane/pane.component.ts @@ -13,7 +13,7 @@ import { HitBoundsTest, } from '../../canvas/canvas-bounds-container'; import { CursorHandler } from '../../canvas/cursor.handler'; -import { FullChartConfig, YAxisAlign, YAxisConfig } from '../../chart.config'; +import { FullChartConfig, YAxisConfig } from '../../chart.config'; import { DrawingManager } from '../../drawers/drawing-manager'; import EventBus from '../../events/event-bus'; import { CanvasInputListenerComponent } from '../../inputlisteners/canvas-input-listener.component'; @@ -49,7 +49,6 @@ export class PaneComponent extends ChartBaseElement { public ht: HitBoundsTest; public yExtentComponents: YExtentComponent[] = []; - public yExtentComponentsChangedSubject: Subject = new Subject(); get scale() { return this.mainExtent.scale; @@ -127,10 +126,8 @@ export class PaneComponent extends ChartBaseElement { /** * Creates a new GridComponent instance with the provided parameters. * @param {string} uuid - The unique identifier of the pane. - * @param {ScaleModel} scale - The scale model used to calculate the scale of the grid. - * @param {YAxisConfig} yAxisState - y Axis Config - * @param {() => NumericAxisLabel[]} yAxisLabelsGetter - * @param {() => Unit} yAxisBaselineGetter + * @param {ScaleModel} scaleModel - The scale model used to calculate the scale of the grid. + * @param {NumericYAxisLabelsGenerator} yAxisLabelsGenerator - The generator used to create the labels for the y-axis. * @returns {GridComponent} - The newly created GridComponent instance. */ private createGridComponent( @@ -162,8 +159,8 @@ export class PaneComponent extends ChartBaseElement { * Creates a handler for Y-axis panning of the chart. * @private * @param {string} uuid - The unique identifier of the chart pane. - * @param {ScaleModel} scale - The scale model of the chart. - * @returns [Unsubscriber, DragNDropYComponent] + * @param {ScaleModel} scaleModel - The scale model of the chart. + * @returns {Unsubscriber} */ private createYPanHandler(uuid: string, scale: ScaleModel): [Unsubscriber, DragNDropYComponent] { const chartPaneId = CanvasElement.PANE_UUID(uuid); @@ -259,7 +256,6 @@ export class PaneComponent extends ChartBaseElement { yExtentComponent.activate(); this.yExtentComponents.push(yExtentComponent); this.canvasBoundsContainer.updateYAxisWidths(); - this.yExtentComponentsChangedSubject.next(); return yExtentComponent; } @@ -269,36 +265,6 @@ export class PaneComponent extends ChartBaseElement { // re-index extents this.yExtentComponents.forEach((c, idx) => (c.idx = idx)); this.canvasBoundsContainer.updateYAxisWidths(); - this.yExtentComponentsChangedSubject.next(); - } - - /** - * Create new pane extent and attach data series to it - */ - public moveDataSeriesToNewExtentComponent( - dataSeries: DataSeriesModel[], - initialPane: PaneComponent, - initialExtent: YExtentComponent, - align: YAxisAlign = 'right', - ) { - const extent = this.createExtentComponent(); - extent.yAxis.setYAxisAlign(align); - dataSeries.forEach(series => series.moveToExtent(extent)); - initialExtent.dataSeries.size === 0 && initialPane.removeExtentComponent(initialExtent); - } - - /** - * Attach data series to existing y axis extent - */ - public moveDataSeriesToExistingExtentComponent( - dataSeries: DataSeriesModel[], - initialPane: PaneComponent, - initialExtent: YExtentComponent, - extentComponent: YExtentComponent, - ) { - dataSeries.forEach(series => series.moveToExtent(extentComponent)); - initialExtent.dataSeries.size === 0 && initialPane.removeExtentComponent(initialExtent); - this.yExtentComponentsChangedSubject.next(); } /** diff --git a/src/chart/components/resizer/bar-resizer.component.ts b/src/chart/components/resizer/bar-resizer.component.ts index 74ad02e..3a6985c 100644 --- a/src/chart/components/resizer/bar-resizer.component.ts +++ b/src/chart/components/resizer/bar-resizer.component.ts @@ -19,8 +19,6 @@ import { BarResizerDrawer } from './bar-resizer.drawer'; import { BoundsProvider } from '../../model/bounds.model'; import { HitTestCanvasModel } from '../../model/hit-test-canvas.model'; -export const RESIZER_HIT_TEST_EXTENSION: number = 8; - /** * Bar separator between panes. * Used to resize the areas height or just draw a fixed line. diff --git a/src/chart/components/x_axis/x-axis-draw.functions.ts b/src/chart/components/x_axis/x-axis-draw.functions.ts index ff996d8..623c824 100644 --- a/src/chart/components/x_axis/x-axis-draw.functions.ts +++ b/src/chart/components/x_axis/x-axis-draw.functions.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 { CanvasBoundsContainer, CanvasElement, X_AXIS_MOBILE_PADDING } from '../../canvas/canvas-bounds-container'; +import { CanvasBoundsContainer, CanvasElement } from '../../canvas/canvas-bounds-container'; import { FullChartConfig } from '../../chart.config'; import { XAxisLabel } from './x-axis-labels.model'; @@ -63,7 +63,7 @@ export function drawXAxisLabel( ctx.fillStyle = label.color; const xTextPos = boxStart + DEFAULT_X_LABEL_PADDING.x; - const yTextPos = xAxisBounds.y + offsetTop + fontSize + X_AXIS_MOBILE_PADDING / 2; + const yTextPos = xAxisBounds.y + offsetTop + fontSize; // -2 for vertical adjustment ctx.fillText(label.text, xTextPos, yTextPos); ctx.restore(); } diff --git a/src/chart/components/x_axis/x-axis-scale.handler.ts b/src/chart/components/x_axis/x-axis-scale.handler.ts index d70943f..90f8c74 100644 --- a/src/chart/components/x_axis/x-axis-scale.handler.ts +++ b/src/chart/components/x_axis/x-axis-scale.handler.ts @@ -14,16 +14,12 @@ import { DragNDropXComponent } from '../dran-n-drop_helper/drag-n-drop-x.compone import { ChartPanComponent } from '../pan/chart-pan.component'; import { HitTestCanvasModel } from '../../model/hit-test-canvas.model'; -// if you drag full X width from left to right - you will have x3 zoom, and vice-versa -const FULL_X_WIDTH_ZOOM_FACTOR = 3; - /** * Handles the mouse drag on X axis - to zoom the viewport horizontally. * @doc-tags scaling,zoom,viewport */ export class XAxisScaleHandler extends ChartBaseElement { lastXStart: Unit = 0; - lastXEnd: Unit = 0; lastXWidth: Unit = 0; lastXPxWidth: Pixel = 0; @@ -101,7 +97,6 @@ export class XAxisScaleHandler extends ChartBaseElement { private onXDragStart = () => { this.lastXStart = this.scale.xStart; - this.lastXEnd = this.scale.xEnd; this.lastXWidth = this.scale.xEnd - this.scale.xStart; const bounds = this.canvasBoundsContainer.getBounds(CanvasElement.X_AXIS); this.lastXPxWidth = bounds.width - this.canvasInputListener.currentPoint.x; @@ -110,44 +105,16 @@ export class XAxisScaleHandler extends ChartBaseElement { }; private onXDragTick = (dragInfo: DragInfo) => { - let { delta: absoluteXDelta } = dragInfo; - - // mouse click or single tap drag - if (!this.touches || this.touches.length === 1) { - const newPxWidth = this.lastXPxWidth - absoluteXDelta; - // cursor goes beyond x-axis - maximum scale is reached - if (newPxWidth < 0) { - return; - } - const xZoomMult = this.lastXPxWidth / newPxWidth; - const newWidth = this.lastXWidth * xZoomMult; - const newXStart = this.lastXStart + (this.lastXWidth - newWidth); - this.scale.setXScale(newXStart, this.scale.xEnd); + const { delta: absoluteXDelta } = dragInfo; + const newPxWidth = this.lastXPxWidth - absoluteXDelta; + // cursor goes beyond x-axis - maximum scale is reached + if (newPxWidth < 0) { return; } - - // if multitouch - take the first two touch events and apply delta the following way: - // the touch on the left keeps the initial delta because left => right gesture - // the touch on the right has the reversed delta because the gesture is right => left - if (this.touches.length > 1) { - const left = Math.min(this.touches[0].clientX, this.touches[1].clientX); - absoluteXDelta = left === this.touches[0].clientX ? absoluteXDelta : -absoluteXDelta; - - let xZoomMult; - if (absoluteXDelta < 0) { - xZoomMult = 1 / (1 + (-absoluteXDelta / this.lastXPxWidth) * (FULL_X_WIDTH_ZOOM_FACTOR - 1)); - } else { - xZoomMult = 1 + (absoluteXDelta / this.lastXPxWidth) * (FULL_X_WIDTH_ZOOM_FACTOR - 1); - } - - const newXWidth = this.lastXWidth * xZoomMult; - const delta = (newXWidth - this.lastXWidth) / 2; - const newXStart = this.lastXStart - delta; - const newXEnd = this.lastXEnd + delta; - if (this.lastXStart !== newXStart || this.lastXEnd !== newXEnd) { - this.scale.setXScale(newXStart, newXEnd); - } - } + const xZoomMult = this.lastXPxWidth / newPxWidth; + const newWidth = this.lastXWidth * xZoomMult; + const newXStart = this.lastXStart + (this.lastXWidth - newWidth); + this.scale.setXScale(newXStart, this.scale.xEnd); }; private onXDragEnd = () => { 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 4084ed6..37ac945 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 @@ -6,7 +6,7 @@ import { NumericAxisLabel } from '../labels_generator/numeric-axis-labels.generator'; import { Bounds } from '../../model/bounds.model'; import { FullChartConfig } from '../../chart.config'; -import { CanvasBoundsContainer, CanvasElement, X_AXIS_MOBILE_PADDING } from '../../canvas/canvas-bounds-container'; +import { CanvasBoundsContainer, CanvasElement } from '../../canvas/canvas-bounds-container'; import { CanvasModel } from '../../model/canvas.model'; import { Drawer } from '../../drawers/drawing-manager'; import { ViewportModel } from '../../model/scaling/viewport.model'; @@ -46,7 +46,7 @@ export class XAxisTimeLabelsDrawer implements Drawer { const color = this.config.colors.xAxis.labelTextColor; const labels = this.labelsProvider(); - this.drawLabels(ctx, labels, bounds, color, fontHeight, fontFamily, offsetTop + X_AXIS_MOBILE_PADDING / 2); + this.drawLabels(ctx, labels, bounds, color, fontHeight, fontFamily, offsetTop); ctx.restore(); } } diff --git a/src/chart/components/y_axis/price_labels/y-axis-labels.model.ts b/src/chart/components/y_axis/price_labels/y-axis-labels.model.ts index d9bb519..9bb85e7 100644 --- a/src/chart/components/y_axis/price_labels/y-axis-labels.model.ts +++ b/src/chart/components/y_axis/price_labels/y-axis-labels.model.ts @@ -69,12 +69,7 @@ export class FancyYAxisLabelsModel extends ChartBaseElement { * an easier way to manage custom y-axis labels, than y-axis labels providers, but doesn't support overlapping avoidance */ public readonly customLabels: Record = {}; - private _labelsProviders: Record> = {}; - - get labelsProviders(): Record> { - return this._labelsProviders; - } - + private labelsProviders: Record> = {}; private labelsPositionRecalculatedSubject: Subject = new Subject(); private animFrameId = `anim_cache_${uuid()}`; @@ -281,5 +276,4 @@ export class FancyYAxisLabelsModel extends ChartBaseElement { */ export interface YAxisLabelsProvider { readonly getUnorderedLabels: () => LabelGroup[]; - yAxisBoundsProvider?: () => Bounds; } 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 b789828..d954988 100644 --- a/src/chart/components/y_axis/y-axis-scale.handler.ts +++ b/src/chart/components/y_axis/y-axis-scale.handler.ts @@ -1,8 +1,3 @@ -/* - * Copyright (C) 2019 - 2024 Devexperts Solutions IE Limited - * 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/. - */ /* * Copyright (C) 2019 - 2024 Devexperts Solutions IE Limited * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. @@ -41,8 +36,6 @@ export class YAxisScaleHandler extends ChartBaseElement { lastYHeight: Unit = 0; lastYPxHeight: Pixel = 0; - private dragNDropYComponent: DragNDropYComponent | undefined; - private dblClickCallback: () => void; private touches: TouchList | undefined; @@ -80,15 +73,7 @@ export class YAxisScaleHandler extends ChartBaseElement { }, ); this.addChildEntity(dragNDropYComponent); - this.dragNDropYComponent = dragNDropYComponent; - } - } - - public isDragging(): boolean { - if (!this.dragNDropYComponent) { - return false; } - return Math.abs(this.dragNDropYComponent.draggedPixels) > 0; } protected doActivate(): void { diff --git a/src/chart/inputhandlers/cross-event-producer.component.ts b/src/chart/inputhandlers/cross-event-producer.component.ts index bf093d0..d126740 100644 --- a/src/chart/inputhandlers/cross-event-producer.component.ts +++ b/src/chart/inputhandlers/cross-event-producer.component.ts @@ -9,47 +9,14 @@ import { CanvasBoundsContainer, HitBoundsTest, HitBoundsTestOptionsPartial } fro import { CanvasInputListenerComponent } from '../inputlisteners/canvas-input-listener.component'; import { ChartBaseElement } from '../model/chart-base-element'; import { Unsubscriber } from '../utils/function.utils'; -import { Pixel } from '../model/scaling/viewport.model'; -import { Hover } from './hover-producer.component'; -import { isMobile } from '../utils/device/browser.utils'; /** * [x, y, uuid - Unique identifier for the subscription] */ export type CrossEvent = [number, number, string]; - -interface CrossToolTouchInfo { - // the placement of crosstool when it's set after long touch and further moving happens - fixed: { - x: Pixel; - y: Pixel; - }; - // // the placement of crosstool after touchMove and touchEnd happened - temp: { - x: Pixel; - y: Pixel; - }; - // true after longTouch event, initial movement and touchEnd happen - isSet: boolean; - /** - * additional flag to determine ordinary and long taps - * crosstool shouldn't be hidden after longtouch event even if coordinates are the same - * becomes true after touchStart and false after longTouchStart - */ - isCommonTap: boolean; -} - export class CrossEventProducerComponent extends ChartBaseElement { panesSubscriptions: Partial> = {}; public crossSubject: BehaviorSubject = new BehaviorSubject(null); - // mobile specific crosstool hover and touch info - public crossToolHover: Hover | null = null; - public crossToolTouchInfo: CrossToolTouchInfo = { - fixed: { x: 0, y: 0 }, - temp: { x: 0, y: 0 }, - isSet: false, - isCommonTap: false, - }; constructor( private canvasInputListener: CanvasInputListenerComponent, private canvasBoundsContainer: CanvasBoundsContainer, @@ -113,9 +80,7 @@ export class CrossEventProducerComponent extends ChartBaseElement { const cross: CrossEvent = [point.x, point.y, uuid]; this.crossSubject.next(cross); closeHoverFired = false; - } - // crosstool should be hidden if hovering nonpane only on desktop - if (!enter && !isMobile()) { + } else { this.crossSubject.next(null); closeHoverFired = true; } diff --git a/src/chart/inputhandlers/hover-producer.component.ts b/src/chart/inputhandlers/hover-producer.component.ts index ecd724c..7ce5cbe 100644 --- a/src/chart/inputhandlers/hover-producer.component.ts +++ b/src/chart/inputhandlers/hover-producer.component.ts @@ -19,7 +19,6 @@ import VisualCandle from '../model/visual-candle'; import { isMobile } from '../utils/device/browser.utils'; import { CrossEvent, CrossEventProducerComponent } from './cross-event-producer.component'; import { TimeZoneModel } from '../model/time-zone.model'; -import { checkChartIsMoving, MainCanvasTouchHandler } from './main-canvas-touch.handler'; export interface BaseHover { readonly x: number; @@ -61,9 +60,8 @@ export class HoverProducerComponent extends ChartBaseElement { get hover(): Hover | null { return this.hoverSubject.getValue(); } - public longTouchActivatedSubject: BehaviorSubject = new BehaviorSubject(false); + private longTouchActivatedSubject: BehaviorSubject = new BehaviorSubject(false); private hoverProducerParts: HoverProducerParts; - xFormatter: DateTimeFormatter = () => ''; constructor( private crossEventProducer: CrossEventProducerComponent, @@ -74,7 +72,6 @@ export class HoverProducerComponent extends ChartBaseElement { private canvasBoundsContainer: CanvasBoundsContainer, private paneManager: PaneManager, private timeZoneModel: TimeZoneModel, - private mainCanvasTouchHandler: MainCanvasTouchHandler, private formatterFactory: (format: string) => (timestamp: number) => string, ) { super(); @@ -130,106 +127,46 @@ export class HoverProducerComponent extends ChartBaseElement { }), ); this.addRxSubscription(this.scale.xChanged.subscribe(() => this.fireLastCross())); - this.addRxSubscription( - merge(this.chartModel.candlesSetSubject, this.timeZoneModel.observeTimeZoneChanged()).subscribe(() => - this.recalculateCrossToolXFormatter(), - ), - ); - //#region crosstool touch events, special handling for mobile this.addRxSubscription( this.canvasInputListener.observeTouchStart().subscribe(event => { - this.crossEventProducer.crossToolTouchInfo.isCommonTap = true; - const { clientX, clientY } = event.touches[0]; - - // if common tap - fire hover - if (!this.longTouchActivatedSubject.getValue()) { - const paneId = this.paneManager.getPaneIfHit({ x: clientX, y: clientY })?.uuid || ''; - this.createAndFireHover([clientX, clientY, paneId]); - } else { - // update crosstool placement coordinates - this.crossEventProducer.crossToolTouchInfo.temp = { - x: clientX - this.canvasBoundsContainer.canvasOnPageLocation.x, - y: clientY - this.canvasBoundsContainer.canvasOnPageLocation.y, - }; + const x = event.touches[0].clientX - this.canvasBoundsContainer.canvasOnPageLocation.x; + const y = event.touches[0].clientY - this.canvasBoundsContainer.canvasOnPageLocation.y; + const candle = this.chartModel.candleFromX(x, true); + const paneId = this.paneManager.getPaneIfHit({ x, y })?.uuid || ''; + if (candle) { + this.createAndFireHover([x, y, paneId]); } }), ); + + // special handling for mobile // on long touch - disable panning and show cross tool const hitTest = this.canvasBoundsContainer.getBoundsHitTest(CanvasElement.ALL_PANES); this.addRxSubscription( this.canvasInputListener.observeLongTouchStart(hitTest).subscribe(event => { - this.crossEventProducer.crossToolHover = null; - this.crossEventProducer.crossToolTouchInfo.isCommonTap = false; - // don't lock chart and show crosshair if chart is being moved, crosstool is not enabled or we do pinch event - const longTouchCrosshairPredicate = - this.mainCanvasTouchHandler.canvasTouchInfo.isMoving || - this.chartModel.config.components.crossTool.type === 'none' || - event.touches.length > 1; - - if (longTouchCrosshairPredicate) { - return; - } - + this.paneManager.chartPanComponent.deactivatePanHandlers(); this.longTouchActivatedSubject.next(true); - this.crossEventProducer.crossToolTouchInfo.isSet = false; - const x = event.touches[0].clientX - this.canvasBoundsContainer.canvasOnPageLocation.x; const y = event.touches[0].clientY - this.canvasBoundsContainer.canvasOnPageLocation.y; - - this.crossEventProducer.crossToolTouchInfo.fixed = { - x, - y, - }; - const paneId = this.paneManager.getPaneIfHit({ x, y })?.uuid || ''; this.createAndFireHover([x, y, paneId]); this.crossEventProducer.crossSubject.next([x, y, paneId]); - this.paneManager.chartPanComponent.setChartPanningOptions(false, false); }), ); this.addRxSubscription( - this.canvasInputListener.observeTouchEndDocument().subscribe(event => { - const { clientX, clientY } = event.changedTouches[0]; - const { fixed, temp } = this.crossEventProducer.crossToolTouchInfo; - - const x = clientX - this.canvasBoundsContainer.canvasOnPageLocation.x; - const y = clientY - this.canvasBoundsContainer.canvasOnPageLocation.y; - - // common tap without moving, hide crosstool - if ( - this.crossEventProducer.crossToolTouchInfo.isCommonTap && - !checkChartIsMoving(x, temp.x, y, temp.y) - ) { - this.paneManager.chartPanComponent.setChartPanningOptions(true, true); + this.canvasInputListener.observeTouchEndDocument().subscribe(() => { + this.paneManager.chartPanComponent.activateChartPanHandlers(); + if (this.longTouchActivatedSubject.getValue()) { this.longTouchActivatedSubject.next(false); this.crossEventProducer.fireCrossClose(); - this.crossEventProducer.crossToolHover = null; - } - - if (!this.crossEventProducer.crossToolTouchInfo.isSet) { - this.crossEventProducer.crossToolTouchInfo.isSet = true; - this.crossEventProducer.crossToolTouchInfo.fixed = { - x, - y, - }; - this.crossEventProducer.crossToolTouchInfo.isSet = true; - } else { - const pane = this.crossEventProducer.crossToolHover?.paneId ?? 'CHART'; - const paneBounds = this.canvasBoundsContainer.getBounds(CanvasElement.PANE_UUID(pane)); - - const paneYStart = paneBounds.y + 5; - const paneYEnd = paneBounds.y + paneBounds.height - 5; - - const xDiff = x - temp.x; - const yDiff = y - temp.y; - - const newX = fixed.x < 0 ? 0 : fixed.x > paneBounds.width ? paneBounds.width : (fixed.x += xDiff); - const newY = fixed.y < paneYStart ? paneYStart : fixed.y > paneYEnd ? paneYEnd : (fixed.y += yDiff); - this.crossEventProducer.crossToolTouchInfo.fixed = { x: newX, y: newY }; } }), ); - //#endregion + this.addRxSubscription( + merge(this.chartModel.candlesSetSubject, this.timeZoneModel.observeTimeZoneChanged()).subscribe(() => + this.recalculateCrossToolXFormatter(), + ), + ); } /** @@ -261,7 +198,7 @@ export class HoverProducerComponent extends ChartBaseElement { * @returns {Hover | undefined} - The hover object or undefined if there are no candles in the chart model. * @todo Check if uuid is still useful here. */ - public createHover(x: number, y: number, uuid: string): Hover | undefined { + private createHover(x: number, y: number, uuid: string): Hover | undefined { if (this.chartModel.getCandles().length === 0) { return; } @@ -338,12 +275,9 @@ export class HoverProducerComponent extends ChartBaseElement { private fireHover(hover?: Hover) { if (hover) { // special handling for mobile - // set active candle + show cross tool only when crosstool is active + // set active candle + show cross tool only when long tap if (isMobile() && this.config.components.crossTool.type !== 'none') { - const crossToolHover = this.crossEventProducer.crossToolHover; - const candle = crossToolHover - ? crossToolHover.candleHover?.visualCandle.candle - : hover.candleHover?.visualCandle.candle; + const candle = hover.candleHover?.visualCandle.candle; candle && this.chartModel.mainCandleSeries.setActiveCandle(candle); } this.hoverSubject.next(hover); diff --git a/src/chart/inputhandlers/main-canvas-touch.handler.ts b/src/chart/inputhandlers/main-canvas-touch.handler.ts index 70818c6..ed2d8d1 100644 --- a/src/chart/inputhandlers/main-canvas-touch.handler.ts +++ b/src/chart/inputhandlers/main-canvas-touch.handler.ts @@ -8,9 +8,6 @@ import { CanvasInputListenerComponent } from '../inputlisteners/canvas-input-lis import { ScaleModel } from '../model/scale.model'; import { ChartAreaPanHandler } from '../components/chart/chart-area-pan.handler'; import { HitBoundsTest } from '../canvas/canvas-bounds-container'; -import { Pixel } from '../model/scaling/viewport.model'; - -export const PIXELS_FOR_MOVE = 2; /** * Handles chart touch events. @@ -18,15 +15,6 @@ export const PIXELS_FOR_MOVE = 2; export class MainCanvasTouchHandler extends ChartBaseElement { // 2 candles indexes touched by 2 fingers when pinching private touchedCandleIndexes: [number, number] = [0, 0]; - // stores the information about touch events - public canvasTouchInfo: { - touchStart: { x: Pixel; y: Pixel }; - isMoving?: boolean; - } = { - touchStart: { x: 0, y: 0 }, - // uses touch start to determine if chart is being moved - isMoving: false, - }; constructor( private chartAreaPanHandler: ChartAreaPanHandler, private scale: ScaleModel, @@ -60,12 +48,6 @@ export class MainCanvasTouchHandler extends ChartBaseElement { * @returns {void} */ private handleTouchStartEvent(e: TouchEvent) { - const { clientX, clientY } = e.touches[0]; - - if (e.touches.length === 1) { - this.canvasTouchInfo.touchStart = { x: clientX, y: clientY }; - } - if (e.touches.length === 2) { this.chartAreaPanHandler.deactivate(); // @ts-ignore @@ -80,13 +62,6 @@ export class MainCanvasTouchHandler extends ChartBaseElement { * @returns {void} */ private handleTouchMoveEvent(e: TouchEvent): void { - const { clientX, clientY } = e.touches[0]; - const { touchStart } = this.canvasTouchInfo; - - if (e.touches.length === 1) { - this.canvasTouchInfo.isMoving = checkChartIsMoving(clientX, touchStart.x, clientY, touchStart.y); - } - if (e.touches.length === 2) { this.pinchHandler(this.touchedCandleIndexes, this.getXPositions(e)); } @@ -96,7 +71,6 @@ export class MainCanvasTouchHandler extends ChartBaseElement { * @returns {void} */ private handleTouchEndEvent(e: TouchEvent): void { - this.canvasTouchInfo.isMoving = false; // zero touches means the user stopped resizing completely (both fingers are up) if (e.touches.length === 0) { this.chartAreaPanHandler.activate(); @@ -141,6 +115,3 @@ export class MainCanvasTouchHandler extends ChartBaseElement { this.scale.setXScale(first, last); } } - -export const checkChartIsMoving = (x1: Pixel, x2: Pixel, y1: Pixel, y2: Pixel, pixelsToMove: Pixel = PIXELS_FOR_MOVE) => - Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) > pixelsToMove; diff --git a/src/chart/inputlisteners/canvas-input-listener.component.ts b/src/chart/inputlisteners/canvas-input-listener.component.ts index 6f78035..b1d8fb8 100644 --- a/src/chart/inputlisteners/canvas-input-listener.component.ts +++ b/src/chart/inputlisteners/canvas-input-listener.component.ts @@ -248,8 +248,10 @@ export class CanvasInputListenerComponent extends ChartBaseElement { this.addSubscription(subscribeListener(this.element, doubleTapListenerProducer, 'touchend')); // workaround to handle long touch start/end for iOS - const longTouchListeners = (e: TouchEvent, delay = 200) => { + const longTouchListeners = (e: TouchEvent, delay = 700, pixelsForMoveReset = 3) => { e.preventDefault(); + const initialCoords = { x: e.touches[0].clientX, y: e.touches[0].clientY }; + let coords = { x: 0, y: 0 }; let longTouchStart = false; let timerLongTouchStart: ReturnType | null = null; @@ -261,8 +263,13 @@ export class CanvasInputListenerComponent extends ChartBaseElement { const touchMoveHandler = (e: TouchEvent) => { e.preventDefault(); + coords = { x: e.touches[0].clientX, y: e.touches[0].clientY }; // clear long touch timeout if move area is bigger than pixelsForMove - if (e.touches.length > 1) { + if ( + Math.sqrt(Math.pow(coords.x - initialCoords.x, 2) + Math.pow(coords.y - initialCoords.y, 2)) > + pixelsForMoveReset || + e.touches.length > 1 + ) { timerLongTouchStart && clearTimeout(timerLongTouchStart); } }; diff --git a/src/chart/model/candle-series.model.ts b/src/chart/model/candle-series.model.ts index 47d89c9..caca538 100644 --- a/src/chart/model/candle-series.model.ts +++ b/src/chart/model/candle-series.model.ts @@ -162,7 +162,7 @@ export class CandleSeriesModel extends DataSeriesModel { */ recalculateZippedHighLow(): HighLowWithIndex { return (this.zippedHighLow = calculateCandlesHighLow( - this.visualPoints.slice(this.dataIdxStart, this.dataIdxEnd + 1), + this.visualPoints.slice(this.dataIdxStart, this.dataIdxEnd), )); } diff --git a/src/chart/model/data-series.model.ts b/src/chart/model/data-series.model.ts index d9b38a8..c18341a 100644 --- a/src/chart/model/data-series.model.ts +++ b/src/chart/model/data-series.model.ts @@ -5,7 +5,6 @@ */ import { YExtentComponent } from '../components/pane/extent/y-extent-component'; import { DataSeriesYAxisLabelsProvider } from '../components/y_axis/price_labels/data-series-y-axis-labels.provider'; -import { LabelsGroups } from '../components/y_axis/price_labels/y-axis-labels.model'; import { binarySearch, create2DArray, lastOf, slice2DArray } from '../utils/array.utils'; import { floorToDPR } from '../utils/device/device-pixel-ratio.utils'; import { MathUtils } from '../utils/math.utils'; @@ -152,7 +151,6 @@ export class DataSeriesModel< this.addRxSubscription( this.scale.scaleInversedSubject.subscribe(() => { this.recalculateVisualPoints(); - this.extentComponent.dynamicObjectsCanvasModel.fireDraw(); }), ); } @@ -187,7 +185,6 @@ export class DataSeriesModel< * @param extent */ public moveToExtent(extent: YExtentComponent) { - const prevExtent = { ...this.extentComponent }; this.extentComponent.removeDataSeries(this); this.extentComponent = extent; this.scale = extent.scale; @@ -199,29 +196,6 @@ export class DataSeriesModel< ); this.yAxisLabelProvider.yAxisBoundsProvider = extent.getYAxisBounds; this.yAxisLabelProvider.axisState = extent.yAxis?.state; - // move data series labels - const prevExtentMainLabels = prevExtent.yAxis.model.fancyLabelsModel.labelsProviders[LabelsGroups.MAIN]; - const dataSeriesLabelsId = - prevExtentMainLabels && Object.keys(prevExtentMainLabels).find(p => this.parentId && p === this.parentId); - const currentMainLabels = this.extentComponent.yAxis.model.fancyLabelsModel.labelsProviders[LabelsGroups.MAIN]; - if (dataSeriesLabelsId) { - const labelsProvider = prevExtentMainLabels[dataSeriesLabelsId]; - labelsProvider.yAxisBoundsProvider = extent.getYAxisBounds; - // main group is not created yet (new extent without labels) or main group exists but no data series labels so far - if (!currentMainLabels || (currentMainLabels && !currentMainLabels[dataSeriesLabelsId])) { - // create new data series labels group on the new extent - this.extentComponent.yAxis.model.fancyLabelsModel.registerYAxisLabelsProvider( - LabelsGroups.MAIN, - labelsProvider, - dataSeriesLabelsId, - ); - // remove labels from previous extent - prevExtent.yAxis.model.fancyLabelsModel.unregisterYAxisLabelsProvider( - LabelsGroups.MAIN, - dataSeriesLabelsId, - ); - } - } // shut down old subscriptions this.deactivate(); // and apply new ones (with updated scaleModel) diff --git a/src/chart/utils/math.utils.ts b/src/chart/utils/math.utils.ts index 8470903..1bafde7 100644 --- a/src/chart/utils/math.utils.ts +++ b/src/chart/utils/math.utils.ts @@ -1,10 +1,10 @@ +import { StringTMap } from './object.utils'; + /* * Copyright (C) 2019 - 2024 Devexperts Solutions IE Limited * 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 { StringTMap } from './object.utils'; - const MAX_DECIMAL_DIGITS = 14; // Array of powers of 10. Used in roundDecimal to walk through mantissa.