From 84972cdaeb698cc2ee6d325899a7f8dc05ffea10 Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Sun, 5 Apr 2026 22:59:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/chart.js | 155 +++++++++++++++++++++++++++++++-------------- 1 file changed, 107 insertions(+), 48 deletions(-) diff --git a/public/js/chart.js b/public/js/chart.js index 05ce6b0..79b5899 100644 --- a/public/js/chart.js +++ b/public/js/chart.js @@ -15,6 +15,9 @@ class AreaChart { this.p95Type = 'tx'; // 'tx', 'rx', 'both' this.dpr = window.devicePixelRatio || 1; this.padding = { top: 20, right: 16, bottom: 32, left: 56 }; + + this.currentMaxVal = 0; + this.prevMaxVal = 0; this._resize = this.resize.bind(this); window.addEventListener('resize', this._resize); @@ -36,6 +39,21 @@ class AreaChart { setData(data) { if (!data || !data.timestamps) return; + // Store old data for smooth transition before updating this.data + // Only clone if there is data to clone; otherwise use empty set + if (this.data && this.data.timestamps && this.data.timestamps.length > 0) { + this.prevData = { + timestamps: [...this.data.timestamps], + rx: [...this.data.rx], + tx: [...this.data.tx] + }; + } else { + this.prevData = { timestamps: [], rx: [], tx: [] }; + } + + // Smoothly transition max value context too + this.prevMaxVal = this.currentMaxVal || 0; + // Downsample if data is too dense (target ~1500 points for performance) const MAX_POINTS = 1500; if (data.timestamps.length > MAX_POINTS) { @@ -51,6 +69,14 @@ class AreaChart { this.data = data; } + // Refresh currentMaxVal target for interpolation in draw() + let rawMax = 1024; + for (let i = 0; i < this.data.rx.length; i++) { + if (this.showRx) rawMax = Math.max(rawMax, this.data.rx[i] || 0); + if (this.showTx) rawMax = Math.max(rawMax, this.data.tx[i] || 0); + } + this.currentMaxVal = rawMax; + // Calculate P95 (95th percentile) let combined = []; if (this.p95Type === 'tx') { @@ -112,14 +138,15 @@ class AreaChart { return; } - // Find max raw value - let maxDataVal = 1024; // Minimum 1KB/s scale - for (let i = 0; i < rx.length; i++) { - if (this.showRx) maxDataVal = Math.max(maxDataVal, rx[i] || 0); - if (this.showTx) maxDataVal = Math.max(maxDataVal, tx[i] || 0); - } - // Determine consistent unit based on max data value + let maxDataVal = 1024; + if (this.prevMaxVal && this.animProgress < 1) { + // Interpolate the max value context to keep vertical scale smooth + maxDataVal = this.prevMaxVal + (this.currentMaxVal - this.prevMaxVal) * (this.animProgress || 0); + } else { + maxDataVal = this.currentMaxVal; + } + const k = 1024; const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s']; let unitIdx = Math.floor(Math.log(Math.max(1, maxDataVal)) / Math.log(k)); @@ -149,9 +176,13 @@ class AreaChart { const len = timestamps.length; const xStep = chartW / (len - 1); - // Helper to get point + // Helper to get point with smooth value transition const getX = (i) => p.left + i * xStep; - const getY = (val) => p.top + chartH - (val / (maxVal || 1)) * chartH * this.animProgress; + const getY = (val, prevVal = 0) => { + // Interpolate value from previous state to new state + const actualVal = prevVal + (val - prevVal) * this.animProgress; + return p.top + chartH - (actualVal / (maxVal || 1)) * chartH; + }; // Draw grid lines ctx.strokeStyle = 'rgba(99, 102, 241, 0.08)'; @@ -187,16 +218,18 @@ class AreaChart { // Always show last label ctx.fillText(formatTime(timestamps[len - 1]), getX(len - 1), h - 8); + const getPVal = (arr, i) => (arr && i < arr.length) ? arr[i] : 0; + // Draw TX area if (this.showTx) { - this.drawArea(ctx, tx, getX, getY, chartH, p, + this.drawArea(ctx, tx, this.prevData ? this.prevData.tx : null, getX, getY, chartH, p, 'rgba(99, 102, 241, 0.25)', 'rgba(99, 102, 241, 0.02)', '#6366f1', len); } // Draw RX area (on top) if (this.showRx) { - this.drawArea(ctx, rx, getX, getY, chartH, p, + this.drawArea(ctx, rx, this.prevData ? this.prevData.rx : null, getX, getY, chartH, p, 'rgba(6, 182, 212, 0.25)', 'rgba(6, 182, 212, 0.02)', '#06b6d4', len); } @@ -231,22 +264,23 @@ class AreaChart { } } - drawArea(ctx, values, getX, getY, chartH, p, fillColorTop, fillColorBottom, strokeColor, len) { + drawArea(ctx, values, prevValues, getX, getY, chartH, p, fillColorTop, fillColorBottom, strokeColor, len) { if (!values || values.length === 0) return; const useSimple = len > 250; + const getPVal = (i) => (prevValues && i < prevValues.length) ? prevValues[i] : 0; // Fill ctx.beginPath(); - ctx.moveTo(getX(0), getY(values[0] || 0)); + ctx.moveTo(getX(0), getY(values[0] || 0, getPVal(0))); for (let i = 1; i < len; i++) { + const currY = getY(values[i] || 0, getPVal(i)); if (useSimple) { - ctx.lineTo(getX(i), getY(values[i] || 0)); + ctx.lineTo(getX(i), currY); } else { const prevX = getX(i - 1); const currX = getX(i); - const prevY = getY(values[i - 1] || 0); - const currY = getY(values[i] || 0); + const prevY = getY(values[i - 1] || 0, getPVal(i - 1)); const midX = (prevX + currX) / 2; ctx.bezierCurveTo(midX, prevY, midX, currY, currX, currY); } @@ -263,15 +297,15 @@ class AreaChart { // Stroke ctx.beginPath(); - ctx.moveTo(getX(0), getY(values[0] || 0)); + ctx.moveTo(getX(0), getY(values[0] || 0, getPVal(0))); for (let i = 1; i < len; i++) { + const currY = getY(values[i] || 0, getPVal(i)); if (useSimple) { - ctx.lineTo(getX(i), getY(values[i] || 0)); + ctx.lineTo(getX(i), currY); } else { const prevX = getX(i - 1); const currX = getX(i); - const prevY = getY(values[i - 1] || 0); - const currY = getY(values[i] || 0); + const prevY = getY(values[i - 1] || 0, getPVal(i - 1)); const midX = (prevX + currX) / 2; ctx.bezierCurveTo(midX, prevY, midX, currY, currX, currY); } @@ -298,6 +332,9 @@ class MetricChart { this.padding = { top: 10, right: 10, bottom: 20, left: 60 }; this.animProgress = 0; + this.prevMaxVal = 0; + this.currentMaxVal = 0; + this._resize = this.resize.bind(this); window.addEventListener('resize', this._resize); this.resize(); @@ -319,7 +356,30 @@ class MetricChart { } setData(data) { + if (this.data && this.data.values && this.data.values.length > 0) { + this.prevData = JSON.parse(JSON.stringify(this.data)); + } else { + this.prevData = { timestamps: [], values: [], series: null }; + } + + this.prevMaxVal = this.currentMaxVal || 0; this.data = data || { timestamps: [], values: [], series: null }; + + // Target max + if (this.data.series) { + this.currentMaxVal = 100; + } else { + const raw = Math.max(...(this.data.values || []), 0.1); + if (this.unit === '%' && raw <= 100) { + if (raw > 80) this.currentMaxVal = 100; + else if (raw > 40) this.currentMaxVal = 80; + else if (raw > 20) this.currentMaxVal = 50; + else this.currentMaxVal = 25; + } else { + this.currentMaxVal = raw * 1.25; + } + } + this.animate(); } @@ -356,27 +416,18 @@ class MetricChart { return; } - // Determine Y max - let maxVal = 0; - if (series) { - // For stacked CPU, max sum should be 100 - maxVal = 100; - } else { - maxVal = Math.max(...values, 0.1); - if (this.unit === '%' && maxVal <= 100) { - if (maxVal > 80) maxVal = 100; - else if (maxVal > 40) maxVal = 80; - else if (maxVal > 20) maxVal = 50; - else maxVal = 25; - } else { - maxVal = maxVal * 1.25; - } - } + // Determine Y max (interpolated) + const targetMax = this.currentMaxVal || 0.1; + const startMax = this.prevMaxVal || targetMax; + const maxVal = startMax + (targetMax - startMax) * this.animProgress; const len = timestamps.length; const xStep = chartW / (len - 1); const getX = (i) => p.left + i * xStep; - const getY = (val) => p.top + chartH - (val / (maxVal || 1)) * chartH * this.animProgress; + const getY = (val, prevVal = 0) => { + const actualVal = prevVal + (val - prevVal) * this.animProgress; + return p.top + chartH - (actualVal / (maxVal || 1)) * chartH; + }; // Grid ctx.strokeStyle = 'rgba(99, 102, 241, 0.05)'; @@ -421,19 +472,23 @@ class MetricChart { ]; let currentBase = new Array(len).fill(0); + let prevBase = new Array(len).fill(0); modes.forEach(mode => { const vals = series[mode.name]; if (!vals) return; + + const prevVals = (this.prevData && this.prevData.series) ? this.prevData.series[mode.name] : null; + const getPVal = (arr, idx) => (arr && idx < arr.length) ? arr[idx] : 0; ctx.beginPath(); - ctx.moveTo(getX(0), getY(currentBase[0] + vals[0])); + ctx.moveTo(getX(0), getY(currentBase[0] + vals[0], getPVal(prevBase, 0) + getPVal(prevVals, 0))); for (let i = 1; i < len; i++) { - ctx.lineTo(getX(i), getY(currentBase[i] + vals[i])); + ctx.lineTo(getX(i), getY(currentBase[i] + vals[i], getPVal(prevBase, i) + getPVal(prevVals, i))); } - ctx.lineTo(getX(len - 1), getY(currentBase[len - 1])); + ctx.lineTo(getX(len - 1), getY(currentBase[len - 1], getPVal(prevBase, len - 1))); for (let i = len - 1; i >= 0; i--) { - ctx.lineTo(getX(i), getY(currentBase[i])); + ctx.lineTo(getX(i), getY(currentBase[i], getPVal(prevBase, i))); } ctx.closePath(); ctx.fillStyle = mode.color; @@ -441,17 +496,18 @@ class MetricChart { // Stroke ctx.beginPath(); - ctx.moveTo(getX(0), getY(currentBase[0] + vals[0])); + ctx.moveTo(getX(0), getY(currentBase[0] + vals[0], getPVal(prevBase, 0) + getPVal(prevVals, 0))); for (let i = 1; i < len; i++) { - ctx.lineTo(getX(i), getY(currentBase[i] + vals[i])); + ctx.lineTo(getX(i), getY(currentBase[i] + vals[i], getPVal(prevBase, i) + getPVal(prevVals, i))); } ctx.strokeStyle = mode.stroke; ctx.lineWidth = 1; ctx.stroke(); - // Update base + // Update boxes for next series for (let i = 0; i < len; i++) { currentBase[i] += vals[i]; + if (prevBase) prevBase[i] = (prevBase[i] || 0) + getPVal(prevVals, i); } }); @@ -470,16 +526,19 @@ class MetricChart { } else { const useSimple = len > 250; + const prevVals = this.prevData ? this.prevData.values : null; + const getPVal = (i) => (prevVals && i < prevVals.length) ? prevVals[i] : 0; + ctx.beginPath(); - ctx.moveTo(getX(0), getY(values[0])); + ctx.moveTo(getX(0), getY(values[0], getPVal(0))); for (let i = 1; i < len; i++) { + const currY = getY(values[i], getPVal(i)); if (useSimple) { - ctx.lineTo(getX(i), getY(values[i])); + ctx.lineTo(getX(i), currY); } else { const prevX = getX(i - 1); const currX = getX(i); - const prevY = getY(values[i - 1]); - const currY = getY(values[i]); + const prevY = getY(values[i - 1], getPVal(i - 1)); const midX = (prevX + currX) / 2; ctx.bezierCurveTo(midX, prevY, midX, currY, currX, currY); }