don't draw plot w/o data
[pulsecounter.git] / web / index.html
1 <?xml version="1.0" encoding="utf-8"?>
2 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
3         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
4 <html xml:lang="en" lang="en" xmlns="http://www.w3.org/1999/xhtml">
5 <head>
6 <script>
7   var dbg, errordiv;
8   var canvas, ctx;
9   var ww, wh;
10   var hmax, hfact;
11   var tmin, tmax, tfact;
12   var xzero = 60, yzero = 48;
13   var cold_d = [], hot_d = [];
14
15   function showdate(utime) {
16     var dt = new Date(utime*1000);
17     return dt.toLocaleDateString() + " " + dt.toLocaleTimeString();
18   }
19
20   function px(x) {
21     return xzero + ((x - tmin) * tfact);
22   }
23
24   function py(y) { return wh - yzero - (y * hfact);
25   }
26
27   const dow = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
28   const mn = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
29               "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
30
31   function getcombx(tmin, tmax) {
32     var comb = [], lb = [];
33     var trange = tmax - tmin;
34     var inc, inc2, base, base2, t, dt, zoff;
35     var label = function(t) { return "<<" + t.toFixed(1) + ">>"; }
36     function tohour(t) { var dt = new Date(t*1000); return dt.getHours(); }
37     function todow(t) { var dt = new Date(t*1000); return dow[dt.getDay()]; }
38     function todom(t) { var dt = new Date(t*1000); return dt.getDate(); }
39     function tomonth(t) { var dt = new Date(t*1000); return mn[dt.getMonth()]; }
40
41     if (trange < 172800) { /* two days -> one hour */
42       inc = 3600;
43       inc2 = 21600;
44       label = tohour;
45     }
46     else if (trange < 864000) { /* 10 days -> six hours */
47       inc = 21600;
48       inc2 = 86400;
49       label = todow;
50     }
51     else if (trange < 2678400) { /* 31 days -> 1 day */
52       inc = 86400;
53       inc2 = 86400;
54       label = todom;
55     }
56     else { /* ~ one month */
57       /* TODO: make this a separate case with loop over months rather than
58          fixed number of seconds. */
59       inc = 86400;
60       inc2 = 2592000;
61       label = tomonth;
62     }
63
64     dt = new Date(tmin*1000);
65     zoff = 60 * dt.getTimezoneOffset();
66     base = (Math.floor((tmin - zoff - 1) / inc) + 1) * inc + zoff;
67     for (t = base; t < tmax; t += inc)
68       comb.push(t);
69     base2 = (Math.floor((tmin - zoff - 1) / inc2) + 1) * inc2 + zoff;
70     for (t = base2; t < tmax; t += inc2)
71       lb.push([t, label(t)]);
72     //dbg.innerHTML = "inc=" + inc + "<br>inc2=" + inc2 + "<br>"
73     //              + "tmin=" + tmin + " tmax=" + tmax + "<br>"
74     //              + "base=" + base + " base2=" + base2 + "<br>"
75     //              + comb + "<br>" + lb;
76     return [comb, lb];
77   }
78
79   function xaxis() {
80     var comb = getcombx(tmin, tmax);
81     var i;
82
83     ctx.beginPath();
84     for (i = 0; comb[0][i]; i++) {
85       ctx.moveTo(px(comb[0][i]), py(0) + 5);
86       ctx.lineTo(px(comb[0][i]), py(0));
87     }
88     ctx.strokeStyle = "gray";
89     ctx.stroke();
90
91     ctx.beginPath();
92     ctx.moveTo(px(tmin), py(0));
93     ctx.lineTo(px(tmax), py(0));
94     ctx.strokeStyle = "black";
95     ctx.stroke();
96
97     ctx.fillStyle = "black";
98     ctx.font = "bold 16px Courier";
99     ctx.textAlign = "center";
100     ctx.beginPath();
101     for (i = 0; comb[1][i]; i++) {
102       ctx.fillText(comb[1][i][1], px(comb[1][i][0]), py(0) + 20);
103       ctx.moveTo(px(comb[1][i][0]), py(0) + 5);
104       ctx.lineTo(px(comb[1][i][0]), py(0));
105     }
106     ctx.strokeStyle = "black";
107     ctx.stroke();
108
109     ctx.fillStyle = "black";
110     ctx.textAlign = "left";
111     ctx.fillText(showdate(tmin), px(tmin), py(0) + 40);
112     ctx.textAlign = "right";
113     ctx.fillText(showdate(tmax), px(tmax), py(0) + 40);
114   }
115
116   function getcomby(lo, hi) {
117     var comb = [], lb = [];
118     var d = hi - lo;
119     var ord = Math.pow(10, Math.floor(Math.log10(d)));
120     var scl = Math.floor(d / ord);
121     var inc, inc2, first, x, lb;
122
123     if (scl < 2) { inc = 0.1; inc2 = 0.2; }
124     else if (scl < 5) { inc = 0.1; inc2 = 0.5; }
125     else { inc = 0.5; inc2 = 1; }
126     inc *= ord;
127     inc2 *= ord;
128     first = (Math.floor(lo / inc) + 1) * inc;
129     for (x = 0; x < (d / inc) - 1.2; x++)
130       comb.push(first + inc * x);
131     first = (Math.floor(lo / inc2) + 1) * inc2;
132     for (x = 0; x < (d / inc2) - 1.2; x++)
133       lb.push(first + inc2 * x);
134     //dbg.innerHTML = "ord=" + ord + "<br>inc=" + inc + "<br>"
135     //              + comb + "<br>" + lb;
136     return [comb, lb];
137   }
138
139   function yaxis() {
140     var comb = getcomby(0, hmax);
141     var i;
142
143     ctx.beginPath();
144     for (i = 0; comb[0][i]; i++) {
145       ctx.moveTo(px(tmin) - 5, py(comb[0][i]));
146       ctx.lineTo(px(tmax), py(comb[0][i]));
147     }
148     ctx.strokeStyle = "lightgray";
149     ctx.stroke();
150
151     ctx.beginPath();
152     ctx.moveTo(px(tmin), py(0));
153     ctx.lineTo(px(tmin), py(hmax));
154     ctx.strokeStyle = "black";
155     ctx.stroke();
156
157     ctx.fillStyle = "black";
158     ctx.font = "bold 16px Courier";
159     ctx.textAlign = "right";
160     ctx.fillText(0, px(tmin) - 6, py(0));
161     ctx.beginPath();
162     for (i = 0; comb[1][i]; i++) {
163       ctx.fillText(comb[1][i].toFixed(1), px(tmin) - 6, py(comb[1][i]));
164       ctx.moveTo(px(tmin) - 5, py(comb[1][i]));
165       ctx.lineTo(px(tmax), py(comb[1][i]));
166     }
167     ctx.strokeStyle = "gray";
168     ctx.stroke();
169
170     ctx.textAlign = "left";
171     ctx.fillText("l/min", px(tmin) + 4, py(hmax) + 12);
172   }
173
174   /* @ updates global var `hmax` */
175   function differentiate(times) {
176     var res = [];
177     var dv, dt, v;
178
179     for (i = 0; i < times.length - 1; i++) {
180       dv = times[i+1][1] - times[i][1];
181       dt = times[i+1][0] - times[i][0];
182       if (dt != 0 && dv != 0) {
183         v = (dv / dt) * 600 ; /* Litres per min */
184         if (hmax < v) hmax = v;
185         res.push([times[i][0], v]);
186       }
187     }
188     if (i) res.push([times[i][0], v]);
189
190     return res;
191   }
192
193   function drawplot(data, color) {
194     var i;
195
196     if (data.length) {
197       ctx.beginPath();
198       ctx.moveTo(px(data[0][0]), py(data[0][1]));
199       for (i = 1; i < data.length; i++) {
200         ctx.lineTo(px(data[i][0]), py(data[i - 1][1]));
201         ctx.lineTo(px(data[i][0]), py(data[i][1]));
202       }
203       ctx.strokeStyle = color;
204       ctx.stroke();
205     }
206   }
207
208   function showloading() {
209     ctx.fillStyle = "green";
210     ctx.font = "bold 16px Courier";
211     ctx.textAlign="center";
212     ctx.fillText("...loading...", (ww / 2) , (wh / 2) + 8);
213   }
214
215   function showempty() {
216     ctx.fillStyle = "red";
217     ctx.font = "bold 24px Courier";
218     ctx.textAlign="center";
219     ctx.fillText("No data for the requested time interval",
220                  (ww / 2) , (wh / 2) + 8);
221   }
222
223   function clearplot() {
224     ctx.clearRect(0, 0, ww, wh);
225   }
226
227   function redraw() {
228     errordiv.style.visibility = "hidden";
229     errordiv.innerHTML = "";
230     clearplot();
231     if (cold_d.length || hot_d.length) {
232       tfact = (ww - xzero) / (tmax - tmin);
233       hfact = (wh - yzero) / hmax;
234       xaxis();
235       yaxis();
236       drawplot(cold_d, "blue");
237       drawplot(hot_d, "red");
238     } else {
239       showempty();
240     }
241   }
242
243   function gotdata(data) {
244     document.getElementById("curcold").innerHTML =
245       (data.current.cold / 100).toFixed(2);
246     document.getElementById("curhot").innerHTML =
247       (data.current.hot / 100).toFixed(2);
248
249     if (data.cold.length)
250       document.getElementById("totcold").innerHTML =
251         ((data.cold[data.cold.length - 1][1] - data.cold[0][1]) * 10);
252     else document.getElementById("totcold").innerHTML = "0";
253     if (data.hot.length)
254       document.getElementById("tothot").innerHTML =
255         ((data.hot[data.hot.length - 1][1] - data.hot[0][1]) * 10);
256     else document.getElementById("tothot").innerHTML = "0";
257
258     tmin = data.range.lo;
259     tmax = data.range.hi;
260     //dbg.innerHTML = "from " + tmin + " to " + tmax
261     //              + "<br>from " + showdate(tmin) + " to " + showdate(tmax);
262     /* differetiate() updates hmax */
263     hmax = 0;
264     cold_d = differentiate(data.cold);
265     hot_d = differentiate(data.hot);
266     //dbg.innerHTML = "hmax=" + hmax + " hfact=" + hfact + "<br>"
267     //              + cold_d + "<br>" + hot_d;
268     redraw();
269   }
270
271   function sendreq(qstr) {
272     var url = "query.cgi" + qstr;
273     var xmlhttp = new XMLHttpRequest();
274
275     //dbg.innerHTML = url;
276     xmlhttp.onreadystatechange = function() {
277       if (xmlhttp.readyState == 4)
278         if (xmlhttp.status == 200) {
279           // dbg.innerHTML = xmlhttp.responseText;
280           var myData = JSON.parse(xmlhttp.responseText);
281           gotdata(myData);
282         } else {
283           errordiv.style.visibility = "visible";
284           errordiv.style.display = "block";
285           errordiv.innerHTML = xmlhttp.responseText;
286         }
287     }
288     xmlhttp.open("GET", url, true);
289     clearplot();
290     showloading();
291     xmlhttp.send();
292   }
293
294   function iso2qu(idate) {
295     return idate.replace("T", "+").replace("0Z", "");
296   }
297
298   function sendquery(lo, hi) {
299     return sendreq("?lo=" + iso2qu(lo) + "&hi=" + iso2qu(hi));
300   }
301
302   function resize() {
303     ww = window.innerWidth - 4;
304     if (ww > window.innerHeight) ww = window.innerHeight;
305     wh = ww / 2;
306     canvas.width = ww;
307     canvas.height = wh;
308     canvas.style.width = ww + "px";
309     canvas.style.height = wh + "px";
310     redraw();
311   }
312
313   function daystart(date) {
314     date.setMilliseconds(0);
315     date.setSeconds(0);
316     date.setMinutes(0);
317     date.setHours(0);
318     return date;
319   }
320
321   function prevweek() {
322     var tdy = daystart(new Date());
323     var dow = tdy.getDay();
324     var wstart, wend;
325
326     wstart = new Date(1*tdy - 86400000 * (dow + 7));
327     wend = new Date(1*wstart + 86400000 * 7);
328     sendquery(wstart.toISOString(), wend.toISOString());
329   }
330
331   function thisweek() {
332     var tdy = daystart(new Date());
333     var dow = tdy.getDay();
334     var wstart, wend;
335
336     wstart = new Date(1*tdy - 86400000 * dow);
337     wend = new Date(1*wstart + 86400000 * 7);
338     sendquery(wstart.toISOString(), wend.toISOString());
339   }
340
341   function beforeyesterday() {
342     var tdy = daystart(new Date());
343     var ytd = new Date(1*tdy - 86400000);
344     var byd = new Date(1*ytd - 86400000);
345     sendquery(byd.toISOString(), ytd.toISOString());
346   }
347
348   function yesterday() {
349     var tdy = daystart(new Date());
350     var ytd = new Date(1*tdy - 86400000);
351     sendquery(ytd.toISOString(), tdy.toISOString());
352   }
353
354   function today() {
355     var tdy = daystart(new Date());
356     var tmr = new Date(1*tdy + 86400000);
357     sendquery(tdy.toISOString(), tmr.toISOString());
358   }
359
360   function initialize() {
361     var qstr = window.location.search;
362
363     dbg = document.getElementById("debug");
364     errordiv = document.getElementById("errormsg");
365     canvas = document.getElementById("plot");
366     ctx = canvas.getContext("2d");
367     resize();
368     if (qstr) sendreq(qstr);
369     else today();
370     
371     document.getElementById("today").onclick = today;
372     document.getElementById("yesterday").onclick = yesterday;
373     document.getElementById("beforeyesterday").onclick = beforeyesterday;
374     document.getElementById("thisweek").onclick = thisweek;
375     document.getElementById("prevweek").onclick = prevweek;
376   }
377
378   /* Set up */
379   window.onload = initialize;
380   window.onresize = resize;
381 </script>
382 <style>
383 @font-face {
384   font-family: PipeDream;
385   src: url('PIPED.TTF') format('truetype');
386   /* Free to use font from http://www.mlink.net/~paterson/jpfonts.htm */
387 }
388 h1 {
389   margin-top: 5px;
390   text-align: center;
391   font-family: PipeDream;
392   font-size: 64px;
393   font-weight: normal;
394   background-color: lightgray;
395 }
396 br {
397   clear: both;
398 }
399 div#currentvals {
400   width: 18em;
401   margin-left: auto;
402   margin-right: auto;
403   margin-bottom: 10px;
404   text-align: center;
405   font-size: 150%;
406 }
407 div#totalvals {
408   width: 18em;
409   margin-left: auto;
410   margin-right: auto;
411   margin-bottom: 10px;
412   text-align: center;
413   font-size: 100%;
414 }
415 div.current {
416   position: relative;
417   padding: 0.2em;
418   border: solid 1px black;
419   margin: 0.2em;
420 }
421 div.cold {
422   float: left;
423   background-color: #d0e0ff;
424 }
425 div.hot {
426   float: right;
427   background-color: #ffd0e0;
428 }
429 canvas#plot {
430   padding: 0px;
431   margin: auto;
432   display: block;
433   width: 640px;
434   height: 320px;
435   /* border: solid 1px black; */
436 }
437 div#errormsg {
438   visibility: hidden;
439   color: red;
440   text-align: center;
441 }
442 div#queries {
443   margin-left: auto;
444   margin-right: auto;
445   margin-bottom: 10px;
446   text-align: center;
447 }
448 div.query {
449   display: inline-block;
450   position: relative;
451   width: 8em;
452   height: 8em;
453   border: solid 1px black;
454   border-radius: 1em;
455   background-color: lightgray;
456   cursor: pointer;
457 }
458 div.label {
459   display: block;
460   width: 100%;
461   position: absolute;
462   top: 50%;
463   transform: translate(0, -50%);
464 }
465 body {
466   margin: 0px;
467 }
468 </style>
469 <title>Water Meters</title>
470 </head><body>
471 <h1>WATER METERS</h1>
472 <div id="currentvals">
473   Current Readings (m<sup>3</sup>)
474   <div class="current cold" id="curcold">cold</div>
475   <div class="current hot" id="curhot">hot</div>
476 </div>
477 <br />
478 <canvas id="plot" width="640" height = "320"></canvas>
479 <br />
480 <div id="errormsg"></div>
481 <br />
482 <div id="totalvals">
483   Total for the period (l)
484   <div class="current cold" id="totcold">cold</div>
485   <div class="current hot" id="tothot">hot</div>
486 </div>
487 <br />
488 <div id="queries">
489  <div class="query" id="prevweek"><div class="label">PREVIOUS WEEK</div></div>
490  <div class="query" id="beforeyesterday"><div class="label">DAY
491    BEFORE YESTERDAY</div></div>
492  <div class="query" id="yesterday"><div class="label">YESTERDAY</div></div>
493  <div class="query" id="today"><div class="label">TODAY</div></div>
494  <div class="query" id="thisweek"><div class="label">THIS WEEK</div></div>
495 </div>
496 <br />
497 <div id="debug"></div>
498 </body>