bitsofchris Claude Opus 4.7 (1M context) commited on
Commit
c71ac1b
Β·
1 Parent(s): 614270d

Hero gauges + 'next round hour' semantics

Browse files

- src/weather_ui.py: new hero_gauges() returns three side-by-side
Plotly Indicator gauges β€” Ecowitt now (black), Toto next hour
(blue), NWS next hour (red). Forecast gauges show a delta vs the
current reading. Background bands tint cool/warm so the eye can
read the temperature regime without checking the axis.
- src/weather_ui.py: hero_markdown target switches from now+1h to
now.ceil('h'). At 3:55 the hero forecast is for 4 PM; at 4:01 it
flips to 5 PM. Row labels read '(next hour)' instead of '(1 h-ahead)'.
- app.py: build the gauge figure each refresh from realtime + the
nearest Toto/NWS values to the next round hour. Render the gauge
trio directly above the hero table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +28 -2
  2. src/weather_ui.py +56 -3
app.py CHANGED
@@ -22,6 +22,7 @@ from src.forecast import forecast_series
22
  from src.weather_ui import (
23
  aligned_comparison_markdown,
24
  combined_figure,
 
25
  hero_markdown,
26
  residual_figure,
27
  )
@@ -274,6 +275,30 @@ def refresh():
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,7 +318,7 @@ def refresh():
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 ----------------------------------------------------------
@@ -435,6 +460,7 @@ with gr.Blocks(title="Toto Weather Forecast", theme=gr.themes.Soft()) as demo:
435
  gr.Markdown(SUBTITLE)
436
 
437
  hero_md = gr.Markdown()
 
438
  gr.Markdown(f"### πŸ“… {VIEW_WEEK['label']}")
439
  week_plot = gr.Plot(label="Weekly")
440
  comparison_md = gr.Markdown()
@@ -504,7 +530,7 @@ with gr.Blocks(title="Toto Weather Forecast", theme=gr.themes.Soft()) as demo:
504
  "Full spec: [`docs/toto-inference.md`](https://huggingface.co/spaces/bitsofchris/time-series-ai-weather-forecast/blob/main/docs/toto-inference.md)."
505
  )
506
 
507
- outputs = [hero_md, comparison_md, week_plot, scoreboard_md, residual_plot]
508
  demo.load(refresh, outputs=outputs)
509
 
510
 
 
22
  from src.weather_ui import (
23
  aligned_comparison_markdown,
24
  combined_figure,
25
+ hero_gauges,
26
  hero_markdown,
27
  residual_figure,
28
  )
 
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
  resid_fig = residual_figure(resid_df) if not resid_df.empty else None
319
 
320
  persist.push_db_async()
321
+ return hero, gauge_fig, comparison_md, week["fig"], scoreboard, resid_fig
322
 
323
 
324
  # --- scoreboard ----------------------------------------------------------
 
460
  gr.Markdown(SUBTITLE)
461
 
462
  hero_md = gr.Markdown()
463
+ gauge_plot = gr.Plot(label="Now vs next-hour forecasts", show_label=False)
464
  gr.Markdown(f"### πŸ“… {VIEW_WEEK['label']}")
465
  week_plot = gr.Plot(label="Weekly")
466
  comparison_md = gr.Markdown()
 
530
  "Full spec: [`docs/toto-inference.md`](https://huggingface.co/spaces/bitsofchris/time-series-ai-weather-forecast/blob/main/docs/toto-inference.md)."
531
  )
532
 
533
+ outputs = [hero_md, gauge_plot, comparison_md, week_plot, scoreboard_md, residual_plot]
534
  demo.load(refresh, outputs=outputs)
535
 
536
 
