Moved the webapp to a webpack with some typescript, updated server wtih new endpoints

This commit is contained in:
Daniel Ledda
2020-11-14 01:06:29 +01:00
parent 6dba158ff0
commit 4e57a8eb47
21 changed files with 11118 additions and 20964 deletions

110
webapp/src/ClimateChart.ts Normal file
View File

@@ -0,0 +1,110 @@
import Chart, {ChartPoint} from "chart.js";
import {generateClimateChartConfig} from "./climateChartConfig";
interface Snapshot {
id: number,
temp: number,
humidity: number,
co2: number,
time: string,
}
interface SnapshotRecords {
snapshots: Snapshot[]
}
class ClimateChart {
private readonly urlEndpoint: string;
private chart: Chart | null;
private latestSnapshot: Snapshot | null;
private onLoadedCallback: () => void = () => {};
constructor(
private readonly rootUrl: string,
private readonly canvasId: string,
private readonly minutesDisplayed: number = 60
) {
if (minutesDisplayed > 0 && Math.floor(minutesDisplayed) == minutesDisplayed) {
this.minutesDisplayed = minutesDisplayed;
} else {
throw new Error(`invalid minutes passed to display in chart: ${minutesDisplayed}`);
}
this.urlEndpoint = this.rootUrl + "data/";
this.urlEndpoint += "since/" + this.minutesDisplayed;
this.initChart()
}
onLoaded(callback: () => void) {
this.onLoadedCallback = callback;
}
private static isCanvas(el: HTMLElement): el is HTMLCanvasElement {
return el.tagName === "canvas";
}
private async getInitialDataBlob(): Promise<SnapshotRecords> {
const data = await fetch(this.urlEndpoint);
return await data.json();
}
private async initChart() {
const canvasElement = document.getElementById(this.canvasId);
let ctx: CanvasRenderingContext2D;
if (ClimateChart.isCanvas(canvasElement)) {
ctx = canvasElement.getContext('2d');
} else {
throw new Error(`improper HTML element passed, needed type canvas, got ${canvasElement.tagName}`);
}
const payload = await this.getInitialDataBlob();
this.latestSnapshot = payload.snapshots[payload.snapshots.length - 1];
this.chart = new Chart(ctx, generateClimateChartConfig(this.jsonToChartPoints(payload)));
setInterval(() => this.updateFromServer(), 30 * 1000);
this.onLoadedCallback();
}
private jsonToChartPoints(json: SnapshotRecords): {humidity: ChartPoint[], temp: ChartPoint[], co2: ChartPoint[]} {
const humidity = [];
const co2 = [];
const temp = [];
for (let i = 0; i < json.snapshots.length; i++) {
const snapshot = json.snapshots[json.snapshots.length - i - 1];
co2.push({x: snapshot.time, y: snapshot.co2});
temp.push({x: snapshot.time, y: snapshot.temp});
humidity.push({x: snapshot.time, y: snapshot.humidity});
}
return {humidity, co2, temp};
}
private async updateFromServer() {
const currentTime = (new Date(this.latestSnapshot.time)).toISOString();
const url = "/" + this.rootUrl + "data?since=" + currentTime;
const payload: SnapshotRecords = await (await fetch(url)).json();
if (payload.snapshots.length > 0) {
this.latestSnapshot = payload.snapshots[payload.snapshots.length - 1];
this.removeExpiredData(currentTime);
this.insertSnapshots(...payload.snapshots);
this.chart.update();
}
}
private insertSnapshots(...snapshots: Snapshot[]) {
for (const snapshot of snapshots) {
this.chart.data.datasets[0].data.push(snapshot.humidity);
this.chart.data.datasets[1].data.push(snapshot.temp);
this.chart.data.datasets[2].data.push(snapshot.co2);
}
}
private removeExpiredData(latestTime: string) {
for (let i = 0; i < this.chart.data.datasets[0].data.length; i++) {
const timeOnPoint = (this.chart.data.datasets[0].data[i] as ChartPoint).x as string;
const timeElapsedSincePoint = Date.parse(latestTime) - Date.parse(timeOnPoint);
if (timeElapsedSincePoint > this.minutesDisplayed * 60000) {
this.chart.data.datasets[0].data.splice(i, 1);
this.chart.data.datasets[1].data.splice(i, 1);
this.chart.data.datasets[2].data.splice(i, 1);
}
}
}
}
export default ClimateChart;

View File

@@ -0,0 +1,94 @@
import {ChartConfiguration, ChartPoint, TimeUnit} from "chart.js";
interface ClimateChartSettings {
humidity: ChartPoint[];
temp: ChartPoint[];
co2: ChartPoint[];
colors?: {
humidity?: string;
temp?: string;
co2?: string;
}
}
const defaultHumidityColor = 'rgb(45,141,45)';
const defaultTempColor = 'rgb(0,134,222)';
const defaultCo2Color = 'rgb(194,30,30)';
export function generateClimateChartConfig(settings: ClimateChartSettings): ChartConfiguration {
return {
type: 'line',
data: {
datasets: [{
label: 'Humidity',
data: settings.humidity,
borderColor: settings.colors?.humidity ?? defaultHumidityColor,
fill: false,
yAxisID: 'y-axis-3',
}, {
label: 'Temperature',
data: settings.temp,
borderColor: settings.colors?.temp ?? defaultTempColor,
fill: false,
yAxisID: 'y-axis-2',
}, {
label: 'Co2 Concentration',
data: settings.co2,
borderColor: settings.colors?.co2 ?? defaultCo2Color,
fill: false,
yAxisID: 'y-axis-1',
}]
},
options: {
title: {
display: true,
text: 'Ledda\'s Room Climate',
},
scales: {
xAxes: [{
type: 'time',
time: {
unit: 'second' as TimeUnit
}
}],
yAxes: [{
type: 'linear',
display: true,
position: 'right',
id: 'y-axis-1',
ticks: {
fontColor: settings.colors?.co2 ?? defaultCo2Color,
suggestedMin: 400,
suggestedMax: 1100,
},
}, {
type: 'linear',
display: true,
position: 'left',
id: 'y-axis-2',
ticks: {
fontColor: settings.colors?.temp ?? defaultTempColor,
suggestedMin: 10,
suggestedMax: 35,
},
gridLines: {
drawOnChartArea: false,
},
}, {
type: 'linear',
display: true,
position: 'left',
id: 'y-axis-3',
ticks: {
fontColor: settings.colors?.humidity ?? defaultHumidityColor,
suggestedMin: 15,
suggestedMax: 85,
},
gridLines: {
drawOnChartArea: false,
},
}],
}
},
}
}

27
webapp/src/main.ts Normal file
View File

@@ -0,0 +1,27 @@
import ClimateChart from "./ClimateChart";
const ROOT_URL: string = "climate/";
const CHART_DOM_ID: string = "myChart";
function createClimateChart() {
const pathname = window.location.pathname;
let minutesDisplayed = 60;
const argsStart = pathname.search(/\?minute-span=/);
if (argsStart !== -1) {
const parsedMins = Number(pathname[12]);
if (!isNaN(parsedMins) && parsedMins > 0) {
minutesDisplayed = parsedMins;
}
}
return new ClimateChart(ROOT_URL, CHART_DOM_ID, minutesDisplayed);
}
const overlay = document.createElement('div');
overlay.innerText = 'Loading data...';
overlay.className = 'overlay';
document.getRootNode().appendChild(overlay);
const climateChart = createClimateChart();
climateChart.onLoaded(() => {
overlay.classList.add('hidden');
})