mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 15:49:31 -07:00
add hourly graph
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -161,4 +161,4 @@ class ExtendedForecast extends WeatherDisplay {
|
||||
}
|
||||
|
||||
// register display
|
||||
registerDisplay(new ExtendedForecast(6, 'extended-forecast'));
|
||||
registerDisplay(new ExtendedForecast(7, 'extended-forecast'));
|
||||
|
||||
138
server/scripts/modules/hourly-graph.mjs
Normal file
138
server/scripts/modules/hourly-graph.mjs
Normal 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'));
|
||||
@@ -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);
|
||||
|
||||
@@ -93,4 +93,4 @@ class LocalForecast extends WeatherDisplay {
|
||||
}
|
||||
|
||||
// register display
|
||||
registerDisplay(new LocalForecast(5, 'local-forecast'));
|
||||
registerDisplay(new LocalForecast(6, 'local-forecast'));
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -402,4 +402,4 @@ class Radar extends WeatherDisplay {
|
||||
}
|
||||
|
||||
// register display
|
||||
registerDisplay(new Radar(8, 'radar'));
|
||||
registerDisplay(new Radar(9, 'radar'));
|
||||
|
||||
@@ -389,4 +389,4 @@ class RegionalForecast extends WeatherDisplay {
|
||||
}
|
||||
|
||||
// register display
|
||||
registerDisplay(new RegionalForecast(4, 'regional-forecast'));
|
||||
registerDisplay(new RegionalForecast(5, 'regional-forecast'));
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user