Merge remote-tracking branch 'forgejo/qa/dry-trace' into qa/frontend-qa-batch
All checks were successful
PR build (required check) / changes (pull_request) Successful in 7s
secrets-guard / encrypted (pull_request) Successful in 5s
shell-lint / shellcheck (pull_request) Successful in 8s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Successful in 58s
PR build (required check) / build-backend (pull_request) Successful in 1m16s
PR build (required check) / gate (pull_request) Successful in 2s
All checks were successful
PR build (required check) / changes (pull_request) Successful in 7s
secrets-guard / encrypted (pull_request) Successful in 5s
shell-lint / shellcheck (pull_request) Successful in 8s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Successful in 58s
PR build (required check) / build-backend (pull_request) Successful in 1m16s
PR build (required check) / gate (pull_request) Successful in 2s
# Conflicts: # frontend/static/day.js
This commit is contained in:
commit
669e52a0d9
7 changed files with 63 additions and 30 deletions
|
|
@ -279,8 +279,10 @@ def all_time_records(df: pl.DataFrame) -> dict:
|
||||||
|
|
||||||
|
|
||||||
def longest_dry_streak(df: pl.DataFrame) -> tuple[int, str | None]:
|
def longest_dry_streak(df: pl.DataFrame) -> tuple[int, str | None]:
|
||||||
"""Longest run of consecutive days without measurable rain, and the ISO date the
|
"""Longest run of consecutive days without any rain, and the ISO date the streak
|
||||||
streak began. A dry day is precip < RAIN_THRESHOLD; null precip counts as dry."""
|
began. A dry day has no rain at all (precip <= 0); null precip counts as dry. The
|
||||||
|
threshold matches _grade_precip's dry/rain split — any measurable rain, however
|
||||||
|
slight, is a rain day that breaks the streak (not the 0.01" rain-frequency line)."""
|
||||||
if "precip" not in df.columns:
|
if "precip" not in df.columns:
|
||||||
return (0, None)
|
return (0, None)
|
||||||
d = df.select(["date", "precip"]).sort("date")
|
d = df.select(["date", "precip"]).sort("date")
|
||||||
|
|
@ -288,7 +290,7 @@ def longest_dry_streak(df: pl.DataFrame) -> tuple[int, str | None]:
|
||||||
best_len, best_start = 0, None
|
best_len, best_start = 0, None
|
||||||
cur_len, cur_start = 0, None
|
cur_len, cur_start = 0, None
|
||||||
for dt, p in zip(dates, precips):
|
for dt, p in zip(dates, precips):
|
||||||
wet = p is not None and not (isinstance(p, float) and np.isnan(p)) and p >= RAIN_THRESHOLD
|
wet = p is not None and not (isinstance(p, float) and np.isnan(p)) and p > 0
|
||||||
if wet:
|
if wet:
|
||||||
cur_len, cur_start = 0, None
|
cur_len, cur_start = 0, None
|
||||||
else:
|
else:
|
||||||
|
|
@ -305,11 +307,12 @@ def longest_dry_streak(df: pl.DataFrame) -> tuple[int, str | None]:
|
||||||
def _band_stats(samples: np.ndarray) -> dict | None:
|
def _band_stats(samples: np.ndarray) -> dict | None:
|
||||||
if samples.size == 0:
|
if samples.size == 0:
|
||||||
return None
|
return None
|
||||||
# Percentiles for the chart's nested "normal" fan, matching the 9 tier bounds:
|
# Percentiles for the chart's nested "normal" fan. p40-p60 is the Normal band;
|
||||||
# p40-p60 is the Normal band; p25/p75, p10/p90 and p1/p99 mark the successive
|
# p25/p75, p10/p90 and p1/p99 mark the successive Below/Above Normal, Low/High,
|
||||||
# Below/Above Normal, Low/High, Very Low/High and Near-Record edges.
|
# Very Low/High and Near-Record edges. p95 additionally splits the rain fan's top
|
||||||
p1, p10, p25, p40, p50, p60, p75, p90, p99 = np.percentile(
|
# region into Very Heavy (90-95) and Severe (95-99); unused by the temperature fan.
|
||||||
samples, [1, 10, 25, 40, 50, 60, 75, 90, 99]
|
p1, p10, p25, p40, p50, p60, p75, p90, p95, p99 = np.percentile(
|
||||||
|
samples, [1, 10, 25, 40, 50, 60, 75, 90, 95, 99]
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"p1": round(float(p1), 1),
|
"p1": round(float(p1), 1),
|
||||||
|
|
@ -320,6 +323,7 @@ def _band_stats(samples: np.ndarray) -> dict | None:
|
||||||
"p60": round(float(p60), 1),
|
"p60": round(float(p60), 1),
|
||||||
"p75": round(float(p75), 1),
|
"p75": round(float(p75), 1),
|
||||||
"p90": round(float(p90), 1),
|
"p90": round(float(p90), 1),
|
||||||
|
"p95": round(float(p95), 1),
|
||||||
"p99": round(float(p99), 1),
|
"p99": round(float(p99), 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -352,13 +356,14 @@ def _grade_precip(samples: np.ndarray, value) -> dict | None:
|
||||||
|
|
||||||
|
|
||||||
def dry_streaks(dates, precips) -> dict[str, int]:
|
def dry_streaks(dates, precips) -> dict[str, int]:
|
||||||
"""Map each date (ISO string) to days since the last measurable rain, walking a
|
"""Map each date (ISO string) to days since the last day with any rain, walking a
|
||||||
chronological precip series. Missing precip counts as a dry day. `dates` is an
|
chronological precip series. Any measurable rain (precip > 0) resets the count, so
|
||||||
iterable of ``datetime.date`` (a polars Date column's ``.to_list()``)."""
|
this matches _grade_precip's dry/rain split; missing precip counts as a dry day.
|
||||||
|
`dates` is an iterable of ``datetime.date`` (a polars Date column's ``.to_list()``)."""
|
||||||
out: dict[str, int] = {}
|
out: dict[str, int] = {}
|
||||||
streak = 0
|
streak = 0
|
||||||
for d, p in zip(dates, precips):
|
for d, p in zip(dates, precips):
|
||||||
wet = p is not None and not (isinstance(p, float) and np.isnan(p)) and p >= RAIN_THRESHOLD
|
wet = p is not None and not (isinstance(p, float) and np.isnan(p)) and p > 0
|
||||||
streak = 0 if wet else streak + 1
|
streak = 0 if wet else streak + 1
|
||||||
out[_as_date(d).isoformat()] = streak
|
out[_as_date(d).isoformat()] = streak
|
||||||
return out
|
return out
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,9 @@ def test_dry_streaks_walk():
|
||||||
dates = [datetime.date(2024, 1, 1) + datetime.timedelta(days=i) for i in range(5)]
|
dates = [datetime.date(2024, 1, 1) + datetime.timedelta(days=i) for i in range(5)]
|
||||||
precips = [0.5, 0.0, float("nan"), 0.02, 0.005]
|
precips = [0.5, 0.0, float("nan"), 0.02, 0.005]
|
||||||
out = grading.dry_streaks(dates, precips)
|
out = grading.dry_streaks(dates, precips)
|
||||||
assert list(out.values()) == [0, 1, 2, 0, 1] # NaN counts as dry
|
# Any rain > 0 breaks the streak (matching the dry/rain grading split), so the
|
||||||
|
# 0.005" trace day resets to 0; only exact 0.0 and NaN count as dry.
|
||||||
|
assert list(out.values()) == [0, 1, 2, 0, 0]
|
||||||
|
|
||||||
|
|
||||||
# ---- range + day grading over a synthetic record --------------------------------
|
# ---- range + day grading over a synthetic record --------------------------------
|
||||||
|
|
@ -141,7 +143,7 @@ def test_grade_day_normals_and_departure(history):
|
||||||
result = grading.grade_day(history, target, {"tmax": row["tmax"], "tmin": row["tmin"],
|
result = grading.grade_day(history, target, {"tmax": row["tmax"], "tmin": row["tmin"],
|
||||||
"precip": row["precip"]})
|
"precip": row["precip"]})
|
||||||
assert set(result["normals"]["tmax"]) == {"p1", "p10", "p25", "p40", "p50", "p60",
|
assert set(result["normals"]["tmax"]) == {"p1", "p10", "p25", "p40", "p50", "p60",
|
||||||
"p75", "p90", "p99"}
|
"p75", "p90", "p95", "p99"}
|
||||||
expected = max(abs(result["tmax"]["percentile"] - 50), abs(result["tmin"]["percentile"] - 50))
|
expected = max(abs(result["tmax"]["percentile"] - 50), abs(result["tmin"]["percentile"] - 50))
|
||||||
assert result["departure"] == round(expected, 1)
|
assert result["departure"] == round(expected, 1)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { W, PW, H, PL, PR, plotTop, plotBot, xLabY, setChartWidth, chartPalette,
|
||||||
tempChart, precipChart, dryChart, attachChartHover } from "./chart.js";
|
tempChart, precipChart, dryChart, attachChartHover } from "./chart.js";
|
||||||
import { track } from "./digest.js";
|
import { track } from "./digest.js";
|
||||||
import { TIER_COLORS, SCALE_TEMP, drynessColor, pctOrd, esc, todayISO, placeLabel,
|
import { TIER_COLORS, SCALE_TEMP, drynessColor, pctOrd, esc, todayISO, placeLabel,
|
||||||
tierKeySegs, GUIDE_LINK, fmtPrecip, fmtWind, fmtHumid } from "./shared.js";
|
tierKeySegs, GUIDE_LINK, fmtPrecip, fmtPrecipTier, fmtWind, fmtHumid } from "./shared.js";
|
||||||
|
|
||||||
let selected = null; // {lat, lon}
|
let selected = null; // {lat, lon}
|
||||||
|
|
||||||
|
|
@ -323,11 +323,14 @@ function precipCell(d, isFc, isToday) {
|
||||||
const dd = ` data-date="${d.date}"`;
|
const dd = ` data-date="${d.date}"`;
|
||||||
const cls = "rd-c" + (isFc ? " rd-fc" : isToday ? " rd-today" : "");
|
const cls = "rd-c" + (isFc ? " rd-fc" : isToday ? " rd-today" : "");
|
||||||
if (!g) return `<span class="${cls}"${dd}>—</span>`;
|
if (!g) return `<span class="${cls}"${dd}>—</span>`;
|
||||||
if (g.value === 0 && d.dsr != null && d.dsr > 0) {
|
// Only a genuinely dry day (no rain at all) labels the streak; any rain day —
|
||||||
return `<span class="${cls}"${dd} style="--c:${drynessColor(d.dsr)}" title="${d.dsr} days since measurable rain">${d.dsr}d</span>`;
|
// down to sub-0.01" trace that rounds to 0 — shows its depth, tinted by tier.
|
||||||
|
if (g.class === "dry" && d.dsr != null && d.dsr > 0) {
|
||||||
|
return `<span class="${cls}"${dd} style="--c:${drynessColor(d.dsr)}" title="${d.dsr} days since rain">${d.dsr}d</span>`;
|
||||||
}
|
}
|
||||||
const col = TIER_COLORS[g.class] || "";
|
const col = TIER_COLORS[g.class] || "";
|
||||||
return `<span class="${cls}"${dd} style="--c:${col}">${cRain(g.value)}</span>`;
|
const amt = g.class && g.class !== "dry" ? fmtPrecipTier(g.value, g.class, false) : cRain(g.value);
|
||||||
|
return `<span class="${cls}"${dd} style="--c:${col}">${amt}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compact "graded days" table — a ROW per metric, a COLUMN per day (newest first).
|
// Compact "graded days" table — a ROW per metric, a COLUMN per day (newest first).
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { uv } from "./account.js"; // header sign-in entry + notification bell
|
||||||
import { TTL, chunkedFetch, prefetchViews } from "./cache.js";
|
import { TTL, chunkedFetch, prefetchViews } from "./cache.js";
|
||||||
import { initFindButton, setFindLabel } from "./mappicker.js";
|
import { initFindButton, setFindLabel } from "./mappicker.js";
|
||||||
import { TIER_COLORS, PRECIP_COLORS, SCALE_TEMP, SCALE_RAIN, drynessColor, pctOrd,
|
import { TIER_COLORS, PRECIP_COLORS, SCALE_TEMP, SCALE_RAIN, drynessColor, pctOrd,
|
||||||
placeLabel, fmtPrecip, fmtWind, fmtHumid, MONTHS, isoOfDate, monthStart, monthEnd,
|
placeLabel, fmtPrecip, fmtPrecipTier, fmtWind, fmtHumid, MONTHS, isoOfDate, monthStart, monthEnd,
|
||||||
CHUNK_MONTHS, buildChunks, clickOpensPicker, metricBuckets, distStrip,
|
CHUNK_MONTHS, buildChunks, clickOpensPicker, metricBuckets, distStrip,
|
||||||
seasonFilterDropdown, seasonSummaryText, syncSeasonChecks,
|
seasonFilterDropdown, seasonSummaryText, syncSeasonChecks,
|
||||||
applySeasonMonthChange, initSeasonExpand, allMonths, monthsToMask, maskToMonths,
|
applySeasonMonthChange, initSeasonExpand, allMonths, monthsToMask, maskToMonths,
|
||||||
|
|
@ -42,7 +42,7 @@ const SKY_WORDS = [
|
||||||
// The shared weather summary, adapted to the calendar's compact day records
|
// The shared weather summary, adapted to the calendar's compact day records
|
||||||
// (dsr included, so a long-dry no-rain day reads "dry" rather than "clear").
|
// (dsr included, so a long-dry no-rain day reads "dry" rather than "clear").
|
||||||
const weatherType = (rec) =>
|
const weatherType = (rec) =>
|
||||||
wxType(rec.tmax && rec.tmax.v, rec.precip && rec.precip.v, rec.dsr);
|
wxType(rec.tmax && rec.tmax.v, rec.precip && rec.precip.v, rec.dsr, rec.precip && rec.precip.c);
|
||||||
|
|
||||||
let selected = null; // {lat, lon}
|
let selected = null; // {lat, lon}
|
||||||
let data = null; // last /api/v2/calendar response
|
let data = null; // last /api/v2/calendar response
|
||||||
|
|
@ -704,7 +704,7 @@ function attachHover(byDate) {
|
||||||
["humid", line("humid", "Humid", rec.humid, fmtHumid, tempColor(rec.humid))],
|
["humid", line("humid", "Humid", rec.humid, fmtHumid, tempColor(rec.humid))],
|
||||||
["wind", line("wind", "Wind", rec.wind, fmtWind, tempColor(rec.wind))],
|
["wind", line("wind", "Wind", rec.wind, fmtWind, tempColor(rec.wind))],
|
||||||
["gust", line("gust", "Gust", rec.gust, fmtWind, tempColor(rec.gust))],
|
["gust", line("gust", "Gust", rec.gust, fmtWind, tempColor(rec.gust))],
|
||||||
["precip", line("precip", "Precip", rec.precip, fmtPrecip, precipColor)],
|
["precip", line("precip", "Precip", rec.precip, (v) => fmtPrecipTier(v, rec.precip && rec.precip.c), precipColor)],
|
||||||
];
|
];
|
||||||
if (dsrStr) {
|
if (dsrStr) {
|
||||||
const rc = metric === "dsr" ? " tt-r-active" : "";
|
const rc = metric === "dsr" ? " tt-r-active" : "";
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ const PCT_FALLBACK = {
|
||||||
p1: ["p1", "p10", "min", "p50"], p10: ["p10", "p50"],
|
p1: ["p1", "p10", "min", "p50"], p10: ["p10", "p50"],
|
||||||
p25: ["p25", "p30", "p50"], p40: ["p40", "p30", "p50"],
|
p25: ["p25", "p30", "p50"], p40: ["p40", "p30", "p50"],
|
||||||
p60: ["p60", "p70", "p50"], p75: ["p75", "p70", "p50"],
|
p60: ["p60", "p70", "p50"], p75: ["p75", "p70", "p50"],
|
||||||
p90: ["p90", "p50"], p99: ["p99", "p90", "max", "p50"],
|
p90: ["p90", "p50"], p95: ["p95", "p90", "p50"], p99: ["p99", "p90", "max", "p50"],
|
||||||
};
|
};
|
||||||
const pget = (o, k) => {
|
const pget = (o, k) => {
|
||||||
for (const kk of (PCT_FALLBACK[k] || [k])) if (o && o[kk] != null) return o[kk];
|
for (const kk of (PCT_FALLBACK[k] || [k])) if (o && o[kk] != null) return o[kk];
|
||||||
|
|
@ -93,8 +93,12 @@ const TEMP_FAN = [
|
||||||
["p90", "p99", "very-hot"], ["p75", "p90", "hot"], ["p60", "p75", "warm"],
|
["p90", "p99", "very-hot"], ["p75", "p90", "hot"], ["p60", "p75", "warm"],
|
||||||
["p40", "p60", "normal"], ["p25", "p40", "cool"], ["p10", "p25", "cold"], ["p1", "p10", "very-cold"],
|
["p40", "p60", "normal"], ["p25", "p40", "cool"], ["p10", "p25", "cold"], ["p1", "p10", "very-cold"],
|
||||||
];
|
];
|
||||||
|
// Eight rain-intensity tiers (post-split): Trace / Light / Brisk / Typical / Heavy
|
||||||
|
// (60-90) / Very Heavy (90-95) / Severe (95-99) / Extreme. Each band maps a rain-day
|
||||||
|
// percentile range to its calendar tier colour; the p75 vertex inside the single
|
||||||
|
// Heavy tier keeps the fan following the p75 contour.
|
||||||
const RAIN_FAN = [
|
const RAIN_FAN = [
|
||||||
["p90", "p99", "wet-8"], ["p75", "p90", "wet-7"], ["p60", "p75", "wet-6"],
|
["p95", "p99", "wet-8"], ["p90", "p95", "wet-7"], ["p75", "p90", "wet-6"], ["p60", "p75", "wet-6"],
|
||||||
["p40", "p60", "wet-5"], ["p25", "p40", "wet-4"], ["p10", "p25", "wet-3"], ["p1", "p10", "wet-2"],
|
["p40", "p60", "wet-5"], ["p25", "p40", "wet-4"], ["p10", "p25", "wet-3"], ["p1", "p10", "wet-2"],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -212,7 +216,7 @@ export function precipChart(days, n, xFor, C) {
|
||||||
getPct: (d) => (d.precip ? d.precip.percentile : null),
|
getPct: (d) => (d.precip ? d.precip.percentile : null),
|
||||||
// Rain days take their intensity-tier color; no-rain days warm with the dry
|
// Rain days take their intensity-tier color; no-rain days warm with the dry
|
||||||
// streak (tan→red) so a dry spell reads as dry, matching the Dry chart.
|
// streak (tan→red) so a dry spell reads as dry, matching the Dry chart.
|
||||||
dotColor: (d) => (d.precip && d.precip.value > 0 ? (TIER_COLORS[d.precip.class] || C.precip) : (drynessColor(d.dsr) || C.precip)),
|
dotColor: (d) => (d.precip && d.precip.class && d.precip.class !== "dry" ? (TIER_COLORS[d.precip.class] || C.precip) : (drynessColor(d.dsr) || C.precip)),
|
||||||
fmtAxis: (v) => fmtPrecip(v, false),
|
fmtAxis: (v) => fmtPrecip(v, false),
|
||||||
fmtLabel: (v) => fmtPrecip(v, false),
|
fmtLabel: (v) => fmtPrecip(v, false),
|
||||||
labelOn: (v) => v > 0, // only label rain days; dry days rest on the baseline
|
labelOn: (v) => v > 0, // only label rain days; dry days rest on the baseline
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { fmtTemp, onUnitChange } from "./units.js";
|
||||||
import { uv } from "./account.js"; // header sign-in entry + notification bell, and the API-version resolver
|
import { uv } from "./account.js"; // header sign-in entry + notification bell, and the API-version resolver
|
||||||
import { getJSON, TTL, prefetchViews } from "./cache.js";
|
import { getJSON, TTL, prefetchViews } from "./cache.js";
|
||||||
import { initFindButton, setFindLabel } from "./mappicker.js";
|
import { initFindButton, setFindLabel } from "./mappicker.js";
|
||||||
import { TIER_COLORS, PRECIP_COLORS, DRY_COLOR, pctOrd, fmtPrecip, fmtWind, fmtHumid,
|
import { TIER_COLORS, PRECIP_COLORS, DRY_COLOR, pctOrd, fmtPrecip, fmtPrecipTier, fmtWind, fmtHumid,
|
||||||
todayISO, isoOfDate, weatherType, placeLabel } from "./shared.js";
|
todayISO, isoOfDate, weatherType, placeLabel } from "./shared.js";
|
||||||
|
|
||||||
// Color a tier the same way the calendar cell would be colored for it.
|
// Color a tier the same way the calendar cell would be colored for it.
|
||||||
|
|
@ -117,7 +117,8 @@ function render(data) {
|
||||||
// Weather summary from the observed high + precip (omitted for days with no obs yet).
|
// Weather summary from the observed high + precip (omitted for days with no obs yet).
|
||||||
const th = d.metrics.tmax.obs ? d.metrics.tmax.obs.value : null;
|
const th = d.metrics.tmax.obs ? d.metrics.tmax.obs.value : null;
|
||||||
const pr = d.metrics.precip.obs ? d.metrics.precip.obs.value : null;
|
const pr = d.metrics.precip.obs ? d.metrics.precip.obs.value : null;
|
||||||
const wt = th != null || pr != null ? weatherType(th, pr) : null;
|
const prCls = d.metrics.precip.obs ? d.metrics.precip.obs.class : null;
|
||||||
|
const wt = th != null || pr != null ? weatherType(th, pr, undefined, prCls) : null;
|
||||||
dayHead.innerHTML = `
|
dayHead.innerHTML = `
|
||||||
<h2>${nice}</h2>
|
<h2>${nice}</h2>
|
||||||
${wt ? `<p class="day-weather">${wt.icon} ${wt.text}</p>` : ""}
|
${wt ? `<p class="day-weather">${wt.icon} ${wt.text}</p>` : ""}
|
||||||
|
|
@ -157,11 +158,14 @@ function ladderCard(title, m, fmt, kind) {
|
||||||
if (!m || !m.ladder) return "";
|
if (!m || !m.ladder) return "";
|
||||||
const obs = m.obs;
|
const obs = m.obs;
|
||||||
const activeClass = obs ? obs.class : null;
|
const activeClass = obs ? obs.class : null;
|
||||||
|
// The observed precip depth reads "trace" for a sub-0.01" rain day (rounds to 0
|
||||||
|
// but is graded a rain tier); ladder tier ranges keep the plain numeric formatter.
|
||||||
|
const obsFmt = kind === "precip" ? ((v) => fmtPrecipTier(v, activeClass)) : fmt;
|
||||||
|
|
||||||
let summary;
|
let summary;
|
||||||
if (obs) {
|
if (obs) {
|
||||||
const pct = obs.percentile == null ? "" : ` · ${pctOrd(obs.percentile)} pct`;
|
const pct = obs.percentile == null ? "" : ` · ${pctOrd(obs.percentile)} pct`;
|
||||||
summary = `<span class="day-obs" style="--cat:${tierColor(obs.class)}">${fmt(obs.value)}</span>
|
summary = `<span class="day-obs" style="--cat:${tierColor(obs.class)}">${obsFmt(obs.value)}</span>
|
||||||
<span class="day-obs-meta">${obs.grade}${pct}</span>`;
|
<span class="day-obs-meta">${obs.grade}${pct}</span>`;
|
||||||
} else {
|
} else {
|
||||||
summary = `<span class="day-obs-meta">No observation for this day yet</span>`;
|
summary = `<span class="day-obs-meta">No observation for this day yet</span>`;
|
||||||
|
|
@ -184,7 +188,7 @@ function ladderCard(title, m, fmt, kind) {
|
||||||
else if (t.lo == null) val = `< ${fmt(t.hi)}`; // bottom tier: strictly below p1
|
else if (t.lo == null) val = `< ${fmt(t.hi)}`; // bottom tier: strictly below p1
|
||||||
else val = fmtRange(fmt, t.lo, t.hi);
|
else val = fmtRange(fmt, t.lo, t.hi);
|
||||||
const marker = t.c === activeClass && obs
|
const marker = t.c === activeClass && obs
|
||||||
? `<span class="ladder-mark">◀ ${bareVal(fmt, obs.value)}</span>` : "";
|
? `<span class="ladder-mark">◀ ${bareVal(obsFmt, obs.value)}</span>` : "";
|
||||||
return `<div class="ladder-row${active}">
|
return `<div class="ladder-row${active}">
|
||||||
<span class="ladder-sw" style="background:${tierColor(t.c)}"></span>
|
<span class="ladder-sw" style="background:${tierColor(t.c)}"></span>
|
||||||
<span class="ladder-label" style="--cat:${tierColor(t.c)}">${t.label}</span>
|
<span class="ladder-label" style="--cat:${tierColor(t.c)}">${t.label}</span>
|
||||||
|
|
|
||||||
|
|
@ -467,13 +467,28 @@ export const WX_ICONS = {
|
||||||
snow: WX(`<path d="M20 17.6A5 5 0 0 0 18 8h-1.26A8 8 0 1 0 4 16.25"/><line x1="8" y1="18" x2="8" y2="18"/><line x1="12" y1="20" x2="12" y2="20"/><line x1="16" y1="18" x2="16" y2="18"/><line x1="12" y1="16" x2="12" y2="16"/>`),
|
snow: WX(`<path d="M20 17.6A5 5 0 0 0 18 8h-1.26A8 8 0 1 0 4 16.25"/><line x1="8" y1="18" x2="8" y2="18"/><line x1="12" y1="20" x2="12" y2="20"/><line x1="16" y1="18" x2="16" y2="18"/><line x1="12" y1="16" x2="12" y2="16"/>`),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A graded precip depth for display, given its grade class. Reanalysis "trace"
|
||||||
|
// rain — any measurable precip below 0.01" — is graded as a rain tier but rounds to
|
||||||
|
// 0.00" / 0 mm, so its depth prints as "trace" rather than a bone-dry "0.00" that
|
||||||
|
// would contradict the rain grade (and the now-reset dry streak). Genuinely dry
|
||||||
|
// days (class "dry") and real, roundable depths format as usual.
|
||||||
|
export function fmtPrecipTier(v, cls, withUnit = true) {
|
||||||
|
if (cls && cls !== "dry" && v != null && parseFloat(fmtPrecip(v, false)) === 0)
|
||||||
|
return "trace";
|
||||||
|
return fmtPrecip(v, withUnit);
|
||||||
|
}
|
||||||
|
|
||||||
// Plain-language "what was the day like" descriptor from the raw values: a
|
// Plain-language "what was the day like" descriptor from the raw values: a
|
||||||
// temperature word (from the daily high, °F) crossed with a sky/precip word
|
// temperature word (from the daily high, °F) crossed with a sky/precip word
|
||||||
// (precip in inches — falling as snow at/below freezing). `dsr` is optional:
|
// (precip in inches — falling as snow at/below freezing). `dsr` is optional:
|
||||||
// when a long dry streak is known, a no-rain day reads "dry" instead of
|
// when a long dry streak is known, a no-rain day reads "dry" instead of
|
||||||
// "clear" (the calendar passes it; the day page doesn't track streaks).
|
// "clear" (the calendar passes it; the day page doesn't track streaks).
|
||||||
export function weatherType(t, p, dsr) {
|
// `precipCls` is the precip grade class when known: a rain tier means it rained
|
||||||
const wet = p != null && p >= 0.01;
|
// even when the rounded depth reads 0 (trace), so it decides wet/dry ahead of the
|
||||||
|
// depth — matching the dry/rain grading split (precip > 0). Without it, any
|
||||||
|
// positive depth counts as wet.
|
||||||
|
export function weatherType(t, p, dsr, precipCls) {
|
||||||
|
const wet = precipCls != null ? precipCls !== "dry" : (p != null && p > 0);
|
||||||
const freezing = t != null && t <= 34;
|
const freezing = t != null && t <= 34;
|
||||||
const temp = t == null ? "" :
|
const temp = t == null ? "" :
|
||||||
t >= 95 ? "Scorching" : t >= 85 ? "Hot" : t >= 72 ? "Warm" :
|
t >= 95 ? "Scorching" : t >= 85 ? "Hot" : t >= 72 ? "Warm" :
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue