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