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