oops
This commit is contained in:
8
webapp/dist/styles.css
vendored
8
webapp/dist/styles.css
vendored
@@ -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;
|
||||||
|
}
|
||||||
10
webapp/package-lock.json
generated
10
webapp/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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: [{
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"development": false
|
||||||
|
}
|
||||||
@@ -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 = () => {};
|
||||||
};
|
};
|
||||||
@@ -6,5 +6,6 @@
|
|||||||
"target": "es5",
|
"target": "es5",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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};
|
||||||
Reference in New Issue
Block a user