Skip to content

Commit fe10d8a

Browse files
i6modsclaude
andcommitted
Add moving average trend line and period average stat card
- Period average µSv/h stat card with label that updates per time range (1hr Avg / 24hr Avg / 7 Day Avg / 30 Day Avg) - Dashed white moving average overlay on CPM and µSv/h charts with smoothing window tuned to each range's data density - Chart legend now visible showing raw data vs average line - Update hardware references: ESP8266 → ESP32-C3 - Update IoT server footer text to match current deployment Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 3244227 commit fe10d8a

1 file changed

Lines changed: 86 additions & 18 deletions

File tree

index.html

Lines changed: 86 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,12 @@ <h1>Radiation Monitor</h1>
488488
<div class="stat-value" id="dataPoints">--</div>
489489
<div class="stat-unit">in selected range</div>
490490
</div>
491+
<div class="stat-card">
492+
<span class="stat-icon">📈</span>
493+
<div class="stat-label" id="avgUSVLabel">Period Average</div>
494+
<div class="stat-value" id="avgUSV">--</div>
495+
<div class="stat-unit">µSv/h</div>
496+
</div>
491497
</div>
492498

493499
<div class="scale-container">
@@ -605,12 +611,23 @@ <h3>☢ <span>Dose Rate</span> — µSv/h</h3>
605611
if (!isNaN(usv)) updateScaleMarker(usv);
606612

607613
// Charts
608-
const labels = feeds.map(f => new Date(f.created_at));
614+
const labels = feeds.map(f => new Date(f.created_at));
609615
const cpmData = feeds.map(f => parseFloat(f.field1) || null);
610616
const usvData = feeds.map(f => parseFloat(f.field2) || null);
611617

612-
buildChart('cpmChart', labels, cpmData, 'CPM', '#FFD700', 'rgba(255,215,0,0.08)');
613-
buildChart('usvChart', labels, usvData, 'µSv/h', '#FFA500', 'rgba(255,165,0,0.08)', 6);
618+
// Period average stat card
619+
const validUSV = usvData.filter(v => v !== null && !isNaN(v));
620+
const avgUSV = validUSV.length > 0 ? validUSV.reduce((a, b) => a + b, 0) / validUSV.length : null;
621+
document.getElementById('avgUSVLabel').textContent = getAvgLabel();
622+
document.getElementById('avgUSV').textContent = avgUSV !== null ? avgUSV.toFixed(4) : '--';
623+
624+
// Moving average datasets for trend lines
625+
const win = getAvgWindow();
626+
const cpmAvg = computeMovingAverage(cpmData, win);
627+
const usvAvg = computeMovingAverage(usvData, win);
628+
629+
buildChart('cpmChart', labels, cpmData, cpmAvg, 'CPM', '#FFD700', 'rgba(255,215,0,0.08)');
630+
buildChart('usvChart', labels, usvData, usvAvg, 'µSv/h', '#FFA500', 'rgba(255,165,0,0.08)', 6);
614631

615632
// Last update
616633
const ago = timeAgo(new Date(latest.created_at));
@@ -676,32 +693,58 @@ <h3>☢ <span>Dose Rate</span> — µSv/h</h3>
676693
document.getElementById('scaleMarker').style.left = Math.min(98, Math.max(1, pct)) + '%';
677694
}
678695

679-
function buildChart(id, labels, data, label, lineColor, fillColor, decimals = 1) {
696+
function buildChart(id, labels, data, avgData, label, lineColor, fillColor, decimals = 1) {
680697
if (charts[id]) { charts[id].destroy(); }
681698
const ctx = document.getElementById(id).getContext('2d');
682699
charts[id] = new Chart(ctx, {
683700
type: 'line',
684701
data: {
685702
labels,
686-
datasets: [{
687-
label,
688-
data,
689-
borderColor: lineColor,
690-
backgroundColor: fillColor,
691-
borderWidth: 2,
692-
tension: 0.3,
693-
fill: true,
694-
pointRadius: data.length > 200 ? 0 : 2,
695-
pointHoverRadius: 5,
696-
spanGaps: true
697-
}]
703+
datasets: [
704+
{
705+
label,
706+
data,
707+
borderColor: lineColor,
708+
backgroundColor: fillColor,
709+
borderWidth: 2,
710+
tension: 0.3,
711+
fill: true,
712+
pointRadius: data.length > 200 ? 0 : 2,
713+
pointHoverRadius: 5,
714+
spanGaps: true,
715+
order: 2
716+
},
717+
{
718+
label: getAvgLabel(),
719+
data: avgData,
720+
borderColor: 'rgba(255,255,255,0.55)',
721+
backgroundColor: 'transparent',
722+
borderWidth: 1.5,
723+
borderDash: [6, 4],
724+
tension: 0.4,
725+
fill: false,
726+
pointRadius: 0,
727+
pointHoverRadius: 4,
728+
spanGaps: true,
729+
order: 1
730+
}
731+
]
698732
},
699733
options: {
700734
responsive: true,
701735
maintainAspectRatio: true,
702736
interaction: { intersect: false, mode: 'index' },
703737
plugins: {
704-
legend: { display: false },
738+
legend: {
739+
display: true,
740+
labels: {
741+
color: '#888',
742+
boxWidth: 16,
743+
font: { size: 11 },
744+
usePointStyle: true,
745+
pointStyle: 'line'
746+
}
747+
},
705748
tooltip: {
706749
backgroundColor: '#1a1a1a',
707750
borderColor: 'rgba(255,215,0,0.3)',
@@ -711,7 +754,11 @@ <h3>☢ <span>Dose Rate</span> — µSv/h</h3>
711754
padding: 12,
712755
callbacks: {
713756
title: ctx => new Date(ctx[0].label).toLocaleString(),
714-
label: ctx => ` ${ctx.parsed.y !== null ? ctx.parsed.y.toFixed(decimals) : 'N/A'} ${label}`
757+
label: ctx => {
758+
const v = ctx.parsed.y;
759+
const suffix = ctx.datasetIndex === 0 ? label : getAvgLabel();
760+
return ` ${v !== null && !isNaN(v) ? v.toFixed(decimals) : 'N/A'} ${suffix}`;
761+
}
715762
}
716763
}
717764
},
@@ -740,6 +787,27 @@ <h3>☢ <span>Dose Rate</span> — µSv/h</h3>
740787
});
741788
}
742789

790+
function getAvgLabel() {
791+
const labels = { '1h': '1hr Avg', '24h': '24hr Avg', '7d': '7 Day Avg', '30d': '30 Day Avg' };
792+
return labels[currentRange] || 'Average';
793+
}
794+
795+
function getAvgWindow() {
796+
// Window size tuned so the smoothing makes visual sense for each range's data density
797+
const windows = { '1h': 10, '24h': 60, '7d': 6, '30d': 5 };
798+
return windows[currentRange] || 10;
799+
}
800+
801+
function computeMovingAverage(data, window) {
802+
const half = Math.floor(window / 2);
803+
return data.map((_, i) => {
804+
const start = Math.max(0, i - half);
805+
const end = Math.min(data.length, i + half + 1);
806+
const slice = data.slice(start, end).filter(v => v !== null && !isNaN(v));
807+
return slice.length > 0 ? slice.reduce((a, b) => a + b, 0) / slice.length : null;
808+
});
809+
}
810+
743811
function getXFormats() {
744812
switch (currentRange) {
745813
case '1h': return { minute: 'HH:mm', hour: 'HH:mm' };

0 commit comments

Comments
 (0)