it's done i think

This commit is contained in:
Daniel Ledda
2021-04-05 16:19:12 +02:00
parent 729db5ede1
commit 1fba1dbff1
24 changed files with 636 additions and 59 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -5,6 +5,7 @@
<title>Ledda's Room Climate</title>
<link type="text/css" href="/styles.css" rel="stylesheet" />
<script type="application/javascript" src="/dashboard.js"></script>
<link rel="shortcut icon" type="image/jpg" href="/favicon64.png"/>
</head>
<body>
<div id="root"></div>

View File

@@ -11,8 +11,8 @@ html, body {
}
.main-content-grid {
height: 80%;
width: 80%;
width: calc(100% - 12em);
height: calc(100% - 10em);
display: grid;
text-align: center;
}
@@ -199,4 +199,67 @@ h1 {
.legend-widget li.highlighted {
font-weight: bold;
cursor: pointer;
}
.help-box {
position: relative;
background-color: #ffffff;
border: solid 1px #7b5b2f;
width: 40em;
padding: 1em;
z-index: 1;
}
.help-box img {
}
.help-box h1 {
margin: auto;
text-align: center;
}
.help-box .image-advice {
align-items: center;
margin-top: 1em;
margin-bottom: 1em;
display: flex;
}
.help-box .image-advice * {
display: inline-block;
padding: 0.5em;
}
.x-button:before {
content: "×";
font-size: 1em;
line-height: 1em;
}
.x-button {
cursor: pointer;
text-align: center;
background-color: white;
height: 1.2em;
width: 1.2em;
position: absolute;
right: 0;
top: 0;
}
.help-button {
cursor: pointer;
width: 2em;
height: 2em;
margin: 0.5em;
position: absolute;
right: 0;
top: 0;
}
.help-button:hover {
filter: brightness(1.1);
}
.help-button:active {
filter: brightness(0.9);
}
.button:hover {
filter: brightness(1.1);
}
.button:active {
filter: brightness(0.9);
}

View File

@@ -0,0 +1,266 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="64"
height="64"
viewBox="0 0 16.933333 16.933334"
version="1.1"
id="svg3486"
inkscape:version="1.0.2 (394de47547, 2021-03-26)"
sodipodi:docname="favicon.svg"
inkscape:export-filename="/home/ledda/Documents/Projects/climate-server/dashboard/assets/favicon16.png"
inkscape:export-xdpi="24"
inkscape:export-ydpi="24">
<defs
id="defs3480">
<linearGradient
inkscape:collect="always"
id="linearGradient867">
<stop
style="stop-color:#e3e3e3;stop-opacity:1"
offset="0"
id="stop863" />
<stop
style="stop-color:#1c56f8;stop-opacity:1"
offset="0.57295728"
id="stop869" />
<stop
style="stop-color:#002ca7;stop-opacity:1"
offset="1"
id="stop865" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient4914">
<stop
style="stop-color:#cccccc;stop-opacity:1"
offset="0"
id="stop4910" />
<stop
style="stop-color:#070a02;stop-opacity:1"
offset="1"
id="stop4912" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient4894">
<stop
style="stop-color:#f6dbdb;stop-opacity:1"
offset="0"
id="stop4890" />
<stop
style="stop-color:#f14a4a;stop-opacity:1"
offset="0.5394209"
id="stop4908" />
<stop
style="stop-color:#bd0000;stop-opacity:1"
offset="1"
id="stop4892" />
</linearGradient>
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="13.033485 : 17.411423 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="-9.7154793 : 15.520856 : 1"
inkscape:persp3d-origin="8.4666665 : 5.6444447 : 1"
id="perspective4365" />
<linearGradient
inkscape:collect="always"
id="linearGradient4260-6">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop4256" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop4258" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient4260-6"
id="radialGradient4262"
cx="4.7302384"
cy="6.0053997"
fx="4.7302384"
fy="6.0053997"
r="2.4687963"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
id="linearGradient4475">
<stop
style="stop-color:#d0ecac;stop-opacity:1"
offset="0"
id="stop4471" />
<stop
style="stop-color:#070a02;stop-opacity:1"
offset="1"
id="stop4473" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient4475"
id="radialGradient4477"
cx="4.0612016"
cy="5.0891728"
fx="4.0612016"
fy="5.0891728"
r="2.4687963"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient4914"
id="radialGradient4477-6"
cx="4.0612016"
cy="5.0891728"
fx="4.0612016"
fy="5.0891728"
r="2.4687963"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient4894"
id="radialGradient4433"
cx="-5.0382247"
cy="10.337361"
fx="-5.0382247"
fy="10.337361"
r="2.4677463"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.2725204,0.33871684,-0.33871684,-1.2725204,-7.9480302,25.198396)" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient4894"
id="radialGradient4441"
cx="5.30828"
cy="12.653597"
fx="5.30828"
fy="12.653597"
r="2.5259755"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient5215"
id="radialGradient4477-6-5"
cx="4.0612016"
cy="5.0891728"
fx="4.0612016"
fy="5.0891728"
r="2.4687963"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
id="linearGradient5215">
<stop
style="stop-color:#e3e3e3;stop-opacity:1"
offset="0"
id="stop5211" />
<stop
style="stop-color:#070a02;stop-opacity:1"
offset="1"
id="stop5213" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient867"
id="radialGradient4477-6-5-1"
cx="4.0612016"
cy="5.0891728"
fx="4.0612016"
fy="5.0891728"
r="2.4687963"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0341543,1.0341543,-1.0341543,1.0341543,5.1242825,-4.3737261)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="10.24"
inkscape:cx="8.3396846"
inkscape:cy="30.850988"
inkscape:document-units="mm"
inkscape:current-layer="layer3"
inkscape:document-rotation="0"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1043"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata3483">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline">
<g
id="g5377">
<path
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 5.0438123,6.1176582 12.47978,3.1707767"
id="path4106-3" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 4.5225786,6.363151 0.2192101,7.995604"
id="path4106-6" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 4.2469752,6.0364594 11.682943,3.0895779"
id="path4106" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 4.905109,6.4443498 5.1243191,14.439954"
id="path4106-6-4" />
<circle
style="fill:url(#radialGradient4433);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.63382;stroke-opacity:1;stop-color:#000000"
id="path4049-9-9"
cx="-6.2022243"
cy="10.932185"
transform="rotate(-105.09473)"
r="2.4677463" />
<circle
style="fill:url(#radialGradient4441);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.63382;stop-color:#000000"
id="path4049-9-9-3"
cx="6.1492538"
cy="13.494351"
transform="rotate(4.9532955)"
r="2.5259755" />
</g>
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 1"
style="display:inline">
<circle
style="display:inline;mix-blend-mode:multiply;fill:url(#radialGradient4477-6-5-1);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.634;stroke-miterlimit:4;stroke-dasharray:none;stop-color:#000000"
id="path4049"
cy="6.0053997"
cx="4.7302384"
r="2.4687963" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 8.4666667 8.4666667"
version="1.1"
id="svg8"
inkscape:export-filename="/home/ledda/Documents/Projects/climate-server/dashboard/assets/help-button.png"
inkscape:export-xdpi="192"
inkscape:export-ydpi="192"
inkscape:version="1.0.2 (394de47547, 2021-03-26)"
sodipodi:docname="help-button.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="8.4763136"
inkscape:cx="22.273863"
inkscape:cy="37.692587"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
units="px"
width="15in"
inkscape:window-width="1439"
inkscape:window-height="1016"
inkscape:window-x="2257"
inkscape:window-y="209"
inkscape:window-maximized="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<ellipse
style="fill:#fff1de;fill-opacity:1;fill-rule:evenodd;stroke:#c7ab82;stroke-width:0.573164;stroke-opacity:1;stop-color:#000000"
id="path2811"
cx="4.2333336"
cy="4.2333336"
rx="3.9467516"
ry="3.9467514" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:8.0888px;line-height:1.25;font-family:sans-serif;fill:#c7ab82;fill-opacity:1;stroke:none;stroke-width:0.20222;"
x="2.5377054"
y="6.9956398"
id="text2809"><tspan
sodipodi:role="line"
id="tspan2807"
x="2.5377054"
y="6.9956398"
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:#c7ab82;fill-opacity:1;">?</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -3139,6 +3139,16 @@
"escape-string-regexp": "^1.0.5"
}
},
"file-loader": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
"integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==",
"dev": true,
"requires": {
"loader-utils": "^2.0.0",
"schema-utils": "^3.0.0"
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",

View File

@@ -17,6 +17,7 @@
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"css-loader": "^5.0.1",
"style-loader": "^2.0.0",
"file-loader": "^6.0.0",
"terser-webpack-plugin": "^5.0.3",
"ts-loader": "^8.0.18",
"uglifyjs-webpack-plugin": "^2.2.0",

View File

@@ -15,6 +15,7 @@ export interface EventCallback {
timeseriesUpdated: (timeseries: Timeseries) => void;
newTimeseries: (timeseries: Timeseries, scale?: ScaleId) => void;
stateChange: StateChangeCallback<AppState, keyof AppState>;
ready: () => void;
}
type StateChangeCallback<T, K extends keyof T> = (attrName?: K, oldVal?: T[K], newVal?: T[K]) => void;
type EventCallbackListing<K extends keyof EventCallback> = Record<K, EventCallback[K][]>;
@@ -39,6 +40,7 @@ interface AppState {
fatalError: Error | null;
documentReady: boolean;
highlightedTimeseries: string | null;
showingHelp: boolean;
}
type StoreUpdateCallback<T> = (newValue?: T, oldValue?: T) => void;
@@ -62,10 +64,11 @@ function newDefaultState(): AppState {
leftTimeseries: [],
rightTimeseries: [],
highlightedTimeseries: null,
showingHelp: false,
};
}
class AppStateStore {
class AppStateStore {
private readonly subscriptions: IAppStateSubscriptions;
private readonly eventCallbacks: EventCallbackListing<keyof EventCallback>;
private readonly state: AppState;
@@ -77,7 +80,7 @@ class AppStateStore {
for (const key in this.state) {
subscriptions[key] = [];
}
this.eventCallbacks = {newTimeseries: [], timeseriesUpdated: [], stateChange: []};
this.eventCallbacks = {newTimeseries: [], timeseriesUpdated: [], stateChange: [], ready: []};
this.subscriptions = subscriptions as IAppStateSubscriptions;
this.init();
setInterval(() => this.getNewTimeseriesData().catch(e => AppStore().fatalError(e)), this.state.updateIntervalSeconds * 1000);
@@ -86,6 +89,7 @@ class AppStateStore {
async init() {
await this.updateTimeseriesFromSettings();
await this.getNewTimeseriesData();
this.emit("ready");
}
addTimeseriesToScale(timeseries: Timeseries, scale?: ScaleId) {
@@ -191,7 +195,7 @@ class AppStateStore {
setDisplayWindow(newWin: TimeWindow) {
if (newWin.start < newWin.stop) {
if (newWin.stop < this.state.lastUpdateTime) {
if (newWin.stop <= this.state.lastUpdateTime) {
this.state.displayWindow = {...newWin};
this.notifyStoreVal("displayWindow");
this.updateTimeseriesFromSettings();
@@ -270,6 +274,24 @@ class AppStateStore {
this.notifyStoreVal("highlightedTimeseries", name);
}
emulateLastMinsWithWindow() {
this.setDisplayMode("window");
this.setDisplayWindow({
start: this.state.lastUpdateTime - getAppState().minutesDisplayed * 60 + getAppState().utcOffset * 60,
stop: this.state.lastUpdateTime
});
}
showHelp() {
this.state.showingHelp = true;
this.notifyStoreVal("showingHelp");
}
hideHelp() {
this.state.showingHelp = false;
this.notifyStoreVal("showingHelp");
}
serialiseState(): string {
const stateStringParams = [];
if (this.state.displayMode === "pastMins") {

View File

@@ -95,7 +95,7 @@ class Timeseries {
const cacheStopIndex = this.findIndexInCache(stop);
return this.cache.slice(
(cacheStartIndex - (cacheStartIndex) % blockSize),
(cacheStopIndex + blockSize - (cacheStopIndex) % blockSize),
(cacheStopIndex + blockSize - (cacheStopIndex) % blockSize) + blockSize * 2,
);
}
}

View File

@@ -40,8 +40,9 @@ export default class Chart {
constructor(context: CanvasRenderingContext2D) {
this.subscriptions = {scroll: [], mousemove: [], drag: []};
this.ctx = context;
this.initLayout();
this.updateDimensions();
this.leftScale = new Scale();
this.rightScale = new Scale();
this.updateLayout();
this.ctx.fillStyle = "rgb(255,255,255)";
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
this.ctx.fill();
@@ -54,12 +55,12 @@ export default class Chart {
this.ctx.canvas.onwheel = (e) => this.handleScroll(e);
}
private initLayout() {
updateLayout() {
const leftScaleInitialWidth = 50;
const rightScaleInitialWidth = 50;
const verticalMargins = this.margins.bottom + this.margins.top;
const horizontalMargins = this.margins.left + this.margins.right;
this.leftScale = new Scale({
this.leftScale.updateBounds({
top: this.margins.top,
left: this.margins.left,
height: this.ctx.canvas.height - verticalMargins,
@@ -71,17 +72,13 @@ export default class Chart {
height: this.ctx.canvas.height - verticalMargins,
width: this.ctx.canvas.width - (horizontalMargins + leftScaleInitialWidth + rightScaleInitialWidth),
};
this.rightScale = new Scale({
this.rightScale.updateBounds({
top: this.margins.top,
left: this.ctx.canvas.width - this.margins.right - rightScaleInitialWidth,
height: this.ctx.canvas.height - verticalMargins,
width: rightScaleInitialWidth,
});
}
private updateDimensions() {
this.chartBounds.width = Number(getComputedStyle(this.ctx.canvas).width.slice(0, -2)) - (this.margins.left + this.margins.right + this.rightScale.getBounds().width + this.leftScale.getBounds().width);
this.chartBounds.height = Number(getComputedStyle(this.ctx.canvas).height.slice(0, -2)) - (this.margins.bottom + this.margins.top);
this.render();
}
addTimeseries(timeseries: Timeseries, scale?: ScaleId) {
@@ -141,7 +138,6 @@ export default class Chart {
}
render() {
this.updateDimensions();
this.clearCanvas();
this.updateResolution();
this.renderGuides();
@@ -149,15 +145,43 @@ export default class Chart {
this.rightScale.updateIndexRange(this.indexRange);
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.renderTooltips();
}
private renderMargins() {
this.ctx.fillStyle = "rgb(255,255,255)";
this.ctx.fillRect(
this.ctx.canvas.clientLeft - 1,
this.ctx.canvas.clientTop - 1,
this.ctx.canvas.width + 1,
this.margins.top + 1,
);
this.ctx.fillRect(
this.ctx.canvas.clientLeft + this.ctx.canvas.width - this.margins.right - 1,
this.ctx.canvas.clientTop - 1,
this.margins.right + 1,
this.ctx.canvas.height + 1,
);
this.ctx.fillRect(
this.ctx.canvas.clientLeft - 1,
this.ctx.canvas.clientTop - 1,
this.margins.left + 1,
this.ctx.canvas.height + 1,
);
this.ctx.fillRect(
this.ctx.canvas.clientLeft,
this.ctx.canvas.clientTop + this.ctx.canvas.height - this.margins.bottom,
this.ctx.canvas.width,
this.margins.bottom,
);
}
private clearCanvas() {
this.ctx.fillStyle = "rgb(255,255,255)";
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
this.ctx.fill();
}
private updateResolution() {
@@ -174,13 +198,13 @@ export default class Chart {
private renderGuides() {
this.ctx.strokeStyle = "rgb(230, 230, 230)";
this.ctx.lineWidth = 1;
this.ctx.beginPath();
for (const tick of this.rightScale.getTicks()) {
const pos = this.rightScale.getY(tick);
this.ctx.beginPath();
const pos = Math.floor(this.rightScale.getY(tick));
this.ctx.moveTo(this.chartBounds.left, pos);
this.ctx.lineTo(this.chartBounds.left + this.chartBounds.width, pos);
this.ctx.stroke();
}
this.ctx.stroke();
}
private renderTooltips(radius = 20) {
@@ -250,8 +274,8 @@ export default class Chart {
this.ctx.lineWidth = timeseries.getName() === this.highlightedTimeseries ? 2 : 1;
let y = scale.getY(timeseriesPoints[0]);
let x = this.getX(timeseriesPoints[1]);
this.ctx.beginPath();
for (let i = 0; i < timeseriesPoints.length; i += 2 * this.resolution) {
this.ctx.beginPath();
this.ctx.moveTo(Math.round(x), Math.round(y));
y = 0;
x = 0;
@@ -262,13 +286,11 @@ export default class Chart {
y = scale.getY(y / this.resolution);
x = this.getX(x / this.resolution);
this.ctx.lineTo(Math.round(x), Math.round(y));
this.ctx.stroke();
if (this.resolution === 1) {
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, markerColour: string) {

View File

@@ -8,8 +8,8 @@ export default class Scale {
private tickCacheDirty = true;
private bounds: Bounds;
constructor(bounds: Bounds) {
this.bounds = bounds;
constructor(bounds?: Bounds) {
this.bounds = bounds ?? {height: 0, width: 0, top: 0, left: 0};
}
updateIndexRange(indexRange: {start: number, stop: number}) {
@@ -27,6 +27,10 @@ export default class Scale {
this.tickCacheDirty = true;
}
updateBounds(bounds: Bounds) {
Object.assign(this.bounds, bounds);
}
getBounds() {
return Object.assign({}, this.bounds);
}

View File

@@ -2,5 +2,5 @@
"development": false,
"defaultMinuteSpan": 60,
"reloadIntervalSec": 30,
"dataEndpoint": "/climate/api"
"dataEndpoint": "http://tortedda.local/climate/api"
}

View File

@@ -1,4 +1,8 @@
declare module "chart.js/dist/Chart.bundle.min" {
import * as Charts from "chart.js";
export default Charts;
declare module "*.gif" {
const value: string;
export = value;
}
declare module "*.png" {
const value: string;
export = value;
}

View File

@@ -7,6 +7,10 @@ import MessageOverlay from "./MessageOverlay";
import UIComponent from "./UIComponent";
import SelectDisplayModeWidget from "./SelectDisplayModeWidget";
import LegendWidget from "./LegendWidget";
import HelpModal from "./HelpModal";
import * as JSX from "../JSXFactory";
import {AppStore} from "../StateStore";
import HelpButtonImg from "../../assets/help-button.png";
class AppUI extends UIComponent {
private timezoneWidget: TimezoneWidget;
@@ -18,14 +22,21 @@ class AppUI extends UIComponent {
private element: HTMLDivElement = document.createElement("div");
private grid: HTMLDivElement = document.createElement("div");
private messageOverlay: MessageOverlay = new MessageOverlay();
private helpModal: HelpModal = new HelpModal();
constructor() {
super();
this.setupGrid({width: 5, height: 10});
this.element.append(
Object.assign(document.createElement("h1"), { innerText: "Ledda's Room Climate" }),
<img
alt={"Help"}
src={HelpButtonImg}
className={"help-button button"}
onclick={() => AppStore().showHelp()}/>,
<h1>Ledda's Room Climate</h1>,
this.grid,
this.messageOverlay.current(),
this.helpModal.current(),
);
this.element.className = "center";
}
@@ -33,8 +44,8 @@ class AppUI extends UIComponent {
private setupGrid(size: GridSize) {
this.setupWidgets();
this.grid.append(
this.legendWidget.current(),
this.chartWidget.current(),
this.legendWidget.current(),
this.displayModeSettingsWidget.current(),
this.selectModeWidget.current(),
this.timerWidget.current(),
@@ -68,7 +79,6 @@ class AppUI extends UIComponent {
bootstrap(rootNode: string) {
document.getElementById(rootNode).append(this.element);
this.chartWidget.updateDimensions();
}
current(): HTMLElement {

View File

@@ -26,6 +26,8 @@ class ClimateChartWidget extends UIComponent {
}
updateDimensions() {
this.canvasElement.width = 0;
this.canvasElement.height = 0;
const skelStyle = getComputedStyle(this.skeleton.current());
this.canvasElement.height = this.skeleton.current().clientHeight
- Number(skelStyle.paddingTop.slice(0, -2))
@@ -33,6 +35,7 @@ class ClimateChartWidget extends UIComponent {
this.canvasElement.width = this.skeleton.current().clientWidth
- Number(skelStyle.paddingLeft.slice(0, -2))
- Number(skelStyle.paddingRight.slice(0, -2));
this.chart.updateLayout();
}
private setupListeners() {
@@ -41,18 +44,22 @@ class ClimateChartWidget extends UIComponent {
AppStore().subscribeStoreVal("displayWindow", () => this.rerender());
AppStore().on("timeseriesUpdated", () => this.rerender());
AppStore().on("newTimeseries", (timeseries) => this.chart.addTimeseries(timeseries));
AppStore().subscribeStoreVal("documentReady", () => this.initChart());
AppStore().subscribeStoreVal("documentReady", async () => {
await this.initChart();
this.updateDimensions();
window.addEventListener("resize", () => {
this.updateDimensions();
});
});
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};
AppStore().emulateLastMinsWithWindow();
}
const displayedWindow = getAppState().displayWindow;
const beforeIndex = index - displayedWindow.start;
const afterIndex = displayedWindow.stop - index;
const factor = direction === 1 ? 1.1 : 0.9;
@@ -66,12 +73,12 @@ class ClimateChartWidget extends UIComponent {
private handleDrag(deltaX: number, deltaY: number, deltaIndex: number) {
if (getAppState().displayMode === "pastMins") {
AppStore().setDisplayMode("window");
AppStore().emulateLastMinsWithWindow();
}
const displayWindow = getAppState().displayWindow;
const displayedWindow = getAppState().displayWindow;
AppStore().setDisplayWindow({
start: displayWindow.start + deltaIndex,
stop: displayWindow.stop + deltaIndex,
start: displayedWindow.start + deltaIndex,
stop: displayedWindow.stop + deltaIndex,
});
}

View File

@@ -0,0 +1,80 @@
import {AppStore, getAppState} from "../StateStore";
import UIComponent from "./UIComponent";
import * as JSX from "../JSXFactory";
import MovePic from "../../assets/move-example.gif";
import ZoomPic from "../../assets/zoom-example.gif";
import ScrollPic from "../../assets/scroll-date-example.gif";
class HelpModal extends UIComponent {
private element: HTMLElement;
private visible = false;
constructor() {
super();
this.build();
AppStore().subscribeStoreVal("showingHelp", () => this.update());
this.update();
}
private build() {
this.element = (
<div className={"center"}>
<div
className={"overlay center"}
onclick={() => this.hide()}/>
<div className={"help-box"}>
<div
className={"x-button button"}
onclick={() => this.hide()}/>
<h1>Quick Help</h1>
<div className={"image-advice"}>
<img alt={"Animated example of scrolling over display timestamps"} src={ScrollPic}/>
<div>
Clicking the plus and minus buttons will adjust the time spans and minute spans by one minute.
Try scrolling over the numbers or clicking on them for direct editing as well!
</div>
</div>
<div className={"image-advice"}>
<img alt={"Animated example of dragging back and forth on the chart"} src={MovePic}/>
<div>
Dragging over the chart will switch to time window mode and allow you to pan back and forth.
</div>
</div>
<div className={"image-advice"}>
<img alt={"Animated example of zooming in and out on the chart"} src={ZoomPic}/>
<div>
Try scrolling whilst hovering over the chart to zoom in and out.
</div>
</div>
</div>
</div>
);
}
private show() {
this.visible = true;
this.element.classList.remove("hidden");
AppStore().showHelp();
}
private hide() {
this.visible = false;
this.element.classList.add("hidden");
AppStore().hideHelp();
}
update() {
this.visible = getAppState().showingHelp;
if (this.visible) {
this.element.classList.remove("hidden");
} else {
this.element.classList.add("hidden");
}
}
current() {
return this.element;
}
}
export default HelpModal;

View File

@@ -10,23 +10,33 @@ const webpackConfig = {
plugins: [new webpack.ProgressPlugin()],
module: {
rules: [{
test: /\.(ts|tsx)$/,
loader: "ts-loader",
include: [path.resolve(__dirname, "src")],
exclude: [/node_modules/]
}, {
test: /.css$/,
rules: [
{
test: /\.(ts|tsx)$/,
loader: "ts-loader",
include: [path.resolve(__dirname, "src")],
exclude: [/node_modules/]
},
{
test: /.css$/,
use: [{
loader: "style-loader"
}, {
loader: "css-loader",
options: {
sourceMap: true
}
use: [{
loader: "style-loader"
}, {
loader: "css-loader",
options: {
sourceMap: true
}
}]
},
{
test: /\.(png|jpe?g|gif|ttf|woff2?|eot|svg)$/i,
use: [
{
loader: "file-loader",
},
],
}]
}]
},
resolve: {