add hourly graph

This commit is contained in:
Matt Walsh
2022-12-07 15:36:02 -06:00
parent 0331de8b8a
commit 1a7734b620
20 changed files with 358 additions and 22 deletions

View File

@@ -171,7 +171,7 @@ class Almanac extends WeatherDisplay {
}
// register display
const display = new Almanac(7, 'almanac');
const display = new Almanac(8, 'almanac');
registerDisplay(display);
export default display.getSun.bind(display);

View File

@@ -161,4 +161,4 @@ class ExtendedForecast extends WeatherDisplay {
}
// register display
registerDisplay(new ExtendedForecast(6, 'extended-forecast'));
registerDisplay(new ExtendedForecast(7, 'extended-forecast'));

View File

@@ -0,0 +1,138 @@
// hourly forecast list
import STATUS from './status.mjs';
import getHourlyData from './hourly.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
class HourlyGraph extends WeatherDisplay {
constructor(navId, elemId, defaultActive) {
// special height and width for scrolling
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() {
if (!super.getData()) return;
const data = await getHourlyData();
// get interesting data
const temperature = data.map((d) => d.temperature);
const probabilityOfPrecipitation = data.map((d) => d.probabilityOfPrecipitation);
const skyCover = data.map((d) => d.skyCover);
this.data = {
skyCover, temperature, probabilityOfPrecipitation,
};
this.setStatus(STATUS.loaded);
}
drawCanvas() {
if (!this.canvas) this.canvas = this.elem.querySelector('.chart canvas');
// get available space
const boundingRect = this.canvas.getBoundingClientRect();
const availableWidth = boundingRect.width;
const availableHeight = boundingRect.height;
this.canvas.width = availableWidth;
this.canvas.height = availableHeight;
// get context
const ctx = this.canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
// calculate time scale
const timeScale = calcScale(0, 5, this.data.temperature.length - 1, availableWidth);
const startTime = DateTime.now().startOf('hour');
document.querySelector('.x-axis .l-1').innerHTML = formatTime(startTime);
document.querySelector('.x-axis .l-2').innerHTML = formatTime(startTime.plus({ hour: 6 }));
document.querySelector('.x-axis .l-3').innerHTML = formatTime(startTime.plus({ hour: 12 }));
document.querySelector('.x-axis .l-4').innerHTML = formatTime(startTime.plus({ hour: 18 }));
document.querySelector('.x-axis .l-5').innerHTML = formatTime(startTime.plus({ hour: 24 }));
// 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,
});
// temperature
const minTemp = Math.min(...this.data.temperature);
const maxTemp = Math.max(...this.data.temperature);
const midTemp = Math.round((minTemp + maxTemp) / 2);
const tempScale = calcScale(minTemp, availableHeight - 10, maxTemp, 10);
const tempPath = createPath(this.data.temperature, timeScale, tempScale);
drawPath(tempPath, ctx, {
strokeStyle: 'red',
lineWidth: 3,
});
// temperature axis labels
this.elem.querySelector('.y-axis .l-1').innerHTML = maxTemp;
this.elem.querySelector('.y-axis .l-2').innerHTML = midTemp;
this.elem.querySelector('.y-axis .l-3').innerHTML = minTemp;
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) => time.toFormat('ha').slice(0, -1);
// register display
registerDisplay(new HourlyGraph(3, 'hourly-graph'));

View File

@@ -29,7 +29,7 @@ class Hourly extends WeatherDisplay {
async getData(weatherParameters) {
// super checks for enabled
if (!super.getData(weatherParameters)) return;
const superResponse = super.getData(weatherParameters);
let forecast;
try {
// get the forecast
@@ -43,6 +43,9 @@ class Hourly extends WeatherDisplay {
this.data = await Hourly.parseForecast(forecast.properties);
this.getDataCallback();
if (!superResponse) return;
this.setStatus(STATUS.loaded);
this.drawLongCanvas();
}
@@ -66,6 +69,8 @@ class Hourly extends WeatherDisplay {
apparentTemperature: celsiusToFahrenheit(apparentTemperature[idx]),
windSpeed: kilometersToMiles(windSpeed[idx]),
windDirection: directionToNSEW(windDirection[idx]),
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
skyCover: skyCover[idx],
icon: icons[idx],
}));
}
@@ -184,7 +189,20 @@ class Hourly extends WeatherDisplay {
return dayName;
}, '');
}
// make data available outside this class
// promise allows for data to be requested before it is available
async getCurrentData() {
return new Promise((resolve) => {
if (this.data) resolve(this.data);
// data not available, put it into the data callback queue
this.getDataCallbacks.push(() => resolve(this.data));
});
}
}
// register display
registerDisplay(new Hourly(2, 'hourly'));
const display = new Hourly(2, 'hourly', false);
registerDisplay(display);
export default display.getCurrentData.bind(display);

View File

@@ -93,4 +93,4 @@ class LocalForecast extends WeatherDisplay {
}
// register display
registerDisplay(new LocalForecast(5, 'local-forecast'));
registerDisplay(new LocalForecast(6, 'local-forecast'));

View File

@@ -281,7 +281,7 @@ const generateCheckboxes = () => {
if (!availableDisplays) return;
// generate checkboxes
const checkboxes = displays.map((d) => d.generateCheckbox()).filter((d) => d);
const checkboxes = displays.map((d) => d.generateCheckbox(d.defaultEnabled)).filter((d) => d);
// write to page
availableDisplays.innerHTML = '';

View File

@@ -402,4 +402,4 @@ class Radar extends WeatherDisplay {
}
// register display
registerDisplay(new Radar(8, 'radar'));
registerDisplay(new Radar(9, 'radar'));

View File

@@ -389,4 +389,4 @@ class RegionalForecast extends WeatherDisplay {
}
// register display
registerDisplay(new RegionalForecast(4, 'regional-forecast'));
registerDisplay(new RegionalForecast(5, 'regional-forecast'));

View File

@@ -160,4 +160,4 @@ class TravelForecast extends WeatherDisplay {
}
// register display, not active by default
registerDisplay(new TravelForecast(3, 'travel', false));
registerDisplay(new TravelForecast(4, 'travel', false));