// hourly forecast list import STATUS from './status.mjs'; import getHourlyData from './hourly.mjs'; import WeatherDisplay from './weatherdisplay.mjs'; import { registerDisplay, timeZone } from './navigation.mjs'; import { DateTime } from '../vendor/auto/luxon.mjs'; // get available space const availableWidth = 532; const availableHeight = 285; class HourlyGraph extends WeatherDisplay { constructor(navId, elemId, defaultActive) { super(navId, elemId, 'Hourly Graph', defaultActive); // move the top right data into the correct location on load document.addEventListener('DOMContentLoaded', () => { this.moveHeader(); }); } moveHeader() { // get the header const header = this.fillTemplate('top-right', {}); // place the header this.elem.querySelector('.header .right').append(header); } async getData(weatherParameters, refresh) { if (!super.getData(undefined, refresh)) return; const data = await getHourlyData(() => this.stillWaiting()); if (!data) { if (this.isEnabled) this.setStatus(STATUS.failed); return; } // get interesting data const temperature = data.map((d) => d.temperature); const probabilityOfPrecipitation = data.map((d) => d.probabilityOfPrecipitation); const skyCover = data.map((d) => d.skyCover); const dewpoint = data.map((d) => d.dewpoint); this.data = { skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit, dewpoint, }; this.setStatus(STATUS.loaded); } drawCanvas() { if (!this.image) this.image = this.elem.querySelector('.chart img'); this.image.width = availableWidth; this.image.height = availableHeight; // get context const canvas = document.createElement('canvas'); canvas.width = availableWidth; canvas.height = availableHeight; const ctx = canvas.getContext('2d'); ctx.imageSmoothingEnabled = false; // calculate time scale const timeScale = calcScale(0, 5, this.data.temperature.length - 1, availableWidth); const timeStep = this.data.temperature.length / 4; const startTime = DateTime.now().startOf('hour'); let prevTime = startTime; Array(5).fill().forEach((val, idx) => { // track the previous label so a day of week can be added when it changes const label = formatTime(startTime.plus({ hour: idx * timeStep }), prevTime); prevTime = label.ts; // write to page document.querySelector(`.x-axis .l-${idx + 1}`).innerHTML = label.formatted; }); // order is important last line drawn is on top // clouds const percentScale = calcScale(0, availableHeight - 10, 100, 10); const cloud = createPath(this.data.skyCover, timeScale, percentScale); drawPath(cloud, ctx, { strokeStyle: 'lightgrey', lineWidth: 3, }); // precip const precip = createPath(this.data.probabilityOfPrecipitation, timeScale, percentScale); drawPath(precip, ctx, { strokeStyle: 'aqua', lineWidth: 3, }); // calculate temperature scale for min and max of dewpoint and temperature const minScale = Math.min(...this.data.dewpoint, ...this.data.temperature); const maxScale = Math.max(...this.data.dewpoint, ...this.data.temperature); const thirdScale = (maxScale - minScale) / 3; const midScale1 = Math.round(minScale + thirdScale); const midScale2 = Math.round(minScale + (thirdScale * 2)); const tempScale = calcScale(minScale, availableHeight - 10, maxScale, 10); // dewpoint const dewpointPath = createPath(this.data.dewpoint, timeScale, tempScale); drawPath(dewpointPath, ctx, { strokeStyle: 'green', lineWidth: 3, }); // temperature const tempPath = createPath(this.data.temperature, timeScale, tempScale); drawPath(tempPath, ctx, { strokeStyle: 'red', lineWidth: 3, }); // temperature axis labels // limited to 3 characters, sacraficing degree character const degree = String.fromCharCode(176); this.elem.querySelector('.y-axis .l-1').innerHTML = (maxScale + degree).substring(0, 3); this.elem.querySelector('.y-axis .l-2').innerHTML = (midScale2 + degree).substring(0, 3); this.elem.querySelector('.y-axis .l-3').innerHTML = (midScale1 + degree).substring(0, 3); this.elem.querySelector('.y-axis .l-4').innerHTML = (minScale + degree).substring(0, 3); // set the image source this.image.src = canvas.toDataURL(); // change the units in the header this.elem.querySelector('.temperature').innerHTML = `Temperature ${String.fromCharCode(176)}${this.data.temperatureUnit}`; this.elem.querySelector('.dewpoint').innerHTML = `Dewpoint ${String.fromCharCode(176)}${this.data.temperatureUnit}`; super.drawCanvas(); this.finishDraw(); } } // create a scaling function from two points const calcScale = (x1, y1, x2, y2) => { const m = (y2 - y1) / (x2 - x1); const b = y1 - m * x1; return (x) => m * x + b; }; // create a path as an array of [x,y] const createPath = (data, xScale, yScale) => data.map((d, i) => [xScale(i), yScale(d)]); // draw a path with shadow const drawPath = (path, ctx, options) => { // first shadow ctx.beginPath(); ctx.strokeStyle = 'black'; ctx.lineWidth = (options?.lineWidth ?? 2) + 2; ctx.moveTo(path[0][0], path[0][1]); path.slice(1).forEach((point) => ctx.lineTo(point[0], point[1] + 2)); ctx.stroke(); // then colored line ctx.beginPath(); ctx.strokeStyle = options?.strokeStyle ?? 'red'; ctx.lineWidth = (options?.lineWidth ?? 2); ctx.moveTo(path[0][0], path[0][1]); path.slice(1).forEach((point) => ctx.lineTo(point[0], point[1])); ctx.stroke(); }; // format as 1p, 12a, etc. const formatTime = (time, prev) => { // if the day of the week changes, show the day of the week in the label let format = 'ha'; if (prev.weekday !== time.weekday) format = 'ccc ha'; const ts = time.setZone(timeZone()); return { ts, formatted: ts.toFormat(format).slice(0, -1), }; }; // register display registerDisplay(new HourlyGraph(4, 'hourly-graph'));