PageSourceSearch

https://maxbo.me/js/weather.js

js maxbo.me collected 2026-09-28 09:08:45 UTC 12,340 bytes, 334 lines download raw bytes

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  + '&current=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.