big update with better scales, massive ui additions, etc.
This commit is contained in:
@@ -6,12 +6,14 @@ import {GridSize} from "./GridWidget";
|
||||
import MessageOverlay from "./MessageOverlay";
|
||||
import UIComponent from "./UIComponent";
|
||||
import SelectDisplayModeWidget from "./SelectDisplayModeWidget";
|
||||
import LegendWidget from "./LegendWidget";
|
||||
|
||||
class AppUI extends UIComponent {
|
||||
private timezoneWidget: TimezoneWidget;
|
||||
private selectModeWidget: SelectDisplayModeWidget;
|
||||
private displayModeSettingsWidget: DisplayModeWidget;
|
||||
private timerWidget: TimerWidget;
|
||||
private legendWidget: LegendWidget;
|
||||
private chartWidget: ClimateChartWidget;
|
||||
private element: HTMLDivElement = document.createElement("div");
|
||||
private grid: HTMLDivElement = document.createElement("div");
|
||||
@@ -31,6 +33,7 @@ class AppUI extends UIComponent {
|
||||
private setupGrid(size: GridSize) {
|
||||
this.setupWidgets();
|
||||
this.grid.append(
|
||||
this.legendWidget.current(),
|
||||
this.chartWidget.current(),
|
||||
this.displayModeSettingsWidget.current(),
|
||||
this.selectModeWidget.current(),
|
||||
@@ -50,10 +53,13 @@ class AppUI extends UIComponent {
|
||||
row: "auto", col: 5, width: 1, height: 2,
|
||||
});
|
||||
this.timezoneWidget = new TimezoneWidget({
|
||||
row: "auto", col: 5, width: 1, height: 2,
|
||||
row: "auto", col: 5, width: 1, height: 1,
|
||||
});
|
||||
this.timerWidget = new TimerWidget({
|
||||
row: "auto", col: 5, width: 1, height: 3,
|
||||
row: "auto", col: 5, width: 1, height: 2,
|
||||
});
|
||||
this.legendWidget = new LegendWidget({
|
||||
row: "auto", col: 5, width: 1, height: 2,
|
||||
});
|
||||
this.chartWidget = new ClimateChartWidget({
|
||||
row: 1, col: 1, width: 4, height: 10,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {AppStore, DisplayMode, getAppState} from "../StateStore";
|
||||
import GridWidget, {GridProps} from "./GridWidget";
|
||||
import UIComponent from "./UIComponent";
|
||||
import ClimateChart, {ScaleId} from "../ClimateChart";
|
||||
import Chart, {ScaleId} from "../chart/Chart";
|
||||
|
||||
class ClimateChartWidget extends UIComponent {
|
||||
private readonly skeleton: GridWidget;
|
||||
private chart: ClimateChart | null = null;
|
||||
private chart: Chart | null = null;
|
||||
private initialised: boolean;
|
||||
private displayMode: DisplayMode = "pastMins";
|
||||
private latestSnapshotInChartTime: number;
|
||||
@@ -22,6 +22,7 @@ class ClimateChartWidget extends UIComponent {
|
||||
const now = new Date().getTime() / 1000;
|
||||
this.latestSnapshotInChartTime = now - getAppState().minutesDisplayed * 60;
|
||||
this.setupListeners();
|
||||
this.updateDisplayMode();
|
||||
}
|
||||
|
||||
updateDimensions() {
|
||||
@@ -42,6 +43,36 @@ class ClimateChartWidget extends UIComponent {
|
||||
AppStore().on("newTimeseries", (timeseries) => this.chart.addTimeseries(timeseries));
|
||||
AppStore().subscribeStoreVal("documentReady", () => this.initChart());
|
||||
AppStore().subscribeStoreVal("utcOffset", () => this.updateTimezone());
|
||||
AppStore().subscribeStoreVal("highlightedTimeseries", (name) => this.chart.highlightTimeseries(name));
|
||||
}
|
||||
|
||||
private handleScroll(direction: number, magnitude: number, index: number) {
|
||||
let displayedWindow = getAppState().displayWindow;
|
||||
if (getAppState().displayMode === "pastMins") {
|
||||
AppStore().setDisplayMode("window");
|
||||
const now = new Date().getTime() / 1000;
|
||||
displayedWindow = {start: now - getAppState().minutesDisplayed * 60, stop: now};
|
||||
}
|
||||
const beforeIndex = index - displayedWindow.start;
|
||||
const afterIndex = displayedWindow.stop - index;
|
||||
const factor = direction === 1 ? 1.1 : 0.9;
|
||||
const newBeforeIndex = factor * beforeIndex;
|
||||
const newAfterIndex = factor * afterIndex;
|
||||
AppStore().setDisplayWindow({
|
||||
start: index - newBeforeIndex,
|
||||
stop: index + newAfterIndex,
|
||||
});
|
||||
}
|
||||
|
||||
private handleDrag(deltaX: number, deltaY: number, deltaIndex: number) {
|
||||
if (getAppState().displayMode === "pastMins") {
|
||||
AppStore().setDisplayMode("window");
|
||||
}
|
||||
const displayWindow = getAppState().displayWindow;
|
||||
AppStore().setDisplayWindow({
|
||||
start: displayWindow.start + deltaIndex,
|
||||
stop: displayWindow.stop + deltaIndex,
|
||||
});
|
||||
}
|
||||
|
||||
private updateTimezone() {
|
||||
@@ -53,9 +84,11 @@ class ClimateChartWidget extends UIComponent {
|
||||
try {
|
||||
AppStore().addLoad();
|
||||
const ctx = this.canvasElement.getContext("2d", {alpha: false});
|
||||
this.chart = new ClimateChart(ctx);
|
||||
this.chart = new Chart(ctx);
|
||||
getAppState().leftTimeseries.forEach(timeseries => this.chart.addTimeseries(timeseries, ScaleId.Left));
|
||||
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;
|
||||
} catch (e) {
|
||||
|
||||
@@ -7,7 +7,9 @@ class DisplayModeWidget extends UIComponent {
|
||||
private skeleton: GridWidget;
|
||||
private minsCounterRef: number;
|
||||
private windowStartTimeRef: number;
|
||||
private windowStartTimeInputRef: number;
|
||||
private windowStopTimeRef: number;
|
||||
private windowStopTimeInputRef: number;
|
||||
private windowedDisplayRef: number;
|
||||
private minsDisplayRef: number;
|
||||
private mainDisplay: HTMLElement;
|
||||
@@ -24,20 +26,35 @@ class DisplayModeWidget extends UIComponent {
|
||||
AppStore().subscribeStoreVal("minutesDisplayed", () => this.updateDisplay());
|
||||
AppStore().subscribeStoreVal("displayMode", () => this.updateDisplay());
|
||||
AppStore().subscribeStoreVal("displayWindow", () => this.updateDisplay());
|
||||
AppStore().subscribeStoreVal("utcOffset", () => this.updateDisplay());
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
private WindowStartTime({ ctx }: {ctx: DisplayModeWidget}) {
|
||||
ctx.windowStartTimeInputRef = ctx.makeRef(<input
|
||||
type={"datetime-local"}
|
||||
onblur={() => ctx.onWindowStartInputBlur()}
|
||||
/>);
|
||||
ctx.windowStartTimeRef = ctx.makeRef(<div
|
||||
className={"display-mode-widget-date"}>
|
||||
{new Date(getAppState().displayWindow.start).toLocaleString()}
|
||||
className={"display-mode-widget-date"}
|
||||
onwheel={(e: WheelEvent) => ctx.onStartTimeInputScroll(e)}
|
||||
onclick={() => ctx.onWindowStartDisplayClick()}>
|
||||
{new Date(getAppState().displayWindow.start + getAppState().utcOffset * 60 * 60 * 1000).toLocaleString()}
|
||||
</div>);
|
||||
return ctx.fromRef(ctx.windowStartTimeRef);
|
||||
}
|
||||
|
||||
private WindowStopTime({ctx}: {ctx: DisplayModeWidget}) {
|
||||
ctx.windowStopTimeInputRef = ctx.makeRef(<input
|
||||
value={new Date()}
|
||||
type={"datetime-local"}
|
||||
onblur={() => ctx.onWindowStopInputBlur()}
|
||||
/>);
|
||||
ctx.windowStopTimeRef = ctx.makeRef(<div
|
||||
className={"display-mode-widget-date"}>
|
||||
{new Date(getAppState().displayWindow.stop).toLocaleString()}
|
||||
className={"display-mode-widget-date"}
|
||||
onwheel={(e: WheelEvent) => ctx.onStopTimeInputScroll(e)}
|
||||
onclick={() => ctx.onWindowStopDisplayClick()}>
|
||||
{new Date(getAppState().displayWindow.stop + getAppState().utcOffset * 60 * 60 * 1000).toLocaleString()}
|
||||
</div>);
|
||||
return ctx.fromRef(ctx.windowStopTimeRef);
|
||||
}
|
||||
@@ -48,12 +65,26 @@ class DisplayModeWidget extends UIComponent {
|
||||
value={getAppState().minutesDisplayed.toString()}
|
||||
onblur={(e: FocusEvent) => ctx.onMinutesCounterInputBlur(e)}/>);
|
||||
ctx.minsCounterRef = ctx.makeRef(
|
||||
<div className={"min-count"} onclick={onclick}>
|
||||
<div className={"min-count"} onclick={onclick} onwheel={(e: WheelEvent) => ctx.onMinutesCounterInputScroll(e)}>
|
||||
{getAppState().minutesDisplayed.toString()}
|
||||
</div>);
|
||||
return ctx.fromRef(ctx.minsCounterRef);
|
||||
}
|
||||
|
||||
private onMinutesCounterInputScroll(e: WheelEvent) {
|
||||
AppStore().setMinutesDisplayed(getAppState().minutesDisplayed + e.deltaY);
|
||||
}
|
||||
|
||||
private onStopTimeInputScroll(e: WheelEvent) {
|
||||
const oldWin = getAppState().displayWindow;
|
||||
AppStore().setDisplayWindow({start: oldWin.start, stop: oldWin.stop - e.deltaY * 60});
|
||||
}
|
||||
|
||||
private onStartTimeInputScroll(e: WheelEvent) {
|
||||
const oldWin = getAppState().displayWindow;
|
||||
AppStore().setDisplayWindow({start: oldWin.start - e.deltaY * 60, stop: oldWin.stop});
|
||||
}
|
||||
|
||||
private onMinutesCounterInputBlur(e: FocusEvent) {
|
||||
const input = Number((e.target as HTMLInputElement).value);
|
||||
if (!isNaN(input)) {
|
||||
@@ -90,6 +121,51 @@ class DisplayModeWidget extends UIComponent {
|
||||
input.selectionEnd = input.value.length;
|
||||
}
|
||||
|
||||
private onWindowStopDisplayClick() {
|
||||
const stopTimeDisplay = this.fromRef(this.windowStopTimeRef);
|
||||
(stopTimeDisplay as HTMLInputElement).valueAsDate = new Date(getAppState().displayWindow.stop);
|
||||
const stopTimeInputDisplay = this.fromRef(this.windowStopTimeInputRef) as HTMLInputElement;
|
||||
stopTimeDisplay.replaceWith(stopTimeInputDisplay);
|
||||
const date = new Date(getAppState().displayWindow.stop * 1000 + getAppState().utcOffset * 60 * 60 * 1000);
|
||||
stopTimeInputDisplay.value = `${date.toLocaleDateString()}, ${date.toLocaleTimeString()}`;
|
||||
stopTimeInputDisplay.focus();
|
||||
}
|
||||
|
||||
private onWindowStopInputBlur() {
|
||||
const stopTimeInput = this.fromRef(this.windowStopTimeInputRef);
|
||||
const val = new Date((stopTimeInput as HTMLInputElement).value).getTime() / 1000;
|
||||
if (!isNaN(val)) {
|
||||
AppStore().setDisplayWindow({
|
||||
start: getAppState().displayWindow.start,
|
||||
stop: val
|
||||
});
|
||||
}
|
||||
stopTimeInput.replaceWith(this.fromRef(this.windowStopTimeRef));
|
||||
}
|
||||
|
||||
private onWindowStartDisplayClick() {
|
||||
const startTimeDisplay = this.fromRef(this.windowStartTimeRef);
|
||||
(startTimeDisplay as HTMLInputElement).valueAsDate = new Date(getAppState().displayWindow.start);
|
||||
const startTimeInputDisplay = this.fromRef(this.windowStartTimeInputRef) as HTMLInputElement;
|
||||
startTimeDisplay.replaceWith(startTimeInputDisplay);
|
||||
const date = new Date(getAppState().displayWindow.start * 1000 + getAppState().utcOffset * 60 * 60 * 1000);
|
||||
startTimeInputDisplay.value = `${date.toLocaleDateString()}, ${date.toLocaleTimeString()}`;
|
||||
startTimeInputDisplay.focus();
|
||||
|
||||
}
|
||||
|
||||
private onWindowStartInputBlur() {
|
||||
const startTimeInput = this.fromRef(this.windowStartTimeInputRef);
|
||||
const val = new Date((startTimeInput as HTMLInputElement).value).getTime() / 1000;
|
||||
if (!isNaN(val)) {
|
||||
AppStore().setDisplayWindow({
|
||||
start: val,
|
||||
stop: getAppState().displayWindow.stop,
|
||||
});
|
||||
}
|
||||
startTimeInput.replaceWith(this.fromRef(this.windowStartTimeRef));
|
||||
}
|
||||
|
||||
private MinusButton(props: {onclick: () => any}) {
|
||||
return <div
|
||||
className={"minus-button"}
|
||||
@@ -147,8 +223,11 @@ class DisplayModeWidget extends UIComponent {
|
||||
private updateDisplay() {
|
||||
if (getAppState().displayMode === "window") {
|
||||
this.mainDisplay.children.item(0).replaceWith(this.fromRef(this.windowedDisplayRef));
|
||||
this.fromRef(this.windowStartTimeRef).innerText = new Date(getAppState().displayWindow.start * 1000).toLocaleString();
|
||||
this.fromRef(this.windowStopTimeRef).innerText = new Date(getAppState().displayWindow.stop * 1000).toLocaleString();
|
||||
const offset = getAppState().utcOffset * 60 * 60;
|
||||
const startDate = new Date((getAppState().displayWindow.start + offset) * 1000);
|
||||
const stopDate = new Date((getAppState().displayWindow.stop + offset) * 1000);
|
||||
this.fromRef(this.windowStartTimeRef).innerText = startDate.toLocaleString();
|
||||
this.fromRef(this.windowStopTimeRef).innerText = stopDate.toLocaleString();
|
||||
} else {
|
||||
this.mainDisplay.children.item(0).replaceWith(this.fromRef(this.minsDisplayRef));
|
||||
this.fromRef(this.minsCounterRef).innerText = getAppState().minutesDisplayed.toString();
|
||||
|
||||
65
dashboard/src/ui-components/LegendWidget.tsx
Normal file
65
dashboard/src/ui-components/LegendWidget.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import GridWidget, {GridProps} from "./GridWidget";
|
||||
import {AppStore, getAppState} from "../StateStore";
|
||||
import UIComponent from "./UIComponent";
|
||||
import * as JSX from "../JSXFactory";
|
||||
import Timeseries from "../Timeseries";
|
||||
|
||||
class LegendWidget extends UIComponent {
|
||||
private skeleton: GridWidget;
|
||||
private display: HTMLSpanElement = document.createElement("span");
|
||||
private bodyRef: number;
|
||||
|
||||
constructor(gridProps: GridProps) {
|
||||
super();
|
||||
this.display = <this.MainBody ctx={this}/>;
|
||||
this.skeleton = new GridWidget({
|
||||
...gridProps,
|
||||
title: "Legend:",
|
||||
className: "legend-widget",
|
||||
body: this.display,
|
||||
});
|
||||
AppStore().subscribeStoreVal("highlightedTimeseries", () => this.updateDisplay());
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
private updateDisplay() {
|
||||
this.fromRef(this.bodyRef).replaceWith(<this.MainBody ctx={this}/>);
|
||||
}
|
||||
|
||||
private MainBody({ctx}: {ctx: LegendWidget}) {
|
||||
ctx.bodyRef = ctx.makeRef(<div><ctx.TimeseriesList ctx={ctx}/></div>);
|
||||
return ctx.fromRef(ctx.bodyRef);
|
||||
}
|
||||
|
||||
private TimeseriesList({ctx}: { ctx: LegendWidget }) {
|
||||
const highlightedTimeseries = getAppState().highlightedTimeseries;
|
||||
return <ul>
|
||||
{ ...getAppState().rightTimeseries.map(timeseries =>
|
||||
<ctx.TimeseriesLegendEntry
|
||||
timeseries={timeseries}
|
||||
highlighted={timeseries.getName() === highlightedTimeseries}/>) }
|
||||
{ ...getAppState().leftTimeseries.map(timeseries =>
|
||||
<ctx.TimeseriesLegendEntry
|
||||
timeseries={timeseries}
|
||||
highlighted={timeseries.getName() === highlightedTimeseries}/>) }
|
||||
</ul>;
|
||||
}
|
||||
|
||||
private TimeseriesLegendEntry({timeseries, highlighted}: {timeseries: Timeseries, highlighted: boolean}) {
|
||||
const option = new Option();
|
||||
option.style.color = timeseries.getColour();
|
||||
return <li
|
||||
style={`color: ${option.style.color}`}
|
||||
className={highlighted ? "highlighted" : ""}
|
||||
onmouseover={() => AppStore().setHighlightedTimeseries(timeseries.getName())}
|
||||
onmouseout={() => AppStore().setHighlightedTimeseries(null)}>
|
||||
{timeseries.getName()}
|
||||
</li>;
|
||||
}
|
||||
|
||||
current() {
|
||||
return this.skeleton.current();
|
||||
}
|
||||
}
|
||||
|
||||
export default LegendWidget;
|
||||
@@ -8,6 +8,8 @@ export default class SelectDisplayModeWidget extends UIComponent {
|
||||
private gridWidgetSkeleton: GridWidget;
|
||||
private windowInputRef: number;
|
||||
private minSpanInputRef: number;
|
||||
private windowInputContainerRef: number;
|
||||
private minSpanInputContainerRef: number;
|
||||
constructor(gridProps: GridProps) {
|
||||
super();
|
||||
this.mainBody = this.MainBody({ctx: this});
|
||||
@@ -27,6 +29,13 @@ export default class SelectDisplayModeWidget extends UIComponent {
|
||||
const windowedMode = getAppState().displayMode === "window";
|
||||
(this.fromRef(this.windowInputRef) as HTMLInputElement).checked = windowedMode;
|
||||
(this.fromRef(this.minSpanInputRef) as HTMLInputElement).checked = !windowedMode;
|
||||
if (!windowedMode) {
|
||||
this.fromRef(this.minSpanInputContainerRef).classList.add("selected");
|
||||
this.fromRef(this.windowInputContainerRef).classList.remove("selected");
|
||||
} else {
|
||||
this.fromRef(this.minSpanInputContainerRef).classList.remove("selected");
|
||||
this.fromRef(this.windowInputContainerRef).classList.add("selected");
|
||||
}
|
||||
}
|
||||
|
||||
private MainBody({ ctx }: { ctx: SelectDisplayModeWidget }) {
|
||||
@@ -35,23 +44,27 @@ export default class SelectDisplayModeWidget extends UIComponent {
|
||||
type={"radio"}
|
||||
id={"window"}
|
||||
name={"display-mode"}
|
||||
checked={isInWindowMode}
|
||||
onclick={() => ctx.selectMode("window")}/>);
|
||||
checked={isInWindowMode}/>);
|
||||
ctx.minSpanInputRef = this.makeRef(<input
|
||||
type={"radio"}
|
||||
id={"min-span"}
|
||||
name={"display-mode"}
|
||||
checked={!isInWindowMode}
|
||||
onclick={() => ctx.selectMode("pastMins")}/>);
|
||||
checked={!isInWindowMode}/>);
|
||||
ctx.windowInputContainerRef = this.makeRef(<div
|
||||
className={`display-mode-option${isInWindowMode ? " selected" : ""}`}
|
||||
onclick={() => ctx.selectMode("window")}>
|
||||
{this.fromRef(ctx.windowInputRef)}
|
||||
<label htmlFor={"window"}>Time Window</label>
|
||||
</div>);
|
||||
ctx.minSpanInputContainerRef = this.makeRef(<div
|
||||
className={`display-mode-option${!isInWindowMode ? " selected" : ""}`}
|
||||
onclick={() => ctx.selectMode("pastMins")}>
|
||||
{this.fromRef(ctx.minSpanInputRef)}
|
||||
<label htmlFor={"minSpan"}>Rolling Minute Span</label>
|
||||
</div>);
|
||||
return (<div>
|
||||
<div>
|
||||
{this.fromRef(ctx.windowInputRef)}
|
||||
<label htmlFor={"window"}>Time Window</label>
|
||||
</div>
|
||||
<div>
|
||||
{this.fromRef(ctx.minSpanInputRef)}
|
||||
<label htmlFor={"minSpan"}>Rolling Minute Span</label>
|
||||
</div>
|
||||
{this.fromRef(ctx.windowInputContainerRef)}
|
||||
{this.fromRef(ctx.minSpanInputContainerRef)}
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,16 +20,21 @@ class TimerWidget extends UIComponent {
|
||||
body: this.display,
|
||||
});
|
||||
AppStore().subscribeStoreVal("lastUpdateTime", () => this.resetTimer());
|
||||
AppStore().subscribeStoreVal("utcOffset", () => this.resetTimer());
|
||||
setInterval(() => this.refreshTimer(), 10);
|
||||
this.resetTimer();
|
||||
}
|
||||
|
||||
private resetTimer() {
|
||||
this.nextUpdateTime = getAppState().lastUpdateTime + getAppState().updateIntervalSeconds;
|
||||
this.fromRef(this.lastUpdateRef).innerText = new Date(getAppState().lastUpdateTime * 1000).toLocaleString();
|
||||
this.updateUpdateText();
|
||||
this.refreshTimer();
|
||||
}
|
||||
|
||||
private updateUpdateText() {
|
||||
this.fromRef(this.lastUpdateRef).innerText = new Date(getAppState().lastUpdateTime * 1000 + getAppState().utcOffset * 60 * 60 * 1000).toLocaleString();
|
||||
}
|
||||
|
||||
private MainDisplay({ ctx }: { ctx: TimerWidget }) {
|
||||
ctx.timerRef = ctx.makeRef(<div className={"countdown"}/>);
|
||||
ctx.lastUpdateRef = ctx.makeRef(
|
||||
|
||||
Reference in New Issue
Block a user