278 lines
8.7 KiB
JavaScript
278 lines
8.7 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) {
|
|
// Convert baseData to an array of objects with x and y properties
|
|
let data = baseData.map(i => ({ x: i.x, y: i.y }));
|
|
|
|
// Calculate the Y-axis minimum value
|
|
let b = calculateYMin(baseData, isPercent, yMin, yMax);
|
|
|
|
// Calculate the Y-axis maximum value
|
|
let mf = calculateYMax(baseData, isPercent, yMin, yMax);
|
|
|
|
// Prepend the minimum value
|
|
data.unshift({
|
|
x: xMin,
|
|
y: b,
|
|
});
|
|
|
|
// Append the maximum value
|
|
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) {
|
|
// The X-axis (time axis) range is max - min; accumulate intervals from min to max by index
|
|
const startTime = minTime;
|
|
const endTime = maxTime;
|
|
let timeRange = []; // Initialize the return data array
|
|
const timeGap = (endTime - startTime) / (amount - 1); // Divide into segments
|
|
|
|
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));
|
|
|
|
// Cubic Bezier curve formula
|
|
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; // Assume the first element index is 0
|
|
let smallestDifference = Math.abs(xValue - data[0]); // Initialize difference to the gap between the first element and target
|
|
|
|
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(); // Remove trailing whitespace
|
|
} 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) {
|
|
// Match numbers and units (days, hours, minutes, seconds); assume numbers have at most 10 digits
|
|
let units = time.match(/\d{1,10}[dhms]/g);
|
|
let formattedTime = '';
|
|
let count = 0;
|
|
|
|
// Keep only the two largest units
|
|
for (let unit of units) {
|
|
if (count >= 2) {
|
|
break;
|
|
}
|
|
formattedTime += unit + ' ';
|
|
count++;
|
|
}
|
|
formattedTimes.push(formattedTime.trim()); // Remove trailing whitespace
|
|
}
|
|
return formattedTimes;
|
|
}
|