UNPKG

@devexperts/dxcharts-lite

Version:
348 lines (347 loc) 18.2 kB
/* * Copyright (C) 2019 - 2026 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 { BehaviorSubject, merge } from 'rxjs'; import { filter, switchMap } from 'rxjs/operators'; import { CHART_UUID, CanvasElement } from '../canvas/canvas-bounds-container'; import { CandleHoverProducerPart } from '../model/candle-hover'; import { ChartBaseElement } from '../model/chart-base-element'; import { CompareSeriesHoverProducerPart } from '../model/compare-series-hover'; import { recalculateXFormatter } from '../model/date-time.formatter'; import { isMobile } from '../utils/device/browser.utils'; import { checkChartIsMoving } from './main-canvas-touch.handler'; /** * Produces the Hover event. * Hover is used for displaying values in legend and NOT related to displaying cross tool. */ export class HoverProducerComponent extends ChartBaseElement { get hover() { return this.hoverSubject.getValue(); } constructor(crossEventProducer, scale, config, chartModel, canvasInputListener, canvasBoundsContainer, paneManager, timeZoneModel, mainCanvasTouchHandler, formatterFactory) { super(); this.crossEventProducer = crossEventProducer; this.scale = scale; this.config = config; this.chartModel = chartModel; this.canvasInputListener = canvasInputListener; this.canvasBoundsContainer = canvasBoundsContainer; this.paneManager = paneManager; this.timeZoneModel = timeZoneModel; this.mainCanvasTouchHandler = mainCanvasTouchHandler; this.formatterFactory = formatterFactory; this.hoverSubject = new BehaviorSubject(null); this.longTouchActivatedSubject = new BehaviorSubject(false); /** * When true, mobile long-touch (e.g. 200ms) does not activate crosshair / disable pan. */ this.longTouchCrosshairSuppressed = false; this.xFormatter = () => ''; const candleHoverProducerPart = new CandleHoverProducerPart(this.chartModel); const compareSeriesHoverProducerPart = new CompareSeriesHoverProducerPart(this.chartModel); this.hoverProducerParts = { candleHover: candleHoverProducerPart, compareSeriesHover: compareSeriesHoverProducerPart, }; } /** * This method is responsible for activating the chart hover functionality. It subscribes to several observables to * update the hover when the chart is updated or when the user interacts with it. It also handles special behavior * for mobile devices, such as disabling panning and showing the cross tool on long touch. * * @protected * @memberof ChartHover * @returns {void} */ doActivate() { super.doActivate(); this.addRxSubscription( // required for initial legend initialization, do not show cross tool this.chartModel.candlesSetSubject .pipe( // check the scale is valid before doing candle-based hover event switchMap(() => this.scale.initialViewportValidSubject.pipe(filter(Boolean)))) .subscribe(() => { const lastCandle = this.chartModel.getLastVisualCandle(); lastCandle && this.createAndFireHoverFromCandle(lastCandle); })); this.addRxSubscription(this.chartModel.candlesUpdatedSubject.subscribe(() => { // update hover if its timestamp is equal or greater than last candle's one const lastCandle = this.chartModel.getLastVisualCandle(); if (this.hover !== null && lastCandle !== undefined) { if (lastCandle.candle.timestamp <= this.hover.timestamp) { this.updateHover(lastCandle); } } })); this.addRxSubscription(this.crossEventProducer.crossSubject.subscribe((cross) => { if (cross === null) { this.hoverSubject.next(null); } else { this.createAndFireHover(cross); } })); 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 => { var _a; this.crossEventProducer.crossToolTouchInfo.isCommonTap = true; const { clientX, clientY } = event.touches[0]; // if common tap - fire hover if (!this.longTouchActivatedSubject.getValue()) { const paneId = ((_a = this.paneManager.getPaneIfHit({ x: clientX, y: clientY })) === null || _a === void 0 ? void 0 : _a.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, }; } })); // 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 => { var _a; if (this.longTouchCrosshairSuppressed) { return; } 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.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 = ((_a = this.paneManager.getPaneIfHit({ x, y })) === null || _a === void 0 ? void 0 : _a.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 => { var _a, _b, _c, _d, _e, _f, _g, _h; 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.resetCrossTool(); return; } if (!this.crossEventProducer.crossToolTouchInfo.isSet) { this.crossEventProducer.crossToolTouchInfo.isSet = true; const crossToolHover = this.crossEventProducer.crossToolHover; const anchorX = (_a = crossToolHover === null || crossToolHover === void 0 ? void 0 : crossToolHover.x) !== null && _a !== void 0 ? _a : x; const anchorY = (_b = crossToolHover === null || crossToolHover === void 0 ? void 0 : crossToolHover.y) !== null && _b !== void 0 ? _b : y; this.crossEventProducer.crossToolTouchInfo.temp = { x: anchorX, y: anchorY, }; this.crossEventProducer.crossToolTouchInfo.fixed = { x: anchorX, y: anchorY, }; const paneId = (_e = (_c = crossToolHover === null || crossToolHover === void 0 ? void 0 : crossToolHover.paneId) !== null && _c !== void 0 ? _c : (_d = this.paneManager.getPaneIfHit({ x: anchorX, y: anchorY })) === null || _d === void 0 ? void 0 : _d.uuid) !== null && _e !== void 0 ? _e : ''; const hover = crossToolHover !== null && crossToolHover !== void 0 ? crossToolHover : this.hover; if (!hover) { return; } this.crossEventProducer.crossToolHover = Object.assign(Object.assign({}, hover), { x: anchorX, y: anchorY, paneId }); this.crossEventProducer.crossSubject.next([anchorX, anchorY, paneId]); } else { const pane = (_g = (_f = this.crossEventProducer.crossToolHover) === null || _f === void 0 ? void 0 : _f.paneId) !== null && _g !== void 0 ? _g : '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 }; this.crossEventProducer.crossToolTouchInfo.temp = { x: newX, y: newY }; const hover = (_h = this.crossEventProducer.crossToolHover) !== null && _h !== void 0 ? _h : this.hover; if (!hover) { return; } this.crossEventProducer.crossToolHover = Object.assign(Object.assign({}, hover), { x: newX, y: newY, paneId: pane }); this.crossEventProducer.crossSubject.next([newX, newY, pane]); } })); //#endregion } /** * Recalculates the cross tool X formatter. * @function * @private * @returns {void} */ recalculateCrossToolXFormatter() { const xAxisLabelFormat = this.config.components.crossTool.xAxisLabelFormat; this.xFormatter = recalculateXFormatter(xAxisLabelFormat, this.chartModel.getPeriod(), this.formatterFactory); } /** * Creates a hover object from a VisualCandle object. * @param {VisualCandle} candle - The VisualCandle object to create the hover from. * @returns {Hover | undefined} - The created hover object or undefined if the input is invalid. */ createHoverFromCandle(candle) { const x = candle.xCenter(this.scale); const y = this.scale.toY(candle.close); return this.createHover(x, y, CHART_UUID); } /** * Creates a hover object based on the provided x and y coordinates. * @param {number} x - The x coordinate of the hover. * @param {number} y - The y coordinate of the hover. * @param {string} uuid * @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. */ createHover(x, y, uuid) { if (this.chartModel.getCandles().length === 0) { return; } const candle = this.chartModel.candleFromX(x, true); const timestamp = candle.timestamp; const hover = { x, y, timestamp, timeFormatted: this.xFormatter(timestamp), paneId: uuid, }; // eslint-disable-next-line no-restricted-syntax const combinedHoverParts = Object.entries(this.hoverProducerParts).reduce((res, part) => (Object.assign(Object.assign({}, res), { [part[0]]: part[1].getData(hover) })), // eslint-disable-next-line no-restricted-syntax {}); return Object.assign(Object.assign({}, hover), combinedHoverParts); } /** * Creates a hover from a VisualCandle object and fires it. * @param {VisualCandle} candle - The VisualCandle object to create the hover from. */ createAndFireHoverFromCandle(candle) { const hover = this.createHoverFromCandle(candle); this.fireHover(hover); } /** * Update current hover using a VisualCandle and fires it. * @param {VisualCandle} candle - The VisualCandle object to create the hover from. */ updateHover(candle) { var _a, _b; const updatedHover = this.createHoverFromCandle(candle); if (this.hover && updatedHover) { const shouldKeepSetTouchCrosshair = isMobile() && this.longTouchActivatedSubject.getValue() && this.crossEventProducer.crossSubject.getValue() !== null; const crossToolHover = this.crossEventProducer.crossToolHover; const hoverX = shouldKeepSetTouchCrosshair ? ((_a = crossToolHover === null || crossToolHover === void 0 ? void 0 : crossToolHover.x) !== null && _a !== void 0 ? _a : this.hover.x) : this.hover.x; const hoverY = shouldKeepSetTouchCrosshair ? ((_b = crossToolHover === null || crossToolHover === void 0 ? void 0 : crossToolHover.y) !== null && _b !== void 0 ? _b : this.hover.y) : this.hover.y; const hover = Object.assign(Object.assign({}, updatedHover), { x: hoverX, y: hoverY }); this.fireHover(hover); } } /** * Creates a hover element at the specified coordinates and fires it with the option to show the cross tool * @param {CrossEvent} [x,y] - The coordinates where the hover element will be created * @param {boolean} [showCrossTool=true] - Whether to show the cross tool or not * @returns {void} */ createAndFireHover([x, y, uuid]) { const hover = this.createHover(x, y, uuid); this.fireHover(hover); } /** * Private method that handles the hover event. If a hover event is provided, it sets the last hover to the provided hover. * If the device is mobile and the cross tool type is not 'none', it sets the active candle to the hovered candle only when a long tap is detected. * The showCrossToolOverride is set to true only when a long tap is detected on mobile devices, otherwise it is set to the value of showCrossTool parameter. * Finally, it fires the EVENT_HOVER event with the provided hover and showCrossToolOverride. * If no hover event is provided, it fires the EVENT_CLOSE_HOVER event. * * @param {Hover} [hover] - The hover event to handle. * @returns {void} */ fireHover(hover) { var _a, _b; if (hover) { // special handling for mobile // set active candle + show cross tool only when crosstool is active if (isMobile() && this.config.components.crossTool.type !== 'none') { const crossToolHover = this.crossEventProducer.crossToolHover; const candle = crossToolHover ? (_a = crossToolHover.candleHover) === null || _a === void 0 ? void 0 : _a.visualCandle.candle : (_b = hover.candleHover) === null || _b === void 0 ? void 0 : _b.visualCandle.candle; candle && this.chartModel.mainCandleSeries.setActiveCandle(candle); } this.hoverSubject.next(hover); } else { this.crossEventProducer.fireCrossClose(); } } /** * Fires the last hover update if there is a last cross. */ fireLastCross() { // fire last hover update const lastCross = this.crossEventProducer.crossSubject.getValue(); if (lastCross) { this.createAndFireHover(lastCross); } } setLongTouchCrosshairSuppressed(value) { this.longTouchCrosshairSuppressed = value; } /** * Resets the current crosshair/touch state back to the default mobile idle state. */ resetCrossTool(clearMobile = false) { this.paneManager.chartPanComponent.setChartPanningOptions(true, true); this.longTouchActivatedSubject.next(false); this.crossEventProducer.fireCrossClose(); this.crossEventProducer.crossToolHover = null; this.crossEventProducer.crossToolTouchInfo.isSet = false; if (clearMobile) { this.crossEventProducer.crossToolTouchInfo.isCommonTap = false; this.crossEventProducer.crossToolTouchInfo.fixed = { x: 0, y: 0 }; this.crossEventProducer.crossToolTouchInfo.temp = { x: 0, y: 0 }; } } /** * Registers a hover producer part with the given id. * * @param {string} id - The id of the hover producer part. * @param {HoverProducerPart} hoverProducerPart - The hover producer part to register. * @returns {void} */ registerHoverProducerPart(id, hoverProducerPart) { this.hoverProducerParts = Object.assign(Object.assign({}, this.hoverProducerParts), { [id]: hoverProducerPart }); } /** * Removes a hover producer part from the hoverProducerParts object. * @param {string} id - The id of the hover producer part to be removed. * @returns {void} */ unregisterHoverProducerPart(id) { delete this.hoverProducerParts[id]; } }