Now panel: show day's high + its feels-like, and peak rain chance with time
Was showing right-now's temp/feels-like/rain% under a headline that's already a whole-day summary -- mismatched framing (a day-level headline next to point-in-time numbers). - Big temp is now today's forecast high, with a small 'High' label. - Feels-like is paired with that same high-temp hour, not right now. - Rain% is the day's peak chance, shown as 'X% chance of rain around HH:00' instead of the current hour's (often much lower) number. summarizeToday() now returns highTemp/highFeelsLike/rainChance alongside the existing headline/note/umbrella, computed from the same already-fetched hourly points. Frontend falls back to currentPoint's own values only if summary is unexpectedly null. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
997ea817cb
commit
335e6e31a8
4 changed files with 47 additions and 7 deletions
|
|
@ -31,8 +31,17 @@ export function summarizeToday(points, nowHour) {
|
||||||
// replaces) — an hour's icon can be non-rain while it still carries a real, if
|
// replaces) — an hour's icon can be non-rain while it still carries a real, if
|
||||||
// slim, numeric chance. Worth surfacing in the headline rather than a flat "Dry"
|
// slim, numeric chance. Worth surfacing in the headline rather than a flat "Dry"
|
||||||
// that silently disagrees with a "5%" sitting right there on an hour card.
|
// that silently disagrees with a "5%" sitting right there on an hour card.
|
||||||
const precipValues = points.map(p => p.precip_prob).filter(v => v != null)
|
// Keep the whole peak point, not just the number, so the "now" panel can show
|
||||||
const maxPrecip = precipValues.length ? Math.max(...precipValues) : null
|
// *when* that peak chance falls ("30% chance of rain around 15:00") rather
|
||||||
|
// than whatever the current hour's own (possibly much lower) number is.
|
||||||
|
const rainPeakPoint = points.reduce((best, p) =>
|
||||||
|
p.precip_prob != null && (!best || p.precip_prob > best.precip_prob) ? p : best, null)
|
||||||
|
const maxPrecip = rainPeakPoint?.precip_prob ?? null
|
||||||
|
|
||||||
|
// Day's high, and the feels-like *for that same hour* — pairing "feels like"
|
||||||
|
// with the temp it actually describes, rather than the current hour's feels
|
||||||
|
// like sitting next to a headline high from a different time of day.
|
||||||
|
const highPoint = points.reduce((max, p) => (p.temp_c > max.temp_c ? p : max), points[0])
|
||||||
|
|
||||||
const laterPoints = points.filter(p => hourOf(p) >= nowHour)
|
const laterPoints = points.filter(p => hourOf(p) >= nowHour)
|
||||||
const earlierPoints = points.filter(p => hourOf(p) < nowHour)
|
const earlierPoints = points.filter(p => hourOf(p) < nowHour)
|
||||||
|
|
@ -67,7 +76,14 @@ export function summarizeToday(points, nowHour) {
|
||||||
// Umbrella nudge: real rain chance either right now or coming up later today.
|
// Umbrella nudge: real rain chance either right now or coming up later today.
|
||||||
const umbrella = laterWetFraction > 0.25 || rainFraction > 0.25
|
const umbrella = laterWetFraction > 0.25 || rainFraction > 0.25
|
||||||
|
|
||||||
return { headline, note, umbrella }
|
return {
|
||||||
|
headline,
|
||||||
|
note,
|
||||||
|
umbrella,
|
||||||
|
highTemp: highPoint.temp_c,
|
||||||
|
highFeelsLike: highPoint.feels_like_c,
|
||||||
|
rainChance: rainPeakPoint ? { prob: rainPeakPoint.precip_prob, ts: rainPeakPoint.ts } : null,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hourOf(point) {
|
function hourOf(point) {
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,16 @@ html, body, #root { height: 100%; margin: 0; font-size: 14px; }
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: clamp(10px, 1.8vw, 24px);
|
gap: clamp(10px, 1.8vw, 24px);
|
||||||
}
|
}
|
||||||
|
.wx-now-temp-block { display: flex; flex-direction: column; align-items: center; line-height: 1; }
|
||||||
.wx-now-temp { font-size: clamp(2.4rem, 5.5vw, 4.6rem); font-weight: 700; line-height: 1; }
|
.wx-now-temp { font-size: clamp(2.4rem, 5.5vw, 4.6rem); font-weight: 700; line-height: 1; }
|
||||||
|
.wx-now-temp-label {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: clamp(0.65rem, 0.85vw, 0.85rem);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .06em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
.wx-now-meta { display: flex; flex-direction: column; gap: 4px; color: var(--text-muted); font-size: clamp(0.8rem, 1.05vw, 1.05rem); }
|
.wx-now-meta { display: flex; flex-direction: column; gap: 4px; color: var(--text-muted); font-size: clamp(0.8rem, 1.05vw, 1.05rem); }
|
||||||
.wx-summary-headline { color: var(--text); font-weight: 600; font-size: clamp(0.95rem, 1.3vw, 1.3rem); }
|
.wx-summary-headline { color: var(--text); font-weight: 600; font-size: clamp(0.95rem, 1.3vw, 1.3rem); }
|
||||||
.wx-summary-note { color: #fbbf24; }
|
.wx-summary-note { color: #fbbf24; }
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,15 @@ export default function Display() {
|
||||||
const currentPoint = data.hourly.find(p => hourOf(p.ts) === nowHour) ?? data.hourly[0]
|
const currentPoint = data.hourly.find(p => hourOf(p.ts) === nowHour) ?? data.hourly[0]
|
||||||
const todayKey = data.hourly[0] ? dateKeyOf(data.hourly[0].ts) : null
|
const todayKey = data.hourly[0] ? dateKeyOf(data.hourly[0].ts) : null
|
||||||
|
|
||||||
|
// The "now" panel shows the day's high (not the current temp) and pairs
|
||||||
|
// "feels like" with that same hour rather than right now — falls back to
|
||||||
|
// currentPoint's own values only if summary is unexpectedly null (no hourly
|
||||||
|
// data yet, which currentPoint being defined already makes unlikely).
|
||||||
|
const displayHighTemp = data.summary?.highTemp ?? currentPoint?.temp_c ?? null
|
||||||
|
const displayFeelsLike = data.summary?.highFeelsLike ?? currentPoint?.feels_like_c ?? null
|
||||||
|
const displayRainChance = data.summary?.rainChance
|
||||||
|
?? (currentPoint?.precip_prob != null ? { prob: currentPoint.precip_prob, ts: currentPoint.ts } : null)
|
||||||
|
|
||||||
const dayKeys = [...new Set(data.threeHourly.map(p => dateKeyOf(p.ts)))].slice(0, 2)
|
const dayKeys = [...new Set(data.threeHourly.map(p => dateKeyOf(p.ts)))].slice(0, 2)
|
||||||
const nextDayGroups = dayKeys.map(key => ({
|
const nextDayGroups = dayKeys.map(key => ({
|
||||||
key,
|
key,
|
||||||
|
|
@ -120,13 +129,16 @@ export default function Display() {
|
||||||
{currentPoint && (
|
{currentPoint && (
|
||||||
<div className="wx-now">
|
<div className="wx-now">
|
||||||
<WeatherIcon icon={currentPoint.icon} size={72} />
|
<WeatherIcon icon={currentPoint.icon} size={72} />
|
||||||
<div className="wx-now-temp">{roundTemp(currentPoint.temp_c)}</div>
|
<div className="wx-now-temp-block">
|
||||||
|
<span className="wx-now-temp">{displayHighTemp != null && roundTemp(displayHighTemp)}</span>
|
||||||
|
<span className="wx-now-temp-label">High</span>
|
||||||
|
</div>
|
||||||
<div className="wx-now-meta">
|
<div className="wx-now-meta">
|
||||||
{data.summary && <span className="wx-summary-headline">{data.summary.headline}</span>}
|
{data.summary && <span className="wx-summary-headline">{data.summary.headline}</span>}
|
||||||
<span>
|
<span>
|
||||||
{currentPoint.feels_like_c != null && <>Feels like {roundTemp(currentPoint.feels_like_c)}</>}
|
{displayFeelsLike != null && <>Feels like {roundTemp(displayFeelsLike)}</>}
|
||||||
{currentPoint.feels_like_c != null && currentPoint.precip_prob != null && ' · '}
|
{displayFeelsLike != null && displayRainChance && ' · '}
|
||||||
{currentPoint.precip_prob != null && <>{Math.round(currentPoint.precip_prob)}% chance of rain</>}
|
{displayRainChance && <>{Math.round(displayRainChance.prob)}% chance of rain around {formatHour(displayRainChance.ts)}</>}
|
||||||
</span>
|
</span>
|
||||||
{data.summary?.note && <span className="wx-summary-note">{data.summary.note}</span>}
|
{data.summary?.note && <span className="wx-summary-note">{data.summary.note}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ export interface Summary {
|
||||||
headline: string // "Scattered showers today", "Rain likely later", ...
|
headline: string // "Scattered showers today", "Rain likely later", ...
|
||||||
note: string | null // e.g. "Thunderstorms possible", alongside the headline
|
note: string | null // e.g. "Thunderstorms possible", alongside the headline
|
||||||
umbrella: boolean // real rain chance now or later today
|
umbrella: boolean // real rain chance now or later today
|
||||||
|
highTemp: number // today's forecast high (°C)
|
||||||
|
highFeelsLike: number | null // feels-like at the same hour as highTemp, not "now"
|
||||||
|
rainChance: { prob: number; ts: string } | null // today's peak precip% + when it falls
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Provider = 'met_office' | 'open_meteo'
|
export type Provider = 'met_office' | 'open_meteo'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue