Files
lucia-frontend/src/module/setChartData.js
2026-03-06 18:57:58 +08:00

278 lines
8.6 KiB
JavaScript

// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// imacat.yang@dsp.im (imacat), 2023/9/23
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module setChartData Chart.js data transformation utilities. */
import getMoment from 'moment';
/**
* Extends backend chart data with extrapolated boundary points for
* line charts. Prepends a calculated minimum and appends a calculated
* maximum data point based on linear extrapolation.
*
* @param {Array<{x: number, y: number}>} baseData - The 10 data points
* from the backend.
* @param {number} xMax - The maximum x-axis timestamp.
* @param {number} xMin - The minimum x-axis timestamp.
* @param {boolean} isPercent - Whether values are percentages (0-1 range).
* @param {number} yMax - The maximum y-axis value for clamping.
* @param {number} yMin - The minimum y-axis value for clamping.
* @returns {Array<{x: number, y: number}>} The extended data array
* with boundary points.
*/
export function setLineChartData(baseData, xMax, xMin, isPercent, yMax, yMin) {
// 將 baseData 轉換為包含 x 和 y 屬性的物件陣列
let data = baseData.map(i => ({ x: i.x, y: i.y }));
// 計算 y 軸最小值
let b = calculateYMin(baseData, isPercent, yMin, yMax);
// 計算 y 軸最大值
let mf = calculateYMax(baseData, isPercent, yMin, yMax);
// 添加最小值
data.unshift({
x: xMin,
y: b,
});
// 添加最大值
data.push({
x: xMax,
y: mf,
});
return data;
};
/**
* Extrapolates the Y-axis minimum boundary value using linear
* interpolation from the first two data points.
*
* @param {Array<{x: number, y: number}>} baseData - The base data points.
* @param {boolean} isPercent - Whether to clamp to 0-1 range.
* @param {number} yMin - The minimum allowed Y value.
* @param {number} yMax - The maximum allowed Y value.
* @returns {number} The extrapolated and clamped Y minimum value.
*/
function calculateYMin(baseData, isPercent, yMin, yMax) {
let a = 0;
let c = 1;
let d = baseData[0].y;
let e = 2;
let f = baseData[1].y;
let b = (e * d - a * d - f * a - f * c) / (e - c - a);
return clampValue(b, isPercent, yMin, yMax);
};
/**
* Extrapolates the Y-axis maximum boundary value using linear
* interpolation from the last two data points.
*
* @param {Array<{x: number, y: number}>} baseData - The base data points.
* @param {boolean} isPercent - Whether to clamp to 0-1 range.
* @param {number} yMin - The minimum allowed Y value.
* @param {number} yMax - The maximum allowed Y value.
* @returns {number} The extrapolated and clamped Y maximum value.
*/
function calculateYMax(baseData, isPercent, yMin, yMax) {
let ma = 9;
let mb = baseData[8].y;
let mc = 10;
let md = baseData[9].y;
let me = 11;
let mf = (mb * me - mb * mc - md * me + md * ma) / (ma - mc);
return clampValue(mf, isPercent, yMin, yMax);
};
/**
* Clamps a value within a specified range. If isPercent is true, the
* value is clamped to [0, 1]; otherwise to [min, max].
*
* @param {number} value - The value to clamp.
* @param {boolean} isPercent - Whether to use the percentage range [0, 1].
* @param {number} min - The minimum bound (used when isPercent is false).
* @param {number} max - The maximum bound (used when isPercent is false).
* @returns {number} The clamped value.
*/
function clampValue(value, isPercent, min, max) {
if (isPercent) {
if (value >= 1) {
return 1;
} else if (value <= 0) {
return 0;
}
} else {
if (value >= max) {
return max;
}
if (value <= min) {
return min;
}
}
return value;
};
/**
* Converts backend chart data timestamps to formatted date strings
* for bar charts.
*
* @param {Array<{x: string, y: number}>} baseData - The data points from
* the backend with ISO timestamp x values.
* @returns {Array<{x: string, y: number}>} Data with x values formatted
* as "YYYY/M/D hh:mm:ss".
*/
export function setBarChartData(baseData) {
let data = baseData.map(i =>{
return {
x: getMoment(i.x).format('YYYY/M/D hh:mm:ss'),
y: i.y
}
})
return data
};
/**
* Divides a time range into evenly spaced time points.
*
* @param {number} minTime - The start time in seconds.
* @param {number} maxTime - The end time in seconds.
* @param {number} amount - The number of time points to generate.
* @returns {Array<number>} An array of evenly spaced, rounded time
* values in seconds.
*/
export function timeRange(minTime, maxTime, amount) {
// x 軸(時間軸)的範圍是最大-最小,從最小值按照 index 累加間距到最大值
const startTime = minTime;
const endTime = maxTime;
let timeRange = []; // return數據初始化
const timeGap = (endTime - startTime) / (amount - 1); // 切分成多少段
for (let i = 0; i < amount; i++) {
timeRange.push(startTime + timeGap * i);
}
timeRange = timeRange.map(value => Math.round(value));
return timeRange;
};
/**
* Generates smooth Y-axis values using cubic Bezier interpolation
* to produce evenly spaced ticks matching the X-axis divisions.
*
* @param {Array<{x: number, y: number}>} data - The source data points.
* @param {number} yAmount - The desired number of Y-axis ticks.
* @param {number} yMax - The maximum Y value (unused, kept for API).
* @returns {Array<number>} An array of interpolated Y values.
*/
export function yTimeRange(data, yAmount, yMax) {
const yRange = [];
const yGap = (1/ (yAmount-1));
// 貝茲曲線公式
const threebsr = function (t, a1, a2, a3, a4) {
return (
(1 - t) * (1 - t) * (1 - t) * a1 +
3 * t * (1 - t)* (1 - t) * a2 +
3 * t * t * (1 - t) * a3 +
t * t * t * a4
)
};
for (let j = 0; j < data.length - 1; j++) {
for (let i = 0; i <= 1; i += yGap*11) {
yRange.push(threebsr(i, data[j].y, data[j].y, data[j + 1].y, data[j + 1].y));
}
}
if(yRange.length < yAmount) {
let len = yAmount - yRange.length;
for (let i = 0; i < len; i++) {
yRange.push(yRange[yRange.length - 1]);
}
}
else if(yRange.length > yAmount) {
let len = yRange.length - yAmount;
for(let i = 0; i < len; i++) {
yRange.splice(1, 1);
}
}
return yRange;
};
/**
* Finds the index of the closest value in an array to the given target.
*
* @param {Array<number>} data - The array of values to search.
* @param {number} xValue - The target value to find the closest match for.
* @returns {number} The index of the closest value in the array.
*/
export function getXIndex(data, xValue) {
let closestIndex = xValue; // 假定第一个元素的索引是 0
let smallestDifference = Math.abs(xValue - data[0]); // 初始差值设为第一个元素与目标数的差值
for (let i = 0; i < data.length; i++) {
let difference = Math.abs(xValue - data[i]);
if (difference <= smallestDifference) {
closestIndex = i;
smallestDifference = difference;
}
}
return closestIndex;
};
/**
* Formats a duration in seconds to a compact string with d/h/m/s units.
*
* @param {number} seconds - The total number of seconds.
* @returns {string|null} The formatted string (e.g. "2d3h15m30s"),
* or null if the input is NaN.
*/
export function formatTime(seconds) {
if(!isNaN(seconds)) {
const remainingSeconds = seconds % 60;
const minutes = (Math.floor(seconds - remainingSeconds) / 60) % 60;
const hours = (Math.floor(seconds / 3600)) % 24;
const days = Math.floor(seconds / (3600 * 24));
let result = '';
if (days > 0) {
result += `${days}d`;
}
if (hours > 0) {
result += `${hours}h`;
}
if (minutes > 0) {
result += `${minutes}m`;
}
result += `${remainingSeconds}s`;
return result.trim(); // 去除最后一个空格
} else {
return null;
}
}
/**
* Truncates each time string to show only the two largest time units.
*
* @param {Array<string>} times - Array of duration strings (e.g. "2d3h15m30s").
* @returns {Array<string>} Array of truncated strings (e.g. "2d 3h").
*/
export function formatMaxTwo(times) {
const formattedTimes = [];
for (let time of times) {
// 匹配數字和單位(天、小時、分鐘、秒), 假設數字不可能大於10位數
let units = time.match(/\d{1,10}[dhms]/g);
let formattedTime = '';
let count = 0;
// 只保留最大的兩個單位
for (let unit of units) {
if (count >= 2) {
break;
}
formattedTime += unit + ' ';
count++;
}
formattedTimes.push(formattedTime.trim()); // 去除末尾的空格
}
return formattedTimes;
}