1 2import * as Plot from 'https://esm.run/@observablehq/plot' 3import { renderSkyAtLocation } from './sky.js'; 4 5const element = document.getElementById('weather'); 6const time = document.getElementById('time'); 7const conditions = document.getElementById('conditions'); 8const conditionsEmoji = document.getElementById('conditionsEmoji'); 9const currentTemp = document.getElementById('currentTemp'); 10const low = document.getElementById('low'); 11const high = document.getElementById('high'); 12const rainDiv = document.getElementById('rain'); 13const rainBanner = document.getElementById('rain-banner'); 14 15 16const setTime = () => { 17 const time = new Date().toLocaleTimeString('en-AU', { 18 timeZone: 'America/New_York', 19 hour: 'numeric', 20 minute: 'numeric', 21 second: 'numeric', 22 hour12: true 23 }); 24 document.getElementById('time').textContent = time; 25}; 26 27setTime(); 28setInterval(setTime, 1000); 29 30// NYC lat lon 31const lat = 40.7128; 32const lon = -74.0060; 33 34const { gradient, top, bottom } = renderSkyAtLocation(lat, lon); 35 36function isColorTooDark(rgbString) { 37 // Expecting format: "rgb(r, g, b)" 38 const match = rgbString.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); 39 if (!match) return false; // Invalid format 40 const r = parseInt(match[1], 10); 41 const g = parseInt(match[2], 10); 42 const b = parseInt(match[3], 10); 43 // Calculate luminance 44 const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; 45 return luminance < 0.5; 46} 47 48const topTooDark = isColorTooDark(top); 49const bottomTooDark = isColorTooDark(bottom); 50 51const consideredDark = topTooDark || bottomTooDark; 52 53let darkMode = false; 54let usedGradient = false; 55 56function useSkyGradient() { 57 element.style.background = gradient; 58 usedGradient = true; 59 60 if (consideredDark) { 61 darkMode = true; 62 element.classList.add('dark'); 63 element.style.color = 'white'; 64 } 65} 66 67const url = 'https://api.open-meteo.com/v1/forecast?' 68 + `latitude=${lat}&longitude=${lon}` 69 + '&daily=temperature_2m_max,temperature_2m_min' 70 + '¤t=temperature_2m,is_day,weather_code' 71 + '&timezone=America%2FNew_York'; 72 73const simulateRain = false; 74 75function setRainState(isRaining) { 76 if (rainDiv) { 77 rainDiv.style.display = isRaining ? 'block' : 'none'; 78 } 79 80 if (rainBanner) { 81 rainBanner.style.display = isRaining ? 'block' : 'none'; 82 } 83} 84 85setRainState(false); 86 87fetch(url) 88 .then(response => response.json()) 89 .then(data => { 90 const weatherCode = simulateRain ? 63 : data.current.weather_code; 91 const isDay = data.current.is_day === 1; 92 93 currentTemp.textContent = data.current.temperature_2m.toFixed(1) + '°C'; 94 high.textContent = "H: " + data.daily.temperature_2m_max[0].toFixed(1) + '°C'; 95 low.textContent = "L: " + data.daily.temperature_2m_min[0].toFixed(1) + '°C'; 96 97 // weather_code interpretation table as per https://open-meteo.com/en/docs 98 // 0 Clear sky 99 // 1, 2, 3 Mainly clear, partly cloudy, and overcast 100 // 45, 48 Fog and depositing rime fog 101 // 51, 53, 55 Drizzle: Light, moderate, and dense intensity 102 // 56, 57 Freezing Drizzle: Light and dense intensity 103 // 61, 63, 65 Rain: Slight, moderate and heavy intensity 104 // 66, 67 Freezing Rain: Light and heavy intensity 105 // 71, 73, 75 Snow fall: Slight, moderate, and heavy intensity 106 // 77 Snow grains 107 // 80, 81, 82 Rain showers: Slight, moderate, and violent 108 // 85, 86 Snow showers slight and heavy 109 // 95 * Thunderstorm: Slight or moderate 110 // 96, 99 * Thunderstorm with slight and heavy hail 111 switch (weatherCode) { 112 case 0: // Clear sky 113 conditions.textContent = 'Clear sky'; 114 element.style.background = 'linear-gradient(0deg, #87CEEB, #FFD700)'; 115 useSkyGradient(); 116 conditionsEmoji.textContent = isDay ? 'âï¸' : 'ð'; 117 break; 118 case 1: // Mainly clear 119 conditions.textContent = 'Mainly clear'; 120 element.style.background = 'linear-gradient(0deg, #B0E2FF, #FFFACD)'; 121 useSkyGradient(); 122 conditionsEmoji.textContent = isDay ? 'ð¤ï¸' : 'ð'; 123 break; 124 case 2: // Partly cloudy 125 conditions.textContent = 'Partly cloudy'; 126 element.style.background = 'linear-gradient(0deg, #CCCCFF, #F0E68C)'; 127 useSkyGradient(); 128 conditionsEmoji.textContent = isDay ? 'â ' : 'âï¸'; 129 break; 130 case 3: // Overcast 131 conditions.textContent = 'Overcast'; 132 element.style.background = 'linear-gradient(0deg, #A9A9A9, #778899)'; 133 conditionsEmoji.textContent = 'âï¸'; 134 break; 135 case 45: // Fog 136 conditions.textContent = 'Fog'; 137 element.style.background = 'linear-gradient(0deg, #E6E6FA, #D3D3D3)'; 138 conditionsEmoji.textContent = 'ð«ï¸'; 139 break; 140 case 48: // Depositing rime fog 141 conditions.textContent = 'Depositing rime fog'; 142 element.style.background = 'linear-gradient(0deg, #E6E6FA, #D3D3D3)'; 143 conditionsEmoji.textContent = 'ð«ï¸âï¸'; 144 break; 145 case 51: // Light drizzle 146 conditions.textContent = 'Light drizzle';
147 element.style.background = 'linear-gradient(0deg, #B0E0E6, #778899)'; 148 conditionsEmoji.textContent = 'ð¦ï¸'; 149 break; 150 case 53: // Moderate drizzle 151 conditions.textContent = 'Moderate drizzle'; 152 element.style.background = 'linear-gradient(0deg, #ADD8E6, #708090)'; 153 conditionsEmoji.textContent = 'ð¦ï¸'; 154 break; 155 case 55: // Dense drizzle 156 conditions.textContent = 'Dense drizzle'; 157 element.style.background = 'linear-gradient(0deg, #87CEFA, #4682B4)'; 158 conditionsEmoji.textContent = 'ð§ï¸'; 159 break; 160 case 56: // Light freezing drizzle 161 conditions.textContent = 'Light freezing drizzle'; 162 element.style.background = 'linear-gradient(0deg, #E0FFFF, #5F9EA0)'; 163 conditionsEmoji.textContent = 'ð¨ï¸'; 164 break; 165 case 57: // Dense freezing drizzle 166 conditions.textContent = 'Dense freezing drizzle'; 167 element.style.background = 'linear-gradient(0deg, #AFEEEE, #5F9EA0)'; 168 conditionsEmoji.textContent = 'ð¨ï¸'; 169 break; 170 case 61: // Slight rain 171 conditions.textContent = 'Slight rain'; 172 element.style.background = 'linear-gradient(0deg, #4682B4, #B0C4DE)'; 173 conditionsEmoji.textContent = 'ð§ï¸'; 174 break; 175 case 63: // Moderate rain 176 conditions.textContent = 'Moderate rain'; 177 element.style.background = 'linear-gradient(0deg, #4169E1, #B0C4DE)'; 178 conditionsEmoji.textContent = 'ð§ï¸ð§ï¸'; 179 break; 180 case 65: // Heavy rain 181 conditions.textContent = 'Heavy rain'; 182 element.style.background = 'linear-gradient(0deg, #2F4F4F, #708090)'; 183 conditionsEmoji.textContent = 'ð§ï¸ð§ï¸ð§ï¸'; 184 break; 185 case 66: // Light freezing rain 186 conditions.textContent = 'Light freezing rain'; 187 element.style.background = 'linear-gradient(0deg, #6495ED, #4682B4)'; 188 conditionsEmoji.textContent = 'ð§ï¸âï¸'; 189 break; 190 case 67: // Heavy freezing rain 191 conditions.textContent = 'Heavy freezing rain'; 192 element.style.background = 'linear-gradient(0deg, #5F9EA0, #4682B4)'; 193 conditionsEmoji.textContent = 'ð§ï¸âï¸âï¸'; 194 break; 195 case 71: // Slight snowfall 196 conditions.textContent = 'Slight snowfall'; 197 element.style.background = 'linear-gradient(0deg, #FFFAFA, #B0E0E6)'; 198 conditionsEmoji.textContent = 'âï¸'; 199 break; 200 case 73: // Moderate snowfall 201 conditions.textContent = 'Moderate snowfall'; 202 element.style.background = 'linear-gradient(0deg, #E0FFFF, #AFEEEE)'; 203 conditionsEmoji.textContent = 'âï¸âï¸'; 204 break; 205 case 75: // Heavy snowfall 206 conditions.textContent = 'Heavy snowfall'; 207 element.style.background = 'linear-gradient(0deg, #4682B4, #E0FFFF)'; 208 conditionsEmoji.textContent = 'âï¸âï¸âï¸'; 209 break; 210 case 77: // Snow grains 211 conditions.textContent = 'Snow grains'; 212 element.style.background = 'linear-gradient(0deg, #B0E0E6, #AFEEEE)'; 213 conditionsEmoji.textContent = 'ð¨ï¸'; 214 break; 215 case 80: // Slight rain showers 216 conditions.textContent = 'Slight rain showers'; 217 element.style.background = 'linear-gradient(0deg, #B0E0E6, #87CEFA)'; 218 conditionsEmoji.textContent = 'ð¦ï¸'; 219 break; 220 case 81: // Moderate rain showers 221 conditions.textContent = 'Moderate rain showers'; 222 element.style.background = 'linear-gradient(0deg, #87CEFA, #4682B4)'; 223 conditionsEmoji.textContent = 'ð¦ï¸ð¦ï¸'; 224 break; 225 case 82: // Violent rain showers 226 conditions.textContent = 'Violent rain showers'; 227 element.style.background = 'linear-gradient(0deg, #4682B4, #0000FF)'; 228 conditionsEmoji.textContent = 'âï¸'; 229 break; 230 case 85: // Slight snow showers 231 conditions.textContent = 'Slight snow showers'; 232 element.style.background = 'linear-gradient(0deg, #F0FFFF, #B0E0E6)'; 233 conditionsEmoji.textContent = 'ð¨ï¸'; 234 break; 235 case 86: // Heavy snow showers 236 conditions.textContent = 'Heavy snow showers'; 237 element.style.background = 'linear-gradient(0deg, #E0FFFF, #AFEEEE)'; 238 conditionsEmoji.textContent = 'ð¨ï¸ð¨ï¸'; 239 break; 240 case 95: // Thunderstorm: Slight or moderate 241 conditions.textContent = 'Thunderstorm: Slight or moderate'; 242 element.style.background = 'linear-gradient(0deg, #2F4F4F, #800080)'; 243 conditionsEmoji.textContent = 'ð©ï¸'; 244 break; 245 case 96: // Thunderstorm with slight hail 246 conditions.textContent = 'Thunderstorm with slight hail'; 247 element.style.background = 'linear-gradient(0deg, #2F4F4F, #800080)'; 248 conditionsEmoji.textContent = 'âï¸ð¨ï¸'; 249 break; 250 case 99: // Thunderstorm with heavy hail 251 conditions.textContent = 'Thunderstorm with heavy hail'; 252 element.style.background = 'linear-gradient(0deg, #2F4F4F, #800080)'; 253 conditionsEmoji.textContent = 'âï¸âï¸âï¸'; 254 break; 255 default: // Unknown weather code 256 conditions.textContent = 'Unknown weather code'; 257 element.style.background = 'linear-gradient(0deg, #696969, #8B8B8B)'; 258 conditionsEmoji.textContent = 'â'; 259 break; 260 } 261 262 if (consideredDark) { 263 useSkyGradient(); 264 } 265 266 const isRaining = [51, 53, 55, 61, 63, 65, 80, 81, 82].includes(weatherCode); 267 setRainState(isRaining); 268 269 270 const daily = data.daily.time.map((time, i) => ({ 271 time, 272 max: data.daily.temperature_2m_max[i], 273 min: data.daily.temperature_2m_min[i] 274 })); 275 276 const plot = Plot.plot({ 277 marks: [ 278 // Bar for temperature ranges 279 Plot.barX(daily, { 280 y: "time", 281 x1: "min", 282 x2: "max", 283 }), 284 // Dot for current temperature 285 Plot.dot([data.current], { 286 x: "temperature_2m", 287 y: (d) => daily[0].time, 288 fill: darkMode ? "black" : "white", 289 title: (d) => `Current Temp: ${d.temperature_2m}°C` 290 }), 291 Plot.text(daily, { 292 x: "max", 293 y: "time", 294 text: d => `${d.max.toFixed(0)}`, 295 dx: -6, // Position adjustment on x-axis 296 fill: darkMode ? "black" : "white" 297 }), 298 // Text for min temperatures 299 Plot.text(daily, { 300 x: "min", 301 y: "time", 302 text: d => `${d.min.toFixed(0)}`, 303 dx: 6, // Position adjustment on x-axis 304 fill: darkMode ? "black" : "white" 305 }) 306 ], 307 y: { 308 label: null, 309 tickSize: 0, 310 tickFormat: (d) => Plot.formatWeekday("en-AU", "narrow")(new Date(d).getDay()), 311 type: "band", 312 }, 313 x: { 314 // label: "", 315 // include celius in tick mark 316 tickFormat: (d) => `${d}°C`, 317 grid: true 318 }, 319 width: 175, 320 height: 125, 321 marginTop: 5, 322 marginLeft: 20, 323 marginBottom: 20, 324 }); 325 326 document.getElementById('plot-container').appendChild(plot); 327 328 if (usedGradient) { 329 document.getElementById('attribution').style.display = 'block'; 330 } 331 }) 332 .catch(() => { 333 setRainState(false); 334 });
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.