src/weather_ui.py CHANGED
@@ -93,7 +93,9 @@ def hero_markdown(
93
  if isinstance(row, pd.Series) and "short_forecast" in row:
94
  glyph = emoji_for(str(row["short_forecast"]))
95
 
96
- target = pd.Timestamp.now(tz="UTC") + pd.Timedelta(hours=horizon_hours)
 
 
97
 
98
  def _nearest(series, target_ts):
99
  if series is None or series.empty:
@@ -115,8 +117,8 @@ def hero_markdown(
115
  "| Source | Temperature | When |\n"
116
  "|---|---|---|\n"
117
  f"| πŸ“‘ Ecowitt (now) | **{cur_temp:.1f}Β°F** | {eco_when} |\n"
118
- f"{_row(f'πŸ€– Toto ({horizon_hours} h-ahead)', toto_val, toto_idx)}\n"
119
- f"{_row(f'🌎 NWS ({horizon_hours} h-ahead)', nws_val, nws_idx)}"
120
  )
121
  return f"### {glyph} {place}\n\n{table}"
122
 
@@ -178,6 +180,57 @@ def emoji_strip_markdown(nws_df: pd.DataFrame, tz: str, n: int = 12) -> str:
178
  return f"| {hours} |\n{sep}\n| {glyphs} |\n| {temps} |"
179
 
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  def residual_figure(
182
  df: pd.DataFrame,
183
  title: str = "Forecast residual β€” 1 h-ahead prediction minus Ecowitt actual, last 48 h (Β°F)",
 
93
  if isinstance(row, pd.Series) and "short_forecast" in row:
94
  glyph = emoji_for(str(row["short_forecast"]))
95
 
96
+ # 'Next round hour' β€” if it's 3:55, target = 4 PM. If it's 4:01,
97
+ # target = 5 PM. Matches what people intuit by 'in the next hour'.
98
+ target = pd.Timestamp.now(tz="UTC").ceil("h")
99
 
100
  def _nearest(series, target_ts):
101
  if series is None or series.empty:
 
117
  "| Source | Temperature | When |\n"
118
  "|---|---|---|\n"
119
  f"| πŸ“‘ Ecowitt (now) | **{cur_temp:.1f}Β°F** | {eco_when} |\n"
120
+ f"{_row('πŸ€– Toto (next hour)', toto_val, toto_idx)}\n"
121
+ f"{_row('🌎 NWS (next hour)', nws_val, nws_idx)}"
122
  )
123
  return f"### {glyph} {place}\n\n{table}"
124
 
 
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 side-by-side gauges: current Ecowitt temperature, plus each
190
+ model's prediction for the next round hour. Each forecast gauge also
191
+ shows its delta vs the current reading."""
192
+ cool_to_warm = [
193
+ {"range": [20, 40], "color": "rgba(31,119,180,0.18)"},
194
+ {"range": [40, 60], "color": "rgba(31,119,180,0.08)"},
195
+ {"range": [60, 80], "color": "rgba(214,39,40,0.08)"},
196
+ {"range": [80, 100], "color": "rgba(214,39,40,0.18)"},
197
+ ]
198
+ specs = [[{"type": "indicator"}, {"type": "indicator"}, {"type": "indicator"}]]
199
+ fig = make_subplots(rows=1, cols=3, specs=specs)
200
+
201
+ def _ind(value, title, bar_color, with_delta: bool):
202
+ ind = go.Indicator(
203
+ mode="gauge+number+delta" if with_delta else "gauge+number",
204
+ value=value if value is not None else float("nan"),
205
+ title={"text": title, "font": {"size": 14}},
206
+ number={"suffix": " Β°F", "font": {"size": 30}},
207
+ gauge=dict(
208
+ axis=dict(range=list(temp_range), tickwidth=1, tickcolor="#888"),
209
+ bar=dict(color=bar_color, thickness=0.25),
210
+ bgcolor="white",
211
+ borderwidth=1,
212
+ bordercolor="#e0e0e0",
213
+ steps=cool_to_warm,
214
+ threshold=dict(line=dict(color=bar_color, width=4), value=value or 0),
215
+ ),
216
+ delta=(
217
+ dict(reference=cur_temp, suffix=" Β°F", increasing={"color": "#d62728"}, decreasing={"color": "#1f77b4"})
218
+ if with_delta else None
219
+ ),
220
+ )
221
+ return ind
222
+
223
+ fig.add_trace(_ind(cur_temp, "πŸ“‘ Ecowitt (now)", "#222", False), row=1, col=1)
224
+ fig.add_trace(_ind(toto_next, "πŸ€– Toto (next hour)", "#1f77b4", True), row=1, col=2)
225
+ fig.add_trace(_ind(nws_next, "🌎 NWS (next hour)", "#d62728", True), row=1, col=3)
226
+ fig.update_layout(
227
+ height=260,
228
+ margin=dict(l=10, r=10, t=50, b=10),
229
+ paper_bgcolor="rgba(0,0,0,0)",
230
+ )
231
+ return fig
232
+
233
+
234
  def residual_figure(
235
  df: pd.DataFrame,
236
  title: str = "Forecast residual β€” 1 h-ahead prediction minus Ecowitt actual, last 48 h (Β°F)",