bitsofchris Claude Opus 4.7 (1M context) commited on
Commit
071f1ee
Β·
1 Parent(s): cd68687

Drop zoom view, remove past-Toto overlay, dated x-axis

Browse files

- app.py: remove VIEW_ZOOM and zoom_plot. Only the weekly view renders.
- app.py + src/weather_ui.py: stop building / passing past_toto into
combined_figure. The 'past forecasts' overlay used MAX(forecast_made_at)
per target hour, which sampled forecasts at wildly different lags
depending on autorefresh timing β€” that's where the sawtooth came from,
not from Toto. The scoreboard MAE is the honest single number; the
chart overlay was the wrong abstraction. (A fixed-horizon overlay is
a future option.)
- src/weather_ui.py: explicit x-axis tick format 'Month day\nhour AM/PM'
so dates are unambiguous on a 7-day-plus-72h chart.

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

Files changed (2) hide show
  1. app.py +4 -31
  2. src/weather_ui.py +8 -15
app.py CHANGED
@@ -30,18 +30,7 @@ CACHE_TTL_SECONDS = AUTO_REFRESH_SECONDS - 60 # so autorefresh always refetches
30
  DISPLAY_TZ = os.environ.get("DISPLAY_TZ", "America/New_York")
31
  PLACE_NAME = os.environ.get("PLACE_NAME", "Yaphank, NY")
32
 
