UNPKG

@devexperts/dxcharts-lite

Version:
305 lines (304 loc) 10.4 kB
/* * Copyright (C) 2019 - 2025 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, Subject } from 'rxjs'; import { distinctUntilChanged, map, share } from 'rxjs/operators'; import { ChartBaseElement } from '../chart-base-element'; import { keys } from '../../utils/object.utils'; export const calculateZoom = (viewportUnits, viewportPixels) => viewportUnits / viewportPixels; export const unitToPixels = (units, zoom) => units / zoom; export const pixelsToUnits = (px, zoom) => px * zoom; export const unitToPercent = (value, baseline) => ((value - baseline) * 100) / baseline; export const percentToUnit = (percent, baseline) => (percent * baseline) / 100 + baseline; // log calculations export const calcLogValue = (value) => Math.log2(value); export const logValueToUnit = (logValue) => Math.pow(2, logValue); /** * Abstract viewport model. * Viewport has 4 coordinates: xStart, xEnd, yStart and yEnd - all stored in {@link Unit}. * Main methods: * - {@link toX} - converts xUnits to xPixels * - {@link toY} - converts yUnits to yPixels * - {@link fromX} - converts xPixels to xUnits * - {@link fromY} - converts yPixels to yUnits * * To convert unit-pixels and vice versa uses {@link getBounds} method. * zoomX and zoomY are proportions between units and pixels. */ export class ViewportModel extends ChartBaseElement { constructor() { super(...arguments); //region base viewport model props this._xStart = 0; this._xEnd = 0; this._yStart = 0; this._yEnd = 0; this._zoomX = 1; this._zoomY = 1; //endregion // toggle Y inverse mode this._inverseY = false; //region subjects this.changed = new Subject(); this.xChanged = this.changed.pipe(map(() => ({ start: this.xStart, end: this.xEnd, })), distinctUntilChanged((p, c) => p.start === c.start && p.end === c.end), share()); this.yChanged = this.changed.pipe(map(() => ({ start: this.yStart, end: this.yEnd, })), distinctUntilChanged((p, c) => p.start === c.start && p.end === c.end), share()); this.initialViewportValidSubject = new BehaviorSubject(false); } //endregion doActivate() { super.doActivate(); this.addRxSubscription(this.changed.subscribe(() => { !this.initialViewportValidSubject.getValue() && this.initialViewportValidSubject.next(this.isViewportValid()); })); } doDeactivate() { super.doDeactivate(); this.changed.complete(); } //region conversion methods /** * Converts a unit value to pixels based on the current zoom level and xStart value. * @param {Unit} unit - The unit value to be converted to pixels. * @returns {Pixel} - The converted pixel value. */ toX(unit) { return this.getBounds().x + unitToPixels(unit - this.xStart, this.zoomX); } /** * Converts a unit value to pixels based on the current zoom level in the x-axis. * @param {Unit} unit - The unit value to be converted to pixels. * @returns {Pixel} - The converted value in pixels. */ xPixels(unit) { return unitToPixels(unit, this.zoomX); } /** * Converts a given unit value to pixel value in the Y-axis. * @param {Unit} unit - The unit value to be converted to pixel value. * @returns {Pixel} - The pixel value of the given unit value in the Y-axis. */ toY(unit) { const bounds = this.getBounds(); if (this.inverseY) { return bounds.y + unitToPixels(unit - this.yStart, this.zoomY); } else { // inverse by default because canvas calculation [0,0] point starts from top-left corner return bounds.y + bounds.height - unitToPixels(unit - this.yStart, this.zoomY); } } /** * Converts a unit value to pixels based on the current zoom level in the Y axis. * @param {Unit} unit - The unit value to be converted to pixels. * @returns {Pixel} - The converted value in pixels. */ yPixels(unit) { return unitToPixels(unit, this.zoomY); } /** * Converts a pixel value to a unit value based on the current zoom level and xStart value. * @param {Pixel} px - The pixel value to convert to unit value. * @returns {Unit} - The converted unit value. */ fromX(px) { const normalizedPx = px - this.getBounds().x; return pixelsToUnits(normalizedPx + unitToPixels(this.xStart, this.zoomX), this.zoomX); } /** * Converts a pixel value to a unit value along the y-axis. * @param {Pixel} px - The pixel value to be converted. * @returns {void} */ fromY(px) { const bounds = this.getBounds(); const normalizedPx = px - bounds.y; if (this.inverseY) { return pixelsToUnits(normalizedPx + unitToPixels(this.yStart, this.zoomY), this.zoomY); } else { // inverse by default because canvas calculation [0,0] point starts from top-left corner return pixelsToUnits(bounds.height - normalizedPx + unitToPixels(this.yStart, this.zoomY), this.zoomY); } } //endregion /** * Recalculates the zoom factor for the x-axis based on the start and end values of the x-axis. * @function * @name recalculateZoomX * @memberof ClassName * @instance * @returns {void} */ recalculateZoomX() { this.zoomX = this.calculateZoomX(this.xStart, this.xEnd); } /** * Recalculates the zoomY property of the object. * The zoomY property is calculated using the yStart and yEnd properties of the object. * @function * @name recalculateZoomY * @memberof Object * @instance * @returns {void} */ recalculateZoomY() { this.zoomY = this.calculateZoomY(this.yStart, this.yEnd); } /** * Calculates the zoom factor for the x-axis based on the start and end units. * @param {Unit} start - The start unit. * @param {Unit} end - The end unit. * @returns {Zoom} The zoom factor for the x-axis. */ calculateZoomX(start, end) { return calculateZoom(end - start, this.getBounds().width); } /** * Calculates the zoom factor for the Y axis based on the start and end units. * @param {Unit} start - The start unit. * @param {Unit} end - The end unit. * @returns {Zoom} The zoom factor for the Y axis. */ calculateZoomY(start, end) { return calculateZoom(end - start, this.getBounds().height); } /** * Should be called when x/y start/end changes. */ recalculateZoom(fireChanged = true) { this.recalculateZoomX(); this.recalculateZoomY(); fireChanged && this.fireChanged(); } /** * Moves the viewport to exactly xStart..xEnd place. * (you need to fire DRAW event after this) * @param xStart - viewport start in units * @param xEnd - viewport end in units * @param fireChanged - fire changed event */ setXScale(xStart, xEnd, fireChanged = true) { this.xStart = xStart; this.xEnd = xEnd; this.recalculateZoomX(); fireChanged && this.fireChanged(); } /** * Moves the viewport to exactly yStart..yEnd place. * (you need to fire DRAW event after this) * @param yStart - viewport start in units * @param yEnd - viewport end in units * @param fireChanged - fire changed event */ setYScale(yStart, yEnd, fireChanged = true) { this.yStart = yStart; this.yEnd = yEnd; this.recalculateZoomY(); fireChanged && this.fireChanged(); } /** * Exports current state of VM. */ export() { return { xStart: this.xStart, xEnd: this.xEnd, yStart: this.yStart, yEnd: this.yEnd, zoomX: this.zoomX, zoomY: this.zoomY, inverseY: this.inverseY, }; } /** * Applies the state to current VM. * @param state */ apply(state) { this.xStart = state.xStart; this.xEnd = state.xEnd; this.yStart = state.yStart; this.yEnd = state.yEnd; this.zoomX = state.zoomX; this.zoomY = state.zoomY; this.inverseY = state.inverseY; this.fireChanged(); } /** * Emits a notification that the object has changed. * @function * @name fireChanged * @memberof ClassName * @instance * @returns {void} */ fireChanged() { this.changed.next(); } get xStart() { return this._xStart; } set xStart(value) { this._xStart = value; } get xEnd() { return this._xEnd; } set xEnd(value) { this._xEnd = value; } get yStart() { return this._yStart; } set yStart(value) { this._yStart = value; } get yEnd() { return this._yEnd; } set yEnd(value) { this._yEnd = value; } get zoomX() { return this._zoomX; } set zoomX(value) { this._zoomX = value; } get zoomY() { return this._zoomY; } set zoomY(value) { this._zoomY = value; } get inverseY() { return this._inverseY; } set inverseY(value) { this._inverseY = value; } /** * Checks if the viewport is valid. * * @returns {boolean} - Returns true if the viewport is valid, false otherwise. */ isViewportValid(validateZoom = true) { // zoom is checked separately because sometimes we need to recalculate zoom based on X and Y and zoom could be incorrect const isZoomValid = validateZoom === false || (this.zoomX > 0 && this.zoomY > 0); return (this.xStart !== this.xEnd && this.yStart !== this.yEnd && isFinite(this.yStart) && isFinite(this.yEnd) && isZoomValid); } } export const compareStates = (a, b) => !keys(a).some(k => a[k] !== b[k]);