feat(web): UTC-offset time zone picker and labelled barograph axes

Time zone is now a UTC+/-HH dropdown (e.g. UTC+7) instead of raw
minutes. Barograph gains dual value axes (pressure left in hPa,
temperature right in C), start/end time labels and gridlines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 22:10:02 +07:00
co-authored by Claude Opus 4.8
parent 26a5071e86
commit 8dc469be3d
+57 -24
View File
@@ -90,7 +90,15 @@ static const char INDEX_HTML[] PROGMEM = R"HTML(
.head h2{font-family:var(--serif); font-weight:600; font-size:1.15rem; margin:0}
.legend{display:flex; gap:16px; font-size:.76rem; color:var(--muted)}
.legend i{display:inline-block; width:20px; height:2px; vertical-align:middle; margin-right:6px}
#chart{width:100%; height:200px}
.baro{position:relative; padding:0 46px}
#chart{width:100%; height:200px; display:block}
.ax{position:absolute; top:0; height:200px; display:flex; flex-direction:column;
justify-content:space-between; font-family:var(--mono); font-variant-numeric:tabular-nums;
font-size:.7rem; pointer-events:none}
.ax-l{left:0; align-items:flex-start; color:var(--change)}
.ax-r{right:0; align-items:flex-end; color:var(--fair)}
.axx{display:flex; justify-content:space-between; padding:0 46px; margin-top:6px;
font-family:var(--mono); font-variant-numeric:tabular-nums; font-size:.7rem; color:var(--muted)}
.hint{color:var(--muted); font-size:.78rem; margin:10px 0 0}
/* Settings */
@@ -104,11 +112,12 @@ static const char INDEX_HTML[] PROGMEM = R"HTML(
.grid2{display:grid; grid-template-columns:1fr 1fr; gap:14px}
@media(max-width:520px){.grid2{grid-template-columns:1fr}}
label{display:block; font-size:.72rem; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); margin:0 0 6px}
input{
input,select{
width:100%; padding:11px 12px; background:var(--ink2); color:var(--text);
border:1px solid var(--line); border-radius:10px; font-family:var(--mono); font-size:1rem;
}
input:focus{outline:none; border-color:var(--brass); box-shadow:0 0 0 3px rgba(198,161,91,.18)}
select{appearance:none; cursor:pointer}
input:focus,select:focus{outline:none; border-color:var(--brass); box-shadow:0 0 0 3px rgba(198,161,91,.18)}
.actions{display:flex; align-items:center; gap:16px; margin-top:18px}
button{
padding:11px 22px; border:0; border-radius:10px; cursor:pointer;
@@ -162,7 +171,12 @@ static const char INDEX_HTML[] PROGMEM = R"HTML(
<span><i style="background:var(--fair)"></i>Temperature</span>
</div>
</div>
<svg id="chart" viewBox="0 0 640 200" preserveAspectRatio="none" aria-label="Pressure and temperature history"></svg>
<div class="baro">
<div class="ax ax-l" id="axL"></div>
<svg id="chart" viewBox="0 0 640 200" preserveAspectRatio="none" aria-label="Pressure and temperature history"></svg>
<div class="ax ax-r" id="axR"></div>
</div>
<div class="axx"><span id="axT0">&mdash;</span><span id="axT1">&mdash;</span></div>
<p class="hint" id="hint">Collecting data&hellip;</p>
</section>
@@ -173,7 +187,7 @@ static const char INDEX_HTML[] PROGMEM = R"HTML(
<div class="form">
<div class="grid2" style="margin-top:18px">
<div><label>Altitude &middot; metres</label><input id="s-alt" type="number" step="1"></div>
<div><label>Time zone &middot; minutes from UTC</label><input id="s-tz" type="number" step="15"></div>
<div><label>Time zone</label><select id="s-tz"></select></div>
<div><label>Latitude</label><input id="s-lat" type="number" step="0.0001"></div>
<div><label>Longitude</label><input id="s-lon" type="number" step="0.0001"></div>
</div>
@@ -302,34 +316,48 @@ async function refresh(){
await drawChart();
}
function fmtTime(ep){var d=new Date(ep*1000);function p(x){return(x<10?"0":"")+x;}return p(d.getUTCHours())+":"+p(d.getUTCMinutes());}
function fillAxis(id,mx,mn,deg){
var box=document.getElementById(id); box.innerHTML="";
[mx,(mx+mn)/2,mn].forEach(function(v){
var s=document.createElement("span");
s.textContent=deg?(Math.round(v)+"°"):Math.round(v);
box.appendChild(s);
});
}
async function drawChart(){
var data;
try{ data=await (await fetch("/api/history")).json(); }catch(e){ return; }
lastHistory=data||[];
var svg=document.getElementById("chart"), W=640,H=200,pad=6;
while(svg.firstChild) svg.removeChild(svg.firstChild);
if(!data.length){ document.getElementById("hint").textContent="Collecting data…"; updateSetHand(); return; }
if(!data.length){
document.getElementById("hint").textContent="Collecting data…";
document.getElementById("axL").innerHTML=""; document.getElementById("axR").innerHTML="";
document.getElementById("axT0").textContent=""; document.getElementById("axT1").textContent="";
updateSetHand(); return;
}
var ps=data.map(function(d){return d.msl;}), ts=data.map(function(d){return d.temp;}), n=data.length;
function line(vals,color,fillId){
var mn=Math.min.apply(null,vals), mx=Math.max.apply(null,vals), rng=(mx-mn)||1, d="";
for(var i=0;i<n;i++){
var x=pad+(W-2*pad)*(n>1?i/(n-1):0);
var y=H-pad-(H-2*pad)*((vals[i]-mn)/rng);
d+=(i?"L":"M")+x.toFixed(1)+" "+y.toFixed(1);
}
if(fillId){
var area=d+"L"+(W-pad)+" "+(H-pad)+"L"+pad+" "+(H-pad)+"Z";
svg.appendChild(el("path",{d:area,fill:color,opacity:.10}));
}
var pmn=Math.min.apply(null,ps), pmx=Math.max.apply(null,ps);
var tmn=Math.min.apply(null,ts), tmx=Math.max.apply(null,ts);
[0,0.5,1].forEach(function(f){var y=(pad+(H-2*pad)*f).toFixed(1);
svg.appendChild(el("line",{x1:0,y1:y,x2:W,y2:y,stroke:"#22314F","stroke-width":1}));});
function line(vals,mn,mx,color,fill){
var rng=(mx-mn)||1,d="";
for(var i=0;i<n;i++){var x=pad+(W-2*pad)*(n>1?i/(n-1):0);
var y=H-pad-(H-2*pad)*((vals[i]-mn)/rng); d+=(i?"L":"M")+x.toFixed(1)+" "+y.toFixed(1);}
if(fill){var area=d+"L"+(W-pad)+" "+(H-pad)+"L"+pad+" "+(H-pad)+"Z";
svg.appendChild(el("path",{d:area,fill:color,opacity:.10}));}
svg.appendChild(el("path",{d:d,fill:"none",stroke:color,"stroke-width":2,"stroke-linejoin":"round"}));
}
// baseline
svg.appendChild(el("line",{x1:0,y1:H-1,x2:W,y2:H-1,stroke:"#2A3A5A","stroke-width":1}));
line(ps,"#79C2D0",true);
line(ts,"#E4B658",false);
line(ps,pmn,pmx,"#79C2D0",true);
line(ts,tmn,tmx,"#E4B658",false);
fillAxis("axL",pmx,pmn,false);
fillAxis("axR",tmx,tmn,true);
document.getElementById("axT0").textContent=fmtTime(data[0].t);
document.getElementById("axT1").textContent=fmtTime(data[n-1].t);
var hrs=((data[n-1].t-data[0].t)/3600);
document.getElementById("hint").textContent=n+" readings over "+hrs.toFixed(1)+" h · "+
Math.min.apply(null,ps).toFixed(0)+""+Math.max.apply(null,ps).toFixed(0)+" hPa";
document.getElementById("hint").textContent=n+" readings over "+hrs.toFixed(1)+" h · pressure in hPa, temperature in °C";
updateSetHand();
}
@@ -351,11 +379,15 @@ function updateSetHand(){
document.getElementById("deltas").textContent="hPa · "+word;
}
function tzLabel(min){var s=min<0?"-":"+";var a=Math.abs(min);var h=Math.floor(a/60);var m=a%60;return "UTC"+s+h+(m?(":"+(m<10?"0":"")+m):"");}
function buildTz(){var sel=document.getElementById("s-tz");for(var m=-720;m<=840;m+=30){var o=document.createElement("option");o.value=m;o.textContent=tzLabel(m);sel.appendChild(o);}}
async function loadSettings(){
try{
var s=await (await fetch("/api/settings")).json();
document.getElementById("s-alt").value=s.altitude;
document.getElementById("s-tz").value=s.tz;
var tz=document.getElementById("s-tz"); tz.value=String(s.tz);
if(parseInt(tz.value,10)!==s.tz){var o=document.createElement("option");o.value=s.tz;o.textContent=tzLabel(s.tz);tz.appendChild(o);tz.value=String(s.tz);}
document.getElementById("s-lat").value=s.lat;
document.getElementById("s-lon").value=s.lon;
document.getElementById("coords").innerHTML=Number(s.lat).toFixed(2)+"&deg;N, "+Number(s.lon).toFixed(2)+"&deg;E";
@@ -380,6 +412,7 @@ document.getElementById("save").addEventListener("click", async function(){
});
buildGauge();
buildTz();
loadSettings();
refresh();
setInterval(refresh,15000);