Commit ·
26b0dfb
1
Parent(s): 24b9903
Drop hero gauge plot; hero table shows 1 decimal for forecasts
Browse files- Remove hero_gauges and its wiring. Gauge visual didn't earn its
vertical space at any breakpoint vs the existing 3-row hero table.
- hero_markdown forecast rows now show one decimal place (e.g. 62.3°F)
instead of rounding to integer, to match the current-temperature row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- app.py +2 -28
- src/weather_ui.py +1 -79
app.py
CHANGED
|
@@ -22,7 +22,6 @@ from src.forecast import forecast_series
|
|
| 22 |
from src.weather_ui import (
|
| 23 |
aligned_comparison_markdown,
|
| 24 |
combined_figure,
|
| 25 |
-
hero_gauges,
|
| 26 |
hero_markdown,
|
| 27 |
residual_figure,
|
| 28 |
)
|
|
@@ -275,30 +274,6 @@ def refresh():
|
|
| 275 |
nws_temp=week["nws_aligned"].get("temp_f"),
|
| 276 |
horizon_hours=1,
|
| 277 |
)
|
| 278 |
-
|
| 279 |
-
# Gauge trio above the hero table: current Ecowitt + each model's
|
| 280 |
-
# next-round-hour prediction.
|
| 281 |
-
cur_temp_for_gauge = (
|
| 282 |
-
float(realtime["temp_f"]) if realtime and realtime.get("temp_f") is not None
|
| 283 |
-
else float("nan")
|
| 284 |
-
)
|
| 285 |
-
next_hour_utc = pd.Timestamp.now(tz="UTC").ceil("h")
|
| 286 |
-
|
| 287 |
-
def _nearest_value(series, target):
|
| 288 |
-
if series is None or series.empty:
|
| 289 |
-
return None
|
| 290 |
-
idx = series.index.get_indexer([target], method="nearest")[0]
|
| 291 |
-
if idx < 0 or idx >= len(series):
|
| 292 |
-
return None
|
| 293 |
-
return float(series.iloc[idx])
|
| 294 |
-
|
| 295 |
-
toto_temp_fcst = week["totos"].get("temp_f")
|
| 296 |
-
toto_next = _nearest_value(
|
| 297 |
-
toto_temp_fcst.median if toto_temp_fcst is not None else None,
|
| 298 |
-
next_hour_utc,
|
| 299 |
-
)
|
| 300 |
-
nws_next = _nearest_value(week["nws_aligned"].get("temp_f"), next_hour_utc)
|
| 301 |
-
gauge_fig = hero_gauges(cur_temp_for_gauge, toto_next, nws_next)
|
| 302 |
if "temp_f" in week["totos"]:
|
| 303 |
comparison_md = (
|
| 304 |
"### 🆚 Toto vs NWS — same hour, side-by-side\n\n"
|
|
@@ -318,7 +293,7 @@ def refresh():
|
|
| 318 |
resid_fig = residual_figure(resid_df) if not resid_df.empty else None
|
| 319 |
|
| 320 |
persist.push_db_async()
|
| 321 |
-
return hero,
|
| 322 |
|
| 323 |
|
| 324 |
# --- scoreboard ----------------------------------------------------------
|
|
@@ -477,7 +452,6 @@ with gr.Blocks(title="Toto Weather Forecast", theme=gr.themes.Soft()) as demo:
|
|
| 477 |
gr.Markdown(SUBTITLE)
|
| 478 |
|
| 479 |
hero_md = gr.Markdown()
|
| 480 |
-
gauge_plot = gr.Plot(label="Now vs next-hour forecasts", show_label=False)
|
| 481 |
gr.Markdown(f"### 📅 {VIEW_WEEK['label']}")
|
| 482 |
week_plot = gr.Plot(label="Weekly")
|
| 483 |
comparison_md = gr.Markdown()
|
|
@@ -547,7 +521,7 @@ with gr.Blocks(title="Toto Weather Forecast", theme=gr.themes.Soft()) as demo:
|
|
| 547 |
"Full spec: [`docs/toto-inference.md`](https://huggingface.co/spaces/bitsofchris/time-series-ai-weather-forecast/blob/main/docs/toto-inference.md)."
|
| 548 |
)
|
| 549 |
|
| 550 |
-
outputs = [hero_md,
|
| 551 |
demo.load(refresh, outputs=outputs)
|
| 552 |
|
| 553 |
|
|
|
|
| 22 |
from src.weather_ui import (
|
| 23 |
aligned_comparison_markdown,
|
| 24 |
combined_figure,
|
|
|
|
| 25 |
hero_markdown,
|
| 26 |
residual_figure,
|
| 27 |
)
|
|
|
|
| 274 |
nws_temp=week["nws_aligned"].get("temp_f"),
|
| 275 |
horizon_hours=1,
|
| 276 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
if "temp_f" in week["totos"]:
|
| 278 |
comparison_md = (
|
| 279 |
"### 🆚 Toto vs NWS — same hour, side-by-side\n\n"
|
|
|
|
| 293 |
resid_fig = residual_figure(resid_df) if not resid_df.empty else None
|
| 294 |
|
| 295 |
persist.push_db_async()
|
| 296 |
+
return hero, comparison_md, week["fig"], scoreboard, resid_fig
|
| 297 |
|
| 298 |
|
| 299 |
# --- scoreboard ----------------------------------------------------------
|
|
|
|
| 452 |
gr.Markdown(SUBTITLE)
|
| 453 |
|
| 454 |
hero_md = gr.Markdown()
|
|
|
|
| 455 |
gr.Markdown(f"### 📅 {VIEW_WEEK['label']}")
|
| 456 |
week_plot = gr.Plot(label="Weekly")
|
| 457 |
comparison_md = gr.Markdown()
|
|
|
|
| 521 |
"Full spec: [`docs/toto-inference.md`](https://huggingface.co/spaces/bitsofchris/time-series-ai-weather-forecast/blob/main/docs/toto-inference.md)."
|
| 522 |
)
|
| 523 |
|
| 524 |
+
outputs = [hero_md, comparison_md, week_plot, scoreboard_md, residual_plot]
|
| 525 |
demo.load(refresh, outputs=outputs)
|
| 526 |
|
| 527 |
|
src/weather_ui.py
CHANGED
|
@@ -110,7 +110,7 @@ def hero_markdown(
|
|
| 110 |
|
| 111 |
def _row(label: str, val: float | None, ts):
|
| 112 |
when = ts.tz_convert(tz).strftime("%-I %p %Z, %a %b %-d") if ts is not None else "—"
|
| 113 |
-
cell = f"**{val:.
|
| 114 |
return f"| {label} | {cell} | {when} |"
|
| 115 |
|
| 116 |
table = (
|
|
@@ -180,84 +180,6 @@ def emoji_strip_markdown(nws_df: pd.DataFrame, tz: str, n: int = 12) -> str:
|
|
| 180 |
return f"| {hours} |\n{sep}\n| {glyphs} |\n| {temps} |"
|
| 181 |
|
| 182 |
|
| 183 |
-
def hero_gauges(
|
| 184 |
-
cur_temp: float,
|
| 185 |
-
toto_next: float | None,
|
| 186 |
-
nws_next: float | None,
|
| 187 |
-
temp_range: tuple[float, float] = (20.0, 100.0),
|
| 188 |
-
) -> go.Figure:
|
| 189 |
-
"""Three horizontal bar 'thermometers' stacked vertically — readable
|
| 190 |
-
on mobile portrait without squishing. Cool/warm background bands
|
| 191 |
-
give a sense of where on the temperature scale each value sits."""
|
| 192 |
-
rows = [
|
| 193 |
-
("📡 Ecowitt (now)", cur_temp, "#222", None),
|
| 194 |
-
("🤖 Toto (next hr)", toto_next, "#1f77b4", cur_temp),
|
| 195 |
-
("🌎 NWS (next hr)", nws_next, "#d62728", cur_temp),
|
| 196 |
-
]
|
| 197 |
-
|
| 198 |
-
fig = go.Figure()
|
| 199 |
-
lo, hi = temp_range
|
| 200 |
-
# Background shading: cooler on the left, warmer on the right.
|
| 201 |
-
for x0, x1, color in [
|
| 202 |
-
(lo, lo + (hi - lo) * 0.25, "rgba(31,119,180,0.18)"),
|
| 203 |
-
(lo + (hi - lo) * 0.25, lo + (hi - lo) * 0.50, "rgba(31,119,180,0.08)"),
|
| 204 |
-
(lo + (hi - lo) * 0.50, lo + (hi - lo) * 0.75, "rgba(214,39,40,0.08)"),
|
| 205 |
-
(lo + (hi - lo) * 0.75, hi, "rgba(214,39,40,0.18)"),
|
| 206 |
-
]:
|
| 207 |
-
fig.add_shape(
|
| 208 |
-
type="rect",
|
| 209 |
-
x0=x0, x1=x1, y0=-0.5, y1=len(rows) - 0.5,
|
| 210 |
-
fillcolor=color, line=dict(width=0),
|
| 211 |
-
layer="below",
|
| 212 |
-
)
|
| 213 |
-
|
| 214 |
-
y_labels = [r[0] for r in rows]
|
| 215 |
-
for label, value, color, ref in rows:
|
| 216 |
-
if value is None or (isinstance(value, float) and value != value):
|
| 217 |
-
# No data — skip the bar but keep the row label by drawing 0-length.
|
| 218 |
-
fig.add_trace(go.Bar(
|
| 219 |
-
y=[label], x=[lo], orientation="h",
|
| 220 |
-
marker_color="rgba(0,0,0,0)",
|
| 221 |
-
text=["—"], textposition="outside",
|
| 222 |
-
textfont=dict(size=14, color="#888"),
|
| 223 |
-
showlegend=False, hoverinfo="skip",
|
| 224 |
-
))
|
| 225 |
-
continue
|
| 226 |
-
suffix = ""
|
| 227 |
-
if ref is not None:
|
| 228 |
-
d = value - ref
|
| 229 |
-
sign = "+" if d >= 0 else ""
|
| 230 |
-
suffix = f" <span style='color:#888'>({sign}{d:.1f})</span>"
|
| 231 |
-
fig.add_trace(go.Bar(
|
| 232 |
-
y=[label], x=[value - lo], base=[lo], orientation="h",
|
| 233 |
-
marker=dict(color=color, line=dict(color=color, width=0)),
|
| 234 |
-
text=[f"<b>{value:.1f}°F</b>{suffix}"],
|
| 235 |
-
textposition="outside",
|
| 236 |
-
textfont=dict(size=14),
|
| 237 |
-
cliponaxis=False,
|
| 238 |
-
showlegend=False,
|
| 239 |
-
hovertemplate=f"{label}: %{{x:.1f}}°F<extra></extra>",
|
| 240 |
-
))
|
| 241 |
-
|
| 242 |
-
fig.update_xaxes(
|
| 243 |
-
range=[lo, hi], title_text="°F",
|
| 244 |
-
showgrid=True, gridcolor="rgba(0,0,0,0.08)",
|
| 245 |
-
zeroline=False,
|
| 246 |
-
)
|
| 247 |
-
fig.update_yaxes(
|
| 248 |
-
categoryorder="array", categoryarray=list(reversed(y_labels)),
|
| 249 |
-
showgrid=False,
|
| 250 |
-
)
|
| 251 |
-
fig.update_layout(
|
| 252 |
-
height=240,
|
| 253 |
-
margin=dict(l=130, r=90, t=20, b=40),
|
| 254 |
-
bargap=0.35,
|
| 255 |
-
plot_bgcolor="rgba(0,0,0,0)",
|
| 256 |
-
paper_bgcolor="rgba(0,0,0,0)",
|
| 257 |
-
)
|
| 258 |
-
return fig
|
| 259 |
-
|
| 260 |
-
|
| 261 |
def residual_figure(
|
| 262 |
df: pd.DataFrame,
|
| 263 |
title: str = "Forecast residual — 1 h-ahead prediction minus Ecowitt actual, last 48 h (°F)",
|
|
|
|
| 110 |
|
| 111 |
def _row(label: str, val: float | None, ts):
|
| 112 |
when = ts.tz_convert(tz).strftime("%-I %p %Z, %a %b %-d") if ts is not None else "—"
|
| 113 |
+
cell = f"**{val:.1f}°F**" if val is not None else "—"
|
| 114 |
return f"| {label} | {cell} | {when} |"
|
| 115 |
|
| 116 |
table = (
|
|
|
|
| 180 |
return f"| {hours} |\n{sep}\n| {glyphs} |\n| {temps} |"
|
| 181 |
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
def residual_figure(
|
| 184 |
df: pd.DataFrame,
|
| 185 |
title: str = "Forecast residual — 1 h-ahead prediction minus Ecowitt actual, last 48 h (°F)",
|