// 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} 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} 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} 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} times - Array of duration strings (e.g. "2d3h15m30s"). * @returns {Array} 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; }