* -------------------------------------------------------------------------
* This file is part of the MindStudio project.
* Copyright (c) 2025 Huawei Technologies Co.,Ltd.
*
* MindStudio is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
* -------------------------------------------------------------------------
*/
import { clamp } from 'lodash';
import { makeAutoObservable } from 'mobx';
import type { TimeStamp } from './common';
export const GOLDEN_RATE = 0.1236;
export const PAN_RATE = 0.05;
export const MAX_INITIAL_DURATION = 5 * 60 * 1e9;
export const MAX_ZOOM_DURATION = 30 * 60 * 1e9;
export interface DomainRange {
domainStart: TimeStamp;
domainEnd: TimeStamp;
}
const validNumber = (num: number | undefined): number | undefined => {
if (num === undefined || isNaN(num) || !isFinite(num)) {
return undefined;
}
return num;
};
interface ZoomCenterifyArgs {
domain: [ TimeStamp, TimeStamp ];
newDuration: number;
upperBound: TimeStamp;
point?: TimeStamp;
};
const zoomCenterify = ({ domain: [originStart, originEnd], newDuration, upperBound, point }: ZoomCenterifyArgs): [ number, number ] => {
let start = originStart;
let end = originEnd;
const centerPoint = validNumber(point) ?? (start + end) / 2;
const prevDuration = end - start;
if (prevDuration > 0) {
const leftRatio = (centerPoint - start) / prevDuration;
const leftDuration = leftRatio * newDuration;
const rightRatio = (end - centerPoint) / prevDuration;
const rightDuration = rightRatio * newDuration;
if (centerPoint - leftDuration < 0) {
start = 0;
end = Math.min(newDuration, upperBound);
} else if (centerPoint + rightDuration > upperBound) {
end = upperBound;
start = Math.max(upperBound - newDuration, 0);
} else {
start = centerPoint - leftDuration;
end = centerPoint + rightDuration;
}
}
return [start, end];
};
export class Domain {
maxDuration: TimeStamp;
readonly ZOOM_RATE: number = 1 + GOLDEN_RATE;
readonly BOUNDARY_ZOOM_RATE: number = 1 + (GOLDEN_RATE * 10);
private readonly _DEFAULT_DURATION: TimeStamp;
private readonly _UPPER_BOUND: TimeStamp = Number.MAX_VALUE;
private readonly _LOWER_BOUND: TimeStamp;
private _domainStart: TimeStamp;
private _domainEnd: TimeStamp;
private _endTimeAll: TimeStamp;
private _realTimeUpdate: boolean = true;
private _chartViewWidth: number = 0;
private readonly _debouncedSetZoomingHistory;
constructor(isNsMode: boolean, endTimeAll: TimeStamp | undefined, debouncedSetZoomingHistory: (range: DomainRange) => void) {
makeAutoObservable(this);
this._DEFAULT_DURATION = isNsMode ? 2e10 : 2e4;
this._LOWER_BOUND = isNsMode ? 1 : 625;
this._endTimeAll = endTimeAll ?? this._DEFAULT_DURATION;
this._domainStart = 0;
this.maxDuration = endTimeAll === undefined ? this._DEFAULT_DURATION : Math.min(this._UPPER_BOUND, endTimeAll * this.BOUNDARY_ZOOM_RATE);
this._domainEnd = this.maxDuration;
this._debouncedSetZoomingHistory = debouncedSetZoomingHistory;
}
get lowerBound(): TimeStamp {
return this._LOWER_BOUND;
}
get defaultDuration(): TimeStamp {
return this._DEFAULT_DURATION;
}
get duration(): TimeStamp {
return this._domainEnd - this._domainStart;
}
get domainRange(): DomainRange {
return { domainStart: this._domainStart, domainEnd: this._domainEnd };
}
get realTimeUpdate(): boolean {
return this._realTimeUpdate;
}
get isLowerBound(): boolean {
return this.duration === this._LOWER_BOUND;
}
get isUpperBound(): boolean {
return this.duration >= this.maxDuration;
}
get timePerPx(): number {
if (this._chartViewWidth === 0) {
return this.duration;
}
return this.duration / this._chartViewWidth;
}
set endTimeAll(endTimeAll: TimeStamp) {
this._endTimeAll = endTimeAll;
if (this._endTimeAll > 0 && this.maxDuration / this._endTimeAll < 2 * this.BOUNDARY_ZOOM_RATE) {
this.maxDuration = Math.min(this._UPPER_BOUND, this._endTimeAll * this.BOUNDARY_ZOOM_RATE);
}
if (this._endTimeAll > this._domainEnd) {
this._realTimeUpdate && ([this._domainStart, this._domainEnd] = [endTimeAll - this.duration, endTimeAll]);
}
}
set zoom({ zoomCount, zoomPoint }: { zoomCount: number; zoomPoint?: TimeStamp }) {
const notFit = (this.duration === this.maxDuration && zoomCount > 0) ||
(this.duration === this._LOWER_BOUND && zoomCount < 0) ||
(this.duration >= MAX_ZOOM_DURATION && zoomCount > 0);
if (notFit) {
return;
}
const zoomEnd = (this._domainEnd > this._endTimeAll) && zoomCount < 0 ? this._endTimeAll : this._domainEnd;
const zoomCenterLine = (zoomPoint ?? 0) > zoomEnd ? zoomEnd : zoomPoint;
const newDuration = clamp(
this.duration * Math.pow(this.ZOOM_RATE, zoomCount),
this._LOWER_BOUND,
Math.min(this.maxDuration, MAX_ZOOM_DURATION),
);
[this._domainStart, this._domainEnd] = zoomCenterify({
domain: [this._domainStart, zoomEnd],
newDuration,
upperBound: this.maxDuration,
point: zoomCenterLine,
});
if (this._domainEnd !== this._endTimeAll) {
this._realTimeUpdate = false;
}
this._debouncedSetZoomingHistory({ domainStart: this._domainStart, domainEnd: this._domainEnd });
};
set domainRange({ domainStart, domainEnd }: DomainRange) {
if (domainStart === this._domainStart && domainEnd === this._domainEnd) { return; }
const positiveStart = Math.abs(domainStart);
const positiveEnd = Math.abs(domainEnd);
if (positiveStart === 0 && positiveEnd === 0) {
[this._domainStart, this._domainEnd] = [0, this._DEFAULT_DURATION];
} else if (positiveEnd - positiveStart < this._LOWER_BOUND || positiveEnd - positiveStart > this.maxDuration) {
const newDuration = clamp(this.duration, this._LOWER_BOUND, this.maxDuration);
[this._domainStart, this._domainEnd] = zoomCenterify({
domain: [positiveStart, positiveEnd],
newDuration,
upperBound: this.maxDuration,
});
} else {
[this._domainStart, this._domainEnd] = [domainStart, domainEnd];
}
}
set realTimeUpdate(realTimeUpdate: boolean) {
this._realTimeUpdate = realTimeUpdate;
}
set chartViewWidth(width: number) {
if (width === this._chartViewWidth) { return; }
if (this._chartViewWidth !== 0) {
this.maxDuration = width / this._chartViewWidth * this.maxDuration;
} else {
this._chartViewWidth = width;
}
[this._domainStart, this._domainEnd] = zoomCenterify({
domain: [this._domainStart, this._domainEnd],
newDuration: this.timePerPx * width,
upperBound: this.maxDuration,
});
this._chartViewWidth = width;
}
}