Records: month and year records with the period as the headline
Publish Container / publish (push) Successful in 3m55s
Publish Container / publish (push) Successful in 3m55s
- Wettest/driest/hottest/coldest/windiest month and wettest/hottest/coldest/windiest year, each naming the period (e.g. 'September 2026') and flagging partial periods - Average-based records prefer complete periods; counterparts say what they're waiting for - 'Records by calendar month (any year)' table with the date/year of each record Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6e285bfef1
commit
72d813bd61
3 files changed
+100
-7
No files matched your search
@@ -122,3 +122,12 @@ a { color: var(--ui); }
|
||||
.chart-ctl select { font: inherit; font-size: 0.76rem; padding: 0.25rem 0.4rem; min-height: 30px; border-radius: 8px; border: 1px solid var(--border); background: var(--paper); color: var(--ink); }
|
||||
@media (pointer: coarse) { .chart-ctl select { font-size: 16px; min-height: 40px; } }
|
||||
.chart-card .chart-title { flex-wrap: wrap; row-gap: 0.4rem; }
|
||||
|
||||
/* Month / year record cards: the period is the headline */
|
||||
.record-period { font-size: 0.92rem; font-weight: 800; color: var(--ink); letter-spacing: -0.005em; margin-top: 0.1rem; }
|
||||
.rec-sub { text-transform: none; letter-spacing: 0; font-weight: 400; opacity: .75; }
|
||||
.rec-cal td { vertical-align: top; line-height: 1.25; }
|
||||
.rec-cal td b { display: block; font-variant-numeric: tabular-nums; }
|
||||
.rec-yr { display: block; font-size: 0.68rem; color: var(--ink-light); font-weight: 600; white-space: nowrap; }
|
||||
|
||||
.rec-cal tr.empty { opacity: .4; }
|
||||
@@ -1238,11 +1238,41 @@ function renderRecordPeriods(d, ri) {
|
||||
['Windiest Day', nv(n.windiestDay, v => v.toFixed(1) + ' mph'), n.windiestDay],
|
||||
['Wettest Day', nv(n.wettestDay, v => v.toFixed(2) + '"'), n.wettestDay],
|
||||
].map(([l, v, o]) => ri(l, v, o ? dayDate(o.day) : null)).join('') + '</div>';
|
||||
if (d.wettestMonth || d.wettestYear) {
|
||||
html += '<div class="rec-h">Wettest periods</div><div class="records-grid">'
|
||||
+ (d.wettestMonth ? ri('Wettest Month', d.wettestMonth.rain.toFixed(2) + '"', dayDate(d.wettestMonth.month + '-15')) : '')
|
||||
+ (d.wettestYear ? ri('Wettest Year', d.wettestYear.rain.toFixed(2) + '"', dayDate(d.wettestYear.year + '-07-01')) : '')
|
||||
+ '</div>';
|
||||
// Months & years: the period itself (e.g. "October 2026") is the headline of each card
|
||||
const now = new Date();
|
||||
const curMonth = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0'), curYear = String(now.getFullYear());
|
||||
const longMonth = (m) => new Date(m + '-15T12:00:00Z').toLocaleDateString('en-US', { month: 'long', year: 'numeric', timeZone: 'UTC' });
|
||||
const rp = (label, rec, fmtVal, isMonth, waitMsg) => {
|
||||
if (!rec) return `<div class="record-item"><div class="record-label">${label}</div><div class="record-val">—</div><div class="record-period">${waitMsg || 'Not enough data yet'}</div></div>`;
|
||||
const current = rec.key === (isMonth ? curMonth : curYear);
|
||||
return `<div class="record-item${current ? ' record-best' : ''}"><div class="record-label">${label}</div><div class="record-val">${fmtVal(rec.value)}</div>
|
||||
<div class="record-period">${isMonth ? longMonth(rec.key) : rec.key}</div><div class="record-date">${rec.days} day${rec.days === 1 ? '' : 's'} recorded${rec.partial ? ' · partial' : ''}</div></div>`;
|
||||
};
|
||||
const P = JSON.parse(JSON.stringify(d.periods || { months: {}, years: {} }));
|
||||
// An extreme and its opposite landing on the same period just means there's nothing to compare yet
|
||||
const same = (a, b) => a && b && a.key === b.key;
|
||||
const waitM = 'Needs another full month', waitY = 'Needs another year of data';
|
||||
let wM = {}, wY = {};
|
||||
if (same(P.months.hottest, P.months.coldest)) { P.months.coldest = null; wM.coldest = waitM; }
|
||||
if (same(P.months.wettest, P.months.driest)) { P.months.driest = null; wM.driest = waitM; }
|
||||
if (same(P.years.hottest, P.years.coldest)) { P.years.coldest = null; wY.coldest = waitY; }
|
||||
const deg = (v) => v.toFixed(1) + '°F', inch = (v) => v.toFixed(2) + '"', mph = (v) => v.toFixed(0) + ' mph';
|
||||
html += '<div class="rec-h">Months <span class="rec-sub">(avg temp = mean of all readings; partial months are flagged)</span></div><div class="records-grid">'
|
||||
+ rp('Wettest Month', P.months.wettest, inch, true) + rp('Driest Month', P.months.driest, inch, true, wM.driest)
|
||||
+ rp('Hottest Month', P.months.hottest, deg, true) + rp('Coldest Month', P.months.coldest, deg, true, wM.coldest)
|
||||
+ rp('Windiest Month', P.months.windiest, mph, true) + '</div>';
|
||||
html += '<div class="rec-h">Years</div><div class="records-grid">'
|
||||
+ rp('Wettest Year', P.years.wettest, inch, false) + rp('Hottest Year', P.years.hottest, deg, false)
|
||||
+ rp('Coldest Year', P.years.coldest, deg, false, wY.coldest) + rp('Windiest Year', P.years.windiest, mph, false) + '</div>';
|
||||
|
||||
// Records for each calendar month, whichever year they happened in
|
||||
if (d.calendarMonths && d.calendarMonths.some(c => c.years)) {
|
||||
const mn = (i) => new Date(Date.UTC(2001, i, 15)).toLocaleDateString('en-US', { month: 'long', timeZone: 'UTC' });
|
||||
const cell = (x, fmtV) => x ? `<b>${fmtV(x.value)}</b><span class="rec-yr">${x.day ? new Date(x.day + 'T12:00:00Z').toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' }) : ''}</span>` : '<span class="rec-yr">—</span>';
|
||||
const rows = d.calendarMonths.map((c, i) => `<tr${c.years ? '' : ' class="empty"'}><td><b>${mn(i)}</b><span class="rec-yr">${c.years ? c.years + ' year' + (c.years > 1 ? 's' : '') : 'no data'}</span></td>
|
||||
<td>${cell(c.hi, deg)}</td><td>${cell(c.lo, deg)}</td><td>${cell(c.wettestDay, inch)}</td>
|
||||
<td>${c.wettestTotal ? `<b>${inch(c.wettestTotal.value)}</b><span class="rec-yr">${longMonth(c.wettestTotal.month)}</span>` : '<span class="rec-yr">—</span>'}</td><td>${cell(c.gust, mph)}</td></tr>`).join('');
|
||||
html += `<div class="rec-h">Records by calendar month <span class="rec-sub">(any year)</span></div><div class="rec-scroll"><table class="rec-table rec-cal"><tr><th>Month</th><th>Record high</th><th>Record low</th><th>Wettest day</th><th>Wettest month</th><th>Peak gust</th></tr>${rows}</table></div>`;
|
||||
}
|
||||
const mname = (m) => new Date(m + '-15T12:00:00Z').toLocaleDateString('en-US', { month: 'short', year: 'numeric', timeZone: 'UTC' });
|
||||
const row = (label, a) => `<tr><td>${label}</td><td>${a.hi != null ? a.hi.toFixed(1) : '—'}</td><td>${a.lo != null ? a.lo.toFixed(1) : '—'}</td><td>${a.gust != null ? a.gust.toFixed(0) : '—'}</td><td>${a.rain.toFixed(2)}</td><td>${a.days}</td></tr>`;
|
||||
|
||||
+56
-2
@@ -452,13 +452,18 @@ function deriveRecords() {
|
||||
const agg = (keyFn) => {
|
||||
const g = {};
|
||||
for (const [d, v] of days) {
|
||||
const k = keyFn(d); const a = g[k] || (g[k] = { days: 0, rain: 0, hi: null, lo: null, gust: null });
|
||||
const k = keyFn(d); const a = g[k] || (g[k] = { days: 0, rain: 0, hi: null, lo: null, gust: null, tsum: 0, tn: 0 });
|
||||
a.days++; a.rain += v.rain || 0;
|
||||
if (v.tn) { a.tsum += v.tsum; a.tn += v.tn; }
|
||||
if (v.hi && (a.hi == null || v.hi[0] > a.hi)) a.hi = v.hi[0];
|
||||
if (v.lo && (a.lo == null || v.lo[0] < a.lo)) a.lo = v.lo[0];
|
||||
if (v.gust && (a.gust == null || v.gust[0] > a.gust)) a.gust = v.gust[0];
|
||||
}
|
||||
for (const a of Object.values(g)) a.rain = +a.rain.toFixed(2);
|
||||
for (const a of Object.values(g)) {
|
||||
a.rain = +a.rain.toFixed(2);
|
||||
a.avg = a.tn ? +(a.tsum / a.tn).toFixed(1) : null;
|
||||
delete a.tsum; delete a.tn;
|
||||
}
|
||||
return g;
|
||||
};
|
||||
const byYear = agg(d => d.slice(0, 4));
|
||||
@@ -468,6 +473,55 @@ function deriveRecords() {
|
||||
out.months = ym;
|
||||
out.wettestMonth = ym.reduce((b, m) => (!b || m.rain > b.rain ? m : b), null);
|
||||
out.wettestYear = out.years.reduce((b, y) => (!b || y.rain > b.rain ? y : b), null);
|
||||
|
||||
// Month / year records (partial periods are flagged so "coldest month" can't be a 2-week stub unnoticed)
|
||||
const dim = (ym) => new Date(+ym.slice(0, 4), +ym.slice(5, 7), 0).getDate();
|
||||
const mk = (p, key, val, total) => ({ key, value: val, days: p.days, partial: p.days < total * 0.9 });
|
||||
// preferComplete: averages / "driest" are only fair over whole periods, so rank complete ones first and
|
||||
// fall back to partial ones only when no complete period exists yet. Totals and extremes stay valid when partial.
|
||||
const pick = (list, keyName, valFn, dir, totalFn, preferComplete) => {
|
||||
const rank = (onlyComplete) => {
|
||||
let best = null;
|
||||
for (const p of list) {
|
||||
const v = valFn(p); if (v == null) continue;
|
||||
const total = totalFn(p[keyName]);
|
||||
if (onlyComplete && p.days < total * 0.9) continue;
|
||||
if (!best || (dir === 'max' ? v > best.value : v < best.value)) best = mk(p, p[keyName], v, total);
|
||||
}
|
||||
return best;
|
||||
};
|
||||
return (preferComplete && rank(true)) || rank(false);
|
||||
};
|
||||
const mPick = (valFn, dir, preferComplete) => pick(ym, 'month', valFn, dir, dim, preferComplete);
|
||||
const yPick = (valFn, dir, preferComplete) => pick(out.years, 'year', valFn, dir, () => 365, preferComplete);
|
||||
out.periods = {
|
||||
months: {
|
||||
wettest: mPick(p => p.rain, 'max'), driest: mPick(p => p.rain, 'min', true),
|
||||
hottest: mPick(p => p.avg, 'max', true), coldest: mPick(p => p.avg, 'min', true), windiest: mPick(p => p.gust, 'max'),
|
||||
},
|
||||
years: {
|
||||
wettest: yPick(p => p.rain, 'max'), hottest: yPick(p => p.avg, 'max', true),
|
||||
coldest: yPick(p => p.avg, 'min', true), windiest: yPick(p => p.gust, 'max'),
|
||||
},
|
||||
};
|
||||
|
||||
// Records for each calendar month across every year ("record July", "record January", …)
|
||||
const cal = Array.from({ length: 12 }, () => ({ hi: null, lo: null, wettestDay: null, gust: null, wettestTotal: null, years: new Set() }));
|
||||
const monthRain = {};
|
||||
for (const [d, v] of days) {
|
||||
const m = +d.slice(5, 7) - 1, c = cal[m];
|
||||
c.years.add(d.slice(0, 4));
|
||||
if (v.hi && (!c.hi || v.hi[0] > c.hi.value)) c.hi = { value: v.hi[0], day: d };
|
||||
if (v.lo && (!c.lo || v.lo[0] < c.lo.value)) c.lo = { value: v.lo[0], day: d };
|
||||
if (v.rain != null && v.rain > 0 && (!c.wettestDay || v.rain > c.wettestDay.value)) c.wettestDay = { value: v.rain, day: d };
|
||||
if (v.gust && (!c.gust || v.gust[0] > c.gust.value)) c.gust = { value: v.gust[0], day: d };
|
||||
monthRain[d.slice(0, 7)] = (monthRain[d.slice(0, 7)] || 0) + (v.rain || 0);
|
||||
}
|
||||
for (const [ymKey, total] of Object.entries(monthRain)) {
|
||||
const c = cal[+ymKey.slice(5, 7) - 1];
|
||||
if (total > 0 && (!c.wettestTotal || total > c.wettestTotal.value)) c.wettestTotal = { value: +total.toFixed(2), month: ymKey };
|
||||
}
|
||||
out.calendarMonths = cal.map((c, i) => ({ month: i + 1, years: c.years.size, hi: c.hi, lo: c.lo, wettestDay: c.wettestDay, gust: c.gust, wettestTotal: c.wettestTotal }));
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user