Update wind and rain measures - now using Beaufort Scale
This commit is contained in:
fraxle
2026-06-04 16:20:31 +01:00
parent ede875f0d0
commit 41acfcd5b9
2 changed files with 74 additions and 52 deletions
+1 -1
View File
@@ -202,7 +202,7 @@ export const COL_DESCRIPTIONS = {
air: { title: 'Air Temperature', desc: 'Measured air temperature at 2 m above the ground. This is the standard thermometer reading — it does not account for sun, wind, or humidity.' },
rh: { title: 'Relative Humidity', desc: 'How much moisture the air holds relative to its maximum capacity at that temperature. High RH makes warm days feel stickier and cold days feel rawer.' },
dew: { title: 'Dew Point', desc: 'The temperature at which air becomes saturated and moisture begins to condense. A useful measure of absolute humidity — above 16 °C it starts to feel muggy; above 21 °C it feels oppressive.' },
wind: { title: 'Wind Speed', desc: 'Mean wind speed at 10 m in mph, with peak gust in brackets where significantly higher. Gust colour indicates strength: amber 25-39 mph, orange 40-54 mph, red 55+ mph. Wind dramatically increases heat loss from exposed skin — the basis of wind chill.' },
wind: { title: 'Wind Speed', desc: 'Mean wind speed at 10 m in mph, with peak gust in brackets where significantly higher. Gust colour indicates Beaufort scale severity: amber = Near Gale (32+ mph), orange = Gale (39+ mph), red = Severe Gale or above (55+ mph). Banners above the table classify conditions from Strong Breeze through to Violent Storm using standard wind categories. Wind dramatically increases heat loss from exposed skin — the basis of wind chill.' },
dir: { title: 'Wind Direction', desc: 'The compass direction the wind is blowing from, shown as an arrow and abbreviated label (e.g. SW = south-westerly).' },
cloud: { title: 'Cloud Cover', desc: 'Total cloud cover as a percentage of the sky. High cloud blocks solar radiation and reduces both daytime heating and overnight cooling.' },
sun: { title: 'Sun Elevation', desc: 'The angle of the sun above the horizon in degrees. Below 0° the sun has set. The higher the elevation, the more intense the solar radiation reaching the ground.' },
+73 -51
View File
@@ -116,45 +116,63 @@ export function checkHeatSpike(rows) {
// wet + calm → rain icon (🌧️)
// windy + wet → storm icon (🌩️), and the wind/rain icons are suppressed
// for that hour so a single hour never shows two icons.
const GUST_BLUSTERY = 25 / 2.237; // m/s (= 25 mph) - Beaufort Force 6 threshold
const GUST_STORM = 20; // m/s ≈ 45 mph - Beaufort Force 8+ threshold
const PRECIP_WET = 5; // mm/h - meaningful rain
const PRECIP_HEAVY = 10; // mm/h - heavy rain
const GUST_BLUSTERY = 25 / 2.237; // m/s = 25 mph - Strong Breeze (Force 6)
const GUST_STORM = 39 / 2.237; // m/s = 39 mph - Gale (Force 8) - storm combo trigger
const PRECIP_MODERATE = 2.5; // mm/h - moderate rain threshold
const PRECIP_HEAVY = 7.6; // mm/h - heavy rain (>0.3 in/hr)
const PRECIP_TORRENTIAL = 20; // mm/h - torrential / very heavy
// Beaufort scale lookup - force number and name from gust speed in m/s
function beaufortForce(ms) {
// Wind category lookup from gust speed in m/s.
// Uses simplified 6-tier scale matching common forecast language.
function windCategory(ms) {
const mph = ms * 2.237;
if (mph >= 73) return { force: 12, name: 'Hurricane Force' };
if (mph >= 64) return { force: 11, name: 'Violent Storm' };
if (mph >= 55) return { force: 10, name: 'Storm Force' };
if (mph >= 47) return { force: 9, name: 'Strong Gale' };
if (mph >= 39) return { force: 8, name: 'Gale' };
if (mph >= 32) return { force: 7, name: 'Near Gale' };
return { force: 6, name: 'Strong Breeze' };
if (mph >= 96) return { tier: 6, name: 'Violent Storm', advisory: 'Wind Advisory' };
if (mph >= 74) return { tier: 5, name: 'Storm Force', advisory: 'Storm Warning' };
if (mph >= 55) return { tier: 4, name: 'Severe Gale', advisory: 'Gale Warning' };
if (mph >= 39) return { tier: 3, name: 'Gale', advisory: 'Gale Warning' };
if (mph >= 32) return { tier: 2, name: 'Near Gale', advisory: 'Wind Advisory' };
return { tier: 1, name: 'Strong Breeze', advisory: null };
}
const BEAUFORT_DESC = {
6: 'Large branches in motion; umbrellas used with difficulty.',
7: 'Whole trees in motion; inconvenience felt when walking against the wind.',
8: 'Twigs break off trees and generally impedes progress.',
9: 'Slight structural damage possible — chimney pots and slates at risk.',
10: 'Trees uprooted; considerable structural damage expected.',
11: 'Very rarely experienced inland; widespread damage expected.',
12: 'Devastating conditions — avoid all outdoor activity.',
const WIND_DESC = {
1: 'Large branches in motion; umbrellas used with difficulty.',
2: 'Whole trees in motion; inconvenience felt when walking against the wind.',
3: 'Twigs break off trees; progress on foot generally impeded.',
4: 'Slight structural damage possible — chimney pots and slates at risk.',
5: 'Trees uprooted; considerable structural damage expected.',
6: 'Very rarely experienced inland; widespread damage — avoid all outdoor activity.',
};
// Banner colour scales with severity
function windBannerColors(force) {
if (force >= 11) return { color: '#0c1020', textColor: '#c0d8ff' };
if (force >= 9) return { color: '#101828', textColor: '#c0d4f0' };
if (force >= 7) return { color: '#141e2e', textColor: '#c0d0e8' };
return { color: '#1a2030', textColor: '#c0d8f0' };
// Rain intensity lookup from mm/h
function rainCategory(mmh) {
if (mmh >= PRECIP_TORRENTIAL) return { name: 'Torrential Rain', desc: 'extreme rainfall' };
if (mmh >= PRECIP_HEAVY) return { name: 'Heavy Rain', desc: 'heavy rainfall' };
if (mmh >= PRECIP_MODERATE) return { name: 'Moderate Rain', desc: 'moderate rainfall' };
return { name: 'Rain', desc: 'light rain' };
}
// Combined wind+rain event naming
function stormTitle(windTier, rainMmh) {
const isHeavyRain = rainMmh >= PRECIP_HEAVY;
if (windTier >= 5) return 'Severe Storm';
if (windTier >= 4) return isHeavyRain ? 'Windstorm with Heavy Rain' : 'Windstorm with Rain';
if (windTier >= 3) return isHeavyRain ? 'Gales with Heavy Rain' : 'Gales with Rain';
if (windTier >= 2) return isHeavyRain ? 'Blustery Heavy Rain' : 'Blustery Rain';
return isHeavyRain ? 'Blustery Showers' : 'Blustery Rain';
}
// Banner colour scales with wind severity
function windBannerColors(tier) {
if (tier >= 5) return { color: '#0c1020', textColor: '#c0d8ff' };
if (tier >= 4) return { color: '#101828', textColor: '#c0d4f0' };
if (tier >= 2) return { color: '#141e2e', textColor: '#c0d0e8' };
return { color: '#1a2030', textColor: '#c0d8f0' };
}
const gustOf = r => r.gust ?? r.va ?? 0;
const precipOf = r => r.precip ?? 0;
const isWindyHour = r => { const g = gustOf(r); return isFinite(g) && g >= GUST_BLUSTERY; };
const isWetHour = r => { const p = precipOf(r); return isFinite(p) && p >= PRECIP_WET; };
const isWetHour = r => { const p = precipOf(r); return isFinite(p) && p >= PRECIP_MODERATE; };
const isStormHour = r => isWindyHour(r) && isWetHour(r);
export function checkWind(rows) {
@@ -165,19 +183,17 @@ export function checkWind(rows) {
const maxGust = Math.max(...affected.map(gustOf).filter(isFinite));
const mph = Math.round(maxGust * 2.237);
const { force, name } = beaufortForce(maxGust);
const desc = BEAUFORT_DESC[force] || '';
const { color, textColor } = windBannerColors(force);
const { tier, name, advisory } = windCategory(maxGust);
const desc = WIND_DESC[tier] || '';
const { color, textColor } = windBannerColors(tier);
const title = force >= 10 ? `${name} Winds`
: force >= 8 ? `${name} Warning`
: name;
const title = advisory ? `${advisory} ${name}` : name;
return {
id: 'wind',
emoji: force >= 10 ? '🌪️' : '💨',
emoji: tier >= 5 ? '🌪️' : '💨',
title,
message: `Gusts to ${mph} mph (Beaufort Force ${force}). ${desc}`,
message: `Gusts to ${mph} mph. ${desc}`,
color,
textColor,
type: 'weather',
@@ -193,20 +209,25 @@ export function checkWet(rows) {
const isoRange = [affected[0].iso, affected[affected.length - 1].iso];
const maxPrecip = Math.max(...affected.map(precipOf).filter(isFinite));
const isHeavy = maxPrecip >= PRECIP_HEAVY;
const { name: rainName } = rainCategory(maxPrecip);
const isTorrential = maxPrecip >= PRECIP_TORRENTIAL;
const isHeavy = maxPrecip >= PRECIP_HEAVY;
return {
id: 'wet',
emoji: '🌧️',
title: 'Heavy Rain',
message: isHeavy
? `Heavy rain expected — up to ${maxPrecip.toFixed(1)} mm/h at peak. Roads may flood; allow extra travel time.`
: `Wet spell — up to ${maxPrecip.toFixed(1)} mm/h rain at peak. Keep a brolly handy.`,
title: rainName,
message: isTorrential
? `Torrential rain — up to ${maxPrecip.toFixed(1)} mm/h at peak. Flash flooding possible; avoid travel if you can.`
: isHeavy
? `Heavy rain — up to ${maxPrecip.toFixed(1)} mm/h at peak. Roads may flood; allow extra travel time.`
: `Moderate rain expected — up to ${maxPrecip.toFixed(1)} mm/h at peak. Keep a brolly handy.`,
color: '#15212e',
textColor: '#bcd6ee',
type: 'weather',
nightOnly: false,
isoRange, // only show cell icon during the wet hours
isoRange,
};
}
@@ -216,21 +237,22 @@ export function checkStorm(rows) {
if (affected.length === 0) return null;
const isoRange = [affected[0].iso, affected[affected.length - 1].iso];
const maxGust = Math.max(...affected.map(gustOf).filter(isFinite));
const maxGust = Math.max(...affected.map(gustOf).filter(isFinite));
const maxPrecip = Math.max(...affected.map(precipOf).filter(isFinite));
const mph = Math.round(maxGust * 2.237);
const { force, name } = beaufortForce(maxGust);
const { color, textColor } = windBannerColors(force);
const mph = Math.round(maxGust * 2.237);
const { tier } = windCategory(maxGust);
const { color, textColor } = windBannerColors(tier);
const windDesc = WIND_DESC[tier] || '';
const title = force >= 10 ? `${name} — Severe Storm`
: force >= 8 ? `${name} with Rain`
: 'Storm Conditions';
const isTorrential = maxPrecip >= PRECIP_TORRENTIAL;
const isHeavy = maxPrecip >= PRECIP_HEAVY;
const rainLabel = isTorrential ? 'torrential rain' : isHeavy ? 'heavy rain' : 'rain';
return {
id: 'storm',
emoji: '🌩️',
title,
message: `Gusts to ${mph} mph (Beaufort Force ${force}) with ${maxPrecip.toFixed(1)} mm/h rain. ${BEAUFORT_DESC[force] || ''} Take care outdoors.`,
title: stormTitle(tier, maxPrecip),
message: `Gusts to ${mph} mph with ${rainLabel} (${maxPrecip.toFixed(1)} mm/h). ${windDesc}`,
color,
textColor,
type: 'weather',