// average helper function average(arr) { return arr.reduce((a, b) => a + b, 0) / arr.length; } // interval mappings const periodKeys = ['hourly','daily','weekly','monthly','yearly']; const periodLabels = ['Hourly','Daily','Weekly','Monthly','Yearly']; // bucket definitions const periodConfig = { hourly: { keyFn: r => r.timestamp.slice(0,13), labelFn: k => k.replace('T',' ') }, daily: { keyFn: r => r.timestamp.slice(0,10), labelFn: k => k }, weekly: { keyFn: r => { const d = new Date(r.timestamp), y = d.getFullYear(); const w = Math.ceil((((d - new Date(y,0,1))/864e5) + new Date(y,0,1).getDay()+1)/7); return `${y}-W${w}`; }, labelFn: k => k }, monthly: { keyFn: r => r.timestamp.slice(0,7), labelFn: k => k }, yearly: { keyFn: r => r.timestamp.slice(0,4), labelFn: k => k } }; // global Chart.js instance let trendChart, dataTable; // fetch readings from server async function fetchReadings() { const res = await fetch('/api/readings'); return res.ok ? res.json() : []; } // determine direction based on dock door # function getDirection(dock) { return ( (dock>=124 && dock<=138) || (dock>=202 && dock<=209) ) ? 'Inbound' : 'Outbound'; } // render DataTable function renderTable(allReadings) { const tbody = $('#trendTable tbody').empty(); allReadings.forEach(r => { const ts = new Date(r.timestamp).toLocaleString('en-US', { timeZone:'Europe/London' }); const dir = getDirection(r.dockDoor); tbody.append(` ${ts} ${r.temperature.toFixed(1)} ${r.humidity.toFixed(1)} ${r.heatIndex.toFixed(1)} ${r.dockDoor} ${dir} `); }); // initialize or redraw DataTable if ($.fn.DataTable.isDataTable('#trendTable')) { dataTable.clear().rows.add($('#trendTable tbody tr')).draw(); } else { dataTable = $('#trendTable').DataTable({ paging: true, pageLength: 25, ordering: true, order: [[0,'desc']], autoWidth: false, scrollX: true }); } } // draw or update chart async function drawTrend() { const all = await fetchReadings(); const slider = document.getElementById('periodSlider'); const periodKey = periodKeys[slider.value]; const cfg = periodConfig[periodKey]; // group & stats const groups = {}; all.forEach(r => { const key = cfg.keyFn(r); groups[key] = groups[key] || { temps:[], hums:[], his:[] }; groups[key].temps.push(r.temperature); groups[key].hums.push(r.humidity); groups[key].his.push(r.heatIndex); }); const labels = Object.keys(groups).sort(); const stats = labels.map(k => { const g = groups[k]; return { temp: { avg: average(g.temps).toFixed(2), min: Math.min(...g.temps), max: Math.max(...g.temps) }, hum: { avg: average(g.hums).toFixed(2), min: Math.min(...g.hums), max: Math.max(...g.hums) }, hi: { avg: average(g.his).toFixed(2), min: Math.min(...g.his), max: Math.max(...g.his) } }; }); // selected toggles const checks = Array.from(document.querySelectorAll('#metricToggles input:checked')); const datasets = checks.map(chk => { const m = chk.dataset.metric, s = chk.dataset.stat; return { label: `${m.toUpperCase()} ${s.toUpperCase()}`, data: stats.map(x => x[m][s]), fill: false, tension: 0.1 }; }); const ctx = document.getElementById('trendChart').getContext('2d'); if (trendChart) { trendChart.data.labels = labels.map(cfg.labelFn); trendChart.data.datasets = datasets; trendChart.update(); } else { trendChart = new Chart(ctx, { type: 'line', data: { labels: labels.map(cfg.labelFn), datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true }, tooltip: { mode:'index', intersect:false }, zoom: { pan: { enabled:true, mode:'x', modifierKey:'ctrl' }, zoom: { wheel:{enabled:true}, pinch:{enabled:true}, mode:'x' } } }, scales: { x: { display:true }, y: { display:true } } } }); } // always update table below renderTable(all); } // wire up controls document.addEventListener('DOMContentLoaded', () => { // initial draw drawTrend(); // slider document.getElementById('periodSlider') .addEventListener('input', e => { document.getElementById('periodLabel').textContent = periodLabels[e.target.value]; drawTrend(); }); // toggles document.querySelectorAll('#metricToggles input') .forEach(chk => chk.addEventListener('change', drawTrend)); });