added live data
[tridge/solar.git] / live / graphs_xml.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 var is_chrome = (navigator.userAgent.toLowerCase().indexOf('chrome') != -1);
9
10 /*
11   return the variables set after a '#' as a hash
12  */
13 function parse_hashvariables() {
14    var ret = [];
15    var url = window.location.hash.slice(1);
16    var vars = url.split(';');
17    for (var i=0; i<vars.length; i++) {
18      var x = vars[i].split('=');
19      if (x.length == 2) {
20        ret[x[0]] = x[1];
21      }
22    }
23    return ret;
24 }
25
26 hashvars = parse_hashvariables();
27
28 /*
29   rewrite the URL hash so you can bookmark particular dates
30  */
31 function rewrite_hashvars(vars) {
32   var hash = '';
33   for (var x in vars) {
34     hash += '' + x + '=' + vars[x] + ';';
35   }
36   hash = hash.slice(0,hash.length-1);
37   window.location.hash = hash;
38 }
39
40 /*
41   round a date back to midnight
42  */
43 function date_round(d) {
44   var d2 = new Date(d);
45   d2.setHours(0);
46   d2.setMinutes(0);
47   d2.setSeconds(0);
48   d2.setMilliseconds(0);
49   return d2;
50 }
51
52 /*
53   the date in Canberra
54  */
55 function canberraDate() {
56   var d = new Date();
57   return date_round(new Date(d.getTime() + (tz_difference*60*60*1000)));
58 }
59
60 /*
61   work out timezone
62  */
63 pvdate = date_round(new Date());
64 period_days = 1;
65 auto_averaging = 1;
66 tz_difference = 11 + (pvdate.getTimezoneOffset()/60);
67
68
69
70 /* marker for whether we are in a redraw with new data */
71 in_redraw = false;
72
73 /*
74   show a HTML heading
75  */
76 function heading(h) {
77   if (!in_redraw) {
78     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>");
79   }
80 }
81
82 /*
83   create a div for a graph
84  */
85 function graph_div(divname) {
86   if (!in_redraw) {
87     document.write(
88                    '<table><tr>' +
89                    '<td valign="top"><div id="' + divname + '" style="width:700px; height:350px;"></div></td>' +
90                    '<td valign="top">&nbsp;&nbsp;</td>' +
91                    '<td valign="top"><div id="' + divname + ':labels"></div></td>' +
92                    '</tr></table>\n');
93   }
94 }
95
96
97 /*
98   hide/show a div
99  */
100 function hide_div(divname, hidden) {
101   var div = document.getElementById(divname);
102   if (hidden) {
103     div.style.display = "none";
104   } else {
105     div.style.display = "block";
106   }
107 }
108
109 /* unhide the loading div when busy */
110 loading_counter = 0;
111
112 function loading(busy) {
113   if (busy) {
114     loading_counter++;
115     if (loading_counter == 1) {
116       started_loading=new Date();
117       hide_div("loading", false);
118     }
119   } else {
120     if (loading_counter > 0) {
121       loading_counter--;
122       if (loading_counter == 0) {
123         hide_div("loading", true);
124         var d = new Date();
125         var load_time = d.getTime() - started_loading.getTime();
126         writeDebug("Loading took: " + (load_time/1000));
127       }
128     }
129   }
130 }
131
132
133 /* a global call queue */
134 global_queue = new Array();
135 graph_queue = new Array();
136
137 /*
138   run the call queue
139  */
140 function run_queue(q) {
141   var qe = q[0];
142   var t_start = new Date();
143   qe.callback(qe.arg);
144   var t_end = new Date();
145   q.shift();
146   if (q.length > 0) {
147     var tdelay = (t_end.getTime() - t_start.getTime())/4;
148     if (tdelay < 1) {
149       run_queue(q);
150     } else {
151       setTimeout(function() { run_queue(q);}, tdelay);    
152     }
153   }
154 }
155
156 /*
157   queue a call. This is used to serialise long async operations in the
158   browser, so that you get less timeouts. It is especially needed on
159   IE, where the canvas widget is terribly slow.
160  */
161 function queue_call(q, callback, arg) {
162   q.push( { callback: callback, arg : arg });
163   if (q.length == 1) {
164     setTimeout(function() { run_queue(q); }, 1);
165   }
166 }
167
168 function queue_global(callback, arg) {
169   queue_call(global_queue, callback, arg);
170 }
171
172 function queue_graph(callback, arg) {
173   queue_call(graph_queue, callback, arg);
174 }
175
176
177 /*
178   date parser. Not completely general, but good enough
179  */
180 function parse_date(s, basedate) {
181   if (s.length == 5 && s[2] == ':') {
182     /* its a time since midnight */
183     var h = (+s.substring(0, 2));
184     var m = (+s.substring(3));
185     var d = basedate.getTime() + 1000*(h*60*60 + m*60);
186     return d;
187   }
188   if (s.length == 8 && s[2] == ':' && s[5] == ':') {
189     /* its a time since midnight */
190     var h = (+s.substring(0, 2));
191     var m = (+s.substring(3, 5));
192     var sec = (+s.substring(6));
193     var d = basedate.getTime() + 1000*(h*60*60 + m*60 + sec);
194     return d;
195   }
196   if (s.search("-") != -1) {
197     s = s.replace("-", "/", "g");
198   }
199   if (s[2] == '/') {
200     var x = s.split('/');
201     var d = new Date();
202     d.setDate(+x[0]);
203     d.setMonth(x[1]-1);
204     d.setYear(+x[2]);
205     return date_round(d);
206   }
207   if (s.search("/") != -1) {
208     return date_round(new Date(s));
209   }
210   /* assume time in milliseconds since 1970 */
211   return (+s);
212 };
213
214
215 /*
216   return a YYYY-MM-DD date string
217  */
218 function date_YMD(d) {
219   return '' + intLength(d.getFullYear(),4) + '-' + intLength(d.getMonth()+1,2) + '-' + intLength(d.getDate(),2);
220 }
221
222 /*
223   parse the date portion of a filename which starts with YYYY-MM-DD after a directory
224  */
225 function filename_date(filename) {
226   var idx = filename.lastIndexOf("/");
227   if (idx != -1) {
228     filename = filename.substring(idx+1);
229   }
230   if (filename[4] == '-' && filename[7] == '-') {
231     /* looks like a date */
232     var d = date_round(new Date());
233     d.setYear(+filename.substring(0,4));
234     d.setMonth(filename.substring(5,7)-1);
235     d.setDate(filename.substring(8,10));
236     return d;
237   }
238   return pvdate;
239 }
240
241
242 /*
243   parse a CSV value
244  */
245 function parse_value(s) {
246   if (s.substring(0,1) == '"') {
247     s = unescape(s.substring(1,s.length-1));
248     return s;
249   }
250   if (s == '') {
251     return null;
252   }
253   var n = new Number(s);
254   if (isNaN(n)) {
255     return s;
256   }
257   return n;
258 }
259
260 /* keep a cache of loaded CSV files */
261 CSV_Cache = new Array();
262
263
264 /*
265   load a CSV file, returning column names and data via a callback
266  */
267 function load_CSV(filename, callback) {
268
269   /* maybe its in the global cache? */
270   if (CSV_Cache[filename] !== undefined) {
271
272     if (CSV_Cache[filename].pending) {
273       /* its pending load by someone else. Add ourselves to the notify
274          queue so we are told when it is done */
275       CSV_Cache[filename].queue.push({filename:filename, callback:callback});
276       return;
277     }
278
279     /* its ready in the cache - return it via a delayed callback */
280     if (CSV_Cache[filename].data == null) {
281       var d = { filename: CSV_Cache[filename].filename,
282                 labels:   null,
283                 data:     null };
284       queue_global(callback, d);
285     } else {
286       var d = { filename: CSV_Cache[filename].filename,
287                 labels:   CSV_Cache[filename].labels.slice(0),
288                 data:     CSV_Cache[filename].data.slice(0) };
289       queue_global(callback, d);
290     }
291     return;
292   }
293
294   /* mark this one pending */
295   CSV_Cache[filename] = { filename:filename, pending: true, queue: new Array()};
296
297   /*
298     async callback when the CSV is loaded
299    */
300   function load_CSV_callback(caller) {
301     var data = new Array();
302     var labels = new Array();
303
304     if (filename.search(".csv") != -1) {
305       var csv = caller.r.responseText.split(/\n/g);
306
307       /* assume first line is column labels */
308       labels = csv[0].split(/,/g);
309       for (var i=0; i<labels.length; i++) {
310         labels[i] = labels[i].replace(" ", "&nbsp;", "g");
311       }
312
313       /* the rest is data, we assume comma separation */
314       for (var i=1; i<csv.length; i++) {
315         var row = csv[i].split(/,/g);
316         if (row.length <= 1) {
317           continue;
318         }
319         data[i-1] = new Array();
320         data[i-1][0] = parse_date(row[0], caller.basedate);
321         for (var j=1; j<row.length; j++) {
322           data[i-1][j] = parse_value(row[j]);
323         }
324       }
325     } else {
326       var xml = caller.r.responseText.split(/\n/g);
327       var last_date = 0;
328       var addday = false;
329       for (var i=0; i < xml.length; i++) {
330         if (xml[i].search("<hist>") != -1) {
331           continue;
332         }
333         var row = xml[i].split("<");
334         var num_labels = 0;
335         var prefix = "";
336         if (row.length < 2) {
337           continue;
338         }
339         var rdata = new Array();
340         for (var j=1; j<row.length; j++) {
341           var v = row[j].split(">");
342           if (v[0].substring(0,1) == "/") {
343             var tag = v[0].substring(1);
344             if (prefix.substring(prefix.length-tag.length) == tag) {
345               prefix = prefix.substring(0, prefix.length-tag.length);
346               if (prefix.substring(prefix.length-1) == ".") {
347                 prefix = prefix.substring(0, prefix.length-1);          
348               }
349             }
350             continue;
351           } else if (v[1] == "") {
352             if (prefix != "") {
353               prefix += ".";
354             }
355             prefix += v[0];
356             continue;
357           }
358           if (v[0] == "time") {
359             var dtime = parse_date(v[1], caller.basedate);
360             if (last_date != 0 && dtime < last_date) {
361               if (i < xml.length/2) {
362                 /*
363                   the earlier points were from the last night
364                  */
365                 for (var ii=0; ii<data.length; ii++) {
366                   data[ii][0] = data[ii][0] - 24*3600*1000;
367                 }
368                 writeDebug("subtraced day from: " + data.length);
369               } else {
370                 /* we've gone past the end of the day */
371                 addday = true;
372               }
373             }
374             if (addday) {
375               dtime += 24*3600*1000;
376             }
377             last_date = dtime;
378             labels[0] = "time";
379             rdata[0] = dtime;
380           } else if (v[1] != "") {
381             labels[num_labels+1] = prefix + "." + v[0];
382             rdata[num_labels+1] = parseFloat(v[1]);
383             num_labels++;
384           }
385         }
386         data[data.length] = rdata;
387       }
388     }
389     
390     /* save into the global cache */
391     CSV_Cache[caller.filename].labels = labels;
392     CSV_Cache[caller.filename].data   = data;
393
394     /* give the caller a copy of the data (via slice()), as they may
395        want to modify it */
396     var d = { filename: CSV_Cache[filename].filename,
397               labels:   CSV_Cache[filename].labels.slice(0),
398               data:     CSV_Cache[filename].data.slice(0) };
399     queue_global(caller.callback, d);
400
401     /* fire off any pending callbacks */
402     while (CSV_Cache[caller.filename].queue.length > 0) {
403       var qe = CSV_Cache[caller.filename].queue.shift();
404       var d = { filename: filename,
405                 labels:   CSV_Cache[filename].labels.slice(0),
406                 data:     CSV_Cache[filename].data.slice(0) };
407       queue_global(qe.callback, d);
408     }
409     CSV_Cache[caller.filename].pending = false;
410     CSV_Cache[caller.filename].queue   = null;
411   }
412
413   /* make the async request for the file */
414   var caller = new Object();
415   caller.r = new XMLHttpRequest();
416   caller.callback = callback;
417   caller.filename = filename;
418   caller.basedate = filename_date(filename);
419
420   /* check the status when that returns */
421   caller.r.onreadystatechange = function() {
422     if (caller.r.readyState == 4) {
423       if (caller.r.status == 200) {
424         queue_global(load_CSV_callback, caller);
425       } else {
426         /* the load failed */
427         queue_global(caller.callback, { filename: filename, data: null, labels: null });
428         while (CSV_Cache[caller.filename].queue.length > 0) {
429           var qe = CSV_Cache[caller.filename].queue.shift();
430           var d = { filename: CSV_Cache[filename].filename,
431                     labels:   null,
432                     data:     null };
433           queue_global(qe.callback, d);
434         }
435         CSV_Cache[caller.filename].pending = false;
436         CSV_Cache[caller.filename].queue   = null;
437         CSV_Cache[caller.filename].data   = null;
438         CSV_Cache[caller.filename].labels   = null;
439       }
440     }
441   }
442   caller.r.open("GET",filename,true);
443   caller.r.send(null);
444 }
445
446 function array_equal(a1, a2) {
447   if (a1.length != a2.length) {
448     return false;
449   }
450   for (var i=0; i<a1.length; i++) {
451     if (a1[i] != a2[i]) {
452       return false;
453     }
454   }
455   return true;
456 }
457
458 /*
459   combine two arrays that may have different labels
460  */
461 function combine_arrays(a1, l1, a2, l2) {
462   if (array_equal(l1, l2)) {
463     return a1.concat(a2);
464   }
465   /* we have two combine two arrays with different labels */
466   var map = new Array();
467   for (var i=0; i<l1.length; i++) {
468     map[i] = l2.indexOf(l1[i]);
469   }
470   ret = a1.slice(0);
471   for (var y=0; y<a2.length; y++) {
472     var r = new Array();
473     for (var x=0; x<l1.length; x++) {
474       if (map[x] == -1) {
475         r[x] = null;
476       } else {
477         r[x] = a2[y][map[x]];
478       }
479     }
480     ret.push(r);
481   }
482   return ret;
483 }
484
485 /*
486   load a comma separated list of CSV files, combining the data
487  */
488 function load_CSV_array(filenames, callback) {
489   var c = new Object();
490   c.filename = filenames;
491   c.files = filenames.split(',');
492   c.callback = callback;
493   c.data = new Array();
494   c.labels = new Array();
495   c.count = 0;
496
497   /*
498     async callback when a CSV is loaded
499    */
500   function load_CSV_array_callback(d) {
501     c.count++;
502     var i = c.files.indexOf(d.filename);
503     c.data[i] = d.data;
504     c.labels[i] = d.labels;
505     if (c.count == c.files.length) {
506       var ret = { filename: c.filename, data: c.data[0], labels: c.labels[0]};
507       for (var i=1; i<c.files.length; i++) {
508         if (c.data[i] != null) {
509           if (ret.data == null) {
510             ret.data = c.data[i];
511             ret.labels = c.labels[i];
512           } else {
513             ret.data = combine_arrays(ret.data, ret.labels, c.data[i], c.labels[i]);
514           }
515         }
516       }
517       if (ret.data == null) {
518         hide_div("nodata", false);
519       } else {
520         hide_div("nodata", true);
521       }
522       queue_global(c.callback, ret);
523     }
524   }
525
526   for (var i=0; i<c.files.length; i++) {
527     load_CSV(c.files[i], load_CSV_array_callback);
528   }
529 }
530
531 /*
532   format an integer with N digits by adding leading zeros
533   javascript is so lame ...
534  */
535 function intLength(v, N) {
536   var r = v + '';
537   while (r.length < N) {
538     r = "0" + r;
539   }
540   return r;
541 }
542
543
544 /*
545   return the list of CSV files for the inverters for date pvdate
546  */
547 function csv_files() {
548   var list = new Array();
549   var oneday = 24*60*60*1000;
550   var start_date = pvdate.getTime() - (period_days-1)*oneday;
551   if (start_date < first_data.getTime()) {
552     start_date = first_data.getTime();
553   }
554   for (var d=0; d<period_days; d++) {
555     var day = new Date(start_date + (d*oneday));
556     if (day.getTime() > pvdate.getTime()) {
557       break;
558     }
559     for (var i=0; i<serialnums.length; i++) {
560       var f = CSV_directory + date_YMD(day) + "-WR5KA-08:" + 
561         serialnums[i] + ".csv";
562       if (d == 0) {
563         list[i] = f;
564       } else {
565         list[i] += ',' + f;
566       }
567     }
568   }
569   return list;
570 }
571
572 /*
573   return the list of XML files for date pvdate
574  */
575 function xml_files() {
576   var list = new Array();
577   var oneday = 24*60*60*1000;
578   var start_date = pvdate.getTime() - (period_days-1)*oneday;
579   if (start_date < first_data.getTime()) {
580     start_date = first_data.getTime();
581   }
582   for (var d=0; d<period_days; d++) {
583     var day = new Date(start_date + (d*oneday));
584     if (day.getTime() > pvdate.getTime()) {
585       break;
586     }
587     var f = XML_directory + date_YMD(day) + ".xml";
588     if (d == 0) {
589       list = f;
590     } else {
591       list += ',' + f;
592     }
593   }
594   writeDebug("xml files: " + list);
595
596   return [ list ];
597 }
598
599
600 /*
601   return the position of v in an array or -1
602  */
603 function pos_in_array(a, v) {
604   for (var i=0; i<a.length; i++) {
605     if (a[i] == v) {
606       return i;
607     }
608   }
609   return -1;
610 }
611
612 /*
613   see if v exists in array a
614  */
615 function in_array(a, v) {
616   return pos_in_array(a, v) != -1;
617 }
618
619
620 /*
621   return a set of columns from a CSV file
622  */
623 function get_csv_data(filenames, columns, callback) {
624   var caller = new Object();
625   caller.d = new Array();
626   caller.columns = columns.slice(0);
627   caller.filenames = filenames.slice(0);
628   caller.callback = callback;
629
630   /* initially blank data - we can tell a load has completed when it
631      is filled in */
632   for (var i=0; i<caller.filenames.length; i++) {
633     caller.d[i] = { filename: caller.filenames[i], labels: null, data: null};
634   }
635
636   /* process one loaded CSV, mapping the data for
637      the requested columns */
638   function process_one_csv(d) {
639     var labels = new Array();
640
641     if (d.data == null) {
642       queue_global(caller.callback, d);
643       return;
644     }
645
646     /* form the labels */
647     labels[0] = "Time";
648     for (var i=0; i<caller.columns.length; i++) {
649       labels[i+1] = caller.columns[i];
650     }
651
652     /* get the column numbers */
653     var cnums = new Array();
654     cnums[0] = 0;
655     for (var i=0; i<caller.columns.length; i++) {
656       cnums[i+1] = pos_in_array(d.labels, caller.columns[i]);
657     }
658   
659     /* map the data */
660     var data = new Array();
661     for (var i=0; i<d.data.length; i++) {
662       data[i] = new Array();
663       for (var j=0; j<cnums.length; j++) {
664         data[i][j] = d.data[i][cnums[j]];
665       }
666     }
667     d.data = data;
668     d.labels = labels;
669
670     for (var f=0; f<caller.filenames.length; f++) { 
671       if (d.filename == caller.d[f].filename) {
672         caller.d[f].labels = labels;
673         caller.d[f].data = data;
674       }
675     }
676
677     /* see if all the files are now loaded */
678     for (var f=0; f<caller.filenames.length; f++) { 
679       if (caller.d[f].data == null) {
680         return;
681       }
682     }
683
684     /* they are all loaded - make the callback */
685     queue_global(caller.callback, caller.d);
686   }
687
688   /* start the loading */
689   for (var i=0; i<caller.filenames.length; i++) {
690     load_CSV_array(caller.filenames[i], process_one_csv);
691   }
692 }
693
694
695 /*
696   apply a function to a set of data, giving it a new label
697  */
698 function apply_function(d, func, label) {
699   if (func == null) {
700     return;
701   }
702   for (var i=0; i<d.data.length; i++) {
703     var r = d.data[i];
704     d.data[i] = r.slice(0,1);
705     d.data[i][1] = func(r.slice(1))
706   }
707   d.labels = d.labels.slice(0,1);
708   d.labels[1] = label;
709 }
710
711
712 /* currently displayed graphs, indexed by divname */
713 global_graphs = new Array();
714
715 /*
716   find a graph by divname
717  */
718 function graph_find(divname) {
719   for (var i=0; i<global_graphs.length; i++) {
720     var g = global_graphs[i];
721     if (g.divname == divname) {
722       return g;
723     }
724   }
725   return null;
726 }
727
728 function nameAnnotation(ann) {
729   return "(" + ann.series + ", " + ann.xval + ")";
730 }
731
732 annotations = [];
733
734 /*
735   try to save an annotation via annotation.cgi
736  */
737 function save_annotation(ann) {
738   var r = new XMLHttpRequest();
739   r.open("GET", 
740          "cgi/annotation.cgi?series="+escape(ann.series)+"&xval="+ann.xval+"&text="+escape(ann.text), true);
741   r.send(null);  
742 }
743
744 function round_annotations() {
745   for (var i=0; i<annotations.length; i++) {
746     annotations[i].xval = round_time(annotations[i].xval, defaultAttrs.averaging);
747   }
748 }
749
750 /*
751   load annotations from annotations.csv
752  */
753 function load_annotations(g) {
754   function callback(d) {
755     var anns_by_name = new Array();
756     annotations = [];
757     for (var i=0; i<d.data.length; i++) {
758       var xval = d.data[i][0] + (tz_difference*60*60*1000);
759       xval = round_time(xval, defaultAttrs.averaging);
760       if (xval.valueOf() < pvdate.valueOf() || 
761           xval.valueOf() >= (pvdate.valueOf() + (24*60*60*1000))) {
762         continue;
763       }
764       var ann = {
765       xval: xval.valueOf(),
766       series: d.data[i][1],
767       shortText: '!',
768       text: decodeURIComponent(d.data[i][2])
769       };
770       var a = anns_by_name[nameAnnotation(ann)];
771       if (a == undefined) {
772         anns_by_name[nameAnnotation(ann)] = annotations.length;
773         annotations.push(ann);
774       } else {
775         annotations[a] = ann;
776         if (ann.text == '') {
777           annotations.splice(a,1);
778         }
779       }
780     }
781     for (var i=0; i<global_graphs.length; i++) {
782       var g = global_graphs[i];
783       g.setAnnotations(annotations);
784     }
785   }
786
787   load_CSV("../CSV/annotations.csv", callback);
788 }
789
790 function annotation_highlight(ann, point, dg, event) {
791   saveBg = ann.div.style.backgroundColor;
792   ann.div.style.backgroundColor = '#ddd';
793 }
794
795 function annotation_unhighlight(ann, point, dg, event) {
796   ann.div.style.backgroundColor = saveBg;
797 }
798
799 /*
800   handle annotation updates
801  */
802 function annotation_click(ann, point, dg, event) {
803   ann.text = prompt("Enter annotation", ann.text);
804   if (ann.text == null) {
805     return;
806   }
807   for (var i=0; i<annotations.length; i++) {
808     if (annotations[i].xval == ann.xval && annotations[i].series == ann.series) {
809       annotations[i].text = ann.text;
810       if (ann.text == '' || ann.text == null) {
811         ann.text = '';
812         writeDebug("removing annnotation");
813         annotations.splice(i,1);
814         i--;
815       }
816     }
817   }
818   for (var i=0; i<global_graphs.length; i++) {
819     var g = global_graphs[i];
820     if (g.series_names.indexOf(ann.series) != -1) {
821       g.setAnnotations(annotations);
822     }
823   }
824   save_annotation(ann);
825 }
826
827 /*
828   add a new annotation to one graph
829  */
830 function annotation_add_graph(g, p, ann) {
831   var anns = g.annotations();
832   if (p.annotation) {
833     /* its an update */
834     if (ann.text == '') {
835       var idx = anns.indexOf(p);
836       if (idx != -1) {
837         anns.splice(idx,1);
838       }
839     } else {
840       p.annotation.text = ann.text;
841     }
842   } else {
843     anns.push(ann);
844   }
845   g.setAnnotations(anns);
846 }
847
848 /*
849   add a new annotation
850  */
851 function annotation_add(event, p) {
852   var ann = {
853   series: p.name,
854   xval: p.xval - (tz_difference*60*60*1000),
855   shortText: '!',
856   text: prompt("Enter annotation", ""),
857   };
858   if (ann.text == '' || ann.text == null) {
859     return;
860   }
861   for (var i=0; i<global_graphs.length; i++) {
862     var g = global_graphs[i];
863     if (g.series_names.indexOf(p.name) != -1) {
864       annotation_add_graph(g, p, ann);
865     }
866   }
867
868   save_annotation(ann);
869 }
870
871
872 /* default dygraph attributes */
873 defaultAttrs = {
874  width: 700,
875  height: 350,
876  strokeWidth: 1,
877  averaging: 1,
878  annotationMouseOverHandler: annotation_highlight,
879  annotationMouseOutHandler: annotation_unhighlight,
880  annotationClickHandler: annotation_click,
881  pointClickCallback: annotation_add
882 };
883
884 /*
885   round to averaged time
886  */
887 function round_time(t, n) {
888   var t2 = t / (60*1000);
889   t2 = Math.round((t2/n)-0.5);
890   t2 *= n * 60 * 1000;
891   return new Date(t2);
892 }
893
894 /*
895   average some data over time
896  */
897 function average_data(data, n) {
898   var ret = new Array();
899   var y;
900   var counts = new Array();
901   for (y=0; y<data.length; y++) {
902     var t = round_time(data[y][0], n);
903     if (ret.length > 0 && t.getTime() > ret[ret.length-1][0].getTime() + (6*60*60*1000)) {
904       /* there is a big gap - insert a missing value */
905       var t0 = ret[ret.length-1][0];
906       var tavg = Math.round((t0.getTime()+t.getTime())/2);
907       var t2 = new Date(tavg);
908       var y2 = ret.length;
909       ret[y2] = new Array();
910       ret[y2][0] = t2;
911       counts[y2] = new Array();
912       for (var x=1; x<ret[y2-1].length; x++) {
913         ret[y2][x] = null;
914         counts[y2][x] = 0;
915       }
916     }
917     var y2 = ret.length;
918     if (ret.length > 0 && t.getTime() == ret[ret.length-1][0].getTime()) {
919       var y2 = ret.length-1;
920       for (var x=1; x<data[y].length; x++) {
921         if (data[y][x] != null) {
922           ret[y2][x] += data[y][x];
923           counts[y2][x]++;
924         }
925       }
926     } else {
927       counts[y2] = new Array();
928       ret[y2] = data[y];
929       ret[y2][0] = t;
930       for (var x=1; x<ret[y2].length; x++) {
931         if (ret[y2][x] != null) {
932           counts[y2][x] = 1;
933         }
934       }
935     }
936   }
937   for (y2=0; y2<ret.length; y2++) {
938     for (var x=1; x<ret[y2].length; x++) {
939       if (ret[y2][x] != null) {
940         ret[y2][x] /= counts[y2][x];
941       }
942     }
943   }
944   return ret;
945 }
946
947 /*
948   graph results from a set of CSV files:
949     - apply func1 to the name columns within each file
950     - apply func2 between the files
951  */
952 function graph_csv_files_func(divname, filenames, columns, func1, func2, attrs) {
953   /* load the csv files */
954   var caller = new Object();
955   caller.divname   = divname;
956   caller.filenames = filenames.slice(0);
957   caller.columns   = columns.slice(0);
958   caller.func1     = func1;
959   caller.func2     = func2;
960   caller.attrs     = attrs;
961
962   if (attrs.series_base != undefined) {
963     caller.colname = attrs.series_base;  
964   } else if (columns.length == 1) {
965     caller.colname = columns[0]
966   } else {
967     caller.colname = divname;
968   }
969
970   /* called when all the data is loaded and we're ready to apply the
971      functions and graph */
972   function loaded_callback(d) {
973
974     if (d[0] == undefined) {
975       loading(false);
976       return;
977     }
978
979     for (var i=0; i<caller.filenames.length; i++) {
980       apply_function(d[i], caller.func1, caller.colname);
981     }
982
983     /* work out the y offsets to align the times */
984     var yoffsets = new Array();
985     for (var i=0; i<caller.filenames.length; i++) {
986       yoffsets[i] = 0;
987     }
988
989     if (caller.attrs.missingValue !== undefined) {
990       missingValue = attrs.missingValue;
991     } else {
992       missingValue = null;
993     }
994     
995     /* map the data */
996     var data = d[0].data;
997     for (var y=0; y<data.length; y++) {
998       if (data[y][1] == missingValue || data[y][1] == null) {
999         data[y][1] = null;
1000       }
1001       for (var f=1; f<caller.filenames.length; f++) {
1002         var y2 = y + yoffsets[f];
1003         if (y2 >= d[f].data.length) {
1004           y2 = d[f].data.length-1;
1005         }
1006         if (y2 < 0) {
1007           y2 = 0;
1008         }
1009         while (y2 > 0 && d[f].data[y2][0] > data[y][0]) {
1010           y2--;
1011         }
1012         while (y2 < (d[f].data.length-1) && d[f].data[y2][0] < data[y][0]) {
1013           y2++;
1014         }
1015         yoffsets[f] = y2 - y;
1016         if (d[f].data[y2][0] != data[y][0] || d[f].data[y2][1] == missingValue || d[f].data[y2][1] == null) {
1017           data[y][f+1] = null;
1018         } else {
1019           data[y][f+1] = d[f].data[y2][1];
1020         }
1021       }
1022     }
1023     
1024     labels = new Array();
1025     if (caller.colname.constructor == Array) {
1026       labels = caller.colname.slice(0);
1027     } else {
1028       labels[0] = d[0].labels[0];
1029       for (var i=0; i<caller.filenames.length; i++) {
1030         labels[i+1] = caller.colname + (i+1);
1031       }
1032     }
1033
1034     var d2 = { labels: labels, data: data };
1035     apply_function(d2, caller.func2, caller.colname);
1036     
1037     /* add the labels to the given graph attributes */
1038     caller.attrs.labels = d2.labels;
1039     
1040     for (a in defaultAttrs) {
1041       if (caller.attrs[a] == undefined) {
1042         caller.attrs[a] = defaultAttrs[a];
1043       }
1044     }
1045
1046     caller.attrs['labelsDiv'] = divname + ":labels";
1047
1048     /* we need to create a new one, as otherwise we can't remove
1049        the annotations */       
1050     for (var i=0; i<global_graphs.length; i++) {
1051         var g = global_graphs[i];
1052         if (g.divname == divname) {
1053           global_graphs.splice(i, 1);
1054           g.destroy();
1055           break;
1056         }
1057     }
1058
1059     var max_points = 900;
1060     if (is_IE) {
1061       max_points = 100;
1062     }
1063     if (auto_averaging) {
1064       if (d2.data != null && (d2.data.length/defaultAttrs.averaging) > max_points) {
1065         var averaging_times = [ 1, 2, 5, 10, 15, 20, 30, 60, 120, 240, 480 ];
1066         var tdiff = 1;
1067         var num_minutes = (d2.data[d2.data.length-1][0] - d2.data[0][0]) / (60*1000);
1068         for (var i=0; i<averaging_times.length-1; i++) {
1069           if (num_minutes / averaging_times[i] <= max_points) {
1070             break;
1071           }
1072         }
1073         set_averaging(averaging_times[i]);
1074         round_annotations();
1075       }
1076     }
1077
1078     var avg_data;
1079     if (attrs.averaging == false) {
1080       avg_data = d2.data.slice(0);
1081       for (var y=0; y<avg_data.length; y++) {
1082         avg_data[y][0] = new Date(avg_data[y][0]);
1083       }
1084     } else {
1085       avg_data = average_data(d2.data, defaultAttrs.averaging);
1086     }
1087
1088     if (attrs.maxtime != undefined) {
1089       var start = new Date() - (attrs.maxtime * 60 * 1000);
1090       var y;
1091       for (y=avg_data.length-1; y>0; y--) {
1092         if (avg_data[y][0] < start) {
1093           break;
1094         }
1095       }    
1096       avg_data = avg_data.slice(y);
1097     }
1098
1099     /* create a new dygraph */
1100     if (hashvars['nograph'] != '1') {
1101       g = new Dygraph(document.getElementById(divname), avg_data, caller.attrs);
1102       g.series_names = caller.attrs.labels;
1103       g.divname = divname;
1104       g.setAnnotations(annotations);
1105       global_graphs.push(g);
1106     }
1107
1108     loading(false);
1109   }
1110
1111
1112   /* fire off a request to load the data */
1113   loading(true);
1114   heading(divname);
1115   graph_div(divname);
1116
1117   function graph_callback(caller) {
1118     get_csv_data(caller.filenames, caller.columns, loaded_callback);
1119   }
1120
1121   queue_graph(graph_callback, caller);
1122 }
1123
1124
1125 function product(v) {
1126   var r = v[0];
1127   for (var i=1; i<v.length; i++) {
1128     r *= v[i];
1129   }
1130   return r;
1131 }
1132
1133 function sum(v) {
1134   var r = 0;
1135   for (var i=0; i<v.length; i++) {
1136     if (v[i] == null) {
1137       return null;
1138     }
1139     r += v[i];
1140   }
1141   return r;
1142 }
1143
1144
1145
1146 /*
1147   graph one column from a set of CSV files
1148  */
1149 function graph_csv_files(divname, filenames, column, attrs) {
1150   return graph_csv_files_func(divname, filenames, [column], null, null, attrs);
1151 }
1152
1153 /*
1154   graph one column from a set of CSV files as a sum over multiple files
1155  */
1156 function graph_sum_csv_files(divname, filenames, column, attrs) {
1157   return graph_csv_files_func(divname, filenames, [column], null, sum, attrs);
1158 }
1159
1160 /*
1161   show all the live data graphs
1162  */
1163 function show_graphs() {
1164   hide_div("nodata", true);
1165
1166   pvdate_base = pvdate.getTime();
1167
1168   graph_csv_files_func("Recent Data (W)",
1169                        xml_files(),
1170                        [ "msg.ch1.watts", "msg.ch2.watts", "msg.ch3.watts" ],
1171                        null, null,
1172                        { includeZero: true,
1173                            averaging: false,
1174                            maxtime: 15,
1175                            series_base: [ 'Time', 'Chan1', 'Chan2', 'Chan3' ]});
1176
1177   function total(v) {
1178     return pow = v[0] + v[1] + v[2];
1179   }
1180
1181   graph_csv_files_func("Total (W)",
1182                        xml_files(),
1183                        [ "msg.ch1.watts", "msg.ch2.watts", "msg.ch3.watts" ],
1184                        total, null,
1185                        { includeZero: true,
1186                          series_base: 'Total'});
1187
1188   graph_csv_files("Temperature (C)",
1189                   xml_files(),
1190                   "msg.tmpr",
1191                   { includeZero: false });
1192
1193   graph_csv_files_func("Channels (W)",
1194                        xml_files(),
1195                        [ "msg.ch1.watts", "msg.ch2.watts", "msg.ch3.watts" ],
1196                        null, null,
1197                        { includeZero: true,
1198                          series_base: [ 'Time', 'Chan1', 'Chan2', 'Chan3' ]});
1199   load_annotations();
1200
1201   in_redraw = true;
1202 }
1203
1204 /*
1205   called when the user selects a date
1206  */
1207 function set_date(e) {
1208   var dp = datePickerController.getDatePicker("pvdate");
1209   pvdate = date_round(dp.date);
1210   hashvars['date'] = date_YMD(pvdate);
1211   rewrite_hashvars(hashvars);
1212   writeDebug("redrawing for: " + pvdate);
1213   annotations = new Array();
1214   show_graphs();
1215 }
1216
1217 /*
1218   setup the datepicker widget
1219  */
1220 function setup_datepicker() {
1221     document.getElementById("pvdate").value = 
1222       intLength(pvdate.getDate(),2) + "/" + intLength(pvdate.getMonth()+1, 2) + "/" + pvdate.getFullYear();
1223     datePickerController.addEvent(document.getElementById("pvdate"), "change", set_date);
1224 }
1225
1226
1227 /* 
1228    called to reload every few minutes
1229  */
1230 function reload_timer() {
1231   /* flush the old CSV cache */
1232   CSV_Cache = new Array();
1233   writeDebug("reloading on timer");
1234   if (loading_counter == 0) {
1235     show_graphs();
1236   }
1237   setup_reload_timer();
1238 }
1239
1240 /*
1241   setup for automatic reloads
1242  */
1243 function setup_reload_timer() {
1244   setTimeout(reload_timer, 300000);    
1245 }
1246
1247
1248 /*
1249   toggle display of a div
1250  */
1251 function toggle_div(divname)
1252 {
1253   var div = document.getElementById(divname);
1254   var img = document.getElementById("img-" + divname);
1255   var current_display = div.style.display;
1256   var old_src = img.getAttribute("src");
1257   if (current_display != "none") {
1258     div.style.display = "none";
1259     img.setAttribute("src", old_src.replace("_unhide", "_hide"));
1260   } else {
1261     div.style.display = "block";
1262     img.setAttribute("src", old_src.replace("_hide", "_unhide"));
1263   }
1264 }
1265
1266 /*
1267   change display period
1268  */
1269 function change_period(p) {
1270   p = +p;
1271   if (period_days != p) {
1272     period_days = p;
1273     auto_averaging = 1;
1274     set_averaging(1);
1275     show_graphs();
1276   }
1277 }
1278
1279 /*
1280   change averaging
1281  */
1282 function change_averaging() {
1283   var v = +document.getElementById('averaging').value;
1284   defaultAttrs.averaging = v;
1285   auto_averaging = 0;
1286   show_graphs();
1287 }
1288
1289 /*
1290   change averaging
1291  */
1292 function set_averaging(v) {
1293   var a = document.getElementById('averaging');
1294   a.value = v;
1295   defaultAttrs.averaging = v;
1296 }