control rollperiod and show load time
[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   return the variables set after a '#' as a hash
10  */
11 function parse_hashvariables() {
12    var ret = [];
13    var url = window.location.hash.slice(1);
14    var vars = url.split(';');
15    for (var i=0; i<vars.length; i++) {
16      var x = vars[i].split('=');
17      if (x.length == 2) {
18        ret[x[0]] = x[1];
19      }
20    }
21    return ret;
22 }
23
24 hashvars = parse_hashvariables();
25
26 /*
27   rewrite the URL hash so you can bookmark particular dates
28  */
29 function rewrite_hashvars(vars) {
30   var hash = '';
31   for (var x in vars) {
32     hash += '' + x + '=' + vars[x] + ';';
33   }
34   hash = hash.slice(0,hash.length-1);
35   window.location.hash = hash;
36 }
37
38 /*
39   round a date back to midnight
40  */
41 function date_round(d) {
42   var d2 = new Date(d);
43   d2.setHours(0);
44   d2.setMinutes(0);
45   d2.setSeconds(0);
46   d2.setMilliseconds(0);
47   return d2;
48 }
49
50 /*
51   the date in Canberra
52  */
53 function canberraDate() {
54   var d = new Date();
55   return date_round(new Date(d.getTime() + (tz_difference*60*60*1000)));
56 }
57
58 /*
59   work out timezone
60  */
61 pvdate = new Date();
62 tz_difference = 11 + (pvdate.getTimezoneOffset()/60);
63
64
65
66 /* marker for whether we are in a redraw with new data */
67 in_redraw = false;
68
69 /*
70   show a HTML heading
71  */
72 function heading(h) {
73   if (!in_redraw) {
74     document.write("<h3><a STYLE='text-decoration:none' href=\"javascript:toggle_div('"+h+"')\"><img src='icons/icon_unhide_16.png' border='0' id='img-"+h+"'></a>&nbsp;"+h+"</h3>");
75   }
76 }
77
78 /*
79   create a div for a graph
80  */
81 function graph_div(divname) {
82   if (!in_redraw) {
83     document.write(
84                    '<table><tr>' +
85                    '<td valign="top"><div id="' + divname + '" style="width:700px; height:350px;"></div></td>' +
86                    '<td valign="top">&nbsp;&nbsp;</td>' +
87                    '<td valign="top"><div id="' + divname + ':labels"></div></td>' +
88                    '</tr></table>\n');
89   }
90 }
91
92
93 /*
94   hide/show a div
95  */
96 function hide_div(divname, hidden) {
97   var div = document.getElementById(divname);
98   if (hidden) {
99     div.style.display = "none";
100   } else {
101     div.style.display = "block";
102   }
103 }
104
105 /* unhide the loading div when busy */
106 loading_counter = 0;
107
108 function loading(busy) {
109   if (busy) {
110     loading_counter++;
111     if (loading_counter == 1) {
112       started_loading=new Date();
113       hide_div("loading", false);
114     }
115   } else {
116     if (loading_counter > 0) {
117       loading_counter--;
118       if (loading_counter == 0) {
119         hide_div("loading", true);
120         var d = new Date();
121         var load_time = d.getTime() - started_loading.getTime();
122         writeDebug("Loading took: " + (load_time/1000));
123       }
124     }
125   }
126 }
127
128
129 /* a global call queue */
130 global_queue = new Array();
131
132 if (is_IE) {
133   /* IE is _very_ slow at digraphs, we need bigger pauses to stop
134      it complaining */
135   job_delay = 100;
136 } else {
137   job_delay = 10;
138 }
139
140 /*
141   run the call queue
142  */
143 function run_queue() {
144   var qe = global_queue[0];
145   qe.callback(qe.arg);
146   global_queue.shift();
147   if (global_queue.length > 0) {
148     setTimeout(run_queue, job_delay);    
149   }
150 }
151
152 /*
153   queue a call. This is used to serialise long async operations in the
154   browser, so that you get less timeouts. It is especially needed on
155   IE, where the canvas widget is terribly slow.
156  */
157 function queue_call(callback, arg) {
158   global_queue.push( { callback: callback, arg : arg });
159   if (global_queue.length == 1) {
160     setTimeout(run_queue, job_delay);
161   }
162 }
163
164
165 /*
166   date parser. Not completely general, but good enough
167  */
168 function parse_date(s) {
169   if (s.length == 5 && s[2] == ':') {
170     /* its a time since midnight */
171     var h = (+s.substring(0, 2));
172     var m = (+s.substring(3));
173     var d = new Date(pvdate);
174     d.setHours(h);
175     d.setMinutes(m);
176     return d;
177   }
178   if (s.search("-") != -1) {
179     s = s.replace("-", "/", "g");
180   }
181   if (s[2] == '/') {
182     var x = s.split('/');
183     var d = new Date();
184     d.setDate(+x[0]);
185     d.setMonth(x[1]-1);
186     d.setYear(+x[2]);
187     return date_round(d);
188   }
189   if (s.search("/") != -1) {
190     return new Date(s);
191   }
192   /* assume time in milliseconds since 1970 */
193   return (+s);
194 };
195
196
197 /*
198   return a YYYY-MM-DD date string
199  */
200 function date_YMD(d) {
201   return '' + d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate();
202 }
203
204 /*
205   parse a CSV value
206  */
207 function parse_value(s) {
208   if (s.substring(0,1) == '"') {
209     s = unescape(s.substring(1,s.length-1));
210     return s;
211   }
212   var n = new Number(s);
213   if (isNaN(n)) {
214     return s;
215   }
216   return n;
217 }
218
219 /* keep a cache of loaded CSV files */
220 CSV_Cache = new Array();
221
222
223 /*
224   load a CSV file, returing column names and data via a callback
225  */
226 function load_CSV(filename, callback) {
227
228   /* maybe its in the global cache? */
229   if (CSV_Cache[filename] !== undefined) {
230
231     if (CSV_Cache[filename].pending) {
232       /* its pending load by someone else. Add ourselves to the notify
233          queue so we are told when it is done */
234       CSV_Cache[filename].queue.push({filename:filename, callback:callback});
235       return;
236     }
237
238     /* its ready in the cache - return it via a delayed callback */
239     if (CSV_Cache[filename].data == null) {
240       var d = { filename: CSV_Cache[filename].filename,
241                 labels:   null,
242                 data:     null };
243       hide_div("nodata", false);
244       queue_call(callback, d);
245     } else {
246       var d = { filename: CSV_Cache[filename].filename,
247                 labels:   CSV_Cache[filename].labels.slice(0),
248                 data:     CSV_Cache[filename].data.slice(0) };
249       hide_div("nodata", true);
250       queue_call(callback, d);
251     }
252     return;
253   }
254
255   /* mark this one pending */
256   CSV_Cache[filename] = { filename:filename, pending: true, queue: new Array()};
257
258   /*
259     async callback when the CSV is loaded
260    */
261   function load_CSV_callback(caller) {
262     /* split by lines */
263     var csv = caller.r.responseText.split(/\n/g);
264
265     /* assume first line is column labels */
266     var labels = csv[0].split(/,/g);
267     for (var i=0; i<labels.length; i++) {
268       labels[i] = labels[i].replace(" ", "&nbsp;", "g");
269     }
270
271     /* the rest is data, we assume comma separation */
272     var data = new Array();
273     for (var i=1; i<csv.length; i++) {
274       var row = csv[i].split(/,/g);
275       if (row.length <= 1) {
276         continue;
277       }
278       data[i-1] = new Array();
279       data[i-1][0] = parse_date(row[0]);
280       for (var j=1; j<row.length; j++) {
281         data[i-1][j] = parse_value(row[j]);
282       }
283     }
284     
285     /* save into the global cache */
286     CSV_Cache[caller.filename].labels = labels;
287     CSV_Cache[caller.filename].data   = data;
288
289     /* give the caller a copy of the data (via slice()), as they may
290        want to modify it */
291     var d = { filename: CSV_Cache[filename].filename,
292               labels:   CSV_Cache[filename].labels.slice(0),
293               data:     CSV_Cache[filename].data.slice(0) };
294     queue_call(caller.callback, d);
295
296     /* fire off any pending callbacks */
297     while (CSV_Cache[caller.filename].queue.length > 0) {
298       var qe = CSV_Cache[caller.filename].queue.shift();
299       var d = { filename: filename,
300                 labels:   CSV_Cache[filename].labels.slice(0),
301                 data:     CSV_Cache[filename].data.slice(0) };
302       queue_call(qe.callback, d);
303     }
304     CSV_Cache[caller.filename].pending = false;
305     CSV_Cache[caller.filename].queue   = null;
306   }
307
308   /* make the async request for the file */
309   var caller = new Object();
310   caller.r = new XMLHttpRequest();
311   caller.callback = callback;
312   caller.filename = filename;
313
314   /* check the status when that returns */
315   caller.r.onreadystatechange = function() {
316     if (caller.r.readyState == 4) {
317       if (caller.r.status == 200) {
318         load_CSV_callback(caller);
319         hide_div("nodata", true);
320       } else {
321         /* the load failed */
322         hide_div("nodata", false);
323         queue_call(caller.callback, { filename: filename, data: null, labels: null });
324         while (CSV_Cache[caller.filename].queue.length > 0) {
325           var qe = CSV_Cache[caller.filename].queue.shift();
326           var d = { filename: CSV_Cache[filename].filename,
327                     labels:   null,
328                     data:     null };
329           queue_call(qe.callback, d);
330         }
331         CSV_Cache[caller.filename].pending = false;
332         CSV_Cache[caller.filename].queue   = null;
333         CSV_Cache[caller.filename].data   = null;
334         CSV_Cache[caller.filename].labels   = null;
335       }
336     }
337   }
338   caller.r.open("GET",filename,true);
339   caller.r.send(null);
340 }
341
342 /*
343   format an integer with N digits by adding leading zeros
344   javascript is so lame ...
345  */
346 function intLength(v, N) {
347   var r = v + '';
348   while (r.length < N) {
349     r = "0" + r;
350   }
351   return r;
352 }
353
354
355 /*
356   return the list of CSV files for the inverters for date pvdate
357  */
358 function days_csv_files() {
359   var list = new Array();
360   for (var i=0; i<serialnums.length; i++) {
361     list[i] = CSV_directory + date_YMD(pvdate) + "-WR5KA-08:" + 
362       serialnums[i] + ".csv";
363   }
364   return list;
365 }
366
367
368 /*
369   return the position of v in an array or -1
370  */
371 function pos_in_array(a, v) {
372   for (var i=0; i<a.length; i++) {
373     if (a[i] == v) {
374       return i;
375     }
376   }
377   return -1;
378 }
379
380 /*
381   see if v exists in array a
382  */
383 function in_array(a, v) {
384   return pos_in_array(a, v) != -1;
385 }
386
387
388 /*
389   return a set of columns from a CSV file
390  */
391 function get_csv_data(filenames, columns, callback) {
392   var caller = new Object();
393   caller.d = new Array();
394   caller.columns = columns.slice(0);
395   caller.filenames = filenames.slice(0);
396   caller.callback = callback;
397
398   /* initially blank data - we can tell a load has completed when it
399      is filled in */
400   for (var i=0; i<caller.filenames.length; i++) {
401     caller.d[i] = { filename: caller.filenames[i], labels: null, data: null};
402   }
403
404   /* process one loaded CSV, mapping the data for
405      the requested columns */
406   function process_one_csv(d) {
407     var labels = new Array();
408
409     if (d.data == null) {
410       queue_call(caller.callback, d);
411       return;
412     }
413
414     /* form the labels */
415     labels[0] = "Time";
416     for (var i=0; i<caller.columns.length; i++) {
417       labels[i+1] = caller.columns[i];
418     }
419
420     /* get the column numbers */
421     var cnums = new Array();
422     cnums[0] = 0;
423     for (var i=0; i<caller.columns.length; i++) {
424       cnums[i+1] = pos_in_array(d.labels, caller.columns[i]);
425     }
426   
427     /* map the data */
428     var data = new Array();
429     for (var i=0; i<d.data.length; i++) {
430       data[i] = new Array();
431       for (var j=0; j<cnums.length; j++) {
432         data[i][j] = d.data[i][cnums[j]];
433       }
434     }
435     d.data = data;
436     d.labels = labels;
437
438     for (var f=0; f<caller.filenames.length; f++) { 
439       if (d.filename == caller.d[f].filename) {
440         caller.d[f].labels = labels;
441         caller.d[f].data = data;
442       }
443     }
444
445     /* see if all the files are now loaded */
446     for (var f=0; f<caller.filenames.length; f++) { 
447       if (caller.d[f].data == null) {
448         return;
449       }
450     }
451
452     /* they are all loaded - make the callback */
453     queue_call(caller.callback, caller.d);
454   }
455
456   /* start the loading */
457   for (var i=0; i<caller.filenames.length; i++) {
458     load_CSV(caller.filenames[i], process_one_csv);
459   }
460 }
461
462
463 /*
464   apply a function to a set of data, giving it a new label
465  */
466 function apply_function(d, func, label) {
467   if (func == null) {
468     return;
469   }
470   for (var i=0; i<d.data.length; i++) {
471     var r = d.data[i];
472     d.data[i] = r.slice(0,1);
473     d.data[i][1] = func(r.slice(1))
474   }
475   d.labels = d.labels.slice(0,1);
476   d.labels[1] = label;
477 }
478
479
480 /* currently displayed graphs, indexed by divname */
481 global_graphs = new Array();
482
483 /*
484   find a graph by divname
485  */
486 function graph_find(divname) {
487   for (var i=0; i<global_graphs.length; i++) {
488     var g = global_graphs[i];
489     if (g.divname == divname) {
490       return g;
491     }
492   }
493   return null;
494 }
495
496 function nameAnnotation(ann) {
497   return "(" + ann.series + ", " + ann.xval + ")";
498 }
499
500 annotations = [];
501
502 /*
503   try to save an annotation via annotation.cgi
504  */
505 function save_annotation(ann) {
506   var r = new XMLHttpRequest();
507   r.open("GET", 
508          "cgi/annotation.cgi?series="+escape(ann.series)+"&xval="+ann.xval+"&text="+escape(ann.text), true);
509   r.send(null);  
510 }
511
512 /*
513   load annotations from annotations.csv
514  */
515 function load_annotations(g) {
516   function callback(d) {
517     var anns_by_name = new Array();
518     annotations = [];
519     for (var i=0; i<d.data.length; i++) {
520       var xval = d.data[i][0] + (tz_difference*60*60*1000);
521       if (xval < pvdate.valueOf() || xval >= (pvdate.valueOf() + (24*60*60*1000))) {
522         continue;
523       }
524       var ann = {
525       xval: xval,
526       series: d.data[i][1],
527       shortText: '!',
528       text: decodeURIComponent(d.data[i][2])
529       };
530       var a = anns_by_name[nameAnnotation(ann)];
531       if (a == undefined) {
532         anns_by_name[nameAnnotation(ann)] = annotations.length;
533         annotations.push(ann);
534       } else {
535         annotations[a] = ann;
536       }
537     }
538     for (var i=0; i<global_graphs.length; i++) {
539       var g = global_graphs[i];
540       g.setAnnotations(annotations);
541     }
542   }
543
544   load_CSV("../CSV/annotations.csv", callback);
545 }
546
547 function annotation_highlight(ann, point, dg, event) {
548   saveBg = ann.div.style.backgroundColor;
549   ann.div.style.backgroundColor = '#ddd';
550 }
551
552 function annotation_unhighlight(ann, point, dg, event) {
553   ann.div.style.backgroundColor = saveBg;
554 }
555
556 /*
557   handle annotation updates
558  */
559 function annotation_click(ann, point, dg, event) {
560   ann.text = prompt("Enter annotation", ann.text);
561   for (var i=0; i<annotations.length; i++) {
562     if (annotations[i].xval == ann.xval && annotations[i].series == ann.series) {
563       annotations[i].text = ann.text;
564       if (ann.text == '') {
565         annotations.splice(i,1);
566         i--;
567       }
568     }
569   }
570   for (var i=0; i<global_graphs.length; i++) {
571     var g = global_graphs[i];
572     if (g.series_names.indexOf(ann.series) != -1) {
573       g.setAnnotations(annotations);
574     }
575   }
576   save_annotation(ann);
577 }
578
579 /*
580   add a new annotation to one graph
581  */
582 function annotation_add_graph(g, p, ann) {
583   var anns = g.annotations();
584   if (p.annotation) {
585     /* its an update */
586     if (ann.text == '') {
587       var idx = anns.indexOf(p);
588       if (idx != -1) {
589         anns.splice(idx,1);
590       }
591     } else {
592       p.annotation.text = ann.text;
593     }
594   } else {
595     anns.push(ann);
596   }
597   g.setAnnotations(anns);
598 }
599
600 /*
601   add a new annotation
602  */
603 function annotation_add(event, p) {
604   var ann = {
605   series: p.name,
606   xval: p.xval,
607   shortText: '!',
608   text: prompt("Enter annotation", ""),
609   };
610   for (var i=0; i<global_graphs.length; i++) {
611     var g = global_graphs[i];
612     if (g.series_names.indexOf(p.name) != -1) {
613       annotation_add_graph(g, p, ann);
614     }
615   }
616
617   save_annotation(ann);
618 }
619
620
621 /* default dygraph attributes */
622 defaultAttrs = {
623  width: 700,
624  height: 350,
625  rollPeriod: 1,
626  strokeWidth: 1,
627  showRoller: true,
628  annotationMouseOverHandler: annotation_highlight,
629  annotationMouseOutHandler: annotation_unhighlight,
630  annotationClickHandler: annotation_click,
631  pointClickCallback: annotation_add
632 };
633
634
635 /*
636   graph results from a set of CSV files:
637     - apply func1 to the name columns within each file
638     - apply func2 between the files
639  */
640 function graph_csv_files_func(divname, filenames, columns, func1, func2, attrs) {
641   /* load the csv files */
642   var caller = new Object();
643   caller.divname   = divname;
644   caller.filenames = filenames.slice(0);
645   caller.columns   = columns.slice(0);
646   caller.func1     = func1;
647   caller.func2     = func2;
648   caller.attrs     = attrs;
649
650   if (attrs.series_base != undefined) {
651     caller.colname = attrs.series_base;  
652   } else if (columns.length == 1) {
653     caller.colname = columns[0]
654   } else {
655     caller.colname = divname;
656   }
657
658   /* called when all the data is loaded and we're ready to apply the
659      functions and graph */
660   function loaded_callback(d) {
661
662     if (d[0] == undefined) {
663       loading(false);
664       return;
665     }
666
667     for (var i=0; i<caller.filenames.length; i++) {
668       apply_function(d[i], caller.func1, caller.colname);
669     }
670
671     /* work out the y offsets to align the times */
672     var yoffsets = new Array();
673     yoffsets[0] = 0;
674     for (var i=1; i<caller.filenames.length; i++) {
675       yoffsets[i] = 0;
676       if (d[i].data[0][0] < d[0].data[0][0]) {
677         while (d[i].data[yoffsets[i]][0] < d[0].data[0][0]) {
678           yoffsets[i]++;
679         }
680       } else if (d[i].data[0][0] > d[0].data[0][0]) {
681         while (d[i].data[0][0] > d[0].data[-yoffsets[i]][0]) {
682           yoffsets[i]--;
683         }
684       }
685     }
686
687     if (caller.attrs.missingValue !== undefined) {
688       missingValue = attrs.missingValue;
689     } else {
690       missingValue = null;
691     }
692     
693     /* map the data */
694     var data = d[0].data;
695     for (var j=0; j<data.length; j++) {
696       if (data[j][1] == missingValue) {
697         data[j][1] = null;
698       }
699       for (var i=1; i<caller.filenames.length; i++) {
700         var y = j + yoffsets[i];
701         if (y < 0 || y >= d[i].data.length || d[i].data[y][1] == missingValue) {
702           data[j][i+1] = null;
703         } else {
704           data[j][i+1] = d[i].data[y][1];
705         }
706       }
707     }
708     
709     labels = new Array();
710     labels[0] = d[0].labels[0];
711     for (var i=0; i<caller.filenames.length; i++) {
712       labels[i+1] = caller.colname + (i+1);
713     }
714
715     var d2 = { labels: labels, data: data };
716     apply_function(d2, caller.func2, caller.colname);
717     
718     /* add the labels to the given graph attributes */
719     caller.attrs.labels = d2.labels;
720     
721     for (a in defaultAttrs) {
722       if (caller.attrs[a] == undefined) {
723         caller.attrs[a] = defaultAttrs[a];
724       }
725     }
726
727     caller.attrs['labelsDiv'] = divname + ":labels";
728
729     /* we need to create a new one, as otherwise we can't remove
730        the annotations */       
731     for (var i=0; i<global_graphs.length; i++) {
732         var g = global_graphs[i];
733         if (g.divname == divname) {
734           global_graphs.splice(i, 1);
735           g.destroy();
736           break;
737         }
738     }
739
740     /* create a new dygraph */
741     g = new Dygraph(document.getElementById(divname), d2.data, caller.attrs);
742     g.series_names = caller.attrs.labels;
743     g.divname = divname;
744     g.setAnnotations(annotations);
745     global_graphs.push(g);
746
747     loading(false);
748   }
749
750   /* fire off a request to load the data */
751   loading(true);
752   heading(divname);
753   graph_div(divname);
754   get_csv_data(caller.filenames, caller.columns, loaded_callback);
755 }
756
757
758 function product(v) {
759   var r = v[0];
760   for (var i=1; i<v.length; i++) {
761     r *= v[i];
762   }
763   return r;
764 }
765
766 function sum(v) {
767   var r = v[0];
768   for (var i=1; i<v.length; i++) {
769     r += v[i];
770   }
771   return r;
772 }
773
774
775
776 /*
777   graph one column from a set of CSV files
778  */
779 function graph_csv_files(divname, filenames, column, attrs) {
780   return graph_csv_files_func(divname, filenames, [column], null, null, attrs);
781 }
782
783 /*
784   graph one column from a set of CSV files as a sum over multiple files
785  */
786 function graph_sum_csv_files(divname, filenames, column, attrs) {
787   return graph_csv_files_func(divname, filenames, [column], null, sum, attrs);
788 }
789
790 /*
791   show all the live data graphs
792  */
793 function show_graphs() {
794   graph_sum_csv_files("Total AC Power (W)",
795                       days_csv_files(),
796                       "Pac",
797                       { includeZero: true });
798
799
800   graph_csv_files("AC Power from each inverter (W) [Pac]",
801                   days_csv_files(),
802                   "Pac",
803                   { includeZero: true });
804
805   graph_csv_files("DC Voltage for each inverter (V) [UpvIst]",
806                   days_csv_files(),
807                   "Upv-Ist",
808                   { includeZero: false,
809                     missingValue: 666 });
810
811   graph_csv_files("Target DC Voltage for each inverter (V) [UpvSoll]",
812                   days_csv_files(),
813                   "Upv-Soll",
814                   { includeZero: false,
815                     missingValue: 666 });
816
817
818   graph_sum_csv_files("Total DC current (A)",
819                       days_csv_files(),
820                       "Ipv",
821                       { includeZero: true });
822
823   graph_csv_files("DC Current for each inverter (A) [Ipv]",
824                   days_csv_files(),
825                   "Ipv",
826                   { includeZero: false });
827
828   graph_csv_files_func("DC Power for each inverter (W) [Ipv*UpvIst]",
829                        days_csv_files(),
830                        [ "Ipv", "Upv-Ist" ],
831                        product, null,
832                        { includeZero: true,
833                          series_base: 'Pdc' });
834
835   graph_csv_files_func("Total DC Power (W)",
836                        days_csv_files(),
837                        [ "Ipv", "Upv-Ist" ],
838                        product, sum,
839                        { includeZero: true });
840
841
842   function efficiency(v) {
843     var dc_pow = v[1] * v[2];
844     if (dc_pow == 0) {
845       return null;
846     }
847     return 100.0*(v[0] / dc_pow);
848   }
849
850   graph_csv_files_func("Inverter efficiencies (%) [(Ipv*UpvIst)/Pac]",
851                        days_csv_files(),
852                        [ "Pac", "Ipv", "Upv-Ist" ],
853                        efficiency, null,
854                        { includeZero: false,
855                          series_base: 'Eff'});
856
857   graph_csv_files("AC Voltage for each inverter (V) [Uac]",
858                   days_csv_files(),
859                   "Uac",
860                   { includeZero: false });
861
862   graph_csv_files("Lifetime Power for each inverter (kWh) [E-total]",
863                   days_csv_files(),
864                   "E-Total",
865                   { includeZero: false });
866
867   graph_sum_csv_files("Total Lifetime Power (kWh)",
868                   days_csv_files(),
869                   "E-Total",
870                   { includeZero: false });
871
872   graph_csv_files("Fan voltage for each inverter (V) [UFan]",
873                   days_csv_files(),
874                   "U-Fan",
875                   { includeZero: true, 
876                     avoidMinZero: true,
877                     valueRange: [0, 12] });
878
879   load_annotations();
880
881   in_redraw = true;
882 }
883
884 /*
885   called when the user selects a date
886  */
887 function set_date(e) {
888   var dp = datePickerController.getDatePicker("pvdate");
889   pvdate = date_round(dp.date);
890   hashvars['date'] = date_YMD(pvdate);
891   rewrite_hashvars(hashvars);
892   writeDebug("redrawing for: " + pvdate);
893   annotations = new Array();
894   show_graphs();
895 }
896
897 /*
898   setup the datepicker widget
899  */
900 function setup_datepicker() {
901     document.getElementById("pvdate").value = 
902       intLength(pvdate.getDate(),2) + "/" + intLength(pvdate.getMonth()+1, 2) + "/" + pvdate.getFullYear();
903     datePickerController.addEvent(document.getElementById("pvdate"), "change", set_date);
904 }
905
906
907 /* 
908    called to reload every few minutes
909  */
910 function reload_timer() {
911   /* flush the old CSV cache */
912   CSV_Cache = new Array();
913   writeDebug("reloading on timer");
914   if (loading_counter == 0) {
915     show_graphs();
916   }
917   setup_reload_timer();
918 }
919
920 /*
921   setup for automatic reloads
922  */
923 function setup_reload_timer() {
924   setTimeout(reload_timer, 300000);    
925 }
926
927
928 /*
929   toggle display of a div
930  */
931 function toggle_div(divname)
932 {
933   div = document.getElementById(divname);
934   img = document.getElementById("img-" + divname);
935   current_display = div.style.display;
936   old_src = img.getAttribute("src");
937   if (current_display != "none") {
938     div.style.display = "none";
939     img.setAttribute("src", old_src.replace("_unhide", "_hide"));
940   } else {
941     div.style.display = "block";
942     img.setAttribute("src", old_src.replace("_hide", "_unhide"));
943   }
944 }
945