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:
+57
-24
@@ -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">—</span><span id="axT1">—</span></div>
|
||||
<p class="hint" id="hint">Collecting data…</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 · metres</label><input id="s-alt" type="number" step="1"></div>
|
||||
<div><label>Time zone · 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)+"°N, "+Number(s.lon).toFixed(2)+"°E";
|
||||
@@ -380,6 +412,7 @@ document.getElementById("save").addEventListener("click", async function(){
|
||||
});
|
||||
|
||||
buildGauge();
|
||||
buildTz();
|
||||
loadSettings();
|
||||
refresh();
|
||||
setInterval(refresh,15000);
|
||||
|
||||
Reference in New Issue
Block a user