v0.12.0-red_lines
This commit is contained in:
+40
-8
@@ -54,22 +54,54 @@ function addPingSample(id, value, error = '') {
|
||||
|
||||
function pingSparkline(samples) {
|
||||
const width = 360, height = 92, padX = 8, padY = 9;
|
||||
if (!samples.length) return `<div class="ping-empty">${esc(tr('devices.pingNoSamples'))}</div>`;
|
||||
|
||||
const valid = samples.map((sample, index) => ({ index, value: sample.value == null ? NaN : Number(sample.value) })).filter(item => Number.isFinite(item.value));
|
||||
if (!valid.length) return `<div class="ping-empty">${esc(tr('devices.pingNoSamples'))}</div>`;
|
||||
const values = valid.map(item => item.value);
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
const min = values.length ? Math.min(...values) : 0;
|
||||
const max = values.length ? Math.max(...values) : 10;
|
||||
const ceiling = Math.max(max, 10);
|
||||
const floor = Math.min(min, 0);
|
||||
const range = Math.max(1, ceiling - floor);
|
||||
const lastIndex = Math.max(1, samples.length - 1);
|
||||
const points = valid.map(item => {
|
||||
const x = padX + (item.index / lastIndex) * (width - padX * 2);
|
||||
const y = height - padY - ((item.value - floor) / range) * (height - padY * 2);
|
||||
const pointFor = (index, value) => {
|
||||
const x = padX + (index / lastIndex) * (width - padX * 2);
|
||||
const y = height - padY - ((value - floor) / range) * (height - padY * 2);
|
||||
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
||||
}).join(' ');
|
||||
};
|
||||
|
||||
// Keep successful ping runs separate so a packet loss never gets hidden by a line
|
||||
// connecting the samples before and after the failed request.
|
||||
const runs = [];
|
||||
let currentRun = [];
|
||||
samples.forEach((sample, index) => {
|
||||
const value = sample.value == null ? NaN : Number(sample.value);
|
||||
if (Number.isFinite(value)) {
|
||||
currentRun.push(pointFor(index, value));
|
||||
} else if (currentRun.length) {
|
||||
runs.push(currentRun);
|
||||
currentRun = [];
|
||||
}
|
||||
});
|
||||
if (currentRun.length) runs.push(currentRun);
|
||||
|
||||
const lineRuns = runs.map(points => points.length === 1
|
||||
? `<circle class="ping-success-point" cx="${points[0].split(',')[0]}" cy="${points[0].split(',')[1]}" r="1.8"></circle>`
|
||||
: `<polyline class="ping-success-line" points="${points.join(' ')}"></polyline>`
|
||||
).join('');
|
||||
|
||||
// Failed requests are packet-loss / unavailability samples. Draw a red band and
|
||||
// an X at each failed position so even a single loss is immediately visible.
|
||||
const losses = samples.map((sample, index) => ({ sample, index })).filter(({ sample }) => sample.value == null).map(({ index }) => {
|
||||
const x = padX + (index / lastIndex) * (width - padX * 2);
|
||||
const bandWidth = Math.max(4, Math.min(10, (width - padX * 2) / Math.max(samples.length, 12)));
|
||||
const left = Math.max(0, x - bandWidth / 2);
|
||||
const markerY = height - padY - 5;
|
||||
return `<g class="ping-loss-marker"><rect x="${left.toFixed(1)}" y="${padY}" width="${bandWidth.toFixed(1)}" height="${height - padY * 2}" rx="2"></rect><path d="M ${(x - 3).toFixed(1)} ${(markerY - 3).toFixed(1)} L ${(x + 3).toFixed(1)} ${(markerY + 3).toFixed(1)} M ${(x + 3).toFixed(1)} ${(markerY - 3).toFixed(1)} L ${(x - 3).toFixed(1)} ${(markerY + 3).toFixed(1)}"></path></g>`;
|
||||
}).join('');
|
||||
|
||||
const guide = [0.25, 0.5, 0.75].map(ratio => `<line x1="${padX}" y1="${(height * ratio).toFixed(1)}" x2="${width - padX}" y2="${(height * ratio).toFixed(1)}"></line>`).join('');
|
||||
return `<svg class="ping-sparkline" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none" role="img" aria-label="${esc(tr('devices.pingLive'))}"><g class="ping-grid-lines">${guide}</g><polyline points="${points}"></polyline></svg>`;
|
||||
return `<svg class="ping-sparkline" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none" role="img" aria-label="${esc(tr('devices.pingLive'))}"><g class="ping-grid-lines">${guide}</g>${lineRuns}${losses}</svg>`;
|
||||
}
|
||||
|
||||
function pingStats(samples) {
|
||||
|
||||
@@ -7141,3 +7141,34 @@ body.chart-fullscreen-open::before { content:""; position:fixed; inset:0; z-inde
|
||||
@media (max-width: 640px) {
|
||||
.technical-config-actions button { flex: 1 1 100%; }
|
||||
}
|
||||
|
||||
/* Ping diagnostics: make packet loss and device unavailability explicit on the chart. */
|
||||
.ping-sparkline .ping-success-line {
|
||||
fill: none;
|
||||
stroke: var(--accent);
|
||||
stroke-width: 2;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
.ping-sparkline .ping-success-point {
|
||||
fill: var(--accent);
|
||||
stroke: var(--accent);
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
.ping-sparkline .ping-loss-marker rect {
|
||||
fill: var(--danger-soft);
|
||||
stroke: var(--danger);
|
||||
stroke-width: 1;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
.ping-sparkline .ping-loss-marker path {
|
||||
fill: none;
|
||||
stroke: var(--danger);
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user