class MiniChart { constructor(canvasId, color = '#7aa2f7') { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext('2d'); this.color = color; this.data = new Array(60).fill(0); // 60秒历史 this.maxVal = 100; this.resize(); window.addEventListener('resize', () => this.resize()); } resize() { var rect = this.canvas.parentElement.getBoundingClientRect(); this.canvas.width = rect.width - 24; this.canvas.height = 60; this.draw(); } update(val) { this.data.push(val); if (this.data.length > 60) this.data.shift(); this.maxVal = Math.max.apply(null, this.data.concat([100])); this.draw(); } draw() { if (!this.ctx) return; var ctx = this.ctx; var w = this.canvas.width; var h = this.canvas.height; ctx.clearRect(0, 0, w, h); ctx.strokeStyle = this.color; ctx.lineWidth = 2; ctx.beginPath(); var self = this; this.data.forEach(function(v, i) { var x = (i / 59) * w; var y = h - (v / self.maxVal) * (h - 10); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // 填充渐变 ctx.lineTo(w, h); ctx.lineTo(0, h); ctx.fillStyle = this.color + '20'; ctx.fill(); } } /* ── Dual-line chart (e.g. download + upload speed) ── */ class DualLineChart { constructor(canvasId, colorA, colorB) { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext('2d'); this.colorA = colorA || '#bb9af7'; // solid line (download) this.colorB = colorB || '#c0a8f0'; // dashed line (upload) this.dataA = new Array(60).fill(0); this.dataB = new Array(60).fill(0); this.maxVal = 100; this.applyResize(); window.addEventListener('resize', () => this.applyResize()); } applyResize() { var rect = this.canvas.parentElement.getBoundingClientRect(); this.canvas.width = rect.width - 24; this.canvas.height = 60; this.draw(); } update(valA, valB) { this.dataA.push(valA); this.dataB.push(valB); if (this.dataA.length > 60) { this.dataA.shift(); this.dataB.shift(); } var all = this.dataA.concat(this.dataB).concat([100]); this.maxVal = Math.max.apply(null, all); this.draw(); } draw() { if (!this.ctx) return; var ctx = this.ctx; var w = this.canvas.width; var h = this.canvas.height; var self = this; // Y-axis: range from -5% to 100%, so zero never hugs the bottom var pad = Math.max(10, self.maxVal * 0.05); var range = self.maxVal + pad; // e.g. 0..100 becomes -5..100 ctx.clearRect(0, 0, w, h); // ── Line A: solid (download) ── ctx.strokeStyle = this.colorA; ctx.lineWidth = 1.8; ctx.setLineDash([]); ctx.beginPath(); this.dataA.forEach(function(v, i) { var x = (i / 59) * w; var y = h - ((v + pad) / range) * (h - 8); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // ── Line B: dashed (upload) ── ctx.strokeStyle = this.colorB; ctx.lineWidth = 1.4; ctx.setLineDash([4, 4]); ctx.beginPath(); this.dataB.forEach(function(v, i) { var x = (i / 59) * w; var y = h - ((v + pad) / range) * (h - 8); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); ctx.setLineDash([]); } } window.MiniChart = MiniChart; window.DualLineChart = DualLineChart;