better labels
[tridge/solar.git] / live / graphs.js
1 /*
2   javascript display of raw SMA webbox data
3   Copyright Andrew Tridgell 2010
4   Released under GNU GPL v3 or later
5  */
6
7
8 /*
9   setup midnight as a date
10  */
11 midnight = new Date();
12 midnight.setHours(0);
13 midnight.setMinutes(0);
14 midnight.setSeconds(0);
15 midnight.setMilliseconds(0);
16
17 pvdate = new Date(midnight);
18
19 /*
20   show a HTML heading
21  */
22 function heading(level, h) {
23   if (!in_redraw) {
24     document.write("<h" + level + ">" + h + "</h" + level + ">\n");
25   }
26 }
27
28 /*
29   create a div for a graph
30  */
31 function graph_div(divname) {
32   if (!in_redraw) {
33     document.write('<div id="' + divname + '" style="width:700px; height:350px;"></div>');
34   }
35 }
36
37 /*
38   hide/show a div
39  */
40 function hide_div(divname, hidden) {
41   var div = document.getElementById(divname);
42   if (hidden) {
43     writeDebug("hiding " + divname);
44     div.style.display = "none";
45   } else {
46     writeDebug("unhiding " + divname);
47     div.style.display = "block";
48   }
49 }
50
51 /* unhide the loading div when busy */
52 loading_counter = 0;
53
54 function loading(busy) {
55   if (busy) {
56     loading_counter++;
57     if (loading_counter == 1) {
58       hide_div("loading", false);
59     }
60   } else {
61     loading_counter--;
62     if (loading_counter == 0) {
63       hide_div("loading", true);
64     }
65   }
66 }
67
68
69 /* a global call queue */
70 global_queue = new Array();
71
72 if (is_IE) {
73   /* IE is _very_ slow at digraphs, we need bigger pauses to stop
74      it complaining */
75   job_delay = 100;
76 } else {
77   job_delay = 10;
78 }
79
80 /*
81   run the call queue
82  */
83 function run_queue() {
84   var qe = global_queue[0];
85   qe.callback(qe.arg);
86   global_queue.shift();
87   if (global_queue.length > 0) {
88     setTimeout(run_queue, job_delay);    
89   }
90 }
91
92 /*
93   queue a call. This is used to serialise long async operations in the
94   browser, so that you get less timeouts. It is especially needed on
95   IE, where the canvas widget is terribly slow.
96  */
97 function queue_call(callback, arg) {
98   global_queue.push( { callback: callback, arg : arg });
99   if (global_queue.length == 1) {
100     setTimeout(run_queue, job_delay);
101   }
102 }
103
104
105 /*
106   date parser. Not completely general, but good enough
107  */
108 function parse_date(s) {
109   if (s.length == 5 && s[2] == ':') {
110     /* its a time since midnight */
111     var h = (+s.substring(0, 2));
112     var m = (+s.substring(3));
113     var d = new Date(midnight);
114     d.setHours(h);
115     d.setMinutes(m);
116     return d;
117   }
118   if (s.search("-") != -1) {
119     s = s.replace("-", "/", "g");
120   }
121   return new Date(s);
122 };
123
124 /* keep a cache of loaded CSV files */
125 CSV_Cache = new Array();
126
127
128 /*
129   load a CSV file, returing column names and data via a callback
130  */
131 function load_CSV(filename, callback) {
132
133   /* maybe its in the global cache? */
134   if (CSV_Cache[filename] !== undefined) {
135
136     if (CSV_Cache[filename].pending) {
137       /* its pending load by someone else. Add ourselves to the notify
138          queue so we are told when it is done */
139       CSV_Cache[filename].queue.push({filename:filename, callback:callback});
140       return;
141     }
142
143     /* its ready in the cache - return it via a delayed callback */
144     var d = { filename: CSV_Cache[filename].filename,
145               labels:   CSV_Cache[filename].labels.slice(0),
146               data:     CSV_Cache[filename].data.slice(0) };
147     queue_call(callback, d);
148     return;
149   }
150
151   /* mark this one pending */
152   CSV_Cache[filename] = { filename:filename, pending: true, queue: new Array()};
153
154   /*
155     async callback when the CSV is loaded
156    */
157   function load_CSV_callback(caller) {
158     /* split by lines */
159     var csv = caller.r.responseText.split(/\n/g);
160
161     /* assume first line is column labels */
162     var labels = csv[0].split(/,/g);
163     for (var i=0; i<labels.length; i++) {
164       labels[i] = labels[i].replace(" ", "&nbsp;", "g");
165     }
166
167     /* the rest is data, we assume comma separation */
168     var data = new Array();
169     for (var i=1; i<csv.length; i++) {
170       var row = csv[i].split(/,/g);
171       if (row.length <= 1) {
172         continue;
173       }
174       data[i-1] = new Array();
175       data[i-1][0] = parse_date(row[0]);
176       for (var j=1; j<row.length; j++) {
177         data[i-1][j] = parseFloat(row[j]);
178       }
179     }
180     
181     /* save into the global cache */
182     CSV_Cache[caller.filename].labels = labels;
183     CSV_Cache[caller.filename].data   = data;
184
185     /* give the caller a copy of the data (via slice()), as they may
186        want to modify it */
187     var d = { filename: CSV_Cache[filename].filename,
188               labels:   CSV_Cache[filename].labels.slice(0),
189               data:     CSV_Cache[filename].data.slice(0) };
190     queue_call(caller.callback, d);
191
192     /* fire off any pending callbacks */
193     while (CSV_Cache[caller.filename].queue.length > 0) {
194       var qe = CSV_Cache[caller.filename].queue.shift();
195       var d = { filename: filename,
196                 labels:   CSV_Cache[filename].labels.slice(0),
197                 data:     CSV_Cache[filename].data.slice(0) };
198       queue_call(qe.callback, d);
199     }
200     CSV_Cache[caller.filename].pending = false;
201     CSV_Cache[caller.filename].queue   = null;
202   }
203
204   /* make the async request for the file */
205   var caller = new Object();
206   caller.r = new XMLHttpRequest();
207   caller.callback = callback;
208   caller.filename = filename;
209
210   /* check the status when that returns */
211   caller.r.onreadystatechange = function() {
212     if (caller.r.readyState == 4) {
213       if (caller.r.status == 200) {
214         load_CSV_callback(caller);
215       } else {
216         /* the load failed */
217         queue_call(caller.callback, { filename: filename, data: null, labels: null });
218         while (CSV_Cache[caller.filename].queue.length > 0) {
219           var qe = CSV_Cache[caller.filename].queue.shift();
220           var d = { filename: CSV_Cache[filename].filename,
221                     labels:   null,
222                     data:     null };
223           queue_call(qe.callback, d);
224         }
225         CSV_Cache[caller.filename].pending = false;
226         CSV_Cache[caller.filename].queue   = null;
227       }
228     }
229   }
230   caller.r.open("GET",filename,true);
231   caller.r.send(null);
232 }
233
234 /*
235   format an integer with N digits by adding leading zeros
236   javascript is so lame ...
237  */
238 function intLength(v, N) {
239   var r = v + '';
240   while (r.length < N) {
241     r = "0" + r;
242   }
243   return r;
244 }
245
246
247 /*
248   return the list of CSV files for the inverters for date pvdate
249  */
250 function days_csv_files() {
251   var list = new Array();
252   writeDebug(pvdate);
253   for (var i=0; i<serialnums.length; i++) {
254     list[i] = CSV_directory + 
255       pvdate.getFullYear() + "-" + 
256       intLength(pvdate.getMonth()+1,2) + "-" + 
257       intLength(pvdate.getDate(),2) + "-WR5KA-08:" + 
258       serialnums[i] + ".csv";
259   }
260   return list;
261 }
262
263
264 /*
265   return the position of v in an array or -1
266  */
267 function pos_in_array(a, v) {
268   for (var i=0; i<a.length; i++) {
269     if (a[i] == v) {
270       return i;
271     }
272   }
273   return -1;
274 }
275
276 /*
277   see if v exists in array a
278  */
279 function in_array(a, v) {
280   return pos_in_array(a, v) != -1;
281 }
282
283
284 /*
285   return a set of columns from a CSV file
286  */
287 function get_csv_data(filenames, columns, callback) {
288   var caller = new Object();
289   caller.d = new Array();
290   caller.columns = columns.slice(0);
291   caller.filenames = filenames.slice(0);
292   caller.callback = callback;
293
294   /* initially blank data - we can tell a load has completed when it
295      is filled in */
296   for (var i=0; i<caller.filenames.length; i++) {
297     caller.d[i] = { filename: caller.filenames[i], labels: null, data: null};
298   }
299
300   /* process one loaded CSV, mapping the data for
301      the requested columns */
302   function process_one_csv(d) {
303     var labels = new Array();
304
305     if (d.data == null) {
306       queue_call(caller.callback, d);
307       return;
308     }
309
310     /* form the labels */
311     labels[0] = "Time";
312     for (var i=0; i<caller.columns.length; i++) {
313       labels[i+1] = caller.columns[i];
314     }
315
316     /* get the column numbers */
317     var cnums = new Array();
318     cnums[0] = 0;
319     for (var i=0; i<caller.columns.length; i++) {
320       cnums[i+1] = pos_in_array(d.labels, caller.columns[i]);
321     }
322   
323     /* map the data */
324     var data = new Array();
325     for (var i=0; i<d.data.length; i++) {
326       data[i] = new Array();
327       for (var j=0; j<cnums.length; j++) {
328         data[i][j] = d.data[i][cnums[j]];
329       }
330     }
331     d.data = data;
332     d.labels = labels;
333
334     for (var f=0; f<caller.filenames.length; f++) { 
335       if (d.filename == caller.d[f].filename) {
336         caller.d[f].labels = labels;
337         caller.d[f].data = data;
338       }
339     }
340
341     /* see if all the files are now loaded */
342     for (var f=0; f<caller.filenames.length; f++) { 
343       if (caller.d[f].data == null) {
344         return;
345       }
346     }
347
348     /* they are all loaded - make the callback */
349     queue_call(caller.callback, caller.d);
350   }
351
352   /* start the loading */
353   for (var i=0; i<caller.filenames.length; i++) {
354     load_CSV(caller.filenames[i], process_one_csv);
355   }
356 }
357
358
359 /*
360   apply a function to a set of data, giving it a new label
361  */
362 function apply_function(d, func, label) {
363   if (func == null) {
364     return;
365   }
366   for (var i=0; i<d.data.length; i++) {
367     var r = d.data[i];
368     d.data[i] = r.slice(0,1);
369     d.data[i][1] = func(r.slice(1))
370   }
371   d.labels = d.labels.slice(0,1);
372   d.labels[1] = label;
373 }
374
375
376 /* currently displayed graphs, indexed by divname */
377 global_graphs = new Array();
378
379 /* default dygraph attributes */
380 defaultAttrs = {
381  width: 700,
382  height: 350,
383  rollPeriod: 5,
384  strokeWidth: 1,
385  showRoller: true
386 }
387
388 /*
389   graph results from a set of CSV files:
390     - apply func1 to the name columns within each file
391     - apply func2 between the files
392  */
393 function graph_csv_files_func(divname, filenames, columns, func1, func2, attrs) {
394   /* load the csv files */
395   var caller = new Object();
396   caller.divname   = divname;
397   caller.filenames = filenames.slice(0);
398   caller.columns   = columns.slice(0);
399   caller.func1     = func1;
400   caller.func2     = func2;
401   caller.attrs     = attrs;
402
403   if (columns.length == 1) {
404     caller.colname = columns[0]
405   } else {
406     caller.colname = divname;
407   }
408
409   /* called when all the data is loaded and we're ready to apply the
410      functions and graph */
411   function loaded_callback(d) {
412
413     if (d[0] == undefined) {
414       writeDebug("unable to load file");
415       /* hide_div("nodata", false); */
416       loading(false);
417       return;
418     }
419
420     for (var i=0; i<caller.filenames.length; i++) {
421       apply_function(d[i], caller.func1, caller.colname);
422     }
423
424     /* work out the y offsets to align the times */
425     var yoffsets = new Array();
426     yoffsets[0] = 0;
427     for (var i=1; i<caller.filenames.length; i++) {
428       yoffsets[i] = 0;
429       if (d[i].data[0][0] < d[0].data[0][0]) {
430         while (d[i].data[yoffsets[i]][0] < d[0].data[0][0]) {
431           yoffsets[i]++;
432         }
433       } else if (d[i].data[0][0] > d[0].data[0][0]) {
434         while (d[i].data[0][0] > d[0].data[-yoffsets[i]][0]) {
435           yoffsets[i]--;
436         }
437       }
438     }
439
440     if (caller.attrs.missingValue !== undefined) {
441       missingValue = attrs.missingValue;
442     } else {
443       missingValue = null;
444     }
445     
446     /* map the data */
447     var data = d[0].data;
448     for (var j=0; j<data.length; j++) {
449       if (data[j][1] == missingValue) {
450         data[j][1] = null;
451       }
452       for (var i=1; i<caller.filenames.length; i++) {
453         var y = j + yoffsets[i];
454         if (y < 0 || y >= d[i].data.length || d[i].data[y][1] == missingValue) {
455           data[j][i+1] = null;
456         } else {
457           data[j][i+1] = d[i].data[y][1];
458         }
459       }
460     }
461     
462     labels = new Array();
463     labels[0] = d[0].labels[0];
464     for (var i=0; i<caller.filenames.length; i++) {
465       labels[i+1] = caller.colname + (i+1);
466     }
467
468     var d2 = { labels: labels, data: data };
469     apply_function(d2, caller.func2, caller.colname);
470     
471     /* add the labels to the given graph attributes */
472     caller.attrs.labels = d2.labels;
473     
474     for (a in defaultAttrs) {
475       if (caller.attrs[a] == undefined) {
476         caller.attrs[a] = defaultAttrs[a];
477       }
478     }
479
480     if (global_graphs[divname] !== undefined) {
481       /* we already have the graph, just update the data */
482       var g = global_graphs[divname];
483       for (var i=0; i<d2.data.length; i++) {
484         /* the rawData_ attribute doesn't take dates */
485         d2.data[i][0] = d2.data[i][0].valueOf();
486       }
487       g.rawData_ = d2.data;
488       g.drawGraph_(g.rawData_);
489     } else {
490       /* create a new dygraph */
491       global_graphs[divname] = new Dygraph(document.getElementById(divname), d2.data, caller.attrs);
492     }
493
494     loading(false);
495   }
496
497   /* fire off a request to load the data */
498   loading(true);
499   graph_div(divname);
500   get_csv_data(caller.filenames, caller.columns, loaded_callback);
501 }
502
503
504 function product(v) {
505   var r = v[0];
506   for (var i=1; i<v.length; i++) {
507     r *= v[i];
508   }
509   return r;
510 }
511
512 function sum(v) {
513   var r = v[0];
514   for (var i=1; i<v.length; i++) {
515     r += v[i];
516   }
517   return r;
518 }
519
520
521
522 /*
523   graph one column from a set of CSV files
524  */
525 function graph_csv_files(divname, filenames, column, attrs) {
526   return graph_csv_files_func(divname, filenames, [column], null, null, attrs);
527 }
528
529 /*
530   graph one column from a set of CSV files as a sum over multiple files
531  */
532 function graph_sum_csv_files(divname, filenames, column, attrs) {
533   return graph_csv_files_func(divname, filenames, [column], null, sum, attrs);
534 }
535
536 /* marker for whether we are in a redraw with new data */
537 in_redraw = false;
538
539 /*
540   show all the live data graphs
541  */
542 function show_graphs() {
543
544   hide_div("nodata", true);
545
546   heading(3, "Total AC Power (W)");
547
548   graph_sum_csv_files("Total AC Power", 
549                       days_csv_files(),
550                       "Pac",
551                       { includeZero: true });
552
553
554   heading(3, "AC Power from each inverter (W) [Pac]");
555
556   graph_csv_files("AC Power", 
557                   days_csv_files(),
558                   "Pac",
559                   { includeZero: true });
560
561   heading(3, "DC Voltage for each inverter (V) [UpvSoll]");
562
563   graph_csv_files("DC Voltage", 
564                   days_csv_files(),
565                   "Upv-Soll",
566                   { includeZero: false,
567                     missingValue: 666 });
568
569   heading(3, "Target DC Voltage for each inverter (V) [UpvIst]");
570
571   graph_csv_files("Target DC Voltage", 
572                   days_csv_files(),
573                   "Upv-Ist",
574                   { includeZero: false,
575                     missingValue: 666 });
576
577   heading(3, "Total DC current (A)");
578
579   graph_sum_csv_files("Total Current", 
580                       days_csv_files(),
581                       "Ipv",
582                       { includeZero: true });
583
584   heading(3, "DC Current for each inverter (A) [Ipv]");
585
586   graph_csv_files("DC Current [Ipv]", 
587                   days_csv_files(),
588                   "Ipv",
589                   { includeZero: false });
590
591   heading(3, "DC Power for each inverter (W) [Ipv*UpvSoll]");
592
593
594   graph_csv_files_func("DC Power", 
595                        days_csv_files(),
596                        [ "Ipv", "Upv-Soll" ],
597                        product, null,
598                        { includeZero: true });
599
600   heading(3, "Total DC Power (W)");
601
602   graph_csv_files_func("Total DC Power", 
603                        days_csv_files(),
604                        [ "Ipv", "Upv-Soll" ],
605                        product, sum,
606                        { includeZero: true });
607
608
609   heading(3, "Inverter efficiencies (%) [(Ipv*UpvSoll)/Pac]");
610
611   function efficiency(v) {
612     var dc_pow = v[1] * v[2];
613     if (dc_pow == 0) {
614       return null;
615     }
616     return 100.0*(v[0] / dc_pow);
617   }
618
619   graph_csv_files_func("Inverter Efficiency", 
620                        days_csv_files(),
621                        [ "Pac", "Ipv", "Upv-Soll" ],
622                        efficiency, null,
623                        { includeZero: false,
624                          valueRange: [0, 100]});
625
626   heading(3, "AC Voltage for each inverter (V) [Uac]");
627
628   graph_csv_files("AC Voltage", 
629                   days_csv_files(),
630                   "Uac",
631                   { includeZero: false });
632
633   heading(3, "Lifetime Power for each inverter (kWh) [E-total]");
634
635   graph_csv_files("Lifetime Power", 
636                   days_csv_files(),
637                   "E-Total",
638                   { includeZero: false });
639
640   heading(3, "Total Lifetime Power (kWh)");
641
642   graph_sum_csv_files("Total Lifetime Power", 
643                   days_csv_files(),
644                   "E-Total",
645                   { includeZero: false });
646
647   heading(3, "Fan voltage for each inverter (V) [UFan]");
648
649   graph_csv_files("Fan Voltage", 
650                   days_csv_files(),
651                   "U-Fan",
652                   { includeZero: true, 
653                     avoidMinZero: true,
654                     valueRange: [0, 12] });
655
656   in_redraw = true;
657 }
658
659 /*
660   called when the user selects a date
661  */
662 function set_date(e) {
663   var dp = datePickerController.getDatePicker("pvdate");
664   pvdate = dp.date;
665   writeDebug("redrawing for: " + pvdate);
666   show_graphs();
667 }
668
669 /*
670   setup the datepicker widget
671  */
672 function setup_datepicker() {
673     document.getElementById("pvdate").value = 
674       intLength(pvdate.getDate(),2) + "/" + intLength(pvdate.getMonth()+1, 2) + "/" + pvdate.getFullYear();
675     datePickerController.addEvent(document.getElementById("pvdate"), "change", set_date);
676 }
677
678
679 /* 
680    called to reload every few minutes
681  */
682 function reload_timer() {
683   /* flush the old CSV cache */
684   CSV_Cache = new Array();
685   writeDebug("reloading on timer");
686   if (loading_counter == 0) {
687     show_graphs();
688   }
689   setup_reload_timer();
690 }
691
692 /*
693   setup for automatic reloads
694  */
695 function setup_reload_timer() {
696   setTimeout(reload_timer, 300000);    
697 }