Added left and right scales
This commit is contained in:
@@ -100,6 +100,10 @@ h1 {
|
||||
display: inline-block;
|
||||
font-size: 30px;
|
||||
margin: 10px 0 10px 0;
|
||||
transition: background-color 100ms;
|
||||
}
|
||||
.display-mode-widget-mins .min-count:hover {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
.display-mode-widget-mins input {
|
||||
border: none;
|
||||
@@ -128,6 +132,12 @@ h1 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.timezone-widget span:nth-child(2) {
|
||||
transition: background-color 100ms;
|
||||
}
|
||||
.timezone-widget span:nth-child(2):hover {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
.timezone-widget input {
|
||||
border: none;
|
||||
display: inline-block;
|
||||
@@ -136,6 +146,3 @@ h1 {
|
||||
margin: 10px 0 10px 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.chart-canvas {
|
||||
}
|
||||
@@ -1,14 +1,29 @@
|
||||
import Timeseries from "./Timeseries";
|
||||
|
||||
interface Scale {
|
||||
timeseries: Timeseries[];
|
||||
valRange: {high: number, low: number};
|
||||
width: number;
|
||||
}
|
||||
|
||||
export enum ScaleId {
|
||||
Left,
|
||||
Right
|
||||
}
|
||||
|
||||
const MIN_PIXELS_PER_POINT = 3;
|
||||
|
||||
export default class ClimateChart {
|
||||
private readonly ctx: CanvasRenderingContext2D;
|
||||
private readonly timeseries: Timeseries[] = [];
|
||||
private readonly leftScale: Scale;
|
||||
private readonly rightScale: Scale;
|
||||
private readonly lastMousePos = {x: 0, y: 0};
|
||||
private readonly indexRange = {start: 0, stop: 0};
|
||||
private readonly valRange = {high: -Infinity, low: Infinity}
|
||||
private readonly margins = {top: 20, bottom: 20};
|
||||
private formatTimestamp = (timestamp: number) => new Date(timestamp * 1000).toLocaleTimeString();
|
||||
private width = 0;
|
||||
private height = 0;
|
||||
private resolution = 1;
|
||||
constructor(context: CanvasRenderingContext2D) {
|
||||
this.ctx = context;
|
||||
this.ctx.fillStyle = "rgb(255,255,255)";
|
||||
@@ -17,6 +32,16 @@ export default class ClimateChart {
|
||||
this.ctx.fill();
|
||||
this.ctx.translate(0.5, 0.5);
|
||||
this.ctx.canvas.onmousemove = (e) => this.handleMouseMove(e);
|
||||
this.leftScale = {
|
||||
timeseries: [],
|
||||
valRange: {high: -Infinity, low: Infinity},
|
||||
width: 0,
|
||||
};
|
||||
this.rightScale = {
|
||||
timeseries: [],
|
||||
valRange: {high: -Infinity, low: Infinity},
|
||||
width: 0,
|
||||
};
|
||||
}
|
||||
|
||||
private updateDimensions() {
|
||||
@@ -24,8 +49,12 @@ export default class ClimateChart {
|
||||
this.height = Number(getComputedStyle(this.ctx.canvas).height.slice(0, -2));
|
||||
}
|
||||
|
||||
addTimeseries(timeseries: Timeseries) {
|
||||
this.timeseries.push(timeseries);
|
||||
addTimeseries(timeseries: Timeseries, scale?: ScaleId) {
|
||||
if (scale === ScaleId.Left) {
|
||||
this.leftScale.timeseries.push(timeseries);
|
||||
} else {
|
||||
this.rightScale.timeseries.push(timeseries);
|
||||
}
|
||||
}
|
||||
|
||||
setRange(range: {start: number, stop: number}) {
|
||||
@@ -33,7 +62,7 @@ export default class ClimateChart {
|
||||
this.indexRange.stop = range.stop;
|
||||
}
|
||||
|
||||
handleMouseMove(event: MouseEvent) {
|
||||
private handleMouseMove(event: MouseEvent) {
|
||||
const {left: canvasX, top: canvasY} = this.ctx.canvas.getBoundingClientRect();
|
||||
const x = event.clientX - canvasX;
|
||||
const y = event.clientY - canvasY;
|
||||
@@ -44,62 +73,112 @@ export default class ClimateChart {
|
||||
|
||||
render() {
|
||||
this.updateDimensions();
|
||||
this.ctx.fillStyle = "rgb(255,255,255)";
|
||||
this.ctx.fillRect(0, 0, this.width, this.height);
|
||||
this.ctx.fill();
|
||||
this.setDisplayRange();
|
||||
this.renderScale();
|
||||
for (const timeseries of this.timeseries) {
|
||||
this.renderTimeseries(timeseries);
|
||||
}
|
||||
this.clearCanvas();
|
||||
this.updateResolution();
|
||||
this.setDisplayRangeForScale(this.leftScale);
|
||||
this.setDisplayRangeForScale(this.rightScale);
|
||||
this.renderRightScale();
|
||||
this.leftScale.timeseries.forEach(timeseries => this.renderTimeseries(timeseries, ScaleId.Left));
|
||||
this.rightScale.timeseries.forEach(timeseries => this.renderTimeseries(timeseries, ScaleId.Right));
|
||||
this.renderLeftScale();
|
||||
this.renderTooltips();
|
||||
}
|
||||
|
||||
private renderScale() {
|
||||
private clearCanvas() {
|
||||
this.ctx.fillStyle = "rgb(255,255,255)";
|
||||
this.ctx.fillRect(0, 0, this.width, this.height);
|
||||
this.ctx.fill();
|
||||
}
|
||||
|
||||
private updateResolution() {
|
||||
const chartWidth = (this.width - this.rightScale.width - this.leftScale.width);
|
||||
const points = this.rightScale.timeseries[0].cachedBetween(this.indexRange.start, this.indexRange.stop).length / 2;
|
||||
const pixelsPerPoint = chartWidth / points;
|
||||
if (pixelsPerPoint < MIN_PIXELS_PER_POINT) {
|
||||
this.resolution = Math.ceil(MIN_PIXELS_PER_POINT / pixelsPerPoint);
|
||||
}
|
||||
}
|
||||
|
||||
private renderLeftScale() {
|
||||
this.ctx.fillStyle = "rgb(255,255,255)";
|
||||
this.ctx.fillRect(0, 0, this.leftScale.width, this.height);
|
||||
this.ctx.fill();
|
||||
this.ctx.strokeStyle = "rgb(230,230,230)";
|
||||
this.ctx.fillStyle = "black";
|
||||
const ticks = 20;
|
||||
const tickHeight = (this.valRange.high - this.valRange.low) / ticks;
|
||||
let currentTick = this.valRange.low;
|
||||
for (let i = 0; i < ticks; i++) {
|
||||
const tickHeight = (this.leftScale.valRange.high - this.leftScale.valRange.low) / ticks;
|
||||
let currentTick = this.leftScale.valRange.low - tickHeight;
|
||||
for (let i = 0; i <= ticks; i++) {
|
||||
currentTick += tickHeight;
|
||||
const pos = Math.round(this.getY(currentTick));
|
||||
const text = currentTick.toFixed(2);
|
||||
const textWidth = this.ctx.measureText(text).width;
|
||||
if (textWidth > this.leftScale.width) {
|
||||
this.leftScale.width = textWidth + 10;
|
||||
}
|
||||
const pos = Math.round(this.getY(currentTick, ScaleId.Left));
|
||||
this.ctx.fillText(text, 0, pos + 4);
|
||||
}
|
||||
}
|
||||
|
||||
private renderRightScale() {
|
||||
this.ctx.strokeStyle = "rgb(230,230,230)";
|
||||
this.ctx.fillStyle = "black";
|
||||
const ticks = 20;
|
||||
const tickHeight = (this.rightScale.valRange.high - this.rightScale.valRange.low) / ticks;
|
||||
let currentTick = this.rightScale.valRange.low - tickHeight;
|
||||
for (let i = 0; i <= ticks; i++) {
|
||||
currentTick += tickHeight;
|
||||
const pos = Math.round(this.getY(currentTick, ScaleId.Right));
|
||||
const text = currentTick.toFixed(2);
|
||||
const textWidth = this.ctx.measureText(text).width;
|
||||
if (textWidth > this.rightScale.width) {
|
||||
this.rightScale.width = textWidth;
|
||||
}
|
||||
this.ctx.fillText(text, this.width - textWidth, pos + 4);
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(40, pos);
|
||||
this.ctx.lineTo(this.width, pos);
|
||||
this.ctx.moveTo(this.leftScale.width, pos);
|
||||
this.ctx.lineTo(this.width - textWidth - 5, pos);
|
||||
this.ctx.stroke();
|
||||
this.ctx.fillText(currentTick.toFixed(2), 0, pos + 4);
|
||||
}
|
||||
}
|
||||
|
||||
private setDisplayRange() {
|
||||
for (const timeseries of this.timeseries) {
|
||||
private setDisplayRangeForScale(scale: Scale) {
|
||||
for (const timeseries of scale.timeseries) {
|
||||
const extrema = timeseries.getExtrema();
|
||||
if (extrema.maxVal > this.valRange.high) {
|
||||
this.valRange.high = extrema.maxVal;
|
||||
if (extrema.maxVal > scale.valRange.high) {
|
||||
scale.valRange.high = extrema.maxVal;
|
||||
}
|
||||
if (extrema.minVal < this.valRange.low) {
|
||||
this.valRange.low = extrema.minVal;
|
||||
if (extrema.minVal < scale.valRange.low) {
|
||||
scale.valRange.low = extrema.minVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderTooltips() {
|
||||
let bestDist = 20;
|
||||
let bestTimeseries = this.timeseries[0];
|
||||
private renderTooltips(radius = 20) {
|
||||
let bestDist = radius;
|
||||
let bestTimeseries = this.rightScale.timeseries[0];
|
||||
let bestIndex = 0;
|
||||
let bestVal = 0;
|
||||
for (const timeseries of this.timeseries) {
|
||||
const cache = timeseries.cachedBetween(this.indexRange.start, this.indexRange.stop);
|
||||
let bestScale = ScaleId.Right;
|
||||
for (const scaleId of [ScaleId.Left, ScaleId.Right]) {
|
||||
for (const timeseries of (scaleId === ScaleId.Right ? this.rightScale : this.leftScale).timeseries) {
|
||||
const cache = timeseries.cachedBetween(
|
||||
this.getIndex(this.lastMousePos.x - radius / 2),
|
||||
this.getIndex(this.lastMousePos.x + radius / 2)
|
||||
);
|
||||
for (let i = 0; i < cache.length; i += 2) {
|
||||
const y = this.getY(cache[i], scaleId);
|
||||
if (y + radius / 2 >= this.lastMousePos.y && y - radius / 2 <= this.lastMousePos.y) {
|
||||
const x = this.getX(cache[i + 1]);
|
||||
const y = this.getY(cache[i]);
|
||||
const dist = Math.sqrt((y - this.lastMousePos.y) ** 2 + (x - this.lastMousePos.x) ** 2);
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
bestTimeseries = timeseries;
|
||||
bestIndex = cache[i + 1];
|
||||
bestVal = cache[i];
|
||||
bestScale = scaleId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +186,7 @@ export default class ClimateChart {
|
||||
this.renderTooltip(
|
||||
`${bestTimeseries.getName()} - (${bestVal.toFixed(2)}, ${this.formatTimestamp(bestIndex)})`,
|
||||
this.getX(bestIndex),
|
||||
this.getY(bestVal),
|
||||
this.getY(bestVal, bestScale),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -117,36 +196,43 @@ export default class ClimateChart {
|
||||
}
|
||||
|
||||
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 - this.rightScale.width - this.leftScale.width) + this.leftScale.width;
|
||||
}
|
||||
|
||||
getY(value: number) {
|
||||
return this.height - (value - this.valRange.low) / (this.valRange.high - this.valRange.low) * this.height;
|
||||
getY(value: number, scale: ScaleId) {
|
||||
const valRange = scale === ScaleId.Left ? this.leftScale.valRange : this.rightScale.valRange;
|
||||
return this.height - (value - valRange.low) / (valRange.high - valRange.low) * (this.height - this.margins.bottom - this.margins.top) - this.margins.top;
|
||||
}
|
||||
|
||||
getIndex(x: number) {
|
||||
return (x / this.width) * this.indexRange.stop;
|
||||
return ((x - this.leftScale.width) / (this.width - this.leftScale.width - this.rightScale.width)) * (this.indexRange.stop - this.indexRange.start) + this.indexRange.start;
|
||||
}
|
||||
|
||||
getValue(y: number) {
|
||||
return ((this.height - y) / this.height) * this.valRange.high;
|
||||
getValue(y: number, scale: ScaleId) {
|
||||
const valRange = scale === ScaleId.Left ? this.leftScale.valRange : this.rightScale.valRange;
|
||||
return ((this.height - y) / this.height) * (valRange.high - valRange.low) + valRange.low;
|
||||
}
|
||||
|
||||
|
||||
private renderTimeseries(timeseries: Timeseries) {
|
||||
private renderTimeseries(timeseries: Timeseries, scale: ScaleId) {
|
||||
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 y = this.getY(timeseriesPoints[0], scale);
|
||||
let x = this.getX(timeseriesPoints[1]);
|
||||
for (let i = 0; i < timeseriesPoints.length; i += 2) {
|
||||
for (let i = 0; i < timeseriesPoints.length; i += 2 * this.resolution) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(Math.round(x), Math.round(y));
|
||||
y = this.getY(timeseriesPoints[i]);
|
||||
x = this.getX(timeseriesPoints[i + 1]);
|
||||
y = 0;
|
||||
x = 0;
|
||||
for (let j = 0; j < this.resolution * 2 && (j + 2 < timeseriesPoints.length); j += 2) {
|
||||
y += timeseriesPoints[i + j];
|
||||
x += timeseriesPoints[i + 1 + j];
|
||||
}
|
||||
y = this.getY(y / this.resolution, scale);
|
||||
x = this.getX(x / this.resolution);
|
||||
this.ctx.lineTo(Math.round(x), Math.round(y));
|
||||
this.ctx.stroke();
|
||||
if (drawBubbles) {
|
||||
if (this.resolution === 1) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.ellipse(x, y, 2, 2, 0, 0, 2 * Math.PI);
|
||||
this.ctx.stroke();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Timeseries from "./Timeseries";
|
||||
import {ScaleId} from "./ClimateChart";
|
||||
|
||||
export class AppStateError extends Error {
|
||||
constructor(message: string) {
|
||||
@@ -11,7 +12,7 @@ export type DisplayMode = "window" | "pastMins";
|
||||
|
||||
export interface EventCallback {
|
||||
newTimeseries: (timeseries: Timeseries) => void;
|
||||
timeseriesUpdated: (timeseries: Timeseries) => void;
|
||||
timeseriesUpdated: (timeseries: Timeseries, scale?: ScaleId) => void;
|
||||
}
|
||||
type EventCallbackListing<K extends keyof EventCallback> = Record<K, EventCallback[K][]>;
|
||||
|
||||
@@ -25,7 +26,8 @@ interface AppState {
|
||||
displayWindow: TimeWindow;
|
||||
minutesDisplayed: number;
|
||||
utcOffset: number;
|
||||
timeseries: Timeseries[],
|
||||
leftTimeseries: Timeseries[],
|
||||
rightTimeseries: Timeseries[],
|
||||
overlayText: string;
|
||||
dataEndpointBase: string;
|
||||
updateIntervalSeconds: number;
|
||||
@@ -63,13 +65,18 @@ class AppStateStore {
|
||||
await this.getNewTimeseriesData();
|
||||
}
|
||||
|
||||
addTimeseries(timeseries: Timeseries) {
|
||||
if (this.state.timeseries.indexOf(timeseries) >= 0) {
|
||||
addTimeseries(timeseries: Timeseries, scale?: ScaleId) {
|
||||
const group = scale === ScaleId.Left ? this.state.leftTimeseries : this.state.rightTimeseries;
|
||||
if (group.indexOf(timeseries) >= 0) {
|
||||
throw new AppStateError("Timeseries has already been added!");
|
||||
}
|
||||
this.state.timeseries.push(timeseries);
|
||||
this.notifyStoreVal("timeseries");
|
||||
this.eventCallbacks["newTimeseries"].forEach(cb => cb(timeseries));
|
||||
if (scale === ScaleId.Left) {
|
||||
group.push(timeseries);
|
||||
} else {
|
||||
group.push(timeseries);
|
||||
}
|
||||
this.notifyStoreVal(scale === ScaleId.Left ? "leftTimeseries" : "rightTimeseries");
|
||||
this.eventCallbacks["newTimeseries"].forEach(cb => cb(timeseries, scale));
|
||||
this.updateTimeseriesFromSettings();
|
||||
}
|
||||
|
||||
@@ -90,29 +97,39 @@ class AppStateStore {
|
||||
stop = this.state.lastUpdateTime;
|
||||
}
|
||||
this.addLoad();
|
||||
console.log(start, stop);
|
||||
for (const timeseries of this.state.timeseries) {
|
||||
for (const timeseries of this.state.leftTimeseries) {
|
||||
await timeseries.updateFromWindow(start, stop);
|
||||
}
|
||||
for (const timeseries of this.state.rightTimeseries) {
|
||||
await timeseries.updateFromWindow(start, stop);
|
||||
}
|
||||
this.finishLoad();
|
||||
for (const timeseries of this.state.timeseries) {
|
||||
this.notifyStoreVal("timeseries");
|
||||
this.eventCallbacks["timeseriesUpdated"].forEach(cb => cb(timeseries));
|
||||
}
|
||||
this.notifyAllTimeseriesUpdated();
|
||||
}
|
||||
|
||||
private async getNewTimeseriesData() {
|
||||
const updateTime = new Date().getTime() / 1000;
|
||||
this.addLoad();
|
||||
for (const timeseries of this.state.timeseries) {
|
||||
for (const timeseries of this.state.leftTimeseries) {
|
||||
await timeseries.getLatest();
|
||||
}
|
||||
for (const timeseries of this.state.rightTimeseries) {
|
||||
await timeseries.getLatest();
|
||||
}
|
||||
this.finishLoad();
|
||||
for (const timeseries of this.state.timeseries) {
|
||||
this.notifyStoreVal("timeseries");
|
||||
this.setLastUpdateTime(updateTime);
|
||||
this.notifyAllTimeseriesUpdated();
|
||||
}
|
||||
|
||||
private notifyAllTimeseriesUpdated() {
|
||||
for (const timeseries of this.state.leftTimeseries) {
|
||||
this.notifyStoreVal("leftTimeseries");
|
||||
this.eventCallbacks["timeseriesUpdated"].forEach(cb => cb(timeseries));
|
||||
}
|
||||
for (const timeseries of this.state.rightTimeseries) {
|
||||
this.notifyStoreVal("rightTimeseries");
|
||||
this.eventCallbacks["timeseriesUpdated"].forEach(cb => cb(timeseries));
|
||||
}
|
||||
this.setLastUpdateTime(updateTime);
|
||||
}
|
||||
|
||||
getState(): AppState {
|
||||
@@ -134,11 +151,13 @@ 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();
|
||||
}
|
||||
} else {
|
||||
throw new AppStateError(`Invalid display window from ${newWin.start} to ${newWin.stop}`);
|
||||
console.warn(`Invalid display window from ${newWin.start} to ${newWin.stop}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class Timeseries {
|
||||
this.loader = loader;
|
||||
this.name = name;
|
||||
this.tolerance = tolerance ?? 0;
|
||||
this.colour = `rgb(${Math.random() * 255},${Math.random() * 255},${Math.random() * 255})`;
|
||||
this.colour = `rgb(${Math.random() * 150},${Math.random() * 150},${Math.random() * 150})`;
|
||||
}
|
||||
|
||||
getExtrema(): Extrema {
|
||||
@@ -51,7 +51,7 @@ class Timeseries {
|
||||
return new Int32Array();
|
||||
} else {
|
||||
return this.cache.slice(
|
||||
this.findIndexInCache(start),
|
||||
this.findIndexInCache(start) - 1,
|
||||
this.findIndexInCache(stop)
|
||||
);
|
||||
}
|
||||
@@ -164,9 +164,13 @@ class Timeseries {
|
||||
}
|
||||
|
||||
private findIndexInCache(soughtIndex: number) {
|
||||
return this.findIndexInCacheBinary(soughtIndex);
|
||||
}
|
||||
|
||||
private findIndexInCacheLinear(soughtIndex: number) {
|
||||
for (let i = 1; i < this.cache.length; i += 2) {
|
||||
if (soughtIndex < this.cache[i]) {
|
||||
return i - 1;
|
||||
return i > 3 ? i - 3 : i - 1;
|
||||
}
|
||||
}
|
||||
return this.cache.length - 2;
|
||||
@@ -174,7 +178,7 @@ class Timeseries {
|
||||
|
||||
private findIndexInCacheBinary(soughtIndex: number, listStart = 0, listStop: number = (this.currentEndPointer / 2)): number {
|
||||
if (listStop - listStart === 1) {
|
||||
return listStart;
|
||||
return listStart * 2 + 1;
|
||||
} else {
|
||||
const middle = Math.floor((listStop + listStart) / 2);
|
||||
const val = this.cache[middle * 2 + 1];
|
||||
@@ -183,7 +187,7 @@ class Timeseries {
|
||||
} else if (val < soughtIndex) {
|
||||
return this.findIndexInCacheBinary(soughtIndex, middle, listStop);
|
||||
} else {
|
||||
return middle;
|
||||
return middle * 2 + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import {ChartConfiguration, ChartPoint, TimeUnit} from "chart.js";
|
||||
|
||||
interface ClimateChartSettings {
|
||||
humidity?: ChartPoint[];
|
||||
temp?: ChartPoint[];
|
||||
co2?: ChartPoint[];
|
||||
colors?: {
|
||||
humidity?: string;
|
||||
temp?: string;
|
||||
co2?: string;
|
||||
}
|
||||
}
|
||||
|
||||
const defaultHumidityColor = "rgb(196,107,107)";
|
||||
const defaultTempColor = "rgb(173,136,68)";
|
||||
const defaultCo2Color = "rgb(52,133,141)";
|
||||
|
||||
export function generateClimateChartConfig(settings: ClimateChartSettings): ChartConfiguration {
|
||||
return {
|
||||
type: "line",
|
||||
data: {
|
||||
datasets: [{
|
||||
label: "Humidity",
|
||||
data: settings.humidity,
|
||||
borderColor: settings.colors?.humidity ?? defaultHumidityColor,
|
||||
fill: false,
|
||||
yAxisID: "y-axis-3",
|
||||
}, {
|
||||
label: "Temperature",
|
||||
data: settings.temp,
|
||||
borderColor: settings.colors?.temp ?? defaultTempColor,
|
||||
fill: false,
|
||||
yAxisID: "y-axis-2",
|
||||
}, {
|
||||
label: "Co2 Concentration",
|
||||
data: settings.co2,
|
||||
borderColor: settings.colors?.co2 ?? defaultCo2Color,
|
||||
fill: false,
|
||||
yAxisID: "y-axis-1",
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
animation: {animateRotate: false, duration: 0, animateScale: false},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
legend: {
|
||||
position: "top",
|
||||
align: "end",
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: "time",
|
||||
time: {
|
||||
unit: "second" as TimeUnit
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
type: "linear",
|
||||
display: true,
|
||||
position: "right",
|
||||
id: "y-axis-1",
|
||||
ticks: {
|
||||
fontColor: settings.colors?.co2 ?? defaultCo2Color,
|
||||
suggestedMin: 400,
|
||||
suggestedMax: 1100,
|
||||
},
|
||||
}, {
|
||||
type: "linear",
|
||||
display: true,
|
||||
position: "left",
|
||||
id: "y-axis-2",
|
||||
ticks: {
|
||||
fontColor: settings.colors?.temp ?? defaultTempColor,
|
||||
suggestedMin: 10,
|
||||
suggestedMax: 35,
|
||||
},
|
||||
gridLines: {
|
||||
drawOnChartArea: false,
|
||||
},
|
||||
}, {
|
||||
type: "linear",
|
||||
display: true,
|
||||
position: "left",
|
||||
id: "y-axis-3",
|
||||
ticks: {
|
||||
fontColor: settings.colors?.humidity ?? defaultHumidityColor,
|
||||
suggestedMin: 15,
|
||||
suggestedMax: 85,
|
||||
},
|
||||
gridLines: {
|
||||
drawOnChartArea: false,
|
||||
},
|
||||
}],
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import {AppStore, getAppState, initStore} from "./StateStore";
|
||||
import AppUI from "./ui-components/AppUI";
|
||||
import Timeseries from "./Timeseries";
|
||||
import {ClayPIDashboardError} from "./errors";
|
||||
import {ScaleId} from "./ClimateChart";
|
||||
|
||||
export {config};
|
||||
|
||||
function getDisplayedMinutes() {
|
||||
@@ -35,23 +37,32 @@ async function init() {
|
||||
fatalError: null,
|
||||
displayWindow: {start: now - getDisplayedMinutes() * 60, stop: now},
|
||||
documentReady: false,
|
||||
timeseries: [],
|
||||
leftTimeseries: [],
|
||||
rightTimeseries: [],
|
||||
});
|
||||
AppStore().addTimeseries(new Timeseries(
|
||||
AppStore().addTimeseries(
|
||||
new Timeseries(
|
||||
"temp",
|
||||
(start, stop) => loadClimateTimeseriesData("temp", start, stop),
|
||||
getAppState().updateIntervalSeconds
|
||||
));
|
||||
AppStore().addTimeseries(new Timeseries(
|
||||
),
|
||||
ScaleId.Left);
|
||||
AppStore().addTimeseries(
|
||||
new Timeseries(
|
||||
"humidity",
|
||||
(start, stop) => loadClimateTimeseriesData("humidity", start, stop),
|
||||
getAppState().updateIntervalSeconds
|
||||
));
|
||||
AppStore().addTimeseries(new Timeseries(
|
||||
),
|
||||
ScaleId.Left
|
||||
);
|
||||
AppStore().addTimeseries(
|
||||
new Timeseries(
|
||||
"co2",
|
||||
(start, stop) => loadClimateTimeseriesData("co2", start, stop),
|
||||
getAppState().updateIntervalSeconds
|
||||
));
|
||||
),
|
||||
ScaleId.Right
|
||||
);
|
||||
const ui = new AppUI();
|
||||
ui.bootstrap("root");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {AppStore, DisplayMode, getAppState} from "../StateStore";
|
||||
import GridWidget, {GridProps} from "./GridWidget";
|
||||
import UIComponent from "./UIComponent";
|
||||
import ClimateChart from "../ClimateChart";
|
||||
import ClimateChart, {ScaleId} from "../ClimateChart";
|
||||
|
||||
class ClimateChartWidget extends UIComponent {
|
||||
private readonly skeleton: GridWidget;
|
||||
@@ -54,9 +54,8 @@ class ClimateChartWidget extends UIComponent {
|
||||
AppStore().addLoad();
|
||||
const ctx = this.canvasElement.getContext("2d", {alpha: false});
|
||||
this.chart = new ClimateChart(ctx);
|
||||
for (const timeseries of getAppState().timeseries) {
|
||||
this.chart.addTimeseries(timeseries);
|
||||
}
|
||||
getAppState().leftTimeseries.forEach(timeseries => this.chart.addTimeseries(timeseries, ScaleId.Left));
|
||||
getAppState().rightTimeseries.forEach(timeseries => this.chart.addTimeseries(timeseries, ScaleId.Right));
|
||||
await this.rerender();
|
||||
this.initialised = true;
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user