@devexperts/dxcharts-lite
Version:
305 lines (304 loc) • 10.4 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 { 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]);