Data table compact hours 1h, 2h, 3h or 4h. Calc max/min or average
This commit is contained in:
fraxle
2026-06-02 23:31:14 +01:00
parent 7b603867dd
commit 5a03077d9e
5 changed files with 147 additions and 11 deletions
+74
View File
@@ -231,6 +231,80 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
return { hourlyRows, days, utcOffsetMs };
}
// ------------------------------------------------------------------------
// aggregateRows(rows, interval) - Compress a day's hourly rows into
// multi-hour buckets for the table view (1h / 2h / 3h / 4h).
//
// Buckets are CLOCK-ALIGNED from midnight, so a 2h view groups 00-01,
// 02-03, ...; 3h groups 00-02, 03-05, ...; etc. Each output row keeps the
// LANDING (first) hour's positional fields - iso, dt, solar elevation,
// global radiation, wind direction, sky/cloud category - so the little
// scope, clock label and wind vane all show the time the column lands on.
//
// Every other column is aggregated with the rule that fits its meaning:
// - SUM : precipitation totals (1mm/h x 3h = 3mm)
// - MAX : risk/peak columns (rain %, UV, gusts, cabin/surface/indoor)
// - MEAN : smooth continuous quantities (air temp, RH, wind, cloud, ...)
// - FELT : worst-case felt temp - the warmest hour if it reaches >=20C
// (heat is the concern), otherwise the coldest (cold concern)
//
// Derived render values (Delta, Burn) are recomputed downstream from the
// aggregated Air / UTCI / UV, so they stay consistent automatically.
//
// interval <= 1 returns the input untouched (zero behaviour change).
// ------------------------------------------------------------------------
const AGG_MEAN = [
'Ta', 'RH', 'dew', 'va', 'dir', 'dif', 'cc', 'ccLow', 'ccMid', 'ccHigh',
'soilT0', 'soilT6', 'soilM', 'Tmrt', 'eh', 'visKm', 'aqi', 'effectiveRad',
'grassPollen', 'birchPollen', 'alderPollen', 'mugwortPollen', 'olivePollen', 'ragweedPollen',
];
const AGG_MAX = [
'precipProb', 'uv', 'uvA', 'uvB', 'gust',
'vehicleT', 'concreteT', 'indoorT', 'managedT',
];
const AGG_SUM = ['precip', 'snow'];
const AGG_FELT = ['utci', 'utciAdj'];
export function aggregateRows(rows, interval) {
if (!rows || rows.length === 0 || !interval || interval <= 1) return rows;
// Numbers for a field across the group, skipping null / NaN.
const nums = (group, f) => group.map(r => r[f]).filter(v => v != null && !isNaN(v));
const mean = (vals) => vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : null;
const maxV = (vals) => vals.length ? Math.max(...vals) : null;
const felt = (vals) => {
if (!vals.length) return null;
const hi = Math.max(...vals);
return hi >= 20 ? hi : Math.min(...vals);
};
// Split rows into clock-aligned buckets by floor(localHour / interval).
const buckets = [];
let cur = null, curKey = null;
for (const r of rows) {
const hour = parseInt(r.iso.slice(11, 13), 10);
const key = Math.floor(hour / interval);
if (key !== curKey) { cur = []; buckets.push(cur); curKey = key; }
cur.push(r);
}
return buckets.map(group => {
// Start from the landing row so positional fields (iso, dt, elev, glob,
// wd, compass, cloudCat) carry through unchanged, then overwrite the
// aggregatable columns.
const out = { ...group[0] };
AGG_MEAN.forEach(f => { out[f] = mean(nums(group, f)); });
AGG_MAX.forEach(f => { out[f] = maxV(nums(group, f)); });
AGG_SUM.forEach(f => { out[f] = group.reduce((a, r) => a + (r[f] || 0), 0); });
AGG_FELT.forEach(f => { out[f] = felt(nums(group, f)); });
// Bucket span markers used by the table for "now" highlighting and for
// matching event cell-tags that fall on any hour within the bucket.
out.isoHours = group.map(r => r.iso.slice(0, 13));
out.isoEnd = group[group.length - 1].iso;
return out;
});
}
// ------------------------------------------------------------------------
// computeWhyFeelsLike(row, env) - Break down the felt-temp delta into its
// contributing factors for the "Why it feels like this" panel.