@devexperts/dxcharts-lite
Version:
181 lines (180 loc) • 7.93 kB
JavaScript
/*
* 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 { animationFrameScheduler } from 'rxjs';
import { throttleTime, filter, debounceTime } from 'rxjs/operators';
import { CanvasElement } from '../../../../../canvas/canvas-bounds-container';
import { pixelsToUnits } from '../../../../../model/scaling/viewport.model';
import { deviceDetector } from '../../../../../utils/device/device-detector.utils';
import { ONE_FRAME_MS } from '../../../../../utils/numeric-constants.utils';
import { EVENT_RESIZED } from '../../../../../events/events';
import { DEFAULT_THROTTLE_MS, LARGE_SCREEN_PIXEL_THRESHOLD, LARGE_SCREEN_THROTTLE_MS, LOW_WHEEL_EVENTS_THRESHOLD, SAFARI_THROTTLE_MS, WHEEL_MONITORING_INTERVAL_MS, } from '../../safari-performance.model';
import { ChartAreaPanHandler } from '../../../../../components/chart/chart-area-pan.handler';
/**
* SafariChartAreaPanHandler is a class that handles the panning and zooming of the chart area for Safari.
*/
export class SafariChartAreaPanHandler extends ChartAreaPanHandler {
constructor(bus, config, scale, canvasInputListener, canvasBoundsContainer, canvasAnimation, chartPanComponent, hitTestCanvasModel) {
super(bus, config, scale, canvasInputListener, canvasBoundsContainer, canvasAnimation, chartPanComponent, hitTestCanvasModel);
this.bus = bus;
this.config = config;
this.scale = scale;
this.canvasInputListener = canvasInputListener;
this.canvasBoundsContainer = canvasBoundsContainer;
this.canvasAnimation = canvasAnimation;
this.chartPanComponent = chartPanComponent;
this.hitTestCanvasModel = hitTestCanvasModel;
this.currentPoint = { x: 0, y: 0 };
this.xDraggedCandlesDelta = 0;
this.lastXStart = 0;
this.wheelThrottleTime = ONE_FRAME_MS;
this.wheelSubscription = null;
this.initFrameId = null;
this.isActivated = false;
this.lastWheelTime = 0;
this.wheelCount = 0;
this.consecutiveLowWheelEvents = 0;
this.chartPanningOptions = { horizontal: true, vertical: true };
}
/**
* This method is used to activate the zoom functionality of the canvas. It extends the doActivate method of the parent class.
* @protected
* @returns {void}
*/
doActivate() {
if (this.isActivated) {
return;
}
this.isActivated = true;
super.doActivate();
this.canvasInputListener.initializeRectCache();
this.createWheelSubscription();
this.initFrameId = requestAnimationFrame(() => {
this.bus.fireDraw();
});
this.addRxSubscription(this.chartPanComponent.chartBaseModel.dataPrependSubject
.asObservable()
.subscribe(({ prependedCandlesWidth }) => {
this.lastXStart += prependedCandlesWidth;
}));
this.addRxSubscription(this.bus
.observe(EVENT_RESIZED)
.pipe(debounceTime(100))
.subscribe(() => {
if (this.scale && typeof this.scale.haltAnimation === 'function') {
this.scale.haltAnimation();
}
}));
}
/**
* Creates wheel subscription with optimized throttling
*/
createWheelSubscription() {
if (this.wheelSubscription) {
this.wheelSubscription.unsubscribe();
this.wheelSubscription = null;
}
const allPanesHitTest = this.canvasBoundsContainer.getBoundsHitTest(CanvasElement.ALL_PANES);
const canvasBounds = this.canvasBoundsContainer.getBounds(CanvasElement.CANVAS);
const totalPixels = canvasBounds.width * canvasBounds.height;
if (totalPixels > 0) {
if (totalPixels > LARGE_SCREEN_PIXEL_THRESHOLD) {
this.wheelThrottleTime = LARGE_SCREEN_THROTTLE_MS;
}
else {
this.wheelThrottleTime = SAFARI_THROTTLE_MS;
}
}
else {
this.wheelThrottleTime = DEFAULT_THROTTLE_MS;
}
this.wheelSubscription = this.canvasInputListener
.observeWheel(allPanesHitTest)
.pipe(filter(() => this.chartPanningOptions.horizontal && this.chartPanningOptions.vertical), filter((e) => {
const device = deviceDetector();
if (device === 'apple') {
const minDelta = 1.0;
return Math.abs(e.deltaY) > minDelta || Math.abs(e.deltaX) > minDelta;
}
return true;
}), throttleTime(this.wheelThrottleTime, animationFrameScheduler, { trailing: true, leading: true }))
.subscribe(e => {
const now = performance.now();
if (!this.lastWheelTime) {
this.lastWheelTime = now;
this.wheelCount = 0;
}
this.wheelCount++;
if (now - this.lastWheelTime > WHEEL_MONITORING_INTERVAL_MS) {
const wheelEventsPerSecond = Math.round((this.wheelCount * 1000) / (now - this.lastWheelTime));
if (wheelEventsPerSecond < LOW_WHEEL_EVENTS_THRESHOLD) {
this.consecutiveLowWheelEvents++;
if (this.consecutiveLowWheelEvents >= 1) {
this.triggerPerformanceCleanup();
this.consecutiveLowWheelEvents = 0;
}
}
else {
this.consecutiveLowWheelEvents = 0;
}
this.lastWheelTime = now;
this.wheelCount = 0;
}
const device = deviceDetector();
const direction = device === 'apple' || device === 'mobile' ? -1 : 1;
const deltaX = 0 + e.deltaX * direction;
const deltaY = 0 + e.deltaY * direction;
if (e.ctrlKey) {
const zoomSensitivity = this.calculateDynamicSesitivity(e, this.config.scale.zoomSensitivity.wheel);
this.zoomXHandler(e, zoomSensitivity);
this.bus.fireDraw();
return;
}
if (deltaY !== 0 && Math.abs(deltaY) > Math.abs(deltaX)) {
const zoomSensitivity = this.calculateDynamicSesitivity(e, this.config.scale.zoomSensitivity.wheel);
this.zoomXHandler(e, zoomSensitivity);
this.bus.fireDraw();
}
if (deltaX !== 0 && Math.abs(deltaX) > Math.abs(deltaY)) {
const unitsDelta = pixelsToUnits(deltaX, this.scale.zoomX);
this.scale.moveXStart(this.scale.xStart - unitsDelta);
this.bus.fireDraw();
return;
}
});
this.addRxSubscription(this.wheelSubscription);
}
/**
* Manual cleanup trigger for immediate performance recovery
*/
triggerPerformanceCleanup() {
window.dispatchEvent(new Event('resize'));
if (window.gc) {
window.gc();
}
if (this.initFrameId) {
cancelAnimationFrame(this.initFrameId);
this.initFrameId = null;
}
if (this.scale && typeof this.scale.haltAnimation === 'function') {
this.scale.haltAnimation();
}
this.consecutiveLowWheelEvents = 0;
this.wheelCount = 0;
this.lastWheelTime = 0;
}
doDeactivate() {
if (this.wheelSubscription) {
this.wheelSubscription.unsubscribe();
this.wheelSubscription = null;
}
if (this.initFrameId) {
cancelAnimationFrame(this.initFrameId);
this.initFrameId = null;
}
this.isActivated = false;
super.doDeactivate();
}
}