diff --git a/app-dist/static/styles.css b/app-dist/static/styles.css
index b404333..9a23d0a 100644
--- a/app-dist/static/styles.css
+++ b/app-dist/static/styles.css
@@ -1,7 +1,16 @@
+@import url('https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@200;400;500&family=Roboto:wght@200;300;400&display=swap');
+
+:root {
+ --accent-dark: #4f6fa0;
+ --accent-light: #fbfbff;
+}
+
html, body {
margin: 0;
height: 100%;
- background-color: #fff1de;
+ background-color: var(--accent-light);
+ font-family: 'Roboto', sans-serif;
+ font-weight: lighter;
}
.overlay {
@@ -44,9 +53,9 @@ html, body {
h1 {
display: block;
- font-family: "Georgia", serif;
+ font-family: 'Roboto Slab', serif;
font-weight: normal;
- color: #7b5b2f;
+ color: var(--accent-dark);
}
.widget {
@@ -55,14 +64,15 @@ h1 {
flex-direction: column;
margin: 0.5em;
padding: 1em;
- border: 0.1em #c7ab82 solid;
+ border: 0.1em var(--accent-dark) solid;
+ border-radius: 0.4em;
background-color: white;
}
.widget h2 {
- font-family: "Georgia", serif;
+ font-family: 'Roboto', sans-serif;
font-weight: normal;
- color: #7b5b2f;
+ color: var(--accent-dark);
font-size: 1em;
display: block;
}
@@ -204,7 +214,7 @@ h1 {
.help-box {
position: relative;
background-color: #ffffff;
- border: solid 1px #7b5b2f;
+ border: solid 1px var(--accent-dark);
width: 40em;
padding: 1em;
z-index: 1;
diff --git a/dashboard/assets/help-button.svg b/dashboard/assets/help-button.svg
index df07aeb..2e062c4 100644
--- a/dashboard/assets/help-button.svg
+++ b/dashboard/assets/help-button.svg
@@ -57,7 +57,7 @@
inkscape:groupmode="layer"
id="layer1">
?
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Bitstream Charter';-inkscape-font-specification:'Bitstream Charter';stroke-width:0.20222;fill:#4f6fa0;fill-opacity:1;">?
diff --git a/dashboard/src/StateStore.ts b/dashboard/src/StateStore.ts
index 9cf2767..b975939 100644
--- a/dashboard/src/StateStore.ts
+++ b/dashboard/src/StateStore.ts
@@ -131,37 +131,37 @@ class AppStateStore {
start = this.state.lastUpdateTime - this.state.minutesDisplayed * 60;
stop = this.state.lastUpdateTime;
}
- this.addLoad();
- try {
- for (const timeseries of this.state.leftTimeseries) {
- await timeseries.updateFromWindow(start, stop);
- }
- for (const timeseries of this.state.rightTimeseries) {
- await timeseries.updateFromWindow(start, stop);
- }
- } catch (e) {
- AppStore().fatalError(e);
+ const allTimeseries = this.state.leftTimeseries.concat(this.state.rightTimeseries);
+ const allHistoriesComplete = !allTimeseries.some(timeseries => !timeseries.historyIsComplete());
+ if (start < this.getExtrema().minIndex && allHistoriesComplete) {
+ return;
}
- this.finishLoad();
- this.notifyAllTimeseriesUpdated();
+ this.addLoad();
+ const timeseriesFetches: Promise[] = [];
+ for (const timeseries of allTimeseries) {
+ timeseriesFetches.push(timeseries.updateFromWindow(start, stop));
+ }
+ await Promise.allSettled(timeseriesFetches).then(() => {
+ this.finishLoad();
+ this.notifyAllTimeseriesUpdated();
+ }).catch((e) => this.fatalError(e));
}
private async getNewTimeseriesData() {
const updateTime = new Date().getTime() / 1000;
this.addLoad();
- try {
- for (const timeseries of this.state.leftTimeseries) {
- await timeseries.getLatest();
- }
- for (const timeseries of this.state.rightTimeseries) {
- await timeseries.getLatest();
- }
- } catch (e) {
- AppStore().fatalError(e);
+ const timeseriesFetches: Promise[] = [];
+ for (const timeseries of this.state.leftTimeseries) {
+ timeseriesFetches.push(timeseries.getLatest());
}
- this.finishLoad();
- this.setLastUpdateTime(updateTime);
- this.notifyAllTimeseriesUpdated();
+ for (const timeseries of this.state.rightTimeseries) {
+ timeseriesFetches.push(timeseries.getLatest());
+ }
+ await Promise.allSettled(timeseriesFetches).then(() => {
+ this.finishLoad();
+ this.setLastUpdateTime(updateTime);
+ this.notifyAllTimeseriesUpdated();
+ }).catch((e) => this.fatalError(e));
}
private notifyAllTimeseriesUpdated() {
@@ -195,16 +195,53 @@ class AppStateStore {
setDisplayWindow(newWin: TimeWindow) {
if (newWin.start < newWin.stop) {
- if (newWin.stop <= this.state.lastUpdateTime) {
- this.state.displayWindow = {...newWin};
- this.notifyStoreVal("displayWindow");
- this.updateTimeseriesFromSettings();
+ const extrema = this.getExtrema();
+ if (newWin.stop >= extrema.maxIndex) {
+ newWin.stop = extrema.maxIndex;
}
+ this.state.displayWindow = {...newWin};
+ this.updateTimeseriesFromSettings().then(() => {
+ const newExtrema = this.getExtrema();
+ this.state.displayWindow = {
+ start: Math.max(newExtrema.minIndex, this.state.displayWindow.start),
+ stop: Math.min(newExtrema.maxIndex, this.state.displayWindow.stop),
+ };
+ this.notifyStoreVal("displayWindow");
+ });
} else {
console.warn(`Invalid display window from ${newWin.start} to ${newWin.stop}`);
}
}
+ shiftDisplayWindow(deltaX: number) {
+ const oldWin = {...this.state.displayWindow};
+ this.state.displayWindow.start += deltaX;
+ this.state.displayWindow.stop += deltaX;
+ this.updateTimeseriesFromSettings().then(() => {
+ const newExtrema = this.getExtrema();
+ const blockedBack = newExtrema.minIndex > this.state.displayWindow.start && deltaX < 0;
+ const blockedForward = newExtrema.maxIndex < this.state.displayWindow.stop && deltaX >= 0;
+ if (blockedBack || blockedForward) {
+ this.state.displayWindow = oldWin;
+ }
+ this.notifyStoreVal("displayWindow");
+ });
+ }
+
+ getExtrema() {
+ let minIndex = Infinity;
+ let maxIndex = -Infinity;
+ let minVal = Infinity;
+ let maxVal = -Infinity;
+ for (const timeseries of this.state.leftTimeseries.concat(this.state.rightTimeseries)) {
+ minIndex = Math.min(minIndex, timeseries.getExtrema().minIndex);
+ maxIndex = Math.max(maxIndex, timeseries.getExtrema().maxIndex);
+ minVal = Math.min(minIndex, timeseries.getExtrema().minVal);
+ maxVal = Math.max(maxIndex, timeseries.getExtrema().maxVal);
+ }
+ return {minIndex, maxIndex, minVal, maxVal};
+ }
+
setMinutesDisplayed(mins: number) {
if (mins > 0) {
this.state.minutesDisplayed = Math.ceil(mins);
diff --git a/dashboard/src/Timeseries.ts b/dashboard/src/Timeseries.ts
index 9afb914..b4a9815 100644
--- a/dashboard/src/Timeseries.ts
+++ b/dashboard/src/Timeseries.ts
@@ -33,6 +33,7 @@ class Timeseries {
private valExtremaOverride?: { high: number, low: number };
private colour: string;
private tolerance: number;
+ private historyComplete = false;
constructor(options: TimeseriesOptions) {
this.cache = new Int32Array();
@@ -58,7 +59,7 @@ class Timeseries {
getExtremaInRange(start: number, stop: number): Extrema {
let maxVal = -Infinity;
let minVal = Infinity;
- for (let i = this.findIndexInCache(start) - 1; i < this.findIndexInCache(stop) - 1; i += 2) {
+ for (let i = this.findIndexInCache(start) - 1; i < this.findIndexInCache(stop) + 1; i += 2) {
if (this.cache[i] < minVal) {
minVal = this.cache[i];
}
@@ -69,8 +70,8 @@ class Timeseries {
return {
minIndex: this.extrema.minIndex,
maxIndex: this.extrema.maxIndex,
- maxVal: this.valExtremaOverride.high > maxVal ? this.valExtremaOverride.high : maxVal,
- minVal: this.valExtremaOverride.low < minVal ? this.valExtremaOverride.low : minVal,
+ maxVal: this.valExtremaOverride ? Math.max(this.valExtremaOverride.high, maxVal) : maxVal,
+ minVal: this.valExtremaOverride ? Math.min(this.valExtremaOverride.low, minVal) : minVal,
};
}
@@ -95,7 +96,7 @@ class Timeseries {
const cacheStopIndex = this.findIndexInCache(stop);
return this.cache.slice(
(cacheStartIndex - (cacheStartIndex) % blockSize),
- (cacheStopIndex + blockSize - (cacheStopIndex) % blockSize) + blockSize * 2,
+ (cacheStopIndex + blockSize - (cacheStopIndex) % blockSize) + blockSize,
);
}
}
@@ -175,13 +176,17 @@ class Timeseries {
if (atLeastUntil < priorTo - backDist) {
backDist = priorTo - atLeastUntil;
}
- const result = await this.loader(priorTo - backDist, priorTo);
+ const requestIndexStart = priorTo - backDist;
+ const result = await this.loader(requestIndexStart, priorTo);
const newCache = new Int32Array(this.cache.length + result.length);
newCache.set(result, 0);
newCache.set(this.cache, result.length);
this.cache = newCache;
this.currentEndPointer += result.length;
this.updateExtremaFrom(result);
+ if (this.extrema.minIndex === priorTo) {
+ this.historyComplete = true;
+ }
} catch (e) {
throw new Error(`Error fetching prior data: ${e}`);
}
@@ -234,6 +239,10 @@ class Timeseries {
}
}
}
+
+ historyIsComplete(): boolean {
+ return this.historyComplete;
+ }
}
export default Timeseries;
\ No newline at end of file
diff --git a/dashboard/src/chart/Chart.ts b/dashboard/src/chart/Chart.ts
index de5511e..e96c218 100644
--- a/dashboard/src/chart/Chart.ts
+++ b/dashboard/src/chart/Chart.ts
@@ -46,7 +46,6 @@ export default class Chart {
this.ctx.fillStyle = "rgb(255,255,255)";
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
this.ctx.fill();
- this.ctx.translate(0.5, 0.5);
this.ctx.canvas.onmousemove = (e) => this.handleMouseMove(e);
this.ctx.canvas.onmousedown = (e) => this.dragging = true;
this.ctx.canvas.onmouseup = (e) => this.dragging = false;
@@ -78,7 +77,6 @@ export default class Chart {
height: this.ctx.canvas.height - verticalMargins,
width: rightScaleInitialWidth,
});
- this.render();
}
addTimeseries(timeseries: Timeseries, scale?: ScaleId) {
@@ -138,16 +136,16 @@ export default class Chart {
}
render() {
- this.clearCanvas();
+ this.resetCanvas();
this.updateResolution();
- this.renderGuides();
this.leftScale.updateIndexRange(this.indexRange);
this.rightScale.updateIndexRange(this.indexRange);
+ this.renderGuides();
this.leftScale.listTimeseries().forEach(timeseries => this.renderTimeseries(timeseries, ScaleId.Left));
this.rightScale.listTimeseries().forEach(timeseries => this.renderTimeseries(timeseries, ScaleId.Right));
- this.renderMargins();
this.leftScale.render(this.ctx);
this.rightScale.render(this.ctx);
+ this.renderMargins();
this.renderTooltips();
}
@@ -179,9 +177,11 @@ export default class Chart {
);
}
- private clearCanvas() {
+ private resetCanvas() {
this.ctx.fillStyle = "rgb(255,255,255)";
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
+ this.ctx.resetTransform();
+ this.ctx.translate(0.5, 0.5);
}
private updateResolution() {
@@ -200,7 +200,7 @@ export default class Chart {
this.ctx.lineWidth = 1;
this.ctx.beginPath();
for (const tick of this.rightScale.getTicks()) {
- const pos = Math.floor(this.rightScale.getY(tick));
+ const pos = this.rightScale.getY(tick) | 0;
this.ctx.moveTo(this.chartBounds.left, pos);
this.ctx.lineTo(this.chartBounds.left + this.chartBounds.width, pos);
}
@@ -294,6 +294,7 @@ export default class Chart {
}
private renderTooltip(text: string, x: number, y: number, markerColour: string) {
+ this.ctx.lineWidth = 1;
this.ctx.strokeStyle = "rgb(255,0,0)";
this.ctx.beginPath();
this.ctx.ellipse(x, y, 5, 5, 0, 0, 2 * Math.PI);
diff --git a/dashboard/src/chart/Scale.ts b/dashboard/src/chart/Scale.ts
index 214e180..62ba88a 100644
--- a/dashboard/src/chart/Scale.ts
+++ b/dashboard/src/chart/Scale.ts
@@ -7,6 +7,7 @@ export default class Scale {
private tickCache: number[] = [];
private tickCacheDirty = true;
private bounds: Bounds;
+ private margins: {top: number, bottom: number} = {top: 10, bottom: 10};
constructor(bounds?: Bounds) {
this.bounds = bounds ?? {height: 0, width: 0, top: 0, left: 0};
@@ -72,7 +73,8 @@ export default class Scale {
}
getY(value: number) {
- return this.bounds.top + this.bounds.height - (value - this.valRange.low) / (this.valRange.high - this.valRange.low) * this.bounds.height;
+ const internalHeight = this.bounds.height - this.margins.top - this.margins.bottom;
+ return this.bounds.top + this.bounds.height - this.margins.bottom - (value - this.valRange.low) / (this.valRange.high - this.valRange.low) * internalHeight;
}
getValue(y: number) {
diff --git a/dashboard/src/climateTimeseries.ts b/dashboard/src/climateTimeseries.ts
index 087b98b..0fda09f 100644
--- a/dashboard/src/climateTimeseries.ts
+++ b/dashboard/src/climateTimeseries.ts
@@ -6,21 +6,18 @@ export const newCo2Timeseries = (tolerance: number) => new Timeseries({
name: "CO₂ (ppm)",
loader: (start, stop) => loadClimateTimeseriesData("co2", start, stop),
tolerance,
- valueRangeOverride: { high: 800, low: 400 },
});
export const newTempTimeseries = (tolerance: number) => new Timeseries({
name: "Temperature (°C)",
loader: (start, stop) => loadClimateTimeseriesData("temp", start, stop),
tolerance,
- valueRangeOverride: { high: 30, low: 10 },
});
export const newHumidityTimeseries = (tolerance: number) => new Timeseries({
name: "Humidity (%)",
loader: (start, stop) => loadClimateTimeseriesData("humidity", start, stop),
tolerance,
- valueRangeOverride: { high: 75, low: 40 },
});
async function loadClimateTimeseriesData(dataType: "temp" | "humidity" | "co2", start?: number, stop?: number) {
@@ -50,7 +47,7 @@ async function loadClimateTimeseriesData(dataType: "temp" | "humidity" | "co2",
}
return new Int32Array(data.buffer);
} catch (e) {
- const message = "timerseries data couldn't be loaded from the server";
+ const message = "timeseries data couldn't be loaded from the server";
throw new ClayPIDashboardError(`${message}: ${e}`, message);
}
}
\ No newline at end of file
diff --git a/dashboard/src/config.json b/dashboard/src/config.json
index 0bedc3f..5624b06 100644
--- a/dashboard/src/config.json
+++ b/dashboard/src/config.json
@@ -1,6 +1,6 @@
{
- "development": false,
+ "development": true,
"defaultMinuteSpan": 60,
"reloadIntervalSec": 30,
- "dataEndpoint": "/climate/api"
+ "dataEndpoint": "http://192.168.178.21:4040/climate/api"
}
diff --git a/dashboard/src/types.d.ts b/dashboard/src/types.d.ts
index 9a5934e..bc39be9 100644
--- a/dashboard/src/types.d.ts
+++ b/dashboard/src/types.d.ts
@@ -5,4 +5,8 @@ declare module "*.gif" {
declare module "*.png" {
const value: string;
export = value;
+}
+declare module "*.svg" {
+ const value: string;
+ export = value;
}
\ No newline at end of file
diff --git a/dashboard/src/ui-components/AppUI.tsx b/dashboard/src/ui-components/AppUI.tsx
index 77c753b..d148aba 100644
--- a/dashboard/src/ui-components/AppUI.tsx
+++ b/dashboard/src/ui-components/AppUI.tsx
@@ -10,7 +10,7 @@ import LegendWidget from "./LegendWidget";
import HelpModal from "./HelpModal";
import * as JSX from "../JSXFactory";
import {AppStore} from "../StateStore";
-import HelpButtonImg from "../../assets/help-button.png";
+import HelpButtonImg from "../../assets/help-button.svg";
class AppUI extends UIComponent {
private timezoneWidget: TimezoneWidget;
diff --git a/dashboard/src/ui-components/ClimateChartWidget.ts b/dashboard/src/ui-components/ClimateChartWidget.ts
index 779573c..02e9a37 100644
--- a/dashboard/src/ui-components/ClimateChartWidget.ts
+++ b/dashboard/src/ui-components/ClimateChartWidget.ts
@@ -75,11 +75,7 @@ class ClimateChartWidget extends UIComponent {
if (getAppState().displayMode === "pastMins") {
AppStore().emulateLastMinsWithWindow();
}
- const displayedWindow = getAppState().displayWindow;
- AppStore().setDisplayWindow({
- start: displayedWindow.start + deltaIndex,
- stop: displayedWindow.stop + deltaIndex,
- });
+ AppStore().shiftDisplayWindow(deltaIndex);
}
private updateTimezone() {
@@ -96,8 +92,8 @@ class ClimateChartWidget extends UIComponent {
getAppState().rightTimeseries.forEach(timeseries => this.chart.addTimeseries(timeseries, ScaleId.Right));
this.chart.on("scroll", (...args) => this.handleScroll(...args));
this.chart.on("drag", (...args) => this.handleDrag(...args));
- await this.rerender();
this.initialised = true;
+ await this.rerender();
} catch (e) {
AppStore().fatalError(e);
} finally {