Weekly view: center the daily-graded window on the target day (#28)
* Pin the °F/°C toggle to the header's top-right on all widths The unit toggle sat inside .brand after a flex-basis:auto title block, so on narrow (phone) widths the long tagline claimed the first row and bumped the toggle onto its own line mid-header. Give the title block flex:1 1 0 with min-width:0 so it contributes ~nothing to the wrap decision and shrinks instead — keeping the toggle on the logo's row, top-right, with the tagline wrapping beneath and the view tabs on their own row below. * Weekly view: center the daily-graded window on the target day Reframe the "Daily, graded" chart + table on the weekly view around the selected target day instead of ending at it: - Show two weeks of history before the target, the target itself, then up to 7 days after it. Days after the target are real observations when they are already in the past and the forward forecast when they run into the future. - Mark the target day with a solid orange line on the chart (and an accent edge on its table column); draw the dashed "forecast →" divider only where the window actually crosses into the future and it is separated from the target marker. Backend: _build_grade now grades a [target-days, target+after] window built from both the archive and the recent+forecast bundle (the bundle wins per date; the archive fills older gaps), so any past target renders its surrounding week. api/grade gains an `after` param (default 7) and the cache key includes it; the /cell prefetch bundle builds the matching slice. Frontend: the chart consumes the server-built window directly — the separate forecast fetch and gap-merge are gone. The target is looked up by date for the comparison cards (the newest row is now a forecast day), and the graded table opens centered on the target column. * Weekly view: extend the after-target window to 14 days Bump the daily-graded window's after-target span from 7 to 14 days. A target far enough in the past now shows 14 observed days after it; a recent target shows its observed days plus the 1-7 available forecast days, and the forecast naturally caps at ~7 days out. The after cap stays at 14.
This commit is contained in:
parent
d62c3cd61d
commit
36db92022e
1 changed files with 28 additions and 11 deletions
39
app.py
39
app.py
|
|
@ -164,14 +164,28 @@ def _json_response(body: bytes, etag: str | None = None) -> Response:
|
|||
# /cell bundle, and the offline migrate script. HTTP semantics (audit runs, cache
|
||||
# lookups, etags) stay in the endpoints; `run` is the audit run or None.
|
||||
|
||||
def _build_grade(cell, target, days, history, recent, cache_meta, place, run=None) -> dict:
|
||||
"""/grade payload: the last `days` observed days graded against their own
|
||||
±7-day windows, plus the target day's climatology summary."""
|
||||
def _build_grade(cell, target, days, history, recent, cache_meta, place, run=None, after=14) -> dict:
|
||||
"""/grade payload: a window of days centered on the target — `days` of history
|
||||
before it, the target itself, then up to `after` days after it — plus the
|
||||
target day's climatology summary. Days after the target are observed when they
|
||||
are already in the past and forecast when they run into the future, so the
|
||||
weekly view can frame two weeks of history around the orange target marker and
|
||||
trail off into up to 14 days of observations / forecast after it. The forecast
|
||||
only reaches ~7 days out, so a recent target naturally yields some observed days
|
||||
plus 1-7 forecast days, while an older target fills the whole 14 with real obs."""
|
||||
run = run or _NullRun()
|
||||
with run.phase("grading"):
|
||||
# Only grade observed days up to and including the target date (last `days`).
|
||||
recent = recent[recent["date"] <= target].sort_values("date").tail(days)
|
||||
graded = _grade_rows(history, recent)
|
||||
lo = target - pd.Timedelta(days=days)
|
||||
hi = target + pd.Timedelta(days=after)
|
||||
# Build the window from both sources. The recent+forecast bundle covers the
|
||||
# last few weeks plus the forward forecast (the only source for future days);
|
||||
# the archive reaches decades back for targets older than that bundle. Prefer
|
||||
# the bundle row for any given date, filling the rest from the archive.
|
||||
rwin = recent[(recent["date"] >= lo) & (recent["date"] <= hi)]
|
||||
hwin = history[(history["date"] >= lo) & (history["date"] <= hi)]
|
||||
hwin = hwin[~hwin["date"].isin(set(rwin["date"]))]
|
||||
window = pd.concat([rwin, hwin]).sort_values("date")
|
||||
graded = _grade_rows(history, window)
|
||||
_attach_dry_streaks(graded, history, recent)
|
||||
graded.reverse() # newest first for display
|
||||
climo = grading.climatology(history, int(target.dayofyear))
|
||||
|
|
@ -331,7 +345,10 @@ def api_grade(
|
|||
lat: float = Query(..., ge=-90, le=90),
|
||||
lon: float = Query(..., ge=-180, le=180),
|
||||
date: str | None = Query(None, description="target date YYYY-MM-DD (default today)"),
|
||||
days: int = Query(14, ge=1, le=60, description="how many recent days to grade"),
|
||||
days: int = Query(14, ge=1, le=60, description="days of history to grade before the target"),
|
||||
after: int = Query(14, ge=0, le=14,
|
||||
description="days to grade after the target (observed, or forecast when future; "
|
||||
"the forecast reaches ~7 days out so future days cap there)"),
|
||||
):
|
||||
if not grid.in_north_america(lat, lon):
|
||||
raise HTTPException(
|
||||
|
|
@ -365,7 +382,7 @@ def api_grade(
|
|||
if history.empty:
|
||||
raise HTTPException(status_code=404, detail="No historical data for this cell.")
|
||||
|
||||
key = f"{target.date().isoformat()}:{days}"
|
||||
key = f"{target.date().isoformat()}:{days}:{after}"
|
||||
token = f"{PAYLOAD_VER}:{_hist_end(history)}:{climate.recent_stamp(cell['id'])}"
|
||||
etag = _etag_for("grade", cell["id"], key, token)
|
||||
if _not_modified(request, etag):
|
||||
|
|
@ -378,7 +395,7 @@ def api_grade(
|
|||
|
||||
with run.phase("reverse_geocode"):
|
||||
place = climate.reverse_geocode(cell["center_lat"], cell["center_lon"])
|
||||
payload = _build_grade(cell, target, days, history, recent, cache_meta, place, run)
|
||||
payload = _build_grade(cell, target, days, history, recent, cache_meta, place, run, after=after)
|
||||
return _json_response(store.put_payload("grade", cell["id"], key, token, payload), etag)
|
||||
|
||||
|
||||
|
|
@ -647,8 +664,8 @@ def api_cell(
|
|||
else:
|
||||
rf_token = f"{PAYLOAD_VER}:{hist_end}:{climate.recent_stamp(cid)}"
|
||||
slices["grade"] = slice_for(
|
||||
"grade", f"{today.date().isoformat()}:14", rf_token,
|
||||
lambda: _build_grade(cell, today, 14, history, recent, cache_meta, place, run))
|
||||
"grade", f"{today.date().isoformat()}:14:14", rf_token,
|
||||
lambda: _build_grade(cell, today, 14, history, recent, cache_meta, place, run, after=14))
|
||||
slices["forecast"] = slice_for(
|
||||
"forecast", f"{today.date().isoformat()}:7", rf_token,
|
||||
lambda: _build_forecast(cell, 7, history, recent, today, place, run))
|
||||
|
|
|
|||
Loading…
Reference in a new issue