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

@@ -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));
}
}

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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");
}

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 UIComponent from "./UIComponent";
import ClimateChart from "./ClimateChart";
import ClimateChart from "../ClimateChart";
class ClimateChartWidget extends UIComponent {
private readonly skeleton: GridWidget;

View File

@@ -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 {

View File

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

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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;