This commit is contained in:
Daniel Ledda
2020-11-18 22:51:30 +01:00
parent 841861ee87
commit 4b0c5bc9ab
8 changed files with 100 additions and 78 deletions

View File

@@ -1,6 +1,7 @@
html, body { html, body {
margin: 0; margin: 0;
height: 100%; height: 100%;
background-color: #fff1de;
} }
.overlay { .overlay {
@@ -35,3 +36,10 @@ html, body {
width: calc(100% - 20vw); width: calc(100% - 20vw);
height: calc(100% - 20vw); height: calc(100% - 20vw);
} }
#myChart {
background-color: white;
padding: 2vw;
border-radius: 1vw;
border: 3px #c7ab82 solid;
}

View File

@@ -451,6 +451,11 @@
"moment": "^2.10.2" "moment": "^2.10.2"
} }
}, },
"@types/chartist": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@types/chartist/-/chartist-0.11.0.tgz",
"integrity": "sha512-YDJuUm0TkKj2WW6GlYmhOuBkaYzZBGJMvZz1X+Qp0Oj8oY3aozQ/YeWw4aNhfpyk5df0DKf6psjMftJI+GThtA=="
},
"@types/eslint": { "@types/eslint": {
"version": "7.2.4", "version": "7.2.4",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.4.tgz", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.4.tgz",
@@ -1467,6 +1472,11 @@
"moment": "^2.10.2" "moment": "^2.10.2"
} }
}, },
"chartist": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/chartist/-/chartist-0.11.4.tgz",
"integrity": "sha512-H4AimxaUD738/u9Mq8t27J4lh6STsLi4BQHt65nOtpLk3xyrBPaLiLMrHw7/WV9CmsjGA02WihjuL5qpSagLYw=="
},
"chartjs-color": { "chartjs-color": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz", "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",

View File

@@ -1,6 +1,7 @@
import Chart from "chart.js/dist/Chart.bundle.min"; import Chart from "chart.js/dist/Chart.bundle.min";
import type {ChartPoint} from "chart.js"; import type {ChartPoint} from "chart.js";
import {generateClimateChartConfig} from "./climateChartConfig"; import {generateClimateChartConfig} from "./climateChartConfig";
import {config} from "./main";
interface Snapshot { interface Snapshot {
id: number, id: number,
@@ -25,11 +26,17 @@ class ClimateChart {
private onLoadedCallback: () => void = () => {}; private onLoadedCallback: () => void = () => {};
private onErrorCallback: (e: Error) => void = () => {}; private onErrorCallback: (e: Error) => void = () => {};
private errorLog: string = ""; private errorLog: string = "";
private readonly rootUrl: string; private readonly dataEndpointBase: string;
private readonly canvasId: string; private readonly domId: string;
private readonly minutesDisplayed: number = 60; private readonly minutesDisplayed: number = 60;
constructor(rootUrl: string, canvasId: string, minutesDisplayed: number) { constructor(domId: string, minutesDisplayed: number) {
this.domId = domId;
if (config.development) {
this.dataEndpointBase = "http://tortedda.local/climate/data";
} else {
this.dataEndpointBase = "data";
}
this.minutesDisplayed = Math.floor(minutesDisplayed); this.minutesDisplayed = Math.floor(minutesDisplayed);
if (minutesDisplayed < 0 || Math.floor(minutesDisplayed) !== minutesDisplayed) { if (minutesDisplayed < 0 || Math.floor(minutesDisplayed) !== minutesDisplayed) {
console.warn(`Minutes passed were ${ minutesDisplayed }, which is invalid. ${ this.minutesDisplayed } minutes are being shown instead.`); console.warn(`Minutes passed were ${ minutesDisplayed }, which is invalid. ${ this.minutesDisplayed } minutes are being shown instead.`);
@@ -37,17 +44,8 @@ class ClimateChart {
this.initChart().catch((e) => {this.logError(e);}); this.initChart().catch((e) => {this.logError(e);});
} }
private async getInitialDataBlob(): Promise<SnapshotRecords> {
const data = await fetch("data?since=" + new Date((new Date().getTime() - this.minutesDisplayed * 60000)).toISOString());
const payload = await data.json();
if (payload.snapshots.length < 0) {
throw new Error("Bad response - no snapshots found!");
}
return payload;
}
private async initChart() { private async initChart() {
const canvasElement = document.getElementById(this.canvasId); const canvasElement = document.getElementById(this.domId);
let ctx: CanvasRenderingContext2D; let ctx: CanvasRenderingContext2D;
if (ClimateChart.isCanvas(canvasElement)) { if (ClimateChart.isCanvas(canvasElement)) {
ctx = canvasElement.getContext('2d'); ctx = canvasElement.getContext('2d');
@@ -59,8 +57,8 @@ class ClimateChart {
const payload = await this.getInitialDataBlob(); const payload = await this.getInitialDataBlob();
this.latestSnapshot = payload.snapshots[0]; this.latestSnapshot = payload.snapshots[0];
this.insertSnapshots(...payload.snapshots); this.insertSnapshots(...payload.snapshots);
this.rerender();
setInterval(async () => this.updateFromServer().catch(e => this.logError(e)), 30 * 1000); setInterval(async () => this.updateFromServer().catch(e => this.logError(e)), 30 * 1000);
this.chart.update();
this.onLoadedCallback(); this.onLoadedCallback();
} }
catch (e) { catch (e) {
@@ -68,18 +66,29 @@ class ClimateChart {
} }
} }
private async getInitialDataBlob(): Promise<SnapshotRecords> {
const minutesAsDate = (new Date().getTime() - this.minutesDisplayed * 60000);
const dataEndpoint = `${ this.dataEndpointBase }?since=${ new Date(minutesAsDate).toISOString() }`;
const payload = await (await fetch(dataEndpoint)).json();
if (payload.snapshots.length < 0) {
throw new Error("Bad response - no snapshots found!");
}
return payload;
}
private async updateFromServer() { private async updateFromServer() {
const lastTimeInChart = (new Date(this.latestSnapshot.time)).toISOString(); const lastTimeInChart = this.latestSnapshot.time;
const url = "data?since=" + lastTimeInChart; const url = `${ this.dataEndpointBase }?since=${ new Date(this.latestSnapshot.time + "+00:00").toISOString() }`;
try { try {
const payload: SnapshotRecords = await (await fetch(url)).json(); const payload: SnapshotRecords = await (await fetch(url)).json();
if (payload.snapshots.length > 0) { if (payload.snapshots.length > 0) {
const latestSnapshotIsNew = new Date(payload.snapshots[0].time).getTime() > new Date(lastTimeInChart).getTime(); const newLatestTime = new Date(payload.snapshots[0].time).getTime();
if (latestSnapshotIsNew) { if (newLatestTime > new Date(lastTimeInChart).getTime()) {
this.removeExpiredPointsAfter(lastTimeInChart); console.log(payload);
this.removePointsOlderThan(newLatestTime - this.minutesDisplayed * 60000);
this.latestSnapshot = payload.snapshots[0]; this.latestSnapshot = payload.snapshots[0];
this.insertSnapshots(...payload.snapshots); this.insertSnapshots(...payload.snapshots);
this.chart.update(); this.rerender();
} }
} }
} }
@@ -88,6 +97,10 @@ class ClimateChart {
} }
} }
private rerender() {
this.chart.update();
}
private insertSnapshots(...snapshots: Snapshot[]) { private insertSnapshots(...snapshots: Snapshot[]) {
for (const snapshot of snapshots.reverse()) { for (const snapshot of snapshots.reverse()) {
this.humidityPointList().push({x: snapshot.time, y: snapshot.humidity}); this.humidityPointList().push({x: snapshot.time, y: snapshot.humidity});
@@ -96,11 +109,10 @@ class ClimateChart {
} }
} }
private removeExpiredPointsAfter(referenceTime: string) { private removePointsOlderThan(referenceTime: number) {
for (let i = 0; i < this.humidityPointList().length; i++) { for (let i = 0; i < this.humidityPointList().length; i++) {
const timeOnPoint = this.humidityPointList()[i].x; const timeOnPoint = this.humidityPointList()[i].x;
const timeElapsedSinceReference = Date.parse(referenceTime) - Date.parse(timeOnPoint); if (new Date(timeOnPoint).getTime() < referenceTime) {
if (timeElapsedSinceReference > this.minutesDisplayed * 60000) {
this.humidityPointList().splice(i, 1); this.humidityPointList().splice(i, 1);
this.tempPointList().splice(i, 1); this.tempPointList().splice(i, 1);
this.co2PointList().splice(i, 1); this.co2PointList().splice(i, 1);
@@ -119,7 +131,7 @@ class ClimateChart {
} }
private co2PointList(): ClimatePoint[] { private co2PointList(): ClimatePoint[] {
return this.chart.data.datasets[1].data as ClimatePoint[]; return this.chart.data.datasets[2].data as ClimatePoint[];
} }
onLoaded(callback: () => void) { onLoaded(callback: () => void) {

View File

@@ -11,9 +11,9 @@ interface ClimateChartSettings {
} }
} }
const defaultHumidityColor = 'rgb(45,141,45)'; const defaultHumidityColor = 'rgb(196,107,107)';
const defaultTempColor = 'rgb(0,134,222)'; const defaultTempColor = 'rgb(173,136,68)';
const defaultCo2Color = 'rgb(194,30,30)'; const defaultCo2Color = 'rgb(52,133,141)';
export function generateClimateChartConfig(settings: ClimateChartSettings): ChartConfiguration { export function generateClimateChartConfig(settings: ClimateChartSettings): ChartConfiguration {
return { return {
@@ -43,6 +43,11 @@ export function generateClimateChartConfig(settings: ClimateChartSettings): Char
title: { title: {
display: true, display: true,
text: 'Ledda\'s Room Climate', text: 'Ledda\'s Room Climate',
fontSize: 50,
},
legend: {
position: "top",
align: "end",
}, },
scales: { scales: {
xAxes: [{ xAxes: [{

View File

@@ -0,0 +1,3 @@
{
"development": false
}

View File

@@ -1,4 +1,6 @@
import ClimateChart from "./ClimateChart"; import ClimateChart from "./ClimateChart";
import config from "./config.json";
export {config};
const CHART_DOM_ID: string = "myChart"; const CHART_DOM_ID: string = "myChart";
let rootUrl: string = ""; let rootUrl: string = "";
@@ -16,7 +18,7 @@ function createClimateChart() {
minutesDisplayed = parsedMins; minutesDisplayed = parsedMins;
} }
} }
return new ClimateChart(rootUrl, CHART_DOM_ID, minutesDisplayed); return new ClimateChart(CHART_DOM_ID, minutesDisplayed);
} }
const overlay = document.createElement('div'); const overlay = document.createElement('div');
@@ -33,7 +35,7 @@ document.onreadystatechange = (e) => {
}); });
climateChart.onErrored((e) => { climateChart.onErrored((e) => {
overlay.classList.remove('hidden'); overlay.classList.remove('hidden');
textContainer.innerText = `An error occurred: ${e}\nTry restarting the page.`; textContainer.innerText = `An error occurred: ${e}\nTry reloading the page.`;
}); });
document.onreadystatechange = () => {}; document.onreadystatechange = () => {};
}; };

View File

@@ -6,5 +6,6 @@
"target": "es5", "target": "es5",
"allowJs": true, "allowJs": true,
"moduleResolution": "Node", "moduleResolution": "Node",
"resolveJsonModule": true
} }
} }

View File

@@ -1,34 +1,11 @@
const path = require('path'); const path = require('path');
const webpack = require('webpack'); const webpack = require('webpack');
const config = require('./src/config.json');
/*
* SplitChunksPlugin is enabled by default and replaced
* deprecated CommonsChunkPlugin. It automatically identifies modules which
* should be splitted of chunk by heuristics using module duplication count and
* module category (i. e. node_modules). And splits the chunks…
*
* It is safe to remove "splitChunks" from the generated configuration
* and was added as an educational example.
*
* https://webpack.js.org/plugins/split-chunks-plugin/
*
*/
/*
* We've enabled TerserPlugin for you! This minifies your app
* in order to load faster and run less javascript.
*
* https://github.com/webpack-contrib/terser-webpack-plugin
*
*/
const TerserPlugin = require('terser-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
module.exports = { const webpackConfig = {
mode: 'production', mode: 'development',
entry: './src/main.ts', entry: './src/main.ts',
plugins: [new webpack.ProgressPlugin()], plugins: [new webpack.ProgressPlugin()],
@@ -45,7 +22,6 @@ module.exports = {
loader: "style-loader" loader: "style-loader"
}, { }, {
loader: "css-loader", loader: "css-loader",
options: { options: {
sourceMap: true sourceMap: true
} }
@@ -57,7 +33,17 @@ module.exports = {
extensions: ['.tsx', '.ts', '.js'] extensions: ['.tsx', '.ts', '.js']
}, },
optimization: { devServer: {
contentBase: path.join(__dirname, "dist/"),
contentBasePublicPath: "/",
port: 3000,
publicPath: "http://localhost:3000/",
hotOnly: true
},
};
if (!config.development) {
webpackConfig.optimization = {
minimizer: [new TerserPlugin()], minimizer: [new TerserPlugin()],
splitChunks: { splitChunks: {
@@ -73,13 +59,8 @@ module.exports = {
minSize: 30000, minSize: 30000,
name: false name: false
} }
}, };
webpackConfig.mode = 'production';
devServer: {
contentBase: path.join(__dirname, "dist/"),
contentBasePublicPath: "/static/",
port: 3000,
publicPath: "http://localhost:3000/",
hotOnly: true
},
} }
module.exports = {...webpackConfig};