From 942db5c18f764c2cd9447e77bbb6784606526c0e Mon Sep 17 00:00:00 2001 From: Daniel Ledda Date: Tue, 23 Mar 2021 10:41:17 +0100 Subject: [PATCH] fixed blurry graphics, improvements to loading and caching --- app-dist/static/styles.css | 4 -- dashboard/src/ClimateChart.ts | 66 +++++++++---------- dashboard/src/StateStore.ts | 4 +- dashboard/src/Timeseries.ts | 34 ++++++---- dashboard/src/main.ts | 20 ++++-- dashboard/src/{ => ui-components}/AppUI.ts | 0 .../{ => ui-components}/ClimateChartWidget.ts | 4 +- .../{ => ui-components}/DisplayModeWidget.tsx | 4 +- .../src/{ => ui-components}/GridWidget.ts | 0 .../src/{ => ui-components}/MessageOverlay.ts | 2 +- .../SelectDisplayModeWidget.tsx | 4 +- .../src/{ => ui-components}/TimerWidget.tsx | 6 +- .../{ => ui-components}/TimezoneWidget.tsx | 4 +- .../src/{ => ui-components}/UIComponent.ts | 0 14 files changed, 86 insertions(+), 66 deletions(-) rename dashboard/src/{ => ui-components}/AppUI.ts (100%) rename dashboard/src/{ => ui-components}/ClimateChartWidget.ts (95%) rename dashboard/src/{ => ui-components}/DisplayModeWidget.tsx (97%) rename dashboard/src/{ => ui-components}/GridWidget.ts (100%) rename dashboard/src/{ => ui-components}/MessageOverlay.ts (95%) rename dashboard/src/{ => ui-components}/SelectDisplayModeWidget.tsx (95%) rename dashboard/src/{ => ui-components}/TimerWidget.tsx (91%) rename dashboard/src/{ => ui-components}/TimezoneWidget.tsx (94%) rename dashboard/src/{ => ui-components}/UIComponent.ts (100%) diff --git a/app-dist/static/styles.css b/app-dist/static/styles.css index 1199edf..15a0c55 100644 --- a/app-dist/static/styles.css +++ b/app-dist/static/styles.css @@ -138,8 +138,4 @@ h1 { } .chart-canvas { - width: 100%; - height: 100%; - margin: 0; - padding: 0; } \ No newline at end of file diff --git a/dashboard/src/ClimateChart.ts b/dashboard/src/ClimateChart.ts index 41150da..16fe34e 100644 --- a/dashboard/src/ClimateChart.ts +++ b/dashboard/src/ClimateChart.ts @@ -12,14 +12,18 @@ export default class ClimateChart { constructor(context: CanvasRenderingContext2D) { this.ctx = context; this.ctx.fillStyle = "rgb(255,255,255)"; - this.width = this.ctx.canvas.width; - this.height = this.ctx.canvas.height; + this.updateDimensions(); this.ctx.fillRect(0, 0, this.width, this.height); this.ctx.fill(); this.ctx.translate(0.5, 0.5); this.ctx.canvas.onmousemove = (e) => this.handleMouseMove(e); } + private updateDimensions() { + this.width = Number(getComputedStyle(this.ctx.canvas).width.slice(0, -2)); + this.height = Number(getComputedStyle(this.ctx.canvas).height.slice(0, -2)); + } + addTimeseries(timeseries: Timeseries) { this.timeseries.push(timeseries); } @@ -39,8 +43,7 @@ export default class ClimateChart { } render() { - this.width = this.ctx.canvas.width; - this.height = this.ctx.canvas.height; + this.updateDimensions(); this.ctx.fillStyle = "rgb(255,255,255)"; this.ctx.fillRect(0, 0, this.width, this.height); this.ctx.fill(); @@ -55,24 +58,18 @@ export default class ClimateChart { private renderScale() { this.ctx.strokeStyle = "rgb(230,230,230)"; this.ctx.fillStyle = "black"; - this.ctx.beginPath(); - const bottom = this.getY(this.valRange.low); - this.ctx.moveTo(40, bottom); - this.ctx.lineTo(this.width, bottom); - this.ctx.fillText(this.valRange.low.toString(), 0, bottom + 4); - const top = this.getY(this.valRange.high); - this.ctx.moveTo(40, top); - this.ctx.lineTo(this.width, top); - this.ctx.fillText(this.valRange.high.toString(), 0, top + 4); const ticks = 20; - const tickHeight = this.height / ticks; - for (let i = 1; i < ticks; i++) { - const pos = Math.floor(tickHeight * i); + const tickHeight = (this.valRange.high - this.valRange.low) / ticks; + let currentTick = this.valRange.low; + for (let i = 0; i < ticks; i++) { + currentTick += tickHeight; + const pos = Math.round(this.getY(currentTick)); + this.ctx.beginPath(); this.ctx.moveTo(40, pos); this.ctx.lineTo(this.width, pos); - this.ctx.fillText(this.getValue(pos).toFixed(2), 0, pos + 4); + this.ctx.stroke(); + this.ctx.fillText(currentTick.toFixed(2), 0, pos + 4); } - this.ctx.stroke(); } private setDisplayRange() { @@ -119,19 +116,19 @@ export default class ClimateChart { this.formatTimestamp = formatter; } - private getX(index: number) { + getX(index: number) { return (index - this.indexRange.start) / (this.indexRange.stop - this.indexRange.start) * this.width; } - private getY(value: number) { + getY(value: number) { return this.height - (value - this.valRange.low) / (this.valRange.high - this.valRange.low) * this.height; } - private getIndex(x: number) { + getIndex(x: number) { return (x / this.width) * this.indexRange.stop; } - private getValue(y: number) { + getValue(y: number) { return ((this.height - y) / this.height) * this.valRange.high; } @@ -139,19 +136,22 @@ export default class ClimateChart { private renderTimeseries(timeseries: Timeseries) { const timeseriesPoints = timeseries.cachedBetween(this.indexRange.start, this.indexRange.stop); this.ctx.strokeStyle = timeseries.getColour(); + const drawBubbles = this.getX(timeseriesPoints[3]) - this.getX(timeseriesPoints[1]) > 6; let y = this.getY(timeseriesPoints[0]); let x = this.getX(timeseriesPoints[1]); - this.ctx.moveTo(Math.floor(x), Math.floor(y)); - this.ctx.beginPath(); - this.ctx.lineTo(Math.floor(x), Math.floor(y)); - this.ctx.ellipse(x, y, 3, 3, 0, 0, 2 * Math.PI); - for (let i = 2; i < timeseriesPoints.length; i += 2) { + for (let i = 0; i < timeseriesPoints.length; i += 2) { + this.ctx.beginPath(); + this.ctx.moveTo(Math.round(x), Math.round(y)); y = this.getY(timeseriesPoints[i]); x = this.getX(timeseriesPoints[i + 1]); - this.ctx.lineTo(Math.floor(x), Math.floor(y)); - this.ctx.ellipse(x, y, 3, 3, 0, 0, 2 * Math.PI); + this.ctx.lineTo(Math.round(x), Math.round(y)); + this.ctx.stroke(); + if (drawBubbles) { + this.ctx.beginPath(); + this.ctx.ellipse(x, y, 2, 2, 0, 0, 2 * Math.PI); + this.ctx.stroke(); + } } - this.ctx.stroke(); } private renderTooltip(text: string, x: number, y: number) { @@ -171,9 +171,9 @@ export default class ClimateChart { } this.ctx.fillStyle = "rgb(255,255,255)"; this.ctx.strokeStyle = "rgb(0,0,0)"; - this.ctx.fillRect(x, y, width, height); - this.ctx.strokeRect(x, y, width, height); + this.ctx.fillRect(Math.round(x), Math.round(y), Math.round(width), Math.round(height)); + this.ctx.strokeRect(Math.round(x), Math.round(y), Math.round(width), Math.round(height)); this.ctx.fillStyle = "rgb(0,0,0)"; - this.ctx.fillText(text, x + 5, y + textHeight + 5); + this.ctx.fillText(text, Math.round(x + 5), Math.round(y + textHeight + 5)); } } diff --git a/dashboard/src/StateStore.ts b/dashboard/src/StateStore.ts index 4c366f7..dc94b24 100644 --- a/dashboard/src/StateStore.ts +++ b/dashboard/src/StateStore.ts @@ -90,6 +90,7 @@ class AppStateStore { stop = this.state.lastUpdateTime; } this.addLoad(); + console.log(start, stop); for (const timeseries of this.state.timeseries) { await timeseries.updateFromWindow(start, stop); } @@ -101,6 +102,7 @@ class AppStateStore { } private async getNewTimeseriesData() { + const updateTime = new Date().getTime() / 1000; this.addLoad(); for (const timeseries of this.state.timeseries) { await timeseries.getLatest(); @@ -110,7 +112,7 @@ class AppStateStore { this.notifyStoreVal("timeseries"); this.eventCallbacks["timeseriesUpdated"].forEach(cb => cb(timeseries)); } - this.setLastUpdateTime(new Date().getTime() / 1000); + this.setLastUpdateTime(updateTime); } getState(): AppState { diff --git a/dashboard/src/Timeseries.ts b/dashboard/src/Timeseries.ts index 7a79b9a..b65181c 100644 --- a/dashboard/src/Timeseries.ts +++ b/dashboard/src/Timeseries.ts @@ -20,11 +20,13 @@ class Timeseries { maxIndex: -Infinity, }; private colour: string; + private tolerance: number; - constructor(name: string, loader: TimeseriesLoader) { + constructor(name: string, loader: TimeseriesLoader, tolerance?: number) { this.cache = new Int32Array(); this.loader = loader; this.name = name; + this.tolerance = tolerance ?? 0; this.colour = `rgb(${Math.random() * 255},${Math.random() * 255},${Math.random() * 255})`; } @@ -70,12 +72,14 @@ class Timeseries { if (this.cache.length === 0) { this.fetching = true; await this.fullFetch(start, stop); - } else if (this.cache[1] > start) { + } + if (this.cache[1] > start + this.tolerance) { this.fetching = true; - await this.fetchPrior(start); - } else if (this.cache[this.currentEndPointer - 1] < stop) { + await this.fetchPrior(this.cache[1], start); + } + if (this.cache[this.currentEndPointer - 1] < stop - this.tolerance) { this.fetching = true; - await this.fetchAnterior(stop); + await this.fetchAfter(this.cache[this.currentEndPointer - 1], stop); } } catch (e) { throw new Error(`Error fetching timeseries data: ${e}`); @@ -87,7 +91,7 @@ class Timeseries { async getLatest() { this.fetching = true; try { - await this.fetchAnterior(this.cache[this.currentEndPointer - 1]); + await this.fetchAfter(this.cache[this.currentEndPointer - 1]); } catch (e) { throw new Error(`Error fetching timeseries data: ${e}`); } @@ -104,10 +108,13 @@ class Timeseries { } } - private async fetchAnterior(after: number) { + private async fetchAfter(after: number, atLeastUntil?: number) { try { - const doubleTimespan = 2 * (this.cache[this.currentEndPointer - 1] - this.cache[1]); - const result = await this.loader(after, after + doubleTimespan); + let forwardDist = 2 * (this.cache[this.currentEndPointer - 1] - this.cache[1]); + if (atLeastUntil && (atLeastUntil > after + forwardDist)) { + forwardDist = atLeastUntil - after; + } + const result = await this.loader(after, after + forwardDist); const newCache = new Int32Array(this.cache.length + result.length); newCache.set(this.cache, 0); newCache.set(result, this.currentEndPointer); @@ -119,10 +126,13 @@ class Timeseries { } } - private async fetchPrior(before: number) { + private async fetchPrior(priorTo: number, atLeastUntil?: number) { try { - const doubleTimespan = 2 * (this.cache[this.currentEndPointer - 1] - this.cache[1]); - const result = await this.loader(before - doubleTimespan, before); + let backDist = 2 * (this.cache[this.currentEndPointer - 1] - this.cache[1]); + if (atLeastUntil < priorTo - backDist) { + backDist = priorTo - atLeastUntil; + } + const result = await this.loader(priorTo - backDist, priorTo); const newCache = new Int32Array(this.cache.length + result.length); newCache.set(result, 0); newCache.set(this.cache, result.length); diff --git a/dashboard/src/main.ts b/dashboard/src/main.ts index 9b1bbb4..6889c62 100644 --- a/dashboard/src/main.ts +++ b/dashboard/src/main.ts @@ -1,6 +1,6 @@ import config from "./config.json"; import {AppStore, getAppState, initStore} from "./StateStore"; -import AppUI from "./AppUI"; +import AppUI from "./ui-components/AppUI"; import Timeseries from "./Timeseries"; import {ClayPIDashboardError} from "./errors"; export {config}; @@ -37,9 +37,21 @@ async function init() { documentReady: false, timeseries: [], }); - AppStore().addTimeseries(new Timeseries("temp", (start, stop) => loadClimateTimeseriesData("temp", start, stop))); - AppStore().addTimeseries(new Timeseries("humidity", (start, stop) => loadClimateTimeseriesData("humidity", start, stop))); - AppStore().addTimeseries(new Timeseries("co2", (start, stop) => loadClimateTimeseriesData("co2", start, stop))); + AppStore().addTimeseries(new Timeseries( + "temp", + (start, stop) => loadClimateTimeseriesData("temp", start, stop), + getAppState().updateIntervalSeconds + )); + AppStore().addTimeseries(new Timeseries( + "humidity", + (start, stop) => loadClimateTimeseriesData("humidity", start, stop), + getAppState().updateIntervalSeconds + )); + AppStore().addTimeseries(new Timeseries( + "co2", + (start, stop) => loadClimateTimeseriesData("co2", start, stop), + getAppState().updateIntervalSeconds + )); const ui = new AppUI(); ui.bootstrap("root"); } diff --git a/dashboard/src/AppUI.ts b/dashboard/src/ui-components/AppUI.ts similarity index 100% rename from dashboard/src/AppUI.ts rename to dashboard/src/ui-components/AppUI.ts diff --git a/dashboard/src/ClimateChartWidget.ts b/dashboard/src/ui-components/ClimateChartWidget.ts similarity index 95% rename from dashboard/src/ClimateChartWidget.ts rename to dashboard/src/ui-components/ClimateChartWidget.ts index 2de1e65..320f988 100644 --- a/dashboard/src/ClimateChartWidget.ts +++ b/dashboard/src/ui-components/ClimateChartWidget.ts @@ -1,7 +1,7 @@ -import {AppStore, DisplayMode, getAppState, TimeWindow} from "./StateStore"; +import {AppStore, DisplayMode, getAppState} from "../StateStore"; import GridWidget, {GridProps} from "./GridWidget"; import UIComponent from "./UIComponent"; -import ClimateChart from "./ClimateChart"; +import ClimateChart from "../ClimateChart"; class ClimateChartWidget extends UIComponent { private readonly skeleton: GridWidget; diff --git a/dashboard/src/DisplayModeWidget.tsx b/dashboard/src/ui-components/DisplayModeWidget.tsx similarity index 97% rename from dashboard/src/DisplayModeWidget.tsx rename to dashboard/src/ui-components/DisplayModeWidget.tsx index 288dde5..b158577 100644 --- a/dashboard/src/DisplayModeWidget.tsx +++ b/dashboard/src/ui-components/DisplayModeWidget.tsx @@ -1,6 +1,6 @@ import GridWidget, {GridProps} from "./GridWidget"; -import {AppStore, DisplayMode, getAppState} from "./StateStore"; -import * as JSX from "./JSXFactory"; +import {AppStore, DisplayMode, getAppState} from "../StateStore"; +import * as JSX from "../JSXFactory"; import UIComponent from "./UIComponent"; class DisplayModeWidget extends UIComponent { diff --git a/dashboard/src/GridWidget.ts b/dashboard/src/ui-components/GridWidget.ts similarity index 100% rename from dashboard/src/GridWidget.ts rename to dashboard/src/ui-components/GridWidget.ts diff --git a/dashboard/src/MessageOverlay.ts b/dashboard/src/ui-components/MessageOverlay.ts similarity index 95% rename from dashboard/src/MessageOverlay.ts rename to dashboard/src/ui-components/MessageOverlay.ts index 8489321..48a7952 100644 --- a/dashboard/src/MessageOverlay.ts +++ b/dashboard/src/ui-components/MessageOverlay.ts @@ -1,4 +1,4 @@ -import {AppStore, getAppState} from "./StateStore"; +import {AppStore, getAppState} from "../StateStore"; import UIComponent from "./UIComponent"; class MessageOverlay extends UIComponent { diff --git a/dashboard/src/SelectDisplayModeWidget.tsx b/dashboard/src/ui-components/SelectDisplayModeWidget.tsx similarity index 95% rename from dashboard/src/SelectDisplayModeWidget.tsx rename to dashboard/src/ui-components/SelectDisplayModeWidget.tsx index 47610da..8186c91 100644 --- a/dashboard/src/SelectDisplayModeWidget.tsx +++ b/dashboard/src/ui-components/SelectDisplayModeWidget.tsx @@ -1,7 +1,7 @@ import UIComponent from "./UIComponent"; -import * as JSX from "./JSXFactory"; +import * as JSX from "../JSXFactory"; import GridWidget, {GridProps} from "./GridWidget"; -import {AppStore, DisplayMode, getAppState} from "./StateStore"; +import {AppStore, DisplayMode, getAppState} from "../StateStore"; export default class SelectDisplayModeWidget extends UIComponent { private mainBody: HTMLElement; diff --git a/dashboard/src/TimerWidget.tsx b/dashboard/src/ui-components/TimerWidget.tsx similarity index 91% rename from dashboard/src/TimerWidget.tsx rename to dashboard/src/ui-components/TimerWidget.tsx index ac448d6..fce6156 100644 --- a/dashboard/src/TimerWidget.tsx +++ b/dashboard/src/ui-components/TimerWidget.tsx @@ -1,7 +1,7 @@ -import {AppStore, getAppState} from "./StateStore"; +import {AppStore, getAppState} from "../StateStore"; import GridWidget, {GridProps} from "./GridWidget"; import UIComponent from "./UIComponent"; -import * as JSX from "./JSXFactory"; +import * as JSX from "../JSXFactory"; class TimerWidget extends UIComponent { private readonly display: HTMLElement; @@ -26,7 +26,7 @@ class TimerWidget extends UIComponent { private resetTimer() { this.nextUpdateTime = getAppState().lastUpdateTime + getAppState().updateIntervalSeconds; - this.fromRef(this.lastUpdateRef).innerText = new Date(getAppState().lastUpdateTime).toLocaleString(); + this.fromRef(this.lastUpdateRef).innerText = new Date(getAppState().lastUpdateTime * 1000).toLocaleString(); this.refreshTimer(); } diff --git a/dashboard/src/TimezoneWidget.tsx b/dashboard/src/ui-components/TimezoneWidget.tsx similarity index 94% rename from dashboard/src/TimezoneWidget.tsx rename to dashboard/src/ui-components/TimezoneWidget.tsx index a96ea7e..1602a02 100644 --- a/dashboard/src/TimezoneWidget.tsx +++ b/dashboard/src/ui-components/TimezoneWidget.tsx @@ -1,7 +1,7 @@ import GridWidget, {GridProps} from "./GridWidget"; -import {AppStore, getAppState} from "./StateStore"; +import {AppStore, getAppState} from "../StateStore"; import UIComponent from "./UIComponent"; -import * as JSX from "./JSXFactory"; +import * as JSX from "../JSXFactory"; class TimezoneWidget extends UIComponent { private skeleton: GridWidget; diff --git a/dashboard/src/UIComponent.ts b/dashboard/src/ui-components/UIComponent.ts similarity index 100% rename from dashboard/src/UIComponent.ts rename to dashboard/src/ui-components/UIComponent.ts