/** * Pure math utilities for SVG chart rendering. * Scales, path generation, sparklines, and axis computation. */ /** * Calculate chart dimensions with padding */ export function calculateChartDimensions(width, height, padding) { return { width, height, padding, chartWidth: width - padding * 2, chartHeight: height - padding * 2, }; } /** * Calculate scale configuration for a set of values */ export function calculateScale(values, includeZero = true) { const min = includeZero ? Math.min(...values, 0) : Math.min(...values); const max = Math.max(...values); const range = max - min || 1; return { min, max, range }; } /** * Create a linear scale function */ export function createLinearScale(domain, range) { const [domainMin, domainMax] = domain; const [rangeMin, rangeMax] = range; const domainRange = domainMax - domainMin || 1; const rangeRange = rangeMax - rangeMin; return (value) => { const normalized = (value - domainMin) / domainRange; return rangeMin + normalized * rangeRange; }; } /** * Generate evenly spaced tick values */ export function generateTicks(min, max, count) { const step = (max - min) / (count - 1); return Array.from({ length: count }, (_, i) => min + step * i); } /** * Generate SVG path for line chart */ export function generateLinePath(points, curve = false) { if (points.length === 0) { return ''; } const pathSegments = points.map((point, i) => { if (i === 0) { return `M ${point.x} ${point.y}`; } if (curve && i > 0) { const prev = points[i - 1]; const cp1x = prev.x + (point.x - prev.x) / 3; const cp1y = prev.y; const cp2x = point.x - (point.x - prev.x) / 3; const cp2y = point.y; return `C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${point.x} ${point.y}`; } return `L ${point.x} ${point.y}`; }); return pathSegments.join(' '); } /** * Generate SVG path for area under line */ export function generateAreaPath(linePath, firstX, lastX, baselineY) { return `${linePath} L ${lastX} ${baselineY} L ${firstX} ${baselineY} Z`; } /** * Calculate points for a sparkline */ export function calculateSparklinePoints(data, width, height) { if (data.length === 0) { return []; } const scale = calculateScale(data, false); const xScale = createLinearScale([0, data.length - 1], [0, width]); const yScale = createLinearScale([scale.min, scale.max], [height, 0]); return data.map((value, index) => ({ x: xScale(index), y: yScale(value), })); } /** * Format timestamp as HH:MM:SS for chart axis labels * * @param timestamp - Unix timestamp in milliseconds * @returns Formatted time string (e.g., "14:30:05") */ export function formatChartTime(timestamp) { const date = new Date(timestamp); const hours = date.getHours().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, '0'); const seconds = date.getSeconds().toString().padStart(2, '0'); return `${hours}:${minutes}:${seconds}`; } /** * Calculate auto-scaling Y-axis bounds based on visible data with padding. * Automatically adds 10% padding above and below the data range. * * @param values - Array of numeric values to calculate scale for * @param options - Optional configuration * @param options.paddingPercent - Padding percentage (default: 0.1 = 10%) * @param options.minPadding - Minimum padding in data units (default: 5) * @param options.includeZero - Whether to always include zero in the scale (default: false) * @returns Scale configuration with min, max, and range */ export function calculateAutoYScale(values, options = {}) { const { paddingPercent = 0.1, minPadding = 5, includeZero = false, } = options; // Default scale when no data if (values.length === 0) { return { min: 0, max: 100, range: 100 }; } const dataMin = Math.min(...values); const dataMax = Math.max(...values); const dataRange = dataMax - dataMin; // Calculate padding (percentage of range, or minimum padding if range is small) const padding = Math.max(dataRange * paddingPercent, minPadding); let min = Math.floor(dataMin - padding); const max = Math.ceil(dataMax + padding); // Optionally include zero in the scale if (includeZero) { min = Math.min(min, 0); } else { // Never go below zero for typical chart data min = Math.max(0, min); } return { min, max, range: max - min, }; } /** * Get color from chart series palette by index. * Cycles through palette if index exceeds palette length. * * @param index - Zero-based index of the data series * @param palette - Array of color hex strings * @returns Hex color string */ export function getSeriesColor(index, palette) { if (palette.length === 0) { return '#3B82F6'; // Default blue } return palette[index % palette.length] ?? palette[0]; } //# sourceMappingURL=chart.js.map