33
- # Two fixed views β€” no more dropdowns.
34
- VIEW_ZOOM = {
35
- # 24h history is the longest window where Ecowitt reliably keeps 5-min
36
- # data; beyond ~30h the cloud has already downsampled to 30-min and the
37
- # zoom view stops being zoomed. 8h forecast keeps the chart ~75% history,
38
- # ~25% future.
39
- "label": "Last 24 h Β· 8 h forecast (5-min cadence)",
40
- "cycle_type": "5min",
41
- "resample": "5min",
42
- "history_hours": 24,
43
- "horizon_hours": 8,
44
- }
45
  VIEW_WEEK = {
46
  "label": "Past 7 days Β· 72 h forecast (hourly cadence)",
47
  "cycle_type": "30min",
@@ -190,15 +179,6 @@ def _build_view(view: dict, log_conn, log_to_scoreboard: bool) -> dict:
190
  now = pd.Timestamp.now(tz="UTC").floor(resample)
191
  visible_steps = int(round(hours / step_hours))
192
  visible_history = history.tail(visible_steps)
193
- since_unix = (
194
- int(visible_history.index.min().timestamp()) if not visible_history.empty else None
195
- )
196
- past_toto: dict[str, pd.DataFrame] = {}
197
- for m in METRICS:
198
- col = m["col"]
199
- pt = forecast_log.historical_predictions(log_conn, "toto", col, since_unix=since_unix)
200
- if not pt.empty:
201
- past_toto[col] = pt
202
 
203
  fig = combined_figure(
204
  history=visible_history,
@@ -206,7 +186,6 @@ def _build_view(view: dict, log_conn, log_to_scoreboard: bool) -> dict:
206
  nws_df=nws_future,
207
  metrics=METRICS,
208
  now=now,
209
- past_toto=past_toto,
210
  )
211
  return {
212
  "fig": fig,
@@ -221,10 +200,7 @@ def refresh():
221
  realtime = fetch_realtime_snapshot()
222
  log_conn = forecast_log.connect()
223
 
224
- # Weekly view is the canonical one logged to the scoreboard (hourly
225
- # cadence keeps target_ts aligned with NWS hourly periods).
226
  week = _build_view(VIEW_WEEK, log_conn, log_to_scoreboard=True)
227
- zoom = _build_view(VIEW_ZOOM, log_conn, log_to_scoreboard=False)
228
 
229
  # Hero uses the weekly history + the NWS period containing "now".
230
  nws_df_raw = week["nws_df_raw"]
@@ -250,7 +226,7 @@ def refresh():
250
  scoreboard = render_scoreboard(log_conn)
251
 
252
  persist.push_db_async()
253
- return hero, comparison_md, zoom["fig"], week["fig"], scoreboard
254
 
255
 
256
  # --- scoreboard ----------------------------------------------------------
@@ -370,10 +346,7 @@ with gr.Blocks(title="Toto Weather Forecast", theme=gr.themes.Soft()) as demo:
370
 
371
  scoreboard_md = gr.Markdown()
372
 
373
- gr.Markdown(f"### πŸ” Zoomed-in view β€” {VIEW_ZOOM['label']}")
374
- zoom_plot = gr.Plot(label="Zoomed-in")
375
-
376
- gr.Markdown(f"### πŸ“… Weekly view β€” {VIEW_WEEK['label']}")
377
  week_plot = gr.Plot(label="Weekly")
378
 
379
  with gr.Accordion("How the scoreboard is calculated", open=False):
@@ -418,7 +391,7 @@ with gr.Blocks(title="Toto Weather Forecast", theme=gr.themes.Soft()) as demo:
418
  "Full spec: [`docs/toto-inference.md`](https://huggingface.co/spaces/bitsofchris/time-series-ai-weather-forecast/blob/main/docs/toto-inference.md)."
419
  )
420
 
421
- outputs = [hero_md, comparison_md, zoom_plot, week_plot, scoreboard_md]
422
  demo.load(refresh, outputs=outputs)
423
 
424
 
 
30
  DISPLAY_TZ = os.environ.get("DISPLAY_TZ", "America/New_York")
31
  PLACE_NAME = os.environ.get("PLACE_NAME", "Yaphank, NY")
32
 
33
+ # Single canonical view β€” weekly, hourly cadence.
 
 
 
 
 
 
 
 
 
 
 
34
  VIEW_WEEK = {
35
  "label": "Past 7 days Β· 72 h forecast (hourly cadence)",
36
  "cycle_type": "30min",
 
179
  now = pd.Timestamp.now(tz="UTC").floor(resample)
180
  visible_steps = int(round(hours / step_hours))
181
  visible_history = history.tail(visible_steps)
 
 
 
 
 
 
 
 
 
182
 
183
  fig = combined_figure(
184
  history=visible_history,
 
186
  nws_df=nws_future,
187
  metrics=METRICS,
188
  now=now,
 
189
  )
190
  return {
191
  "fig": fig,
 
200
  realtime = fetch_realtime_snapshot()
201
  log_conn = forecast_log.connect()
202
 
 
 
203
  week = _build_view(VIEW_WEEK, log_conn, log_to_scoreboard=True)
 
204
 
205
  # Hero uses the weekly history + the NWS period containing "now".
206
  nws_df_raw = week["nws_df_raw"]
 
226
  scoreboard = render_scoreboard(log_conn)
227
 
228
  persist.push_db_async()
229
+ return hero, comparison_md, week["fig"], scoreboard
230
 
231
 
232
  # --- scoreboard ----------------------------------------------------------
 
346
 
347
  scoreboard_md = gr.Markdown()
348
 
349
+ gr.Markdown(f"### πŸ“… {VIEW_WEEK['label']}")
 
 
 
350
  week_plot = gr.Plot(label="Weekly")
351
 
352
  with gr.Accordion("How the scoreboard is calculated", open=False):
 
391
  "Full spec: [`docs/toto-inference.md`](https://huggingface.co/spaces/bitsofchris/time-series-ai-weather-forecast/blob/main/docs/toto-inference.md)."
392
  )
393
 
394
+ outputs = [hero_md, comparison_md, week_plot, scoreboard_md]
395
  demo.load(refresh, outputs=outputs)
396
 
397
 
src/weather_ui.py CHANGED
@@ -187,8 +187,6 @@ def combined_figure(
187
  nws_df: pd.DataFrame | None,
188
  metrics: list[dict],
189
  now: pd.Timestamp | None = None,
190
- past_toto: dict[str, pd.DataFrame] | None = None,
191
- past_nws: dict[str, pd.DataFrame] | None = None,
192
  ) -> go.Figure:
193
  """Three stacked subplots sharing the x-axis."""
194
  fig = make_subplots(
@@ -214,18 +212,6 @@ def combined_figure(
214
  ),
215
  row=i, col=1,
216
  )
217
- # Past Toto forecasts vs the same hours' actuals.
218
- if past_toto and col in past_toto:
219
- pt = past_toto[col]
220
- fig.add_trace(
221
- go.Scatter(
222
- x=pt.index, y=pt["p50"].values,
223
- name="πŸ€– Toto (past forecasts)", mode="lines",
224
- line=dict(color="rgba(31,119,180,0.55)", width=1.5),
225
- showlegend=showlegend, legendgroup="toto-past",
226
- ),
227
- row=i, col=1,
228
- )
229
  if toto is not None:
230
  fig.add_trace(
231
  go.Scatter(
@@ -268,7 +254,14 @@ def combined_figure(
268
  fig.update_layout(
269
  height=900,
270
  hovermode="x unified",
271
- margin=dict(l=50, r=20, t=50, b=40),
272
  legend=dict(orientation="h", yanchor="bottom", y=1.04, xanchor="right", x=1),
273
  )
 
 
 
 
 
 
 
274
  return fig
 
187
  nws_df: pd.DataFrame | None,
188
  metrics: list[dict],
189
  now: pd.Timestamp | None = None,
 
 
190
  ) -> go.Figure:
191
  """Three stacked subplots sharing the x-axis."""
192
  fig = make_subplots(
 
212
  ),
213
  row=i, col=1,
214
  )
 
 
 
 
 
 
 
 
 
 
 
 
215
  if toto is not None:
216
  fig.add_trace(
217
  go.Scatter(
 
254
  fig.update_layout(
255
  height=900,
256
  hovermode="x unified",
257
+ margin=dict(l=50, r=20, t=50, b=60),
258
  legend=dict(orientation="h", yanchor="bottom", y=1.04, xanchor="right", x=1),
259
  )
260
+ # Explicit date + hour on the x-axis so the reader doesn't have to guess
261
+ # what day a tick refers to.
262
+ fig.update_xaxes(
263
+ tickformat="%b %-d\n%-I %p",
264
+ ticklabelmode="instant",
265
+ showgrid=True,
266
+ )
267
  return fig