added Envir code
[tridge/junkcode.git] / Envir / 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 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] == "stime") {
359             var dtime = parse_date(v[1], caller.basedate);
360             labels[0] = "time";
361             rdata[0] = dtime;
362           } else if (v[0] == "time") {
363             var dtime = parse_date(v[1], caller.basedate);
364             if (last_date != 0 && dtime < last_date) {
365               if (i < xml.length/2) {
366                 /*
367                   the earlier points were from the last night
368                  */
369                 for (var ii=0; ii<data.length; ii++) {
370                   data[ii][0] = data[ii][0] - 24*3600*1000;
371                 }
372                 writeDebug("subtraced day from: " + data.length);
373               } else {
374                 /* we've gone past the end of the day */
375                 addday = true;
376               }
377             }
378             if (addday) {
379               dtime += 24*3600*1000;
380             }
381             last_date = dtime;
382             labels[0] = "time";
383             rdata[0] = dtime;
384           } else if (v[1] != "") {
385             labels[num_labels+1] = prefix + "." + v[0];
386             rdata[num_labels+1] = parseFloat(v[1]);
387             num_labels++;
388           }
389         }
390         data[data.length] = rdata;
391       }
392     }
393     
394     /* save into the global cache */
395     CSV_Cache[caller.filename].labels = labels;
396     CSV_Cache[caller.filename].data   = data;
397
398     /* give the caller a copy of the data (via slice()), as they may
399        want to modify it */
400     var d = { filename: CSV_Cache[filename].filename,
401               labels:   CSV_Cache[filename].labels.slice(0),
402               data:     CSV_Cache[filename].data.slice(0) };
403     queue_global(caller.callback, d);
404
405     /* fire off any pending callbacks */
406     while (CSV_Cache[caller.filename].queue.length > 0) {
407       var qe = CSV_Cache[caller.filename].queue.shift();
408       var d = { filename: filename,
409                 labels:   CSV_Cache[filename].labels.slice(0),
410                 data:     CSV_Cache[filename].data.slice(0) };
411       queue_global(qe.callback, d);
412     }
413     CSV_Cache[caller.filename].pending = false;
414     CSV_Cache[caller.filename].queue   = null;
415   }
416
417   /* make the async request for the file */
418   var caller = new Object();
419   caller.r = new XMLHttpRequest();
420   caller.callback = callback;
421   caller.filename = filename;
422   caller.basedate = filename_date(filename);
423
424   /* check the status when that returns */
425   caller.r.onreadystatechange = function() {
426     if (caller.r.readyState == 4) {
427       if (caller.r.status == 200) {
428         queue_global(load_CSV_callback, caller);
429       } else {
430         /* the load failed */
431         queue_global(caller.callback, { filename: filename, data: null, labels: null });
432         while (CSV_Cache[caller.filename].queue.length > 0) {
433           var qe = CSV_Cache[caller.filename].queue.shift();
434           var d = { filename: CSV_Cache[filename].filename,
435                     labels:   null,
436                     data:     null };
437           queue_global(qe.callback, d);
438         }
439         CSV_Cache[caller.filename].pending = false;
440         CSV_Cache[caller.filename].queue   = null;
441         CSV_Cache[caller.filename].data   = null;
442         CSV_Cache[caller.filename].labels   = null;
443       }
444     }
445   }
446   caller.r.open("GET",filename,true);
447   caller.r.send(null);
448 }
449
450 function array_equal(a1, a2) {
451   if (a1.length != a2.length) {
452     return false;
453   }
454   for (var i=0; i<a1.length; i++) {
455     if (a1[i] != a2[i]) {
456       return false;
457     }
458   }
459   return true;
460 }
461
462 /*
463   combine two arrays that may have different labels
464  */
465 function combine_arrays(a1, l1, a2, l2) {
466   if (array_equal(l1, l2)) {
467     return a1.concat(a2);
468   }
469   /* we have two combine two arrays with different labels */
470   var map = new Array();
471   for (var i=0; i<l1.length; i++) {
472     map[i] = l2.indexOf(l1[i]);
473   }
474   ret = a1.slice(0);
475   for (var y=0; y<a2.length; y++) {
476     var r = new Array();
477     for (var x=0; x<l1.length; x++) {
478       if (map[x] == -1) {
479         r[x] = null;
480       } else {
481         r[x] = a2[y][map[x]];
482       }
483     }
484     ret.push(r);
485   }
486   return ret;
487 }
488
489 /*
490   load a comma separated list of CSV files, combining the data
491  */
492 function load_CSV_array(filenames, callback) {
493   var c = new Object();
494   c.filename = filenames;
495   c.files = filenames.split(',');
496   c.callback = callback;
497   c.data = new Array();
498   c.labels = new Array();
499   c.count = 0;
500
501   /*
502     async callback when a CSV is loaded
503    */
504   function load_CSV_array_callback(d) {
505     c.count++;
506     var i = c.files.indexOf(d.filename);
507     c.data[i] = d.data;
508     c.labels[i] = d.labels;
509     if (c.count == c.files.length) {
510       var ret = { filename: c.filename, data: c.data[0], labels: c.labels[0]};
511       for (var i=1; i<c.files.length; i++) {
512         if (c.data[i] != null) {
513           if (ret.data == null) {
514             ret.data = c.data[i];
515             ret.labels = c.labels[i];
516           } else {
517             ret.data = combine_arrays(ret.data, ret.labels, c.data[i], c.labels[i]);
518           }
519         }
520       }
521       if (ret.data == null) {
522         hide_div("nodata", false);
523       } else {
524         hide_div("nodata", true);
525       }
526       queue_global(c.callback, ret);
527     }
528   }
529
530   for (var i=0; i<c.files.length; i++) {
531     load_CSV(c.files[i], load_CSV_array_callback);
532   }
533 }
534
535 /*
536   format an integer with N digits by adding leading zeros
537   javascript is so lame ...
538  */
539 function intLength(v, N) {
540   var r = v + '';
541   while (r.length < N) {
542     r = "0" + r;
543   }
544   return r;
545 }
546
547
548 /*
549   return the position of v in an array or -1
550  */
551 function pos_in_array(a, v) {
552   for (var i=0; i<a.length; i++) {
553     if (a[i] == v) {
554       return i;
555     }
556   }
557   return -1;
558 }
559
560 /*
561   see if v exists in array a
562  */
563 function in_array(a, v) {
564   return pos_in_array(a, v) != -1;
565 }
566
567
568 /*
569   return a set of columns from a CSV file
570  */
571 function get_csv_data(filenames, columns, callback) {
572   var caller = new Object();
573   caller.d = new Array();
574   caller.columns = columns.slice(0);
575   caller.filenames = filenames.slice(0);
576   caller.callback = callback;
577
578   /* initially blank data - we can tell a load has completed when it
579      is filled in */
580   for (var i=0; i<caller.filenames.length; i++) {
581     caller.d[i] = { filename: caller.filenames[i], labels: null, data: null};
582   }
583
584   /* process one loaded CSV, mapping the data for
585      the requested columns */
586   function process_one_csv(d) {
587     var labels = new Array();
588
589     if (d.data == null) {
590       queue_global(caller.callback, d);
591       return;
592     }
593
594     /* form the labels */
595     labels[0] = "Time";
596     for (var i=0; i<caller.columns.length; i++) {
597       labels[i+1] = caller.columns[i];
598     }
599
600     /* get the column numbers */
601     var cnums = new Array();
602     cnums[0] = 0;
603     for (var i=0; i<caller.columns.length; i++) {
604       cnums[i+1] = pos_in_array(d.labels, caller.columns[i]);
605     }
606   
607     /* map the data */
608     var data = new Array();
609     for (var i=0; i<d.data.length; i++) {
610       data[i] = new Array();
611       for (var j=0; j<cnums.length; j++) {
612         data[i][j] = d.data[i][cnums[j]];
613       }
614     }
615     d.data = data;
616     d.labels = labels;
617
618     for (var f=0; f<caller.filenames.length; f++) { 
619       if (d.filename == caller.d[f].filename) {
620         caller.d[f].labels = labels;
621         caller.d[f].data = data;
622       }
623     }
624
625     /* see if all the files are now loaded */
626     for (var f=0; f<caller.filenames.length; f++) { 
627       if (caller.d[f].data == null) {
628         return;
629       }
630     }
631
632     /* they are all loaded - make the callback */
633     queue_global(caller.callback, caller.d);
634   }
635
636   /* start the loading */
637   for (var i=0; i<caller.filenames.length; i++) {
638     load_CSV_array(caller.filenames[i], process_one_csv);
639   }
640 }
641
642
643 /*
644   apply a function to a set of data, giving it a new label
645  */
646 function apply_function(d, func, label) {
647   if (func == null) {
648     return;
649   }
650   for (var i=0; i<d.data.length; i++) {
651     var r = d.data[i];
652     d.data[i] = r.slice(0,1);
653     d.data[i][1] = func(r.slice(1))
654   }
655   d.labels = d.labels.slice(0,1);
656   d.labels[1] = label;
657 }
658
659
660 /* currently displayed graphs, indexed by divname */
661 global_graphs = new Array();
662
663 /*
664   find a graph by divname
665  */
666 function graph_find(divname) {
667   for (var i=0; i<global_graphs.length; i++) {
668     var g = global_graphs[i];
669     if (g.divname == divname) {
670       return g;
671     }
672   }
673   return null;
674 }
675
676 function nameAnnotation(ann) {
677   return "(" + ann.series + ", " + ann.xval + ")";
678 }
679
680 annotations = [];
681
682 /*
683   try to save an annotation via annotation.cgi
684  */
685 function save_annotation(ann) {
686   var r = new XMLHttpRequest();
687   r.open("GET", 
688          "cgi/annotation.cgi?series="+escape(ann.series)+"&xval="+ann.xval+"&text="+escape(ann.text), true);
689   r.send(null);  
690 }
691
692 function round_annotations() {
693   for (var i=0; i<annotations.length; i++) {
694     annotations[i].xval = round_time(annotations[i].xval, defaultAttrs.averaging);
695   }
696 }
697
698 /*
699   load annotations from annotations.csv
700  */
701 function load_annotations(g) {
702   function callback(d) {
703     var anns_by_name = new Array();
704     annotations = [];
705     for (var i=0; i<d.data.length; i++) {
706       var xval = d.data[i][0] + (tz_difference*60*60*1000);
707       xval = round_time(xval, defaultAttrs.averaging);
708       if (xval.valueOf() < pvdate.valueOf() || 
709           xval.valueOf() >= (pvdate.valueOf() + (24*60*60*1000))) {
710         continue;
711       }
712       var ann = {
713       xval: xval.valueOf(),
714       series: d.data[i][1],
715       shortText: '!',
716       text: decodeURIComponent(d.data[i][2])
717       };
718       var a = anns_by_name[nameAnnotation(ann)];
719       if (a == undefined) {
720         anns_by_name[nameAnnotation(ann)] = annotations.length;
721         annotations.push(ann);
722       } else {
723         annotations[a] = ann;
724         if (ann.text == '') {
725           annotations.splice(a,1);
726         }
727       }
728     }
729     for (var i=0; i<global_graphs.length; i++) {
730       var g = global_graphs[i];
731       g.setAnnotations(annotations);
732     }
733   }
734
735   load_CSV("../CSV/annotations.csv", callback);
736 }
737
738 function annotation_highlight(ann, point, dg, event) {
739   saveBg = ann.div.style.backgroundColor;
740   ann.div.style.backgroundColor = '#ddd';
741 }
742
743 function annotation_unhighlight(ann, point, dg, event) {
744   ann.div.style.backgroundColor = saveBg;
745 }
746
747 /*
748   handle annotation updates
749  */
750 function annotation_click(ann, point, dg, event) {
751   ann.text = prompt("Enter annotation", ann.text);
752   if (ann.text == null) {
753     return;
754   }
755   for (var i=0; i<annotations.length; i++) {
756     if (annotations[i].xval == ann.xval && annotations[i].series == ann.series) {
757       annotations[i].text = ann.text;
758       if (ann.text == '' || ann.text == null) {
759         ann.text = '';
760         writeDebug("removing annnotation");
761         annotations.splice(i,1);
762         i--;
763       }
764     }
765   }
766   for (var i=0; i<global_graphs.length; i++) {
767     var g = global_graphs[i];
768     if (g.series_names.indexOf(ann.series) != -1) {
769       g.setAnnotations(annotations);
770     }
771   }
772   save_annotation(ann);
773 }
774
775 /*
776   add a new annotation to one graph
777  */
778 function annotation_add_graph(g, p, ann) {
779   var anns = g.annotations();
780   if (p.annotation) {
781     /* its an update */
782     if (ann.text == '') {
783       var idx = anns.indexOf(p);
784       if (idx != -1) {
785         anns.splice(idx,1);
786       }
787     } else {
788       p.annotation.text = ann.text;
789     }
790   } else {
791     anns.push(ann);
792   }
793   g.setAnnotations(anns);
794 }
795
796 /*
797   add a new annotation
798  */
799 function annotation_add(event, p) {
800   var ann = {
801   series: p.name,
802   xval: p.xval - (tz_difference*60*60*1000),
803   shortText: '!',
804   text: prompt("Enter annotation", ""),
805   };
806   if (ann.text == '' || ann.text == null) {
807     return;
808   }
809   for (var i=0; i<global_graphs.length; i++) {
810     var g = global_graphs[i];
811     if (g.series_names.indexOf(p.name) != -1) {
812       annotation_add_graph(g, p, ann);
813     }
814   }
815
816   save_annotation(ann);
817 }
818
819
820 /* default dygraph attributes */
821 defaultAttrs = {
822  width: 700,
823  height: 350,
824  strokeWidth: 1,
825  averaging: 1,
826  annotationMouseOverHandler: annotation_highlight,
827  annotationMouseOutHandler: annotation_unhighlight,
828  annotationClickHandler: annotation_click,
829  pointClickCallback: annotation_add
830 };
831
832 /*
833   round to averaged time
834  */
835 function round_time(t, n) {
836   var t2 = t / (60*1000);
837   t2 = Math.round((t2/n)-0.5);
838   t2 *= n * 60 * 1000;
839   return new Date(t2);
840 }
841
842 /*
843   average some data over time
844  */
845 function average_data(data, n) {
846   var ret = new Array();
847   var y;
848   var counts = new Array();
849   for (y=0; y<data.length; y++) {
850     var t = round_time(data[y][0], n);
851     if (ret.length > 0 && t.getTime() > ret[ret.length-1][0].getTime() + (6*60*60*1000)) {
852       /* there is a big gap - insert a missing value */
853       var t0 = ret[ret.length-1][0];
854       var tavg = Math.round((t0.getTime()+t.getTime())/2);
855       var t2 = new Date(tavg);
856       var y2 = ret.length;
857       ret[y2] = new Array();
858       ret[y2][0] = t2;
859       counts[y2] = new Array();
860       for (var x=1; x<ret[y2-1].length; x++) {
861         ret[y2][x] = null;
862         counts[y2][x] = 0;
863       }
864     }
865     var y2 = ret.length;
866     if (ret.length > 0 && t.getTime() == ret[ret.length-1][0].getTime()) {
867       var y2 = ret.length-1;
868       for (var x=1; x<data[y].length; x++) {
869         if (data[y][x] != null) {
870           ret[y2][x] += data[y][x];
871           counts[y2][x]++;
872         }
873       }
874     } else {
875       counts[y2] = new Array();
876       ret[y2] = data[y];
877       ret[y2][0] = t;
878       for (var x=1; x<ret[y2].length; x++) {
879         if (ret[y2][x] != null) {
880           counts[y2][x] = 1;
881         }
882       }
883     }
884   }
885   for (y2=0; y2<ret.length; y2++) {
886     for (var x=1; x<ret[y2].length; x++) {
887       if (ret[y2][x] != null) {
888         ret[y2][x] /= counts[y2][x];
889       }
890     }
891   }
892   return ret;
893 }
894
895 /*
896   graph results from a set of CSV files:
897     - apply func1 to the name columns within each file
898     - apply func2 between the files
899  */
900 function graph_csv_files_func(divname, filenames, columns, func1, func2, attrs) {
901   /* load the csv files */
902   var caller = new Object();
903   caller.divname   = divname;
904   caller.filenames = filenames.slice(0);
905   caller.columns   = columns.slice(0);
906   caller.func1     = func1;
907   caller.func2     = func2;
908   caller.attrs     = attrs;
909
910   if (attrs.series_base != undefined) {
911     caller.colname = attrs.series_base;  
912   } else if (columns.length == 1) {
913     caller.colname = columns[0]
914   } else {
915     caller.colname = divname;
916   }
917
918   /* called when all the data is loaded and we're ready to apply the
919      functions and graph */
920   function loaded_callback(d) {
921
922     if (d[0] == undefined) {
923       loading(false);
924       return;
925     }
926
927     for (var i=0; i<caller.filenames.length; i++) {
928       apply_function(d[i], caller.func1, caller.colname);
929     }
930
931     /* work out the y offsets to align the times */
932     var yoffsets = new Array();
933     for (var i=0; i<caller.filenames.length; i++) {
934       yoffsets[i] = 0;
935     }
936
937     if (caller.attrs.missingValue !== undefined) {
938       missingValue = attrs.missingValue;
939     } else {
940       missingValue = null;
941     }
942     
943     /* map the data */
944     var data = d[0].data;
945     for (var y=0; y<data.length; y++) {
946       if (data[y][1] == missingValue || data[y][1] == null) {
947         data[y][1] = null;
948       }
949       for (var f=1; f<caller.filenames.length; f++) {
950         var y2 = y + yoffsets[f];
951         if (y2 >= d[f].data.length) {
952           y2 = d[f].data.length-1;
953         }
954         if (y2 < 0) {
955           y2 = 0;
956         }
957         while (y2 > 0 && d[f].data[y2][0] > data[y][0]) {
958           y2--;
959         }
960         while (y2 < (d[f].data.length-1) && d[f].data[y2][0] < data[y][0]) {
961           y2++;
962         }
963         yoffsets[f] = y2 - y;
964         if (d[f].data.length <= y2 || 
965             d[f].data[y2][0] != data[y][0] || 
966             d[f].data[y2][1] == missingValue || 
967             d[f].data[y2][1] == null) {
968           data[y][f+1] = null;
969         } else {
970           data[y][f+1] = d[f].data[y2][1];
971         }
972       }
973     }
974     
975     labels = new Array();
976     if (caller.colname.constructor == Array) {
977       labels = caller.colname.slice(0);
978     } else {
979       labels[0] = d[0].labels[0];
980       for (var i=0; i<caller.filenames.length; i++) {
981         labels[i+1] = caller.colname + (i+1);
982       }
983     }
984
985     var d2 = { labels: labels, data: data };
986     apply_function(d2, caller.func2, caller.colname);
987     
988     /* add the labels to the given graph attributes */
989     caller.attrs.labels = d2.labels;
990     
991     for (a in defaultAttrs) {
992       if (caller.attrs[a] == undefined) {
993         caller.attrs[a] = defaultAttrs[a];
994       }
995     }
996
997     caller.attrs['labelsDiv'] = divname + ":labels";
998
999     /* we need to create a new one, as otherwise we can't remove
1000        the annotations */       
1001     for (var i=0; i<global_graphs.length; i++) {
1002         var g = global_graphs[i];
1003         if (g.divname == divname) {
1004           global_graphs.splice(i, 1);
1005           g.destroy();
1006           break;
1007         }
1008     }
1009
1010     var max_points = 900;
1011     if (is_IE) {
1012       max_points = 100;
1013     }
1014     if (auto_averaging) {
1015       if (d2.data != null && (d2.data.length/defaultAttrs.averaging) > max_points) {
1016         var averaging_times = [ 1, 2, 5, 10, 15, 20, 30, 60, 120, 240, 480 ];
1017         var tdiff = 1;
1018         var num_minutes = (d2.data[d2.data.length-1][0] - d2.data[0][0]) / (60*1000);
1019         for (var i=0; i<averaging_times.length-1; i++) {
1020           if (num_minutes / averaging_times[i] <= max_points) {
1021             break;
1022           }
1023         }
1024         set_averaging(averaging_times[i]);
1025         round_annotations();
1026       }
1027     }
1028
1029     var avg_data;
1030     if (attrs.averaging == false) {
1031       avg_data = d2.data.slice(0);
1032       for (var y=0; y<avg_data.length; y++) {
1033         avg_data[y][0] = new Date(avg_data[y][0]);
1034       }
1035     } else {
1036       avg_data = average_data(d2.data, defaultAttrs.averaging);
1037     }
1038
1039     if (attrs.maxtime != undefined) {
1040       var start = new Date() - (attrs.maxtime * 60 * 1000);
1041       var y;
1042       for (y=avg_data.length-1; y>0; y--) {
1043         if (avg_data[y][0] < start) {
1044           break;
1045         }
1046       }    
1047       avg_data = avg_data.slice(y);
1048     }
1049
1050     /* create a new dygraph */
1051     if (hashvars['nograph'] != '1') {
1052       g = new Dygraph(document.getElementById(divname), avg_data, caller.attrs);
1053       g.series_names = caller.attrs.labels;
1054       g.divname = divname;
1055       g.setAnnotations(annotations);
1056       global_graphs.push(g);
1057     }
1058
1059     loading(false);
1060   }
1061
1062
1063   /* fire off a request to load the data */
1064   loading(true);
1065   heading(divname);
1066   graph_div(divname);
1067
1068   function graph_callback(caller) {
1069     get_csv_data(caller.filenames, caller.columns, loaded_callback);
1070   }
1071
1072   queue_graph(graph_callback, caller);
1073 }
1074
1075
1076 function product(v) {
1077   var r = v[0];
1078   for (var i=1; i<v.length; i++) {
1079     r *= v[i];
1080   }
1081   return r;
1082 }
1083
1084 function sum(v) {
1085   var r = 0;
1086   for (var i=0; i<v.length; i++) {
1087     if (v[i] != null) {
1088       r += v[i];
1089     }
1090   }
1091   return r;
1092 }
1093
1094
1095
1096 /*
1097   graph one column from a set of CSV files
1098  */
1099 function graph_csv_files(divname, filenames, column, attrs) {
1100   return graph_csv_files_func(divname, filenames, [column], null, null, attrs);
1101 }
1102
1103 /*
1104   graph one column from a set of CSV files as a sum over multiple files
1105  */
1106 function graph_sum_csv_files(divname, filenames, column, attrs) {
1107   return graph_csv_files_func(divname, filenames, [column], null, sum, attrs);
1108 }
1109
1110 /*
1111   called when the user selects a date
1112  */
1113 function set_date(e) {
1114   var dp = datePickerController.getDatePicker("pvdate");
1115   pvdate = date_round(dp.date);
1116   hashvars['date'] = date_YMD(pvdate);
1117   rewrite_hashvars(hashvars);
1118   writeDebug("redrawing for: " + pvdate);
1119   annotations = new Array();
1120   show_graphs();
1121 }
1122
1123 /*
1124   setup the datepicker widget
1125  */
1126 function setup_datepicker() {
1127     document.getElementById("pvdate").value = 
1128       intLength(pvdate.getDate(),2) + "/" + intLength(pvdate.getMonth()+1, 2) + "/" + pvdate.getFullYear();
1129     datePickerController.addEvent(document.getElementById("pvdate"), "change", set_date);
1130 }
1131
1132
1133 /* 
1134    called to reload every few minutes
1135  */
1136 function reload_timer() {
1137   /* flush the old CSV cache */
1138   CSV_Cache = new Array();
1139   writeDebug("reloading on timer");
1140   if (loading_counter == 0) {
1141     show_graphs();
1142   }
1143   setup_reload_timer();
1144 }
1145
1146 /*
1147   setup for automatic reloads
1148  */
1149 function setup_reload_timer() {
1150   setTimeout(reload_timer, 300000);    
1151 }
1152
1153
1154 /*
1155   toggle display of a div
1156  */
1157 function toggle_div(divname)
1158 {
1159   var div = document.getElementById(divname);
1160   var img = document.getElementById("img-" + divname);
1161   var current_display = div.style.display;
1162   var old_src = img.getAttribute("src");
1163   if (current_display != "none") {
1164     div.style.display = "none";
1165     img.setAttribute("src", old_src.replace("_unhide", "_hide"));
1166   } else {
1167     div.style.display = "block";
1168     img.setAttribute("src", old_src.replace("_hide", "_unhide"));
1169   }
1170 }
1171
1172 /*
1173   change display period
1174  */
1175 function change_period(p) {
1176   p = +p;
1177   if (period_days != p) {
1178     period_days = p;
1179     auto_averaging = 1;
1180     set_averaging(1);
1181     show_graphs();
1182   }
1183 }
1184
1185 /*
1186   change averaging
1187  */
1188 function change_averaging() {
1189   var v = +document.getElementById('averaging').value;
1190   defaultAttrs.averaging = v;
1191   auto_averaging = 0;
1192   show_graphs();
1193 }
1194
1195 /*
1196   change averaging
1197  */
1198 function set_averaging(v) {
1199   var a = document.getElementById('averaging');
1200   a.value = v;
1201   defaultAttrs.averaging = v;
1202 }