fixed blurry graphics, improvements to loading and caching
This commit is contained in:
@@ -138,8 +138,4 @@ h1 {
|
||||
}
|
||||
|
||||
.chart-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
for (let i = 0; i < timeseriesPoints.length; i += 2) {
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AppStore, getAppState} from "./StateStore";
|
||||
import {AppStore, getAppState} from "../StateStore";
|
||||
import UIComponent from "./UIComponent";
|
||||
|
||||
class MessageOverlay extends UIComponent {
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user