it's done i think
BIN
app-dist/static/favicon16.png
Normal file
|
After Width: | Height: | Size: 555 B |
BIN
app-dist/static/favicon32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app-dist/static/favicon64.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
266
dashboard/assets/favicon.svg
Normal 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 |
BIN
dashboard/assets/help-button.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
78
dashboard/assets/help-button.svg
Normal 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 |
BIN
dashboard/assets/move-example.gif
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
dashboard/assets/scroll-date-example.gif
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
dashboard/assets/zoom-example.gif
Normal file
|
After Width: | Height: | Size: 30 KiB |
10
dashboard/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
"development": false,
|
||||
"defaultMinuteSpan": 60,
|
||||
"reloadIntervalSec": 30,
|
||||
"dataEndpoint": "/climate/api"
|
||||
"dataEndpoint": "http://tortedda.local/climate/api"
|
||||
}
|
||||
|
||||
10
dashboard/src/types.d.ts
vendored
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
80
dashboard/src/ui-components/HelpModal.tsx
Normal 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;
|
||||
@@ -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: {
|
||||
|
||||