允许拖动95带宽展示

This commit is contained in:
CN-JS-HuiBai
2026-04-14 14:53:05 +08:00
parent 77743ce097
commit 5e9dac6197

View File

@@ -23,9 +23,86 @@ class AreaChart {
// Use debounced resize for performance and safety
this._resize = typeof debounce === 'function' ? debounce(this.resize.bind(this), 100) : this.resize.bind(this);
window.addEventListener('resize', this._resize);
// Drag zoom support
this.isDraggingP95 = false;
this.customMaxVal = null;
this.onPointerDown = this.onPointerDown.bind(this);
this.onPointerMove = this.onPointerMove.bind(this);
this.onPointerUp = this.onPointerUp.bind(this);
this.canvas.addEventListener('pointerdown', this.onPointerDown);
window.addEventListener('pointermove', this.onPointerMove);
window.addEventListener('pointerup', this.onPointerUp);
this.resize();
}
onPointerDown(e) {
if (!this.showP95 || !this.p95) return;
const rect = this.canvas.getBoundingClientRect();
const scaleY = this.height / rect.height;
const y = (e.clientY - rect.top) * scaleY;
const p = this.padding;
const chartH = this.height - p.top - p.bottom;
// Calculate current P95 Y position
const k = 1024;
const currentMaxVal = (this.customMaxVal !== null ? this.customMaxVal : (this.currentMaxVal || 1024));
let unitIdx = Math.floor(Math.log(Math.max(1, currentMaxVal)) / Math.log(k));
unitIdx = Math.max(0, Math.min(unitIdx, 4));
const unitFactor = Math.pow(k, unitIdx);
const rawValInUnit = (currentMaxVal * 1.15) / unitFactor;
let niceMaxInUnit;
if (rawValInUnit <= 1) niceMaxInUnit = 1;
else if (rawValInUnit <= 2) niceMaxInUnit = 2;
else if (rawValInUnit <= 5) niceMaxInUnit = 5;
else if (rawValInUnit <= 10) niceMaxInUnit = 10;
else if (rawValInUnit <= 20) niceMaxInUnit = 20;
else if (rawValInUnit <= 50) niceMaxInUnit = 50;
else if (rawValInUnit <= 100) niceMaxInUnit = 100;
else if (rawValInUnit <= 200) niceMaxInUnit = 200;
else if (rawValInUnit <= 500) niceMaxInUnit = 500;
else if (rawValInUnit <= 1000) niceMaxInUnit = 1000;
else niceMaxInUnit = Math.ceil(rawValInUnit / 100) * 100;
const displayMaxVal = this.customMaxVal !== null ? this.customMaxVal : (niceMaxInUnit * unitFactor);
const p95Y = p.top + chartH - (this.p95 / (displayMaxVal || 1)) * chartH;
if (Math.abs(y - p95Y) < 25) {
this.isDraggingP95 = true;
this.canvas.style.cursor = 'ns-resize';
this.canvas.setPointerCapture(e.pointerId);
e.preventDefault();
e.stopPropagation();
}
}
onPointerMove(e) {
if (!this.isDraggingP95) return;
const rect = this.canvas.getBoundingClientRect();
const scaleY = this.height / rect.height;
const y = (e.clientY - rect.top) * scaleY;
const p = this.padding;
const chartH = this.height - p.top - p.bottom;
const dy = p.top + chartH - y;
if (dy > 10) {
this.customMaxVal = (this.p95 * chartH) / dy;
this.draw();
}
}
onPointerUp(e) {
if (this.isDraggingP95) {
this.isDraggingP95 = false;
this.canvas.style.cursor = '';
this.canvas.releasePointerCapture(e.pointerId);
}
}
resize() {
const rect = this.canvas.parentElement.getBoundingClientRect();
this.width = rect.width;
@@ -164,13 +241,10 @@ class AreaChart {
let unitIdx = Math.floor(Math.log(Math.max(1, maxDataVal)) / Math.log(k));
unitIdx = Math.max(0, Math.min(unitIdx, sizes.length - 1));
const unitFactor = Math.pow(k, unitIdx);
const unitLabel = sizes[unitIdx];
// Get value in current units and find a "nice" round max
// Use 1.15 cushion
const rawValInUnit = (maxDataVal * 1.15) / unitFactor;
let niceMaxInUnit;
if (rawValInUnit <= 1) niceMaxInUnit = 1;
else if (rawValInUnit <= 2) niceMaxInUnit = 2;
else if (rawValInUnit <= 5) niceMaxInUnit = 5;
@@ -183,7 +257,16 @@ class AreaChart {
else if (rawValInUnit <= 1000) niceMaxInUnit = 1000;
else niceMaxInUnit = Math.ceil(rawValInUnit / 100) * 100;
const maxVal = niceMaxInUnit * unitFactor;
let maxVal = niceMaxInUnit * unitFactor;
if (this.customMaxVal !== null) {
maxVal = this.customMaxVal;
}
// Recalculate units based on final maxVal (could be zoomed)
let finalUnitIdx = Math.floor(Math.log(Math.max(1, maxVal)) / Math.log(k));
finalUnitIdx = Math.max(0, Math.min(finalUnitIdx, sizes.length - 1));
const finalFactor = Math.pow(k, finalUnitIdx);
const finalUnitLabel = sizes[finalUnitIdx];
const len = timestamps.length;
const xStep = chartW / (len - 1);
@@ -201,21 +284,21 @@ class AreaChart {
ctx.lineWidth = 1;
const gridLines = 4;
for (let i = 0; i <= gridLines; i++) {
const y = p.top + (chartH / gridLines) * i;
ctx.beginPath();
ctx.moveTo(p.left, y);
ctx.lineTo(p.left + chartW, y);
ctx.stroke();
const y = p.top + (chartH / gridLines) * i;
ctx.beginPath();
ctx.moveTo(p.left, y);
ctx.lineTo(p.left + chartW, y);
ctx.stroke();
// Y-axis labels - share the same unit for readability
const valInUnit = niceMaxInUnit * (1 - i / gridLines);
ctx.fillStyle = '#5a6380';
ctx.font = '10px "JetBrains Mono", monospace';
ctx.textAlign = 'right';
// Y-axis labels
const v = maxVal * (1 - i / gridLines);
const valInUnit = v / finalFactor;
ctx.fillStyle = '#5a6380';
ctx.font = '10px "JetBrains Mono", monospace';
ctx.textAlign = 'right';
// Format: "X.X MB/s" or "X MB/s"
const label = (valInUnit % 1 === 0 ? valInUnit : valInUnit.toFixed(1)) + ' ' + unitLabel;
ctx.fillText(label, p.left - 10, y + 3);
const label = (valInUnit % 1 === 0 ? valInUnit : valInUnit.toFixed(1)) + ' ' + finalUnitLabel;
ctx.fillText(label, p.left - 10, y + 3);
}
// X-axis labels (every ~4 hours)
@@ -227,47 +310,42 @@ class AreaChart {
const x = getX(i);
ctx.fillText(formatTime(timestamps[i]), x, h - 8);
}
// 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 data areas with clipping
ctx.save();
ctx.beginPath();
ctx.rect(p.left, p.top, chartW, chartH);
ctx.clip();
// Draw TX area
if (this.showTx) {
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);
'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, this.prevData ? this.prevData.rx : null, getX, getY, chartH, p,
'rgba(6, 182, 212, 0.25)', 'rgba(6, 182, 212, 0.02)',
'#06b6d4', len);
'rgba(6, 182, 212, 0.25)', 'rgba(6, 182, 212, 0.02)', '#06b6d4', len);
}
ctx.restore();
// Draw P95 line
if (this.showP95 && this.p95 && this.animProgress === 1) {
if (this.showP95 && this.p95 && (this.animProgress === 1 || this.isDraggingP95)) {
const p95Y = getY(this.p95);
// Only draw if within visible range
if (p95Y >= p.top && p95Y <= p.top + chartH) {
ctx.save();
ctx.beginPath();
ctx.setLineDash([6, 4]);
ctx.strokeStyle = 'rgba(244, 63, 94, 0.85)'; // --accent-rose
ctx.strokeStyle = 'rgba(244, 63, 94, 0.85)';
ctx.lineWidth = 1.5;
ctx.moveTo(p.left, p95Y);
ctx.lineTo(p.left + chartW, p95Y);
ctx.stroke();
// P95 label background
const label = '95计费: ' + (window.formatBandwidth ? window.formatBandwidth(this.p95) : this.p95.toFixed(2));
ctx.font = 'bold 11px "JetBrains Mono", monospace';
const metrics = ctx.measureText(label);
ctx.fillStyle = 'rgba(244, 63, 94, 0.15)';
ctx.fillRect(p.left + 8, p95Y - 20, metrics.width + 12, 18);
// P95 label text
ctx.fillStyle = '#f43f5e';
ctx.textAlign = 'left';
ctx.fillText(label, p.left + 14, p95Y - 7);