f1_data = d3.csv("f1_weather_history.csv", d3.autoType)
data = f1_data
// --- UNIT PREFERENCE ---
initialUnits = localStorage.getItem("f1_units") === "Imperial"
// --- THE TOGGLE ---
viewof units_toggle = Inputs.toggle({
label: "",
value: initialUnits
})
// --- THE UNITS CELL ---
units = {
const currentUnit = units_toggle ? "Imperial" : "Metric";
localStorage.setItem("f1_units", currentUnit);
return currentUnit;
}Passing through the chicane...
isTesting = false // Set to true to test, false to go live
// 2. DEFINE SLIDERS (Always define them so the code doesn't crash)
//viewof test_temp = Inputs.range([0, 45], {label: "Test Temp (°C)", step: 0.1, value: 25})
//viewof test_hum = Inputs.range([0, 100], {label: "Test Humidity (%)", step: 1, value: 50})
currentHourIndex = {
if (!weather_data || !weather_data.hourly) return 0;
const now = new Date();
// Find the slot that is greater than or equal to current time rounded to the hour
const index = weather_data.hourly.time.findIndex(t => new Date(t) >= new Date(now.setMinutes(0,0,0)));
return Math.max(0, index);
}
// --- WIND DIRECTION CALCULATOR ---
wind_cardinal = {
const degrees = weather_data.current.wind_direction_10m;
const sectors = ["N", "NE", "E", "SE", "S", "SW", "W", "NW", "N"];
return sectors[Math.round(degrees / 45) % 8];
}
// --- WEATHER DESCRIPTION DICTIONARY ---
weather_description = {
if (!weather_data || !weather_data.hourly || !weather_data.current) return "Syncing...";
const prob = weather_data.hourly.precipitation_probability[currentHourIndex];
const code = weather_data.current.weather_code;
const rain_mm = weather_data.current.rain || 0;
// 1. FORCED DRIZZLE (Overcast but radar shows rain)
if (code === 3 && prob > 51) return "Drizzle Conditions";
// official Fog
if (code >=45 && code <=48) return "Foggy Conditions";
// 2. OFFICIAL WMO DRIZZLE (Code 51, 53, 55)
if (code >= 51 && code <= 53) return "Drizzle";
if (code >= 54 && code <= 55) return "Moderate Drizzle";
// 3. OFFICIAL WMO RAIN (Code 61, 63, 65, etc.)
if (code >= 61 && code <= 62) return "Rainy";
if (code >= 63 && code <= 65) return "Raining";
if (code >= 71 && code <= 77) return "Heavy rain";
if (code >= 80 && code <= 82) return "Rain Showers";
// 4. STANDARD SKY CODES
if (code === 0) return "Clear Skies";
if (code === 1) return "Clear";
if (code === 2) return "Partly Cloudy";
if (code === 3) return "Overcast";
return "Storm";
}
weather_emoji = {
if (!weather_data || !weather_data.current) return "📡";
const code = weather_data.current.weather_code;
const prob = weather_data.hourly ? weather_data.hourly.precipitation_probability[currentHourIndex] : 0;
if (code === 0) return "☀️";
if (code === 1) return "🌤️";
if (code === 2) return "⛅";
if (code === 3) return "☁️";
if (code >= 45 && code <= 48) return "🌫️"
if (code >= 51 && code <= 53) return "🌧";
if (code >= 54 && code <= 55) return "🌧";
if (code >= 61 && code <= 67) return "🌧️";
if (code >= 71 && code <= 77) return "🌧";
if (code >= 80 && code <= 82) return "☔️";
if (code >= 95 && code <= 99) return "⛈️";
return "🌡️";
}
//////////////////////////////// GPS LOGIC //////////////////////////////////
weather_data = {
const cacheKey = "f1_weather_cache";
const cacheExpiry = 5 * 60 * 1000;
const now = Date.now();
const cached = JSON.parse(localStorage.getItem(cacheKey));
// 1. FAST PATH: Return cache immediately if fresh
if (!isTesting && cached && (now - cached.timestamp < cacheExpiry)) {
return cached.data;
}
// 2. TESTING PATH: Mock Data
if (isTesting) {
return {
current: {
temperature_2m: 25,
relative_humidity_2m: 64,
wind_speed_10m: 12.5,
wind_direction_10m: 220,
weather_code: 80,
rain: 0
},
hourly: {
time: Array.from({length: 24}, (_, i) => new Date(now + i*3600000).toISOString()),
precipitation_probability: Array(24).fill(52)
}
};
}
// 3. LIVE PATH: GPS + API
const locKey = "weather_race_coords";
const cachedLoc = JSON.parse(localStorage.getItem(locKey) || "null");
let lat, lon;
try {
if (cachedLoc && (now - cachedLoc.timestamp < 600000)) {
lat = cachedLoc.coords.latitude;
lon = cachedLoc.coords.longitude;
} else {
const pos = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: false,
timeout: 10000,
maximumAge: 600000
});
});
lat = pos.coords.latitude;
lon = pos.coords.longitude;
localStorage.setItem(locKey, JSON.stringify({
timestamp: now,
coords: { latitude: lat, longitude: lon }
}));
}
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,relative_humidity_2m,rain,weather_code,wind_speed_10m,wind_direction_10m&hourly=precipitation_probability&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max,wind_direction_10m_dominant,wind_speed_10m_max,relative_humidity_2m_max&forecast_days=10&timezone=auto`;
const w_res = await fetch(url);
const freshData = await w_res.json();
localStorage.setItem(cacheKey, JSON.stringify({
timestamp: now,
data: freshData
}));
return freshData;
} catch (e) {
console.error("GPS/API Error", e);
return null;
}
}
/////////////////// FLAGS AND CONVERSION ///////////////////////////////
country_flag_url = {
const codes = {
"BRN": "bh", // Bahrain
"KSA": "sa", // Saudi Arabia
"AUS": "au", // Australia
"AZE": "az", // Azerbaijan
"USA": "us", // USA
"MON": "mc", // Monaco
"ESP": "es", // Spain
"CAN": "ca", // Canada
"AUT": "at", // Austria
"GBR": "gb", // UK
"HUN": "hu", // Hungary
"BEL": "be", // Belgium
"NED": "nl", // Netherlands
"ITA": "it", // Italy
"SGP": "sg", // Singapore
"JPN": "jp", // Japan
"MEX": "mx", // Mexico
"UAE": "ae", // UAE
"BRA": "br", // Brazil
"QAT": "qa", // Qatar
"CHN": "cn" // China
};
const cleanCode = match.country_code ? match.country_code.trim().toUpperCase() : "";
const twoLetter = codes[cleanCode];
// Return the URL for the flag image
return twoLetter
? `https://flagcdn.com/w40/${twoLetter.toLowerCase()}.png`
: "https://flagcdn.com/w40/un.png";
}
/////////////////////// SESSION COLORS ///////////////////////////////
sessionColor = (name) => {
const n = name.toLowerCase();
// 1. High Priority (Unique Session Types)
if (n.includes("race")) return "#E10600"; // F1 Red
if (n.includes("practice")) return "#FED330"; // F1 Yellow
if (n.includes("qualifying")) return "#FFFFFF"; // White
// 2. Sprint Sessions
if (n.includes("sprint qualify")) return "#B620E0"; // Purple
if (n.includes("sprint")) return "#3671C6"; // Blue
// 3. Low Priority / Generic
if (n.includes("day")) return "#FFFFFF"; // White
return "#cccccc"; // Default Grey
}
//////////////// F1 ////////////////////////////////////
// --- MATCHING ENGINE ---
match = {
if (!weather_data || !weather_data.current || !data) return null;
const curr = weather_data.current;
const prob = weather_data.hourly ? weather_data.hourly.precipitation_probability[currentHourIndex] : 0;
// Match logic: If prob > 50%, look for a "Rain" race
const userRainLabel = (prob > 51 || curr.rain > 0 || curr.weather_code >= 51) ? "Rain" : "Dry";
let best = null;
let minScore = Infinity;
for (let d of data) {
if (d.rain_label !== userRainLabel) continue;
const t_gap = Math.abs(curr.temperature_2m - d.temp_C);
const h_gap = Math.abs(curr.relative_humidity_2m - d.humidity);
const totalScore = (t_gap * 2.0) + (h_gap * 1.0);
if (totalScore < minScore) {
minScore = totalScore;
best = d;
}
}
return best;
}
html`
${(() => {
if (match) {
const placeholder = document.getElementById("loading-placeholder");
if (placeholder) placeholder.remove();
}
return "";
})()}
<div style="
position: relative;
background: #1a1a1a;
padding: 20px;
border-radius: 15px;
border-top: 2px solid #e10600;
text-align: center;
margin: 5px auto;
font-family: 'Titillium Web', sans-serif;
color: white;
width: calc(100% - 10px);
max-width: 600px;
min-height: 600px;
height: auto;
box-sizing: border-box;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
overflow: hidden;
">
<div style="position: absolute; top: 15px; right: 8px; z-index: 100;">
<div class="f1-toggle-container">${viewof units_toggle}</div>
</div>
${match ? html`
<div class="status-container" style="margin-bottom: 10px; display: flex; justify-content: flex-start;">
<small style="color: #aaa; text-transform: uppercase; letter-spacing: 2px; font-weight: bold; display: flex; align-items: center;">
${isTesting ? html`⚠️ TEST MODE` : html`<span class="live-dot"></span> LIVE`}
</small>
</div>
<div style="margin-top: -30px;">
<div style="font-size: 3.5em;">${weather_emoji}</div>
<div style="margin-top: 0px; color: #ffffff; text-transform: uppercase; font-size: 1.2em; letter-spacing: 2px; font-weight: 800; margin-bottom: 5px;">
${weather_description}
</div>
<div style="display: flex; justify-content: center; align-items: baseline; gap: 10px;">
<h2 style="margin: 0; font-size: 4em; white-space: nowrap;">
${units === "Metric" ? weather_data.current.temperature_2m + "°C" : ((weather_data.current.temperature_2m * 9/5) + 32).toFixed(1) + "°F"}
</h2>
<span style="color: #e10600; font-size: 2.5em; font-weight: bold;">/</span>
<h2 style="margin: 0; font-size: 2em; white-space: nowrap;">
${weather_data.current.relative_humidity_2m}% <span style="font-size: 0.4em; color: #888;">HUM.</span>
</h2>
</div>
${weather_data.hourly ? html`
<div class="rain-horizon" style="
margin-top: -10px !important;
margin-bottom: 5px;
display: flex;
overflow-x: auto;
gap: 8px;
padding-bottom: 10px;
scrollbar-width: thin; /* For Firefox */
">
${(() => {
const startIndex = Math.max(0, currentHourIndex);
// Slice 24 hours instead of 12
return weather_data.hourly.time.slice(startIndex, startIndex + 25).map((time, i) => {
const dataIndex = i + startIndex;
const dateObj = new Date(time);
const prob = weather_data.hourly.precipitation_probability[dataIndex];
const rainColor = `hsl(215, 100%, ${100 - (prob / 1.5)}%)`;
let timeLabel = i === 0 ? "NOW" : dateObj.toLocaleTimeString('en-US', {
hour: units === "Metric" ? '2-digit' : 'numeric',
minute: units === "Metric" ? '2-digit' : undefined,
hourCycle: units === "Metric" ? 'h23' : 'h12',
hour12: units === "Imperial"
}).replace(/\s+/g, '');
return html`
<div class="hour-card" style="border-bottom: 3px solid ${rainColor}">
<div style="font-size: 0.7em; color: #aaa; text-transform: uppercase; font-weight: bold;">${timeLabel}</div>
<div style="font-size: 1.2em; font-weight: 900; margin: 4px 0; color: #fff;">${prob}%</div>
<div style="font-size: 0.55em; color: ${rainColor}; font-weight: 900; letter-spacing: 1px;">RAIN</div>
</div>`;
});
})()}
</div>
` : ""}
${(() => {
if (!weather_data.daily) return "";
const convertTemp = (c) => units === "Imperial" ? Math.round((c * 9/5) + 32) : Math.round(c);
const convertWind = (k) => units === "Imperial" ? Math.round(k * 0.621371) : Math.round(k);
const windUnit = units === "Imperial" ? "mph" : "km/h";
const getWindCompass = (deg) => {
const sectors = ["N", "NE", "E", "SE", "S", "SW", "W", "NW", "N"];
return sectors[Math.round(deg / 45) % 8];
};
const todayAvg = convertTemp((weather_data.daily.temperature_2m_max[0] + weather_data.daily.temperature_2m_min[0]) / 2);
const todayProb = weather_data.daily.precipitation_probability_max[0];
return html`
<details style="width: 100%; margin: 5px 0; outline: none;">
<summary style="
list-style: none;
cursor: pointer;
background: #1a1a1a;
border-top: 1px solid rgba(225, 6, 0, 0.2);
border-bottom: 1px solid rgba(225, 6, 0, 0.2);
padding: 6px 10px;
display: flex;
justify-content: space-between;
align-items: center;
">
<span style="color: white; font-size: 1em; font-weight: 900; text-transform: uppercase; letter-spacing: 2px;">
● 10 Day Forecast
</span>
<span style="color: white; font-size: .9em;">▼</span>
</summary>
<div style="background: #1a1a1a; padding: 10px; border-bottom: 1px solid rgba(225, 6, 0, 0.2);">
${weather_data.daily.time.slice(0, 10).map((date, i) => {
// MOBILE FIX: Replace '-' with '/' for better compatibility with mobile Safari/Chrome
const mobileDate = date.replace(/-/g, "/");
const dateObj = new Date(mobileDate);
const dayName = i === 0 ? "TODAY" : dateObj.toLocaleDateString('en-US', { weekday: 'short' }).toUpperCase();
const rainProb = weather_data.daily.precipitation_probability_max[i];
const tMax = convertTemp(weather_data.daily.temperature_2m_max[i]);
const tMin = convertTemp(weather_data.daily.temperature_2m_min[i]);
const humidity = weather_data.daily.relative_humidity_2m_max ? weather_data.daily.relative_humidity_2m_max[i] : "--";
const windSpeed = convertWind(weather_data.daily.wind_speed_10m_max ? weather_data.daily.wind_speed_10m_max[i] : 0);
const windDir = weather_data.daily.wind_direction_10m_dominant ? weather_data.daily.wind_direction_10m_dominant[i] : 0;
const isWeekend = dateObj.getDay() === 0 || dateObj.getDay() === 6;
return html`
<div style="
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
font-family: monospace;
font-size: 1em;
padding: 8px 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
align-items: center;
">
<span style="font-weight: 900; color: ${i === 0 ? '#aaa' : (isWeekend ? '#ffcc00' : '#fff')};">
${dayName}
</span>
<div style="display: flex; flex-direction: column; gap: 1px; justify-content: center;">
${(() => {
const prob = rainProb || 0;
// Calculation: As prob increases, Red and Green decrease from 255 to ~67
const rg = Math.round(255 - (prob * 1.88));
const dynamicBlue = `rgb(${rg}, ${rg === 255 ? 255 : Math.min(rg + 100, 255)}, 255)`;
return html`
<span style="color: ${dynamicBlue}; font-weight: 900; line-height: 1;">
${prob}% 🌧
</span>
<span style="color: #666; font-size: 0.7em; line-height: 1;">
Hum: ${humidity}%
</span>
`;
})()}
</div>
<div style="color: #fff; display: flex; flex-direction: column; gap: 2px; align-items: center; justify-content: center;">
<div style="display: flex; align-items: center; font-weight: bold; line-height: 1;">
<span style="color: #888; font-size: 0.9em;">${getWindCompass(windDir)}</span>
<span style="display: inline-block; transform: rotate(${windDir + 180}deg); color: #e10600; margin-left: 6px; font-size: 1.1em;">↑</span>
<span style="margin-left: 6px;">${windSpeed}</span>
</div>
<span style="font-size: 0.7em; color: #666; text-transform: lowercase; line-height: 1;">${windUnit}</span>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end; align-items: flex-start; padding-right: 8px;">
<div style="display: flex; flex-direction: column; align-items: center; gap: 2px;">
<span style="
font-weight: 900;
color: ${(() => {
const t = weather_data.daily.temperature_2m_max[i];
if (t >= 0) {
// 0 (White) to 45 (Red). 10°C will now be a very subtle off-white.
const factor = Math.min(t / 70, 20);
const rgValue = Math.round(255 - (factor * 255));
return `rgb(255, ${rgValue}, ${rgValue})`;
} else {
// 0 (White) to -45 (Dark Blue)
const factor = Math.min(Math.abs(t) / 20, 20);
const rValue = Math.round(255 - (factor * 255));
const gValue = Math.round(255 - (factor * 255));
const bValue = Math.round(255 - (factor * 116)); // Ends at rgb(0,0,139)
return `rgb(${rValue}, ${gValue}, ${bValue})`;
}
})()};
">
${tMax}°
</span>
<span style="color: #666; font-size: 0.6em; font-weight: 900;">H</span>
</div>
<span style="color: #444; font-weight: bold; margin-top: 2px;">/</span>
<div style="display: flex; flex-direction: column; align-items: center; gap: 2px;">
<span style="
font-weight: 900;
color: ${(() => {
const t = weather_data.daily.temperature_2m_min[i];
if (t >= 0) {
const factor = Math.min(t / 45, 1);
const rgValue = Math.round(255 - (factor * 255));
return `rgb(255, ${rgValue}, ${rgValue})`;
} else {
const factor = Math.min(Math.abs(t) / 45, 1);
const rValue = Math.round(255 - (factor * 255));
const gValue = Math.round(255 - (factor * 255));
const bValue = Math.round(255 - (factor * 116));
return `rgb(${rValue}, ${gValue}, ${bValue})`;
}
})()};
">
${tMin}°
</span>
<span style="color: #666; font-size: 0.6em; font-weight: 900;">L</span>
</div>
</div>
</div>
`;
})}
</div>
</details>
`;
})()}
<div style="margin: 10px 0; display: flex; justify-content: center; gap: 40px;">
<div>
<small style="color: #888; text-transform: uppercase; font-size: 0.7em; display: block; letter-spacing: 1px;">Wind Speed</small>
<span style="font-weight: bold; font-size: 1.1em;">
${units === "Metric" ? weather_data.current.wind_speed_10m + " km/h" : (weather_data.current.wind_speed_10m * 0.621).toFixed(1) + " mph"}
</span>
</div>
<div>
<small style="color: #888; text-transform: uppercase; font-size: 0.7em; display: block; letter-spacing: 1px;">Wind Direction</small>
<span style="font-weight: bold; font-size: 1.1em; display: flex; align-items: center; justify-content: center;">
${weather_data.current.wind_direction_10m}°
<span style="color: #e10600; margin-left: 5px;">${wind_cardinal}</span>
<span style="display: inline-block; transform: rotate(${weather_data.current.wind_direction_10m + 180}deg); color: #e10600; margin-left: 5px; font-size: 1.2em;">↑</span>
</span>
</div>
</div>
<div style="border-top: 1px solid #333; padding-top: 10px; margin-top: 3px; width: 100%;">
<div style="color: #e10600; font-weight: bold; text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 5px; font-size: 1.2em;">
Similar weather felt:
</div>
<div style="display: flex; align-items: baseline; justify-content: center; gap: 6px; line-height: 1;">
<span style="font-size: 1.8em; color: #fff; font-weight: 900; font-style: italic; letter-spacing: -1px;">F1</span>
<span style="font-size: 1.6em; font-weight: 900; color: #fff;">${new Date(match.date_start).getFullYear()}</span>
</div>
<div style="color: #fff; font-weight: 800; text-transform: uppercase; letter-spacing: 1px; font-size: 0.8em; margin-top: 0px;">
${new Date(match.date_start).toLocaleDateString('en-US', { month: 'long', day: 'numeric' })}
</div>
<h1 style="font-size: 1.8em; font-weight: 900; font-style: italic; margin: 4px 0; text-transform: uppercase;">${match.location}</h1>
<div style="display: flex; justify-content: center; align-items: center; gap: 8px; margin-bottom: 8px;">
<img src="${country_flag_url}" style="height: 12px; border-radius: 2px;" />
<span style="color: #aaa; text-transform: uppercase; letter-spacing: 1px; font-size: 0.75em;">${match.country_name}</span>
</div>
<div style="display: flex; justify-content: center; align-items: center; gap: 10px; margin: 10px 0;">
<span class="session-badge" style="background-color: ${sessionColor(match.session_name)}; color: ${['#FED330', '#FFFFFF'].includes(sessionColor(match.session_name)) ? '#000000' : '#FFFFFF'}; padding: 2px 10px; border-radius: 4px; font-size: 0.8em; font-weight: bold; text-transform: uppercase;">
${match.session_name}
</span>
<span style="display: inline-flex; align-items: center; justify-content: center; width: 1.4em; height: 1.4em; font-size: 1.4em; line-height: 1; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5));" title="${match.rain_label === 'Rain' ? 'Wet Track' : 'Dry Track'}">
${match.rain_label === 'Rain' ? "🌧️" : "☀️"}
</span>
</div>
<p style="color: #888; font-size: 0.85em; font-style: italic; margin-top: 8px; margin-bottom: 0;">
Historical: ${units === "Metric" ? match.temp_C + "°C" : ((match.temp_C * 9/5) + 32).toFixed(1) + "°F"} / ${match.humidity}% hum.
</p>
</div>
</div>
` : html`
<div style="height: 600px; display: flex; flex-direction: column; align-items: center; justify-content: center;">
<div class="f1-spinner" style="border: 4px solid rgba(225,6,0,0.1); border-left-color: #e10600; border-radius: 50%; width: 40px; height: 40px; margin: 0 auto 20px; animation: f1-spin 1s linear infinite;"></div>
<p style="color: #e10600; font-weight: bold; letter-spacing: 2px; font-size: 0.8em;">SYNCHRONIZING TELEMETRY...</p>
</div>
`}
</div>
</div>
<div class="signature" style="text-align: center; margin-top: 10px; font-size: 0.8em; color: #666;">
info <a href="https://www.instagram.com/weather_race/" target="_blank" style="color: #e10600; text-decoration: none;">@Weather_Race</a>
</div>
<div style="text-align: center; margin-top: 30px; font-family: 'Titillium Web', sans-serif;">
<div style="display: inline-block; background: rgba(255, 211, 48, 0.1); border: 1px solid rgba(255, 211, 48, 0.3); padding: 4px 12px; border-radius: 4px; margin-bottom: 5px;">
<span style="color: #FED330; font-size: 0.7em; font-weight: 900; text-transform: uppercase; letter-spacing: 1.5px;">
⚠️ Under Construction: More to come
</span>
</div>
</div>
`