fix: mutate polyline points in place on SSE ping, no fetch, no flicker

This commit is contained in:
M1 2026-03-17 07:44:09 +04:00
parent 2c32bc1115
commit 017d489e2e
2 changed files with 27 additions and 19 deletions

View File

@ -9,5 +9,5 @@ export function sparkline(values: number[], width = 120, height = 32): string {
const y = height - ((v - min) / range) * (height - 4) - 2; const y = height - ((v - min) / range) * (height - 4) - 2;
return `${x},${y}`; return `${x},${y}`;
}).join(' '); }).join(' ');
return `<svg width="${width}" height="${height}" class="inline-block"><polyline points="${points}" fill="none" stroke="#60a5fa" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`; return `<svg width="${width}" height="${height}" class="inline-block" data-vals="${values.join(',')}"><polyline points="${points}" fill="none" stroke="#60a5fa" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
} }

View File

@ -75,9 +75,8 @@
} catch {} } catch {}
}, 30000); }, 30000);
// SSE: on each ping, update text fields and fetch fresh sparkline // SSE: on each ping, update text fields and redraw sparkline in place
const _fetchingSparkline = new Set(); watchAccount((ping) => {
watchAccount(async (ping) => {
const card = document.querySelector(`[data-monitor-id="${ping.monitor_id}"]`); const card = document.querySelector(`[data-monitor-id="${ping.monitor_id}"]`);
if (!card) return; if (!card) return;
@ -89,22 +88,31 @@
if (ping.latency_ms != null) card.querySelector('.stat-latency').textContent = ping.latency_ms + 'ms'; if (ping.latency_ms != null) card.querySelector('.stat-latency').textContent = ping.latency_ms + 'ms';
card.querySelector('.stat-last').innerHTML = timeAgo(ping.checked_at); card.querySelector('.stat-last').innerHTML = timeAgo(ping.checked_at);
// Sparkline // Sparkline — append point to existing polyline, drop oldest, no refetch
const sparkEl = card.querySelector('.stat-sparkline'); if (ping.latency_ms != null) {
if (!sparkEl || _fetchingSparkline.has(ping.monitor_id)) return; const sparkEl = card.querySelector('.stat-sparkline');
_fetchingSparkline.add(ping.monitor_id); const polyline = sparkEl?.querySelector('polyline');
try { if (polyline) {
const res = await fetch(`/dashboard/monitors/${ping.monitor_id}/sparkline`, { credentials: 'same-origin' }); const pts = polyline.getAttribute('points').trim().split(' ').filter(Boolean);
if (res.ok) { const W = 120, H = 32;
const tmp = document.createElement('div'); // Parse existing points
tmp.innerHTML = await res.text(); let coords = pts.map(p => p.split(',').map(Number));
const newSvg = tmp.firstElementChild; // Extract just y-values (latencies are encoded in y relative to scale)
const oldSvg = sparkEl.querySelector('svg'); // Easier: maintain a data attr on the svg with the raw values
if (newSvg && oldSvg) oldSvg.replaceWith(newSvg); const svg = sparkEl.querySelector('svg');
else if (newSvg) sparkEl.appendChild(newSvg); let vals = svg?.dataset.vals ? svg.dataset.vals.split(',').map(Number) : [];
vals.push(ping.latency_ms);
if (vals.length > 20) vals.shift();
if (svg) svg.dataset.vals = vals.join(',');
// Redraw polyline in place
const max = Math.max(...vals, 1);
const min = Math.min(...vals, 0);
const range = max - min || 1;
const step = W / Math.max(vals.length - 1, 1);
const newPts = vals.map((v, i) => `${i * step},${H - ((v - min) / range) * (H - 4) - 2}`).join(' ');
polyline.setAttribute('points', newPts);
} }
} catch {} }
_fetchingSparkline.delete(ping.monitor_id);
}); });
</script> </script>