fix: heatmap opacity flash on new packet arrival

When new data arrived, toggleHeatmap() destroyed and recreated the
heat layer, causing a brief flash at full opacity before the CSS
opacity was applied via setTimeout. Now reuses the existing layer
via setLatLngs() for data updates, and hooks the 'add' event for
immediate opacity on first creation. No more flash.

All 12 E2E tests pass locally.
This commit is contained in:
you
2026-03-24 17:53:29 +00:00
parent dbb792bcbb
commit 325fdbe50e
2 changed files with 47 additions and 39 deletions
+25 -25
View File
@@ -22,9 +22,9 @@
<meta name="twitter:title" content="MeshCore Analyzer">
<meta name="twitter:description" content="Real-time MeshCore LoRa mesh network analyzer — live packet visualization, node tracking, channel decryption, and route analysis.">
<meta name="twitter:image" content="https://raw.githubusercontent.com/Kpa-clawbot/meshcore-analyzer/master/public/og-image.png">
<link rel="stylesheet" href="style.css?v=1774373169">
<link rel="stylesheet" href="home.css?v=1774373169">
<link rel="stylesheet" href="live.css?v=1774373169">
<link rel="stylesheet" href="style.css?v=1774374809">
<link rel="stylesheet" href="home.css?v=1774374809">
<link rel="stylesheet" href="live.css?v=1774374809">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="anonymous">
@@ -81,27 +81,27 @@
<main id="app" role="main"></main>
<script src="vendor/qrcode.js"></script>
<script src="roles.js?v=1774373169"></script>
<script src="customize.js?v=1774373169" onerror="console.error('Failed to load:', this.src)"></script>
<script src="region-filter.js?v=1774373169"></script>
<script src="hop-resolver.js?v=1774373169"></script>
<script src="hop-display.js?v=1774373169"></script>
<script src="app.js?v=1774373169"></script>
<script src="home.js?v=1774373169"></script>
<script src="packet-filter.js?v=1774373169"></script>
<script src="packets.js?v=1774373169"></script>
<script src="map.js?v=1774373169" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1774373169" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1774373169" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1774373169" onerror="console.error('Failed to load:', this.src)"></script>
<script src="analytics.js?v=1774373169" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio.js?v=1774373169" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v1-constellation.js?v=1774373169" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-lab.js?v=1774373169" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1774373169" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observers.js?v=1774373169" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observer-detail.js?v=1774373169" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1774373169" onerror="console.error('Failed to load:', this.src)"></script>
<script src="perf.js?v=1774373169" onerror="console.error('Failed to load:', this.src)"></script>
<script src="roles.js?v=1774374809"></script>
<script src="customize.js?v=1774374809" onerror="console.error('Failed to load:', this.src)"></script>
<script src="region-filter.js?v=1774374809"></script>
<script src="hop-resolver.js?v=1774374809"></script>
<script src="hop-display.js?v=1774374809"></script>
<script src="app.js?v=1774374809"></script>
<script src="home.js?v=1774374809"></script>
<script src="packet-filter.js?v=1774374809"></script>
<script src="packets.js?v=1774374809"></script>
<script src="map.js?v=1774374809" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1774374809" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1774374809" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1774374809" onerror="console.error('Failed to load:', this.src)"></script>
<script src="analytics.js?v=1774374809" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio.js?v=1774374809" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v1-constellation.js?v=1774374809" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-lab.js?v=1774374809" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1774374809" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observers.js?v=1774374809" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observer-detail.js?v=1774374809" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1774374809" onerror="console.error('Failed to load:', this.src)"></script>
<script src="perf.js?v=1774374809" onerror="console.error('Failed to load:', this.src)"></script>
</body>
</html>
+22 -14
View File
@@ -722,27 +722,35 @@
}
function toggleHeatmap(on) {
if (heatLayer) { map.removeLayer(heatLayer); heatLayer = null; window._meshcoreHeatLayer = null; }
if (!on || !map) return;
if (!on || !map) {
if (heatLayer) { map.removeLayer(heatLayer); heatLayer = null; window._meshcoreHeatLayer = null; }
return;
}
const points = nodes
.filter(n => n.lat != null && n.lon != null)
.map(n => {
const weight = n.advert_count || 1;
return [n.lat, n.lon, weight];
});
if (points.length && typeof L.heatLayer === 'function') {
var savedOpacity = parseFloat(localStorage.getItem('meshcore-heatmap-opacity'));
if (isNaN(savedOpacity)) savedOpacity = 0.25;
heatLayer = L.heatLayer(points, {
radius: 25, blur: 15, maxZoom: 14, minOpacity: 0.05,
gradient: { 0.2: '#0d47a1', 0.4: '#1565c0', 0.6: '#42a5f5', 0.8: '#ffca28', 1.0: '#ff5722' }
}).addTo(map);
// Set overall layer opacity (affects all gradient colors, not just minimum)
heatLayer.getContainer && heatLayer.getContainer() ?
(heatLayer.getContainer().style.opacity = savedOpacity) :
setTimeout(function() { if (heatLayer._canvas) heatLayer._canvas.style.opacity = savedOpacity; }, 100);
window._meshcoreHeatLayer = heatLayer;
if (!points.length || typeof L.heatLayer !== 'function') return;
var savedOpacity = parseFloat(localStorage.getItem('meshcore-heatmap-opacity'));
if (isNaN(savedOpacity)) savedOpacity = 0.25;
// Update existing layer data without recreating (avoids opacity flash)
if (heatLayer) {
heatLayer.setLatLngs(points);
return;
}
heatLayer = L.heatLayer(points, {
radius: 25, blur: 15, maxZoom: 14, minOpacity: 0.05,
gradient: { 0.2: '#0d47a1', 0.4: '#1565c0', 0.6: '#42a5f5', 0.8: '#ffca28', 1.0: '#ff5722' }
});
// Set opacity on canvas BEFORE it's visible — hook the 'add' event
heatLayer.on('add', function() {
var canvas = heatLayer._canvas || (heatLayer.getContainer && heatLayer.getContainer());
if (canvas) canvas.style.opacity = savedOpacity;
});
heatLayer.addTo(map);
window._meshcoreHeatLayer = heatLayer;
}
let _themeRefreshHandler = null;