portrait hourly graph #167

This commit is contained in:
Matt Walsh
2026-04-19 22:13:53 -05:00
parent 125490c158
commit cd2035490a
9 changed files with 342 additions and 166 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

View File

@@ -7,6 +7,12 @@ import { registerDisplay, timeZone } from './navigation.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs'; import { DateTime } from '../vendor/auto/luxon.mjs';
import settings from './settings.mjs'; import settings from './settings.mjs';
// two chart areas
const chartAreas = [
'.top',
'.bottom',
];
// set up spacing and scales // set up spacing and scales
const scaling = () => { const scaling = () => {
const available = { const available = {
@@ -24,6 +30,11 @@ const scaling = () => {
dataLength.hours = 48; dataLength.hours = 48;
dataLength.xTicks = 6; dataLength.xTicks = 6;
} }
if (settings.portrait?.value && settings.enhanced?.value) {
available.height = 450;
}
return { return {
available, available,
dataLength, dataLength,
@@ -85,10 +96,13 @@ class HourlyGraph extends WeatherDisplay {
// get the image // get the image
if (!this.image) this.image = this.elem.querySelector('.chart img'); if (!this.image) this.image = this.elem.querySelector('.chart img');
if (!this.portraitImage) this.portraitImage = this.elem.querySelector('.bottom .chart img');
// set up image // set up images
this.image.width = available.width; this.image.width = available.width;
this.image.height = available.height; this.image.height = available.height;
this.portraitImage.width = available.width;
this.portraitImage.height = available.height;
// get context // get context
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
@@ -97,31 +111,57 @@ class HourlyGraph extends WeatherDisplay {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false;
// set the canvas for each graph to the top one by default
const contexts = [
ctx,
ctx,
ctx,
ctx,
];
// if in portrait-enhanced, change out the second two contexts with a second canvas
let portraitCanvas;
if (settings.portrait?.value && settings.enhanced?.value) {
portraitCanvas = document.createElement('canvas');
portraitCanvas.width = available.width;
portraitCanvas.height = available.height;
const portraitCtx = portraitCanvas.getContext('2d');
portraitCtx.imageSmoothingEnabled = false;
contexts[2] = portraitCtx;
contexts[3] = portraitCtx;
}
// calculate time scale // calculate time scale
const timeScale = calcScale(0, 5, this.data.temperature.length - 1, available.width); const timeScale = calcScale(0, 5, this.data.temperature.length - 1, available.width);
const timeStep = this.data.temperature.length / (dataLength.xTicks); const timeStep = this.data.temperature.length / (dataLength.xTicks);
const startTime = DateTime.now().startOf('hour'); const startTime = DateTime.now().startOf('hour');
// there are two x axes in portrait
chartAreas.forEach((area) => {
let prevTime = startTime; let prevTime = startTime;
const elem = this.elem.querySelector(area);
Array(dataLength.xTicks + 1).fill().forEach((val, idx) => { Array(dataLength.xTicks + 1).fill().forEach((val, idx) => {
// track the previous label so a day of week can be added when it changes // track the previous label so a day of week can be added when it changes
const label = formatTime(startTime.plus({ hour: idx * timeStep }), prevTime); const label = formatTime(startTime.plus({ hour: idx * timeStep }), prevTime);
prevTime = label.ts; prevTime = label.ts;
// write to page // write to page
document.querySelector(`.x-axis .l-${idx + 1}`).innerHTML = label.formatted; elem.querySelector(`.x-axis .l-${idx + 1}`).innerHTML = label.formatted;
});
}); });
// order is important last line drawn is on top // order is important last line drawn is on top
// clouds // clouds
const percentScale = calcScale(0, available.height - 10, 100, 10); const percentScale = calcScale(0, available.height - 10, 100, 10);
const cloud = createPath(this.data.skyCover, timeScale, percentScale); const cloud = createPath(this.data.skyCover, timeScale, percentScale);
drawPath(cloud, ctx, { drawPath(cloud, contexts[3], {
strokeStyle: 'lightgrey', strokeStyle: 'lightgrey',
lineWidth: 3, lineWidth: 3,
}); });
// precip // precip
const precip = createPath(this.data.probabilityOfPrecipitation, timeScale, percentScale); const precip = createPath(this.data.probabilityOfPrecipitation, timeScale, percentScale);
drawPath(precip, ctx, { drawPath(precip, contexts[2], {
strokeStyle: 'aqua', strokeStyle: 'aqua',
lineWidth: 3, lineWidth: 3,
}); });
@@ -136,14 +176,14 @@ class HourlyGraph extends WeatherDisplay {
// dewpoint // dewpoint
const dewpointPath = createPath(this.data.dewpoint, timeScale, tempScale); const dewpointPath = createPath(this.data.dewpoint, timeScale, tempScale);
drawPath(dewpointPath, ctx, { drawPath(dewpointPath, contexts[1], {
strokeStyle: 'green', strokeStyle: 'green',
lineWidth: 3, lineWidth: 3,
}); });
// temperature // temperature
const tempPath = createPath(this.data.temperature, timeScale, tempScale); const tempPath = createPath(this.data.temperature, timeScale, tempScale);
drawPath(tempPath, ctx, { drawPath(tempPath, contexts[0], {
strokeStyle: 'red', strokeStyle: 'red',
lineWidth: 3, lineWidth: 3,
}); });
@@ -151,6 +191,8 @@ class HourlyGraph extends WeatherDisplay {
// temperature axis labels // temperature axis labels
// limited to 3 characters, sacraficing degree character // limited to 3 characters, sacraficing degree character
const degree = String.fromCharCode(176); const degree = String.fromCharCode(176);
// only fill the upper chart with temperatures
this.elem.querySelector('.y-axis .l-1').innerHTML = (maxScale + degree).substring(0, 3); 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-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-3').innerHTML = (midScale1 + degree).substring(0, 3);
@@ -159,6 +201,11 @@ class HourlyGraph extends WeatherDisplay {
// set the image source // set the image source
this.image.src = canvas.toDataURL(); this.image.src = canvas.toDataURL();
// if a portrait canvas was created set that image as well
if (portraitCanvas) {
this.portraitImage.src = portraitCanvas.toDataURL();
}
// change the units in the header // change the units in the header
this.elem.querySelector('.temperature').innerHTML = `Temperature ${String.fromCharCode(176)}${this.data.temperatureUnit}`; this.elem.querySelector('.temperature').innerHTML = `Temperature ${String.fromCharCode(176)}${this.data.temperatureUnit}`;
this.elem.querySelector('.dewpoint').innerHTML = `Dewpoint ${String.fromCharCode(176)}${this.data.temperatureUnit}`; this.elem.querySelector('.dewpoint').innerHTML = `Dewpoint ${String.fromCharCode(176)}${this.data.temperatureUnit}`;

View File

@@ -7,22 +7,6 @@
width: calc(p.$standard-width - (2 * p.$blue-box-margin)); width: calc(p.$standard-width - (2 * p.$blue-box-margin));
@include u.text-shadow(); @include u.text-shadow();
.portrait-only {
display: none;
.portrait.enhanced & {
display: block;
}
}
.standard-only {
display: block;
.portrait.enhanced & {
display: none;
}
}
.portrait-location { .portrait-location {
text-align: center; text-align: center;
margin-top: 175px; margin-top: 175px;

View File

@@ -1,5 +1,6 @@
@use 'shared/_colors'as c; @use 'shared/_colors'as c;
@use 'shared/_utils'as u; @use 'shared/_utils'as u;
@use 'shared/_positions'as p;
#hourly-graph-html { #hourly-graph-html {
background-image: url(../images/backgrounds/1-chart.png); background-image: url(../images/backgrounds/1-chart.png);
@@ -10,10 +11,22 @@
background-position-x: 0px; background-position-x: 0px;
} }
.header { // change background for portrait
.right { .portrait.enhanced & {
background-image: url(../images/backgrounds/1-chart-portrait.png);
background-position-x: 0px;
}
}
.legend {
.right & {
position: absolute; position: absolute;
.header & {
top: 35px; top: 35px;
}
right: 60px; right: 60px;
width: 360px; width: 360px;
font-family: 'Star4000 Small'; font-family: 'Star4000 Small';
@@ -27,6 +40,13 @@
.temperature { .temperature {
color: red; color: red;
// shift down when only 2 legend elements are shown in portrait
.header & {
.portrait.enhanced & {
margin-top: 12px;
}
}
} }
.dewpoint { .dewpoint {
@@ -42,15 +62,30 @@
} }
} }
} }
}
.weather-display .main.hourly-graph { .weather-display .main.hourly-graph {
&.main { &.main {
.chart-container {
position: absolute;
top: 0px;
height: p.$standard-scroll-height;
&.portrait-only {
top: 495px;
}
>div { >div {
position: absolute; position: absolute;
} }
.portrait.enhanced & {
height: 470px;
}
.label { .label {
font-family: 'Star4000 Small'; font-family: 'Star4000 Small';
font-size: 24pt; font-size: 24pt;
@@ -149,6 +184,10 @@
.wide.enhanced & { .wide.enhanced & {
width: 746px; width: 746px;
} }
.portrait.enhanced & {
height: 450px;
}
} }
} }
@@ -158,6 +197,10 @@
width: 50px; width: 50px;
height: 285px; height: 285px;
.portrait.enhanced & {
height: 450px;
}
.label { .label {
text-align: right; text-align: right;
right: 0px; right: 0px;
@@ -177,6 +220,62 @@
&.l-4 { &.l-4 {
bottom: 0px; bottom: 0px;
} }
.portrait.enhanced & {
&.l-1 {
top: 0px;
}
&.l-2 {
top: calc(445px / 3);
}
&.l-3 {
bottom: calc(445px / 3 - 11px);
}
&.l-4 {
bottom: 0px;
}
}
}
}
&.bottom {
.y-axis .label {
.portrait.enhanced & {
&.l-1 {
top: 0px;
}
&.l-2 {
top: calc(445px / 4);
}
&.l-3 {
top: calc(445px / 2 - 11px);
}
&.l-4 {
bottom: calc(445px / 4 - 11px);
}
&.l-5 {
bottom: 0px;
}
;
}
}
}
}
.chart-container.bottom {
.right {
width: p.$standard-width;
position: absolute;
top: -25px;
} }
} }

View File

@@ -851,3 +851,19 @@ body.kiosk #loading .instructions {
display: inline-block; display: inline-block;
} }
} }
.portrait-only {
display: none;
.portrait.enhanced & {
display: block;
}
}
.standard-only {
display: block;
.portrait.enhanced & {
display: none;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +1,12 @@
<%- include('header.ejs', {title: 'Hourly Graph' , hasTime: false }) %> <%- include('header.ejs', {title: 'Hourly Graph' , hasTime: false }) %>
<div class="main has-scroll hourly-graph can-enhance"> <div class="main has-scroll hourly-graph can-enhance">
<div class="top-right template "> <div class="top-right legend template ">
<div class="temperature">Temperature</div> <div class="temperature">Temperature</div>
<div class="dewpoint">Dewpoint</div> <div class="dewpoint">Dewpoint</div>
<div class="cloud">Cloud %</div> <div class="cloud standard-only">Cloud %</div>
<div class="rain">Precip %</div> <div class="rain standard-only">Precip %</div>
</div> </div>
<div class="top chart-container">
<div class="y-axis"> <div class="y-axis">
<div class="label l-1">75</div> <div class="label l-1">75</div>
<div class="label l-2">65</div> <div class="label l-2">65</div>
@@ -25,3 +26,32 @@
<div class="label l-7">12p</div> <div class="label l-7">12p</div>
</div> </div>
</div> </div>
<div class="bottom chart-container portrait-only">
<div class="right">
<div class="legend portrait-only">
<div class="cloud">Cloud %</div>
<div class="rain">Precip %</div>
</div>
</div>
<div class="y-axis">
<div class="label l-1">100</div>
<div class="label l-2">75%</div>
<div class="label l-3">50%</div>
<div class="label l-4">25%</div>
<div class="label l-5">0%</div>
</div>
<div class="chart">
<img id="chart-area"></img>
</div>
<div class="x-axis">
<div class="label l-1">12a</div>
<div class="label l-2">6a</div>
<div class="label l-3">12p</div>
<div class="label l-4">6p</div>
<div class="label l-5">12a</div>
<div class="label l-6">6a</div>
<div class="label l-7">12p</div>
</div>
</div>
</div>