// dock‑door ranges const doors = [ ...Array.from({length: 138 - 124 + 1}, (_, i) => 124 + i), ...Array.from({length: 201 - 142 + 1}, (_, i) => 142 + i), ...Array.from({length: 209 - 202 + 1}, (_, i) => 202 + i), ]; // store readings per door const doorData = {}; // color by heat index function getColorFromHI(H) { const pct = Math.min(Math.max((H - 70) / 30, 0), 1); const r = 255, g = Math.round(255 * (1 - pct)); return `rgba(${r},${g},0,0.8)`; } // build the row of squares function createGrid() { const row = document.getElementById('dock-row'); doors.forEach(d => { doorData[d] = []; const sq = document.createElement('div'); sq.className = 'dock-square'; sq.dataset.door = d; sq.textContent = d; row.appendChild(sq); }); } // compute stats function computeStats(arr) { if (!arr.length) return null; let sum = 0, max = arr[0], min = arr[0]; arr.forEach(r => { sum += r.heatIndex; if (r.heatIndex > max.heatIndex) max = r; if (r.heatIndex < min.heatIndex) min = r; }); const avg = (sum / arr.length).toFixed(2); const latest = arr[arr.length - 1]; return { latest, max, min, avg }; } // color a square function fillSquare(door, hi) { const sq = document.querySelector(`.dock-square[data-door="${door}"]`); if (sq) sq.style.background = getColorFromHI(hi); } // load initial readings async function loadInitial() { const all = await fetch('/api/readings').then(r => r.json()); all.forEach(r => { if (doorData[r.dockDoor]) doorData[r.dockDoor].push(r); }); Object.entries(doorData).forEach(([d, arr]) => { const stats = computeStats(arr); if (stats) fillSquare(d, stats.latest.heatIndex); }); } // real‑time updates function subscribeRealtime() { const es = new EventSource('/api/stream'); es.addEventListener('new-reading', e => { const r = JSON.parse(e.data); if (doorData[r.dockDoor]) { doorData[r.dockDoor].push(r); fillSquare(r.dockDoor, r.heatIndex); } }); } // tooltips on hover function setupTooltips() { const tooltip = document.getElementById('tooltip'); document.querySelectorAll('.dock-square').forEach(sq => { sq.addEventListener('mouseenter', () => { const d = Number(sq.dataset.door); const stats = computeStats(doorData[d]); if (!stats) return; tooltip.innerHTML = ` Door ${d}
Latest: HI ${stats.latest.heatIndex}, T ${stats.latest.temperature}°F, H ${stats.latest.humidity}%
Time: ${new Date(stats.latest.timestamp).toLocaleString()}
Max: ${stats.max.heatIndex} at ${new Date(stats.max.timestamp).toLocaleString()}
Min: ${stats.min.heatIndex} at ${new Date(stats.min.timestamp).toLocaleString()}
Avg HI: ${stats.avg} `; tooltip.style.display = 'block'; }); sq.addEventListener('mousemove', e => { tooltip.style.top = `${e.clientY + 10}px`; tooltip.style.left = `${e.clientX + 10}px`; }); sq.addEventListener('mouseleave', () => { tooltip.style.display = 'none'; }); }); } // zoom via CSS vars function setupZoom() { const root = document.documentElement.style; let scale = 1; const initial = { size: 60, gap: 8, vgap: 24, wh: 200 }; const wrapper = document.querySelector('.heatmap-wrapper'); wrapper.addEventListener('wheel', e => { if (e.ctrlKey) { e.preventDefault(); scale += e.deltaY * -0.001; scale = Math.min(Math.max(scale, 0.5), 3); root.setProperty('--square-size', `${initial.size * scale}px`); root.setProperty('--gap', `${initial.gap * scale}px`); root.setProperty('--vertical-gap', `${initial.vgap * scale}px`); root.setProperty('--warehouse-height', `${initial.wh * scale}px`); } }); } document.addEventListener('DOMContentLoaded', () => { createGrid(); loadInitial(); subscribeRealtime(); setupTooltips(); setupZoom(); });