fixed blurry graphics, improvements to loading and caching

This commit is contained in:
Daniel Ledda
2021-03-23 10:41:17 +01:00
parent a810c5c71d
commit 942db5c18f
14 changed files with 86 additions and 66 deletions

View File

@@ -138,8 +138,4 @@ h1 {
} }
.chart-canvas { .chart-canvas {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
} }

View File

@@ -12,14 +12,18 @@ export default class ClimateChart {
constructor(context: CanvasRenderingContext2D) { constructor(context: CanvasRenderingContext2D) {
this.ctx = context; this.ctx = context;
this.ctx.fillStyle = "rgb(255,255,255)"; this.ctx.fillStyle = "rgb(255,255,255)";
this.width = this.ctx.canvas.width; this.updateDimensions();
this.height = this.ctx.canvas.height;
this.ctx.fillRect(0, 0, this.width, this.height); this.ctx.fillRect(0, 0, this.width, this.height);
this.ctx.fill(); this.ctx.fill();
this.ctx.translate(0.5, 0.5); this.ctx.translate(0.5, 0.5);
this.ctx.canvas.onmousemove = (e) => this.handleMouseMove(e); 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) { addTimeseries(timeseries: Timeseries) {
this.timeseries.push(timeseries); this.timeseries.push(timeseries);
} }
@@ -39,8 +43,7 @@ export default class ClimateChart {
} }
render() { render() {
this.width = this.ctx.canvas.width; this.updateDimensions();
this.height = this.ctx.canvas.height;
this.ctx.fillStyle = "rgb(255,255,255)"; this.ctx.fillStyle = "rgb(255,255,255)";
this.ctx.fillRect(0, 0, this.width, this.height); this.ctx.fillRect(0, 0, this.width, this.height);
this.ctx.fill(); this.ctx.fill();
@@ -55,24 +58,18 @@ export default class ClimateChart {
private renderScale() { private renderScale() {
this.ctx.strokeStyle = "rgb(230,230,230)"; this.ctx.strokeStyle = "rgb(230,230,230)";
this.ctx.fillStyle = "black"; 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 ticks = 20;
const tickHeight = this.height / ticks; const tickHeight = (this.valRange.high - this.valRange.low) / ticks;
for (let i = 1; i < ticks; i++) { let currentTick = this.valRange.low;
const pos = Math.floor(tickHeight * i); 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.moveTo(40, pos);
this.ctx.lineTo(this.width, pos); this.ctx.lineTo(this.width, pos);
this.ctx.fillText(this.getValue(pos).toFixed(2), 0, pos + 4);
}
this.ctx.stroke(); this.ctx.stroke();
this.ctx.fillText(currentTick.toFixed(2), 0, pos + 4);
}
} }
private setDisplayRange() { private setDisplayRange() {
@@ -119,19 +116,19 @@ export default class ClimateChart {
this.formatTimestamp = formatter; this.formatTimestamp = formatter;
} }
private getX(index: number) { getX(index: number) {
return (index - this.indexRange.start) / (this.indexRange.stop - this.indexRange.start) * this.width; 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; 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; return (x / this.width) * this.indexRange.stop;
} }
private getValue(y: number) { getValue(y: number) {
return ((this.height - y) / this.height) * this.valRange.high; return ((this.height - y) / this.height) * this.valRange.high;
} }
@@ -139,19 +136,22 @@ export default class ClimateChart {
private renderTimeseries(timeseries: Timeseries) { private renderTimeseries(timeseries: Timeseries) {
const timeseriesPoints = timeseries.cachedBetween(this.indexRange.start, this.indexRange.stop); const timeseriesPoints = timeseries.cachedBetween(this.indexRange.start, this.indexRange.stop);
this.ctx.strokeStyle = timeseries.getColour(); this.ctx.strokeStyle = timeseries.getColour();
const drawBubbles = this.getX(timeseriesPoints[3]) - this.getX(timeseriesPoints[1]) > 6;
let y = this.getY(timeseriesPoints[0]); let y = this.getY(timeseriesPoints[0]);
let x = this.getX(timeseriesPoints[1]); let x = this.getX(timeseriesPoints[1]);
this.ctx.moveTo(Math.floor(x), Math.floor(y)); for (let i = 0; i < timeseriesPoints.length; i += 2) {
this.ctx.beginPath(); this.ctx.beginPath();
this.ctx.lineTo(Math.floor(x), Math.floor(y)); this.ctx.moveTo(Math.round(x), Math.round(y));
this.ctx.ellipse(x, y, 3, 3, 0, 0, 2 * Math.PI);
for (let i = 2; i < timeseriesPoints.length; i += 2) {
y = this.getY(timeseriesPoints[i]); y = this.getY(timeseriesPoints[i]);
x = this.getX(timeseriesPoints[i + 1]); x = this.getX(timeseriesPoints[i + 1]);
this.ctx.lineTo(Math.floor(x), Math.floor(y)); this.ctx.lineTo(Math.round(x), Math.round(y));
this.ctx.ellipse(x, y, 3, 3, 0, 0, 2 * Math.PI);
}
this.ctx.stroke(); this.ctx.stroke();
if (drawBubbles) {
this.ctx.beginPath();
this.ctx.ellipse(x, y, 2, 2, 0, 0, 2 * Math.PI);
this.ctx.stroke();
}
}
} }
private renderTooltip(text: string, x: number, y: number) { 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.fillStyle = "rgb(255,255,255)";
this.ctx.strokeStyle = "rgb(0,0,0)"; this.ctx.strokeStyle = "rgb(0,0,0)";
this.ctx.fillRect(x, y, width, height); this.ctx.fillRect(Math.round(x), Math.round(y), Math.round(width), Math.round(height));
this.ctx.strokeRect(x, y, width, 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.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));
} }
} }

View File

@@ -90,6 +90,7 @@ class AppStateStore {
stop = this.state.lastUpdateTime; stop = this.state.lastUpdateTime;
} }
this.addLoad(); this.addLoad();
console.log(start, stop);
for (const timeseries of this.state.timeseries) { for (const timeseries of this.state.timeseries) {
await timeseries.updateFromWindow(start, stop); await timeseries.updateFromWindow(start, stop);
} }
@@ -101,6 +102,7 @@ class AppStateStore {
} }
private async getNewTimeseriesData() { private async getNewTimeseriesData() {
const updateTime = new Date().getTime() / 1000;
this.addLoad(); this.addLoad();
for (const timeseries of this.state.timeseries) { for (const timeseries of this.state.timeseries) {
await timeseries.getLatest(); await timeseries.getLatest();
@@ -110,7 +112,7 @@ class AppStateStore {
this.notifyStoreVal("timeseries"); this.notifyStoreVal("timeseries");
this.eventCallbacks["timeseriesUpdated"].forEach(cb => cb(timeseries)); this.eventCallbacks["timeseriesUpdated"].forEach(cb => cb(timeseries));
} }
this.setLastUpdateTime(new Date().getTime() / 1000); this.setLastUpdateTime(updateTime);
} }
getState(): AppState { getState(): AppState {

View File

@@ -20,11 +20,13 @@ class Timeseries {
maxIndex: -Infinity, maxIndex: -Infinity,
}; };
private colour: string; private colour: string;
private tolerance: number;
constructor(name: string, loader: TimeseriesLoader) { constructor(name: string, loader: TimeseriesLoader, tolerance?: number) {
this.cache = new Int32Array(); this.cache = new Int32Array();
this.loader = loader; this.loader = loader;
this.name = name; this.name = name;
this.tolerance = tolerance ?? 0;
this.colour = `rgb(${Math.random() * 255},${Math.random() * 255},${Math.random() * 255})`; this.colour = `rgb(${Math.random() * 255},${Math.random() * 255},${Math.random() * 255})`;
} }
@@ -70,12 +72,14 @@ class Timeseries {
if (this.cache.length === 0) { if (this.cache.length === 0) {
this.fetching = true; this.fetching = true;
await this.fullFetch(start, stop); await this.fullFetch(start, stop);
} else if (this.cache[1] > start) { }
if (this.cache[1] > start + this.tolerance) {
this.fetching = true; this.fetching = true;
await this.fetchPrior(start); await this.fetchPrior(this.cache[1], start);
} else if (this.cache[this.currentEndPointer - 1] < stop) { }
if (this.cache[this.currentEndPointer - 1] < stop - this.tolerance) {
this.fetching = true; this.fetching = true;
await this.fetchAnterior(stop); await this.fetchAfter(this.cache[this.currentEndPointer - 1], stop);
} }
} catch (e) { } catch (e) {
throw new Error(`Error fetching timeseries data: ${e}`); throw new Error(`Error fetching timeseries data: ${e}`);
@@ -87,7 +91,7 @@ class Timeseries {
async getLatest() { async getLatest() {
this.fetching = true; this.fetching = true;
try { try {
await this.fetchAnterior(this.cache[this.currentEndPointer - 1]); await this.fetchAfter(this.cache[this.currentEndPointer - 1]);
} catch (e) { } catch (e) {
throw new Error(`Error fetching timeseries data: ${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 { try {
const doubleTimespan = 2 * (this.cache[this.currentEndPointer - 1] - this.cache[1]); let forwardDist = 2 * (this.cache[this.currentEndPointer - 1] - this.cache[1]);
const result = await this.loader(after, after + doubleTimespan); 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); const newCache = new Int32Array(this.cache.length + result.length);
newCache.set(this.cache, 0); newCache.set(this.cache, 0);
newCache.set(result, this.currentEndPointer); newCache.set(result, this.currentEndPointer);
@@ -119,10 +126,13 @@ class Timeseries {
} }
} }
private async fetchPrior(before: number) { private async fetchPrior(priorTo: number, atLeastUntil?: number) {
try { try {
const doubleTimespan = 2 * (this.cache[this.currentEndPointer - 1] - this.cache[1]); let backDist = 2 * (this.cache[this.currentEndPointer - 1] - this.cache[1]);
const result = await this.loader(before - doubleTimespan, before); if (atLeastUntil < priorTo - backDist) {
backDist = priorTo - atLeastUntil;
}
const result = await this.loader(priorTo - backDist, priorTo);
const newCache = new Int32Array(this.cache.length + result.length); const newCache = new Int32Array(this.cache.length + result.length);
newCache.set(result, 0); newCache.set(result, 0);
newCache.set(this.cache, result.length); newCache.set(this.cache, result.length);

View File

@@ -1,6 +1,6 @@
import config from "./config.json"; import config from "./config.json";
import {AppStore, getAppState, initStore} from "./StateStore"; import {AppStore, getAppState, initStore} from "./StateStore";
import AppUI from "./AppUI"; import AppUI from "./ui-components/AppUI";
import Timeseries from "./Timeseries"; import Timeseries from "./Timeseries";
import {ClayPIDashboardError} from "./errors"; import {ClayPIDashboardError} from "./errors";
export {config}; export {config};
@@ -37,9 +37,21 @@ async function init() {
documentReady: false, documentReady: false,
timeseries: [], timeseries: [],
}); });
AppStore().addTimeseries(new Timeseries("temp", (start, stop) => loadClimateTimeseriesData("temp", start, stop))); AppStore().addTimeseries(new Timeseries(
AppStore().addTimeseries(new Timeseries("humidity", (start, stop) => loadClimateTimeseriesData("humidity", start, stop))); "temp",
AppStore().addTimeseries(new Timeseries("co2", (start, stop) => loadClimateTimeseriesData("co2", start, stop))); (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(); const ui = new AppUI();
ui.bootstrap("root"); ui.bootstrap("root");
} }

View File

@@ -1,7 +1,7 @@
import {AppStore, DisplayMode, getAppState, TimeWindow} from "./StateStore"; import {AppStore, DisplayMode, getAppState} from "../StateStore";
import GridWidget, {GridProps} from "./GridWidget"; import GridWidget, {GridProps} from "./GridWidget";
import UIComponent from "./UIComponent"; import UIComponent from "./UIComponent";
import ClimateChart from "./ClimateChart"; import ClimateChart from "../ClimateChart";
class ClimateChartWidget extends UIComponent { class ClimateChartWidget extends UIComponent {
private readonly skeleton: GridWidget; private readonly skeleton: GridWidget;

View File

@@ -1,6 +1,6 @@
import GridWidget, {GridProps} from "./GridWidget"; import GridWidget, {GridProps} from "./GridWidget";
import {AppStore, DisplayMode, getAppState} from "./StateStore"; import {AppStore, DisplayMode, getAppState} from "../StateStore";
import * as JSX from "./JSXFactory"; import * as JSX from "../JSXFactory";
import UIComponent from "./UIComponent"; import UIComponent from "./UIComponent";
class DisplayModeWidget extends UIComponent { class DisplayModeWidget extends UIComponent {

View File

@@ -1,4 +1,4 @@
import {AppStore, getAppState} from "./StateStore"; import {AppStore, getAppState} from "../StateStore";
import UIComponent from "./UIComponent"; import UIComponent from "./UIComponent";
class MessageOverlay extends UIComponent { class MessageOverlay extends UIComponent {

View File

@@ -1,7 +1,7 @@
import UIComponent from "./UIComponent"; import UIComponent from "./UIComponent";
import * as JSX from "./JSXFactory"; import * as JSX from "../JSXFactory";
import GridWidget, {GridProps} from "./GridWidget"; import GridWidget, {GridProps} from "./GridWidget";
import {AppStore, DisplayMode, getAppState} from "./StateStore"; import {AppStore, DisplayMode, getAppState} from "../StateStore";
export default class SelectDisplayModeWidget extends UIComponent { export default class SelectDisplayModeWidget extends UIComponent {
private mainBody: HTMLElement; private mainBody: HTMLElement;

View File

@@ -1,7 +1,7 @@
import {AppStore, getAppState} from "./StateStore"; import {AppStore, getAppState} from "../StateStore";
import GridWidget, {GridProps} from "./GridWidget"; import GridWidget, {GridProps} from "./GridWidget";
import UIComponent from "./UIComponent"; import UIComponent from "./UIComponent";
import * as JSX from "./JSXFactory"; import * as JSX from "../JSXFactory";
class TimerWidget extends UIComponent { class TimerWidget extends UIComponent {
private readonly display: HTMLElement; private readonly display: HTMLElement;
@@ -26,7 +26,7 @@ class TimerWidget extends UIComponent {
private resetTimer() { private resetTimer() {
this.nextUpdateTime = getAppState().lastUpdateTime + getAppState().updateIntervalSeconds; 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(); this.refreshTimer();
} }

View File

@@ -1,7 +1,7 @@
import GridWidget, {GridProps} from "./GridWidget"; import GridWidget, {GridProps} from "./GridWidget";
import {AppStore, getAppState} from "./StateStore"; import {AppStore, getAppState} from "../StateStore";
import UIComponent from "./UIComponent"; import UIComponent from "./UIComponent";
import * as JSX from "./JSXFactory"; import * as JSX from "../JSXFactory";
class TimezoneWidget extends UIComponent { class TimezoneWidget extends UIComponent {
private skeleton: GridWidget; private skeleton: GridWidget;