允许拖动95带宽展示
This commit is contained in:
@@ -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);
|
||||
@@ -207,14 +290,14 @@ class AreaChart {
|
||||
ctx.lineTo(p.left + chartW, y);
|
||||
ctx.stroke();
|
||||
|
||||
// Y-axis labels - share the same unit for readability
|
||||
const valInUnit = niceMaxInUnit * (1 - i / gridLines);
|
||||
// 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;
|
||||
const label = (valInUnit % 1 === 0 ? valInUnit : valInUnit.toFixed(1)) + ' ' + finalUnitLabel;
|
||||
ctx.fillText(label, p.left - 10, y + 3);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user