1 /**
  2  * @file graph.js
  3  *
  4  * @brief
  5  * The Graph is an interactive visualization chart to draw (measurement) data
  6  * in time. You can freely move and zoom in the graph by dragging and scrolling
  7  * in the window. The time scale on the axis is adjusted automatically, and
  8  * supports scales ranging from milliseconds to years.
  9  *
 10  * Graph is part of the CHAP Links library.
 11  *
 12  * Graph is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and
 13  * Internet Explorer 6+.
 14  *
 15  * @license
 16  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 17  * use this file except in compliance with the License. You may obtain a copy
 18  * of the License at
 19  *
 20  * http://www.apache.org/licenses/LICENSE-2.0
 21  *
 22  * Unless required by applicable law or agreed to in writing, software
 23  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 24  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 25  * License for the specific language governing permissions and limitations under
 26  * the License.
 27  *
 28  * Copyright (C) 2010-2013 Almende B.V.
 29  *
 30  * @author 	Jos de Jong, <jos@almende.org>
 31  * @date    2013-08-20
 32  * @version 1.3.2
 33  */
 34 
 35 /**
 36  * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library,
 37  * "links"
 38  */
 39 if (typeof links === 'undefined') {
 40     links = {};
 41     // important: do not use var, as "var links = {};" will overwrite
 42     //            the existing links variable value with undefined in IE8, IE7.
 43 }
 44 
 45 
 46 /**
 47  * Ensure the variable google exists
 48  */
 49 if (typeof google === 'undefined') {
 50     google = undefined;
 51     // important: do not use var, as "var google = undefined;" will overwrite
 52     //            the existing google variable value with undefined in IE8, IE7.
 53 }
 54 
 55 
 56 /**
 57  * @constructor links.Graph
 58  * The Graph is a visualization Graphs on a time line
 59  *
 60  * Graph is developed in javascript as a Google Visualization Chart.
 61  *
 62  * @param {Element} container   The DOM element in which the Graph will
 63  *                                  be created. Normally a div element.
 64  */
 65 links.Graph = function(container) {
 66     // create variables and set default values
 67     this.containerElement = container;
 68     this.width = "100%";
 69     this.height = "300px";
 70     this.start = null;
 71     this.end = null;
 72     this.autoDataStep = true;
 73     this.moveable = true;
 74     this.zoomable = true;
 75     this.showTooltip = true;
 76 
 77     this.redrawWhileMoving = true;
 78 
 79     this.legend = undefined;
 80     this.line = {};  // object default style for all lines
 81     this.lines = [];  // array containing specific line styles, colors, etc.
 82     /*
 83      this.defaultColors = ["red", "green", "blue", "magenta",
 84      "purple", "orange", "lime", "darkgreen", "darkblue",
 85      "turquoise", "gray", "darkgray", "darkred",  "chocolate",
 86      "plum", "#808000"];
 87      */
 88     /*
 89      this.defaultColors = ["red", "#008000", "#0000FF", "#FF00FF",
 90      "#800080", "#FFA500", "#00FF00", "#006400", "#00008B",
 91      "#40E0D0", "#808080", "#A9A9A9", "#8B0000",  "#D2691E",
 92      "#DDA0DD", "#808000"];
 93      */
 94     this.defaultColors = [
 95         "#3366CC", "#DC3912", "#FF9900", "#109618",
 96         "#990099", "#0099C6", "#DD4477", "#66AA00",
 97         "#B82E2E", "#316395", "#994499", "#22AA99",
 98         "#AAAA11", "#6633CC", "#E67300", "#8B0707"];
 99 
100     // The axis is drawn from -axisMargin to frame.width+axisMargin. When making
101     // axisMargin smaller, drawing the axis is faster as the axis is shorter.
102     // this makes scrolling faster. But when moving the Graph, the Graph
103     // needs to be redrawn more often, which makes movement less smooth.
104     //this.axisMargin = document.body.clientWidth; // in pixels
105     this.axisMargin = 800;  // in pixels
106 
107     this.mainPadding = 8; // pixels. Todo: make option?
108 
109     // create a default, empty array
110     this.data = [];
111 
112     // create a frame and canvas
113     this._create();
114 };
115 
116 
117 /**
118  * Main drawing logic. This is the function that needs to be called
119  * in the html page, to draw the Graph.
120  *
121  * A data table with the events must be provided, and an options table.
122  * Available options:
123  *  - width        Width for the Graph in pixels or percentage.
124  *  - height       Height for the Graph in pixels or percentage.
125  *  - start        A Date object with the start date of the visible range
126  *  - end          A Date object with the end date of the visible range
127  * TODO: describe all options
128  *
129  *  All options are optional.
130  *
131  * @param {google.visualization.DataTable | Array} data
132  *                                  The data containing the events for the Graph.
133  *                                  Object DataTable is defined in
134  *                                  google.visualization.DataTable.
135  * @param {Object} options          A name/value map containing settings for the
136  *                                  Graph.
137  */
138 links.Graph.prototype.draw = function(data, options) {
139     this._readData(data);
140 
141     if (options != undefined) {
142         // retrieve parameter values
143         if (options.width != undefined)         this.width = options.width;
144         if (options.height != undefined)        this.height = options.height;
145 
146         if (options.start != undefined)         this.start = options.start;
147         if (options.end != undefined)           this.end = options.end;
148         if (options.min != undefined)           this.min = options.min;
149         if (options.max != undefined)           this.max = options.max;
150         if (options.zoomMin != undefined)       this.zoomMin = options.zoomMin;
151         if (options.zoomMax != undefined)       this.zoomMax = options.zoomMax;
152         if (options.scale != undefined)         this.scale = options.scale;
153         if (options.step != undefined)          this.step = options.step;
154         if (options.autoDataStep != undefined)  this.autoDataStep = options.autoDataStep;
155 
156         if (options.moveable != undefined)      this.moveable = options.moveable;
157         if (options.zoomable != undefined)      this.zoomable = options.zoomable;
158 
159         if (options.line != undefined)          this.line = options.line;
160         if (options.lines != undefined)         this.lines = options.lines;
161 
162         if (options.vStart != undefined)        this.vStart = options.vStart;
163         if (options.vEnd != undefined)          this.vEnd = options.vEnd;
164         if (options.vMin != undefined)          this.vMinFixed = options.vMin;
165         if (options.vMax != undefined)          this.vMaxFixed = options.vMax;
166         if (options.vStep != undefined)         this.vStepSize = options.vStep;
167         if (options.vPrettyStep != undefined)   this.vPrettyStep = options.vPrettyStep;
168         if (options.vAreas != undefined)        this.vAreas = options.vAreas;
169 
170         if (options.legend != undefined)        this.legend = options.legend;  // can contain legend.width
171         if (options.tooltip != undefined) {
172             this.showTooltip = (options.tooltip != false);
173             if (typeof options.tooltip === 'function') {
174                 this.tooltipFormatter = options.tooltip;
175             }
176         }
177 
178         // check for deprecated options
179         if (options.intervalMin != undefined) {
180             this.zoomMin = options.intervalMin;
181             console.log('WARNING: Option intervalMin is deprecated. Use zoomMin instead');
182         }
183         if (options.intervalMax != undefined) {
184             this.zoomMax = options.intervalMax;
185             console.log('WARNING: Option intervalMax is deprecated. Use zoomMax instead');
186         }
187 
188         // TODO: add options to set the horizontal and vertical range
189     }
190 
191     // apply size and time range
192     var redrawNow = false;
193     this.setSize(this.width, this.height);
194 
195     this.setVisibleChartRange(this.start, this.end, redrawNow);
196     if (this.scale && this.step) {
197         this.hStep.setScale(this.scale, this.step);
198     }
199 
200     // draw the Graph
201     this.redraw();
202 
203     this.trigger('ready');
204 };
205 
206 /**
207  * fire an event
208  * @param {String} event   The name of an event, for example "rangechange" or "edit"
209  * @param {Object} params  Optional parameters
210  */
211 links.Graph.prototype.trigger = function (event, params) {
212     // fire event via the links event bus
213     links.events.trigger(this, event, params);
214 
215     // fire the ready event
216     if (google && google.visualization && google.visualization.events) {
217         google.visualization.events.trigger(this, event, params);
218     }
219 };
220 
221 
222 /**
223  * Read data into the graph
224  */
225 links.Graph.prototype._readData = function(data) {
226     if (google && google.visualization && google.visualization.DataTable &&
227         data instanceof google.visualization.DataTable) {
228         // read a Google DataTable
229         this.data = [];
230 
231         for (var col = 1, cols = data.getNumberOfColumns(); col < cols; col++) {
232             var dataset = [];
233             for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
234                 dataset.push({"date" : data.getValue(row, 0), "value" : data.getValue(row, col)} );
235             }
236 
237             var graph = {
238                 "label": data.getColumnLabel(col),
239                 "type": undefined,
240                 "dataRange": undefined,
241                 "rowRange": undefined,
242                 "visibleRowRange": undefined,
243                 "data": dataset
244             };
245             this.data.push(graph);
246 
247             // TODO: sort by date, and remove redundant null values
248         }
249     }
250     else {
251         // parse Javascipt array
252         this.data = data || [];
253     }
254 
255     // calculate date and value ranges
256     for (var i = 0, len = this.data.length; i < len; i++) {
257         var graph = this.data[i];
258 
259         var fields;
260         if (graph.type == 'area') {
261             fields = ['start', 'end']; // area
262         }
263         else {
264             fields = ['date']; // 'line' or 'event'
265         }
266 
267         graph.dataRange = this._getDataRange(graph.data);
268         graph.rowRange = this._getRowRange(graph.data, fields);
269     }
270 };
271 
272 /**
273  * @constructor  links.Graph.StepDate
274  * The class StepDate is an iterator for dates. You provide a start date and an
275  * end date. The class itself determines the best scale (step size) based on the
276  * provided start Date, end Date, and minimumStep.
277  *
278  * If minimumStep is provided, the step size is chosen as close as possible
279  * to the minimumStep but larger than minimumStep. If minimumStep is not
280  * provided, the scale is set to 1 DAY.
281  * The minimumStep should correspond with the onscreen size of about 6 characters
282  *
283  * Alternatively, you can set a scale by hand.
284  * After creation, you can initialize the class by executing start(). Then you
285  * can iterate from the start date to the end date via next(). You can check if
286  * the end date is reached with the function end(). After each step, you can
287  * retrieve the current date via get().
288  * The class step has scales ranging from milliseconds, seconds, minutes, hours,
289  * days, to years.
290  *
291  * Version: 1.2
292  *
293  * @param {Date} start          The start date, for example new Date(2010, 9, 21)
294  *                              or new Date(2010, 9, 21, 23, 45, 00)
295  * @param {Date} end            The end date
296  * @param {Number}  minimumStep Optional. Minimum step size in milliseconds
297  */
298 links.Graph.StepDate = function(start, end, minimumStep) {
299 
300     // variables
301     this.current = new Date();
302     this._start = new Date();
303     this._end = new Date();
304 
305     this.autoScale  = true;
306     this.scale = links.Graph.StepDate.SCALE.DAY;
307     this.step = 1;
308 
309     // initialize the range
310     this.setRange(start, end, minimumStep);
311 };
312 
313 /// enum scale
314 links.Graph.StepDate.SCALE = {
315     MILLISECOND: 1,
316     SECOND: 2,
317     MINUTE: 3,
318     HOUR: 4,
319     DAY: 5,
320     WEEKDAY: 6,
321     MONTH: 7,
322     YEAR: 8
323 };
324 
325 
326 /**
327  * Set a new range
328  * If minimumStep is provided, the step size is chosen as close as possible
329  * to the minimumStep but larger than minimumStep. If minimumStep is not
330  * provided, the scale is set to 1 DAY.
331  * The minimumStep should correspond with the onscreen size of about 6 characters
332  * @param {Date} start        The start date and time.
333  * @param {Date} end          The end date and time.
334  * @param {int}  minimumStep  Optional. Minimum step size in milliseconds
335  */
336 links.Graph.StepDate.prototype.setRange = function(start, end, minimumStep) {
337     if (!(start instanceof Date) || !(end instanceof Date)) {
338         //throw  "No legal start or end date in method setRange";
339         return;
340     }
341 
342     this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
343     this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
344 
345     if (this.autoScale) {
346         this.setMinimumStep(minimumStep);
347     }
348 };
349 
350 /**
351  * Set the step iterator to the start date.
352  */
353 links.Graph.StepDate.prototype.start = function() {
354     this.current = new Date(this._start.valueOf());
355     this.roundToMinor();
356 };
357 
358 /**
359  * Round the current date to the first minor date value
360  * This must be executed once when the current date is set to start Date
361  */
362 links.Graph.StepDate.prototype.roundToMinor = function() {
363     // round to floor
364     // IMPORTANT: we have no breaks in this switch! (this is no bug)
365     //noinspection FallthroughInSwitchStatementJS
366     switch (this.scale) {
367         case links.Graph.StepDate.SCALE.YEAR:
368             this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
369             this.current.setMonth(0);
370         case links.Graph.StepDate.SCALE.MONTH:        this.current.setDate(1);
371         case links.Graph.StepDate.SCALE.DAY:          // intentional fall through
372         case links.Graph.StepDate.SCALE.WEEKDAY:      this.current.setHours(0);
373         case links.Graph.StepDate.SCALE.HOUR:         this.current.setMinutes(0);
374         case links.Graph.StepDate.SCALE.MINUTE:       this.current.setSeconds(0);
375         case links.Graph.StepDate.SCALE.SECOND:       this.current.setMilliseconds(0);
376         //case links.Graph.StepDate.SCALE.MILLISECOND: // nothing to do for milliseconds
377     }
378 
379     if (this.step != 1) {
380         // round down to the first minor value that is a multiple of the current step size
381         switch (this.scale) {
382             case links.Graph.StepDate.SCALE.MILLISECOND:  this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step);  break;
383             case links.Graph.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
384             case links.Graph.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
385             case links.Graph.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
386             case links.Graph.StepDate.SCALE.WEEKDAY:      // intentional fall through
387             case links.Graph.StepDate.SCALE.DAY:          this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
388             case links.Graph.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step);  break;
389             case links.Graph.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
390             default: break;
391         }
392     }
393 };
394 
395 /**
396  * Check if the end date is reached
397  * @return {boolean}  true if the current date has passed the end date
398  */
399 links.Graph.StepDate.prototype.end = function () {
400     return (this.current.valueOf() > this._end.valueOf());
401 };
402 
403 /**
404  * Do the next step
405  */
406 links.Graph.StepDate.prototype.next = function() {
407     var prev = this.current.valueOf();
408 
409     // Two cases, needed to prevent issues with switching daylight savings
410     // (end of March and end of October)
411     if (this.current.getMonth() < 6)   {
412         switch (this.scale) {
413             case links.Graph.StepDate.SCALE.MILLISECOND:
414 
415                 this.current = new Date(this.current.valueOf() + this.step); break;
416             case links.Graph.StepDate.SCALE.SECOND:       this.current = new Date(this.current.valueOf() + this.step * 1000); break;
417             case links.Graph.StepDate.SCALE.MINUTE:       this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
418             case links.Graph.StepDate.SCALE.HOUR:
419                 this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
420                 // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
421                 var h = this.current.getHours();
422                 this.current.setHours(h - (h % this.step));
423                 break;
424             case links.Graph.StepDate.SCALE.WEEKDAY:      // intentional fall through
425             case links.Graph.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
426             case links.Graph.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
427             case links.Graph.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
428             default:                      break;
429         }
430     }
431     else {
432         switch (this.scale) {
433             case links.Graph.StepDate.SCALE.MILLISECOND:  this.current = new Date(this.current.valueOf() + this.step); break;
434             case links.Graph.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() + this.step); break;
435             case links.Graph.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() + this.step); break;
436             case links.Graph.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() + this.step); break;
437             case links.Graph.StepDate.SCALE.WEEKDAY:      // intentional fall through
438             case links.Graph.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
439             case links.Graph.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
440             case links.Graph.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
441             default:                      break;
442         }
443     }
444 
445     if (this.step != 1) {
446         // round down to the correct major value
447         switch (this.scale) {
448             case links.Graph.StepDate.SCALE.MILLISECOND:  if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);  break;
449             case links.Graph.StepDate.SCALE.SECOND:       if(this.current.getSeconds() < this.step) this.current.setSeconds(0);  break;
450             case links.Graph.StepDate.SCALE.MINUTE:       if(this.current.getMinutes() < this.step) this.current.setMinutes(0);  break;
451             case links.Graph.StepDate.SCALE.HOUR:         if(this.current.getHours() < this.step) this.current.setHours(0);  break;
452             case links.Graph.StepDate.SCALE.WEEKDAY:      // intentional fall through
453             case links.Graph.StepDate.SCALE.DAY:          if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
454             case links.Graph.StepDate.SCALE.MONTH:        if(this.current.getMonth() < this.step) this.current.setMonth(0);  break;
455             case links.Graph.StepDate.SCALE.YEAR:         break; // nothing to do for year
456             default:                break;
457         }
458     }
459 
460     // safety mechanism: if current time is still unchanged, move to the end
461     if (this.current.valueOf() == prev) {
462         this.current = new Date(this._end.valueOf());
463     }
464 };
465 
466 
467 /**
468  * Get the current datetime
469  * @return {Date}  current The current date
470  */
471 links.Graph.StepDate.prototype.getCurrent = function() {
472     return this.current;
473 };
474 
475 /**
476  * Set a custom scale. Autoscaling will be disabled.
477  * For example setScale(SCALE.MINUTES, 5) will result
478  * in minor steps of 5 minutes, and major steps of an hour.
479  *
480  * @param {links.Graph.StepDate.SCALE} newScale
481  *                               A scale. Choose from SCALE.MILLISECOND,
482  *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
483  *                               SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
484  *                               SCALE.YEAR.
485  * @param {Number}     newStep   A step size, by default 1. Choose for
486  *                               example 1, 2, 5, or 10.
487  */
488 links.Graph.StepDate.prototype.setScale = function(newScale, newStep) {
489     this.scale = newScale;
490 
491     if (newStep > 0) {
492         this.step = newStep;
493     }
494 
495     this.autoScale = false;
496 };
497 
498 /**
499  * Enable or disable autoscaling
500  * @param {boolean} enable  If true, autoascaling is set true
501  */
502 links.Graph.StepDate.prototype.setAutoScale = function (enable) {
503     this.autoScale = enable;
504 };
505 
506 
507 /**
508  * Automatically determine the scale that bests fits the provided minimum step
509  * @param {Number} minimumStep  The minimum step size in milliseconds
510  */
511 links.Graph.StepDate.prototype.setMinimumStep = function(minimumStep) {
512     if (minimumStep == undefined) {
513         return;
514     }
515 
516     var stepYear       = (1000 * 60 * 60 * 24 * 30 * 12);
517     var stepMonth      = (1000 * 60 * 60 * 24 * 30);
518     var stepDay        = (1000 * 60 * 60 * 24);
519     var stepHour       = (1000 * 60 * 60);
520     var stepMinute     = (1000 * 60);
521     var stepSecond     = (1000);
522     var stepMillisecond= (1);
523 
524     // find the smallest step that is larger than the provided minimumStep
525     if (stepYear*1000 > minimumStep)        {this.scale = links.Graph.StepDate.SCALE.YEAR;        this.step = 1000;}
526     if (stepYear*500 > minimumStep)         {this.scale = links.Graph.StepDate.SCALE.YEAR;        this.step = 500;}
527     if (stepYear*100 > minimumStep)         {this.scale = links.Graph.StepDate.SCALE.YEAR;        this.step = 100;}
528     if (stepYear*50 > minimumStep)          {this.scale = links.Graph.StepDate.SCALE.YEAR;        this.step = 50;}
529     if (stepYear*10 > minimumStep)          {this.scale = links.Graph.StepDate.SCALE.YEAR;        this.step = 10;}
530     if (stepYear*5 > minimumStep)           {this.scale = links.Graph.StepDate.SCALE.YEAR;        this.step = 5;}
531     if (stepYear > minimumStep)             {this.scale = links.Graph.StepDate.SCALE.YEAR;        this.step = 1;}
532     if (stepMonth*3 > minimumStep)          {this.scale = links.Graph.StepDate.SCALE.MONTH;       this.step = 3;}
533     if (stepMonth > minimumStep)            {this.scale = links.Graph.StepDate.SCALE.MONTH;       this.step = 1;}
534     if (stepDay*5 > minimumStep)            {this.scale = links.Graph.StepDate.SCALE.DAY;         this.step = 5;}
535     if (stepDay*2 > minimumStep)            {this.scale = links.Graph.StepDate.SCALE.DAY;         this.step = 2;}
536     if (stepDay > minimumStep)              {this.scale = links.Graph.StepDate.SCALE.DAY;         this.step = 1;}
537     if (stepDay/2 > minimumStep)            {this.scale = links.Graph.StepDate.SCALE.WEEKDAY;     this.step = 1;}
538     if (stepHour*4 > minimumStep)           {this.scale = links.Graph.StepDate.SCALE.HOUR;        this.step = 4;}
539     if (stepHour > minimumStep)             {this.scale = links.Graph.StepDate.SCALE.HOUR;        this.step = 1;}
540     if (stepMinute*15 > minimumStep)        {this.scale = links.Graph.StepDate.SCALE.MINUTE;      this.step = 15;}
541     if (stepMinute*10 > minimumStep)        {this.scale = links.Graph.StepDate.SCALE.MINUTE;      this.step = 10;}
542     if (stepMinute*5 > minimumStep)         {this.scale = links.Graph.StepDate.SCALE.MINUTE;      this.step = 5;}
543     if (stepMinute > minimumStep)           {this.scale = links.Graph.StepDate.SCALE.MINUTE;      this.step = 1;}
544     if (stepSecond*15 > minimumStep)        {this.scale = links.Graph.StepDate.SCALE.SECOND;      this.step = 15;}
545     if (stepSecond*10 > minimumStep)        {this.scale = links.Graph.StepDate.SCALE.SECOND;      this.step = 10;}
546     if (stepSecond*5 > minimumStep)         {this.scale = links.Graph.StepDate.SCALE.SECOND;      this.step = 5;}
547     if (stepSecond > minimumStep)           {this.scale = links.Graph.StepDate.SCALE.SECOND;      this.step = 1;}
548     if (stepMillisecond*200 > minimumStep)  {this.scale = links.Graph.StepDate.SCALE.MILLISECOND; this.step = 200;}
549     if (stepMillisecond*100 > minimumStep)  {this.scale = links.Graph.StepDate.SCALE.MILLISECOND; this.step = 100;}
550     if (stepMillisecond*50 > minimumStep)   {this.scale = links.Graph.StepDate.SCALE.MILLISECOND; this.step = 50;}
551     if (stepMillisecond*10 > minimumStep)   {this.scale = links.Graph.StepDate.SCALE.MILLISECOND; this.step = 10;}
552     if (stepMillisecond*5 > minimumStep)    {this.scale = links.Graph.StepDate.SCALE.MILLISECOND; this.step = 5;}
553     if (stepMillisecond > minimumStep)      {this.scale = links.Graph.StepDate.SCALE.MILLISECOND; this.step = 1;}
554 };
555 
556 /**
557  * Snap a date to a rounded value. The snap intervals are dependent on the
558  * current scale and step.
559  * @param {Date} date   the date to be snapped
560  */
561 links.Graph.StepDate.prototype.snap = function(date) {
562     if (this.scale == links.Graph.StepDate.SCALE.YEAR) {
563         var year = date.getFullYear() + Math.round(date.getMonth() / 12);
564         date.setFullYear(Math.round(year / this.step) * this.step);
565         date.setMonth(0);
566         date.setDate(0);
567         date.setHours(0);
568         date.setMinutes(0);
569         date.setSeconds(0);
570         date.setMilliseconds(0);
571     }
572     else if (this.scale == links.Graph.StepDate.SCALE.MONTH) {
573         if (date.getDate() > 15) {
574             date.setDate(1);
575             date.setMonth(date.getMonth() + 1);
576             // important: first set Date to 1, after that change the month.
577         }
578         else {
579             date.setDate(1);
580         }
581 
582         date.setHours(0);
583         date.setMinutes(0);
584         date.setSeconds(0);
585         date.setMilliseconds(0);
586     }
587     else if (this.scale == links.Graph.StepDate.SCALE.DAY ||
588         this.scale == links.Graph.StepDate.SCALE.WEEKDAY) {
589         switch (this.step) {
590             case 5:
591             case 2:
592                 date.setHours(Math.round(date.getHours() / 24) * 24); break;
593             default:
594                 date.setHours(Math.round(date.getHours() / 12) * 12); break;
595         }
596         date.setMinutes(0);
597         date.setSeconds(0);
598         date.setMilliseconds(0);
599     }
600     else if (this.scale == links.Graph.StepDate.SCALE.HOUR) {
601         switch (this.step) {
602             case 4:
603                 date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
604             default:
605                 date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
606         }
607         date.setSeconds(0);
608         date.setMilliseconds(0);
609     } else if (this.scale == links.Graph.StepDate.SCALE.MINUTE) {
610         switch (this.step) {
611             case 15:
612             case 10:
613                 date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
614                 date.setSeconds(0);
615                 break;
616             case 5:
617                 date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
618             default:
619                 date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
620         }
621         date.setMilliseconds(0);
622     }
623     else if (this.scale == links.Graph.StepDate.SCALE.SECOND) {
624         switch (this.step) {
625             case 15:
626             case 10:
627                 date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
628                 date.setMilliseconds(0);
629                 break;
630             case 5:
631                 date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
632             default:
633                 date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
634         }
635     }
636     else if (this.scale == links.Graph.StepDate.SCALE.MILLISECOND) {
637         var step = this.step > 5 ? this.step / 2 : 1;
638         date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
639     }
640 };
641 
642 /**
643  * Check if the current step is a major step (for example when the step
644  * is DAY, a major step is each first day of the MONTH)
645  * @return {boolean} true if current date is major, else false.
646  */
647 links.Graph.StepDate.prototype.isMajor = function() {
648     switch (this.scale) {
649         case links.Graph.StepDate.SCALE.MILLISECOND:
650             return (this.current.getMilliseconds() == 0);
651         case links.Graph.StepDate.SCALE.SECOND:
652             return (this.current.getSeconds() == 0);
653         case links.Graph.StepDate.SCALE.MINUTE:
654             return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
655         // Note: this is no bug. Major label is equal for both minute and hour scale
656         case links.Graph.StepDate.SCALE.HOUR:
657             return (this.current.getHours() == 0);
658         case links.Graph.StepDate.SCALE.WEEKDAY: // intentional fall through
659         case links.Graph.StepDate.SCALE.DAY:
660             return (this.current.getDate() == 1);
661         case links.Graph.StepDate.SCALE.MONTH:
662             return (this.current.getMonth() == 0);
663         case links.Graph.StepDate.SCALE.YEAR:
664             return false;
665         default:
666             return false;
667     }
668 };
669 
670 
671 /**
672  * Returns formatted text for the minor axislabel, depending on the current
673  * date and the scale. For example when scale is MINUTE, the current time is
674  * formatted as "hh:mm".
675  * @param {Date} [date] custom date. if not provided, current date is taken
676  */
677 links.Graph.StepDate.prototype.getLabelMinor = function(date) {
678     var MONTHS_SHORT = ["Jan", "Feb", "Mar",
679         "Apr", "May", "Jun",
680         "Jul", "Aug", "Sep",
681         "Oct", "Nov", "Dec"];
682     var DAYS_SHORT = ["Sun", "Mon", "Tue",
683         "Wed", "Thu", "Fri", "Sat"];
684 
685     if (date == undefined) {
686         date = this.current;
687     }
688 
689     switch (this.scale) {
690         case links.Graph.StepDate.SCALE.MILLISECOND:  return String(date.getMilliseconds());
691         case links.Graph.StepDate.SCALE.SECOND:       return String(date.getSeconds());
692         case links.Graph.StepDate.SCALE.MINUTE:
693             return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2);
694         case links.Graph.StepDate.SCALE.HOUR:
695             return this.addZeros(date.getHours(), 2) + ":" + this.addZeros(date.getMinutes(), 2);
696         case links.Graph.StepDate.SCALE.WEEKDAY:      return DAYS_SHORT[date.getDay()] + ' ' + date.getDate();
697         case links.Graph.StepDate.SCALE.DAY:          return String(date.getDate());
698         case links.Graph.StepDate.SCALE.MONTH:        return MONTHS_SHORT[date.getMonth()];   // month is zero based
699         case links.Graph.StepDate.SCALE.YEAR:         return String(date.getFullYear());
700         default:                                         return "";
701     }
702 };
703 
704 
705 /**
706  * Returns formatted text for the major axislabel, depending on the current
707  * date and the scale. For example when scale is MINUTE, the major scale is
708  * hours, and the hour will be formatted as "hh".
709  * @param {Date} [date] custom date. if not provided, current date is taken
710  */
711 links.Graph.StepDate.prototype.getLabelMajor = function(date) {
712     var MONTHS = ["January", "February", "March",
713         "April", "May", "June",
714         "July", "August", "September",
715         "October", "November", "December"];
716     var DAYS = ["Sunday", "Monday", "Tuesday",
717         "Wednesday", "Thursday", "Friday", "Saturday"];
718 
719     if (date == undefined) {
720         date = this.current;
721     }
722 
723     switch (this.scale) {
724         case links.Graph.StepDate.SCALE.MILLISECOND:
725             return  this.addZeros(date.getHours(), 2) + ":" +
726                 this.addZeros(date.getMinutes(), 2) + ":" +
727                 this.addZeros(date.getSeconds(), 2);
728         case links.Graph.StepDate.SCALE.SECOND:
729             return  date.getDate() + " " +
730                 MONTHS[date.getMonth()] + " " +
731                 this.addZeros(date.getHours(), 2) + ":" +
732                 this.addZeros(date.getMinutes(), 2);
733         case links.Graph.StepDate.SCALE.MINUTE:
734             return  DAYS[date.getDay()] + " " +
735                 date.getDate() + " " +
736                 MONTHS[date.getMonth()] + " " +
737                 date.getFullYear();
738         case links.Graph.StepDate.SCALE.HOUR:
739             return  DAYS[date.getDay()] + " " +
740                 date.getDate() + " " +
741                 MONTHS[date.getMonth()] + " " +
742                 date.getFullYear();
743         case links.Graph.StepDate.SCALE.WEEKDAY:
744         case links.Graph.StepDate.SCALE.DAY:
745             return  MONTHS[date.getMonth()] + " " +
746                 date.getFullYear();
747         case links.Graph.StepDate.SCALE.MONTH:
748             return String(date.getFullYear());
749         default:
750             return "";
751     }
752 };
753 
754 /**
755  * Add leading zeros to the given value to match the desired length.
756  * For example addZeros(123, 5) returns "00123"
757  * @param {int} value   A value
758  * @param {int} len     Desired final length
759  * @return {string}     value with leading zeros
760  */
761 links.Graph.StepDate.prototype.addZeros = function(value, len) {
762     var str = "" + value;
763     while (str.length < len) {
764         str = "0" + str;
765     }
766     return str;
767 };
768 
769 
770 
771 /**
772  * @class StepNumber
773  * The class StepNumber is an iterator for numbers. You provide a start and end
774  * value, and a best step size. StepNumber itself rounds to fixed values and
775  * a finds the step that best fits the provided step.
776  *
777  * If prettyStep is true, the step size is chosen as close as possible to the
778  * provided step, but being a round value like 1, 2, 5, 10, 20, 50, ....
779  *
780  * Example usage:
781  *   var step = new links.Graph.StepNumber(0, 10, 2.5, true);
782  *   step.start();
783  *   while (!step.end()) {
784  *     alert(step.getCurrent());
785  *     step.next();
786  *   }
787  *
788  * Version: 1.0
789  *
790  * @param {number} start       The start value
791  * @param {number} end         The end value
792  * @param {number} step        Optional. Step size. Must be a positive value.
793  * @param {boolean} prettyStep Optional. If true, the step size is rounded
794  *                             To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
795  */
796 links.Graph.StepNumber = function (start, end, step, prettyStep) {
797     this._start = 0;
798     this._end = 0;
799     this._step = 1;
800     this.prettyStep = true;
801     this.precision = 5;
802 
803     this._current = 0;
804     this._setRange(start, end, step, prettyStep);
805 };
806 
807 /**
808  * Set a new range: start, end and step.
809  *
810  * @param {number} start       The start value
811  * @param {number} end         The end value
812  * @param {number} step        Optional. Step size. Must be a positive value.
813  * @param {boolean} prettyStep Optional. If true, the step size is rounded
814  *                             To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
815  */
816 links.Graph.StepNumber.prototype._setRange = function(start, end, step, prettyStep) {
817     this._start = start ? start : 0;
818     this._end = end ? end : 0;
819 
820     this.setStep(step, prettyStep);
821 };
822 
823 /**
824  * Set a new step size
825  * @param {number} step        New step size. Must be a positive value
826  * @param {boolean} prettyStep Optional. If true, the provided step is rounded
827  *                             to a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
828  */
829 links.Graph.StepNumber.prototype.setStep = function(step, prettyStep) {
830     if (step == undefined || step <= 0)
831         return;
832 
833     this.prettyStep = prettyStep;
834     if (this.prettyStep == true)
835         this._step = links.Graph.StepNumber._calculatePrettyStep(step);
836     else
837         this._step = step;
838 
839 
840     if (this._end / this._step > Math.pow(10, this.precision)) {
841         this.precision = undefined;
842     }
843 };
844 
845 /**
846  * Calculate a nice step size, closest to the desired step size.
847  * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an
848  * integer number. For example 1, 2, 5, 10, 20, 50, etc...
849  * @param {number}  step  Desired step size
850  * @return {number}       Nice step size
851  */
852 links.Graph.StepNumber._calculatePrettyStep = function (step) {
853     log10 = function (x) {return Math.log(x) / Math.LN10;};
854 
855     // try three steps (multiple of 1, 2, or 5
856     var step1 = 1 * Math.pow(10, Math.round(log10(step / 1)));
857     var step2 = 2 * Math.pow(10, Math.round(log10(step / 2)));
858     var step5 = 5 * Math.pow(10, Math.round(log10(step / 5)));
859 
860     // choose the best step (closest to minimum step)
861     var prettyStep = step1;
862     if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2;
863     if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5;
864 
865     // for safety
866     if (prettyStep <= 0) {
867         prettyStep = 1;
868     }
869 
870     return prettyStep;
871 };
872 
873 /**
874  * returns the current value of the step
875  * @return {number} current value
876  */
877 links.Graph.StepNumber.prototype.getCurrent = function () {
878     if (this.precision) {
879         return Number((this._current).toPrecision(this.precision));
880     }
881     else {
882         return this._current;
883     }
884 };
885 
886 /**
887  * returns the current step size
888  * @return {number} current step size
889  */
890 links.Graph.StepNumber.prototype.getStep = function () {
891     return this._step;
892 };
893 
894 /**
895  * Set the current value to the largest value smaller than start, which
896  * is a multiple of the step size
897  */
898 links.Graph.StepNumber.prototype.start = function() {
899     if (this.prettyStep)
900         this._current = this._start - this._start % this._step;
901     else
902         this._current = this._start;
903 };
904 
905 /**
906  * Do a step, add the step size to the current value
907  */
908 links.Graph.StepNumber.prototype.next = function () {
909     this._current += this._step;
910 };
911 
912 /**
913  * Returns true whether the end is reached
914  * @return {boolean}  True if the current value has passed the end value.
915  */
916 links.Graph.StepNumber.prototype.end = function () {
917     return (this._current > this._end);
918 };
919 
920 
921 /**
922  * Set a custom scale. Autoscaling will be disabled.
923  * For example setScale(SCALE.MINUTES, 5) will result
924  * in minor steps of 5 minutes, and major steps of an hour.
925  *
926  * @param {links.Graph.StepDate.SCALE} scale
927  *                               A scale. Choose from SCALE.MILLISECOND,
928  *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
929  *                               SCALE.DAY, SCALE.MONTH, SCALE.YEAR.
930  * @param {int}        step      A step size, by default 1. Choose for
931  *                               example 1, 2, 5, or 10.
932  */
933 links.Graph.prototype.setScale = function(scale, step) {
934     this.hStep.setScale(scale, step);
935     this.redraw();
936 };
937 
938 /**
939  * Enable or disable autoscaling
940  * @param {boolean} enable  If true or not defined, autoscaling is enabled.
941  *                          If false, autoscaling is disabled.
942  */
943 links.Graph.prototype.setAutoScale = function(enable) {
944     this.hStep.setAutoScale(enable);
945     this.redraw();
946 };
947 
948 
949 /**
950  * Append suffix "px" to provided value x
951  * @param {int}     x  An integer value
952  * @return {string} the string value of x, followed by the suffix "px"
953  */
954 links.Graph.px = function(x) {
955     return Math.round(x) + "px";
956 };
957 
958 
959 /**
960  * Calculate the factor and offset to convert a position on screen to the
961  * corresponding date and vice versa.
962  * After the method calcConversionFactor is executed once, the methods screenToTime and
963  * timeToScreen can be used.
964  */
965 links.Graph.prototype._calcConversionFactor = function() {
966     this.ttsOffset = this.start.valueOf();
967     this.ttsFactor = this.frame.clientWidth /
968         (this.end.valueOf() - this.start.valueOf());
969 };
970 
971 
972 /**
973  * Convert a position on screen (pixels) to a datetime
974  * Before this method can be used, the method calcConversionFactor must be
975  * executed once.
976  * @param {int}     x    Position on the screen in pixels
977  * @return {Date}   time The datetime the corresponds with given position x
978  */
979 links.Graph.prototype._screenToTime = function(x) {
980     return new Date(x / this.ttsFactor + this.ttsOffset);
981 };
982 
983 /**
984  * Convert a datetime (Date object) into a position on the screen
985  * Before this method can be used, the method calcConversionFactor must be
986  * executed once.
987  * @param {Date}   time A date
988  * @return {int}   x    The position on the screen in pixels which corresponds
989  *                      with the given date.
990  */
991 links.Graph.prototype.timeToScreen = function(time) {
992     return (time.valueOf() - this.ttsOffset) * this.ttsFactor || null;
993 };
994 
995 /**
996  * Create the main frame for the Graph.
997  * This function is executed once when a Graph object is created. The frame
998  * contains a canvas, and this canvas contains all objects like the axis and
999  * events.
1000  */
1001 links.Graph.prototype._create = function () {
1002     // remove all elements from the container element.
1003     while (this.containerElement.hasChildNodes()) {
1004         this.containerElement.removeChild(this.containerElement.firstChild);
1005     }
1006 
1007     this.main = document.createElement("DIV");
1008     this.main.className = "graph-frame";
1009     this.main.style.position = "relative";
1010     this.main.style.overflow = "hidden";
1011     this.containerElement.appendChild(this.main);
1012 
1013     // create the main box where the Graph will be created
1014     this.frame = document.createElement("DIV");
1015     this.frame.style.overflow = "hidden";
1016     this.frame.style.position = "relative";
1017     this.frame.style.height = "200px";  // height MUST be initialized.
1018     // Width and height will be set via setSize();
1019     //this.containerElement.appendChild(this.frame);
1020     this.main.appendChild(this.frame);
1021 
1022     // create a canvas background, which can be used to give the canvas a colored background
1023     this.frame.background = document.createElement("DIV");
1024     this.frame.background.className = "graph-canvas";
1025     this.frame.background.style.position = "relative";
1026     this.frame.background.style.left = links.Graph.px(0);
1027     this.frame.background.style.top = links.Graph.px(0);
1028     this.frame.background.style.width = "100%";
1029     this.frame.appendChild(this.frame.background);
1030 
1031     // create a div to contain the grid lines of the vertical axis
1032     this.frame.vgrid = document.createElement("DIV");
1033     this.frame.vgrid.className = "graph-axis-grid";
1034     this.frame.vgrid.style.position = "absolute";
1035     this.frame.vgrid.style.left = links.Graph.px(0);
1036     this.frame.vgrid.style.top = links.Graph.px(0);
1037     this.frame.vgrid.style.width = "100%";
1038     this.frame.appendChild(this.frame.vgrid);
1039 
1040     // create the canvas inside the frame. all elements will be added to this
1041     // canvas
1042     this.frame.canvas = document.createElement("DIV");
1043     //this.frame.canvas.className = "graph-canvas";
1044     this.frame.canvas.style.position = "absolute";
1045     this.frame.canvas.style.left = links.Graph.px(0);
1046     this.frame.canvas.style.top = links.Graph.px(0);
1047     this.frame.appendChild(this.frame.canvas);
1048     // Width and height will be set via setSize();
1049 
1050     // inside the canvas, create a DOM element "axis" to store all axis related elements
1051     this.frame.canvas.axis = document.createElement("DIV");
1052     this.frame.canvas.axis.style.position = "relative";
1053     this.frame.canvas.axis.style.left = links.Graph.px(0);
1054     this.frame.canvas.axis.style.top = links.Graph.px(0);
1055     this.frame.canvas.appendChild(this.frame.canvas.axis);
1056     this.majorLabels = [];
1057 
1058     // create the graph canvas (HTML canvas element)
1059     this.frame.canvas.graph = document.createElement( "canvas" );
1060     this.frame.canvas.graph.style.position = "absolute";
1061     this.frame.canvas.graph.style.left = links.Graph.px(0);
1062     this.frame.canvas.graph.style.top = links.Graph.px(0);
1063     //this.frame.canvas.graph.width = "800";   // width is adjusted lateron
1064     //this.frame.canvas.graph.height = "200";  // height is adjusted lateron
1065     this.frame.canvas.appendChild(this.frame.canvas.graph);
1066 
1067     isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
1068     if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) {
1069         this.frame.canvas.graph = G_vmlCanvasManager.initElement(this.frame.canvas.graph);
1070     }
1071 
1072     // add event listeners to handle moving and zooming the contents
1073     var me = this;
1074     var onmousedown = function (event) {me._onMouseDown(event);};
1075     var onmousewheel = function (event) {me._onWheel(event);};
1076     var ontouchstart = function (event) {me._onTouchStart(event);};
1077     if (this.showTooltip) {
1078         var onmouseout = function (event) {me._onMouseOut(event);};
1079         var onmousehover = function (event) {me._onMouseHover(event);};
1080     }
1081 
1082     // TODO: these events are never cleaned up... can give a "memory leakage"?
1083     links.Graph.addEventListener(this.frame, "mousedown", onmousedown);
1084     links.Graph.addEventListener(this.frame, "mousemove", onmousehover);
1085     links.Graph.addEventListener(this.frame, "mouseout", onmouseout);
1086     links.Graph.addEventListener(this.frame, "mousewheel", onmousewheel);
1087     links.Graph.addEventListener(this.frame, "touchstart", ontouchstart);
1088     links.Graph.addEventListener(this.frame, "mousedown", function() {me._checkSize();});
1089 
1090     // create a step for drawing the horizontal and vertical axis
1091     this.hStep = new links.Graph.StepDate();     // TODO: rename step to hStep
1092     this.vStep = new links.Graph.StepNumber();
1093 
1094     // the array events contains pointers to all data events. It is used
1095     // to sort and stack the events.
1096     this.eventsSorted = [];
1097 };
1098 
1099 
1100 /**
1101  * Set a new size for the graph
1102  * @param {string} width   Width in pixels or percentage (for example "800px"
1103  *                         or "50%")
1104  * @param {string} height  Height in pixels or percentage  (for example "400px"
1105  *                         or "30%")
1106  */
1107 links.Graph.prototype.setSize = function(width, height) {
1108     // TODO: test if this solves the width as percentage problem in EXT-GWT
1109     this.containerElement.style.width = width;
1110     this.containerElement.style.height = height;
1111 
1112     this.main.style.width = width;
1113     this.main.style.height = height;
1114 
1115     this.frame.style.width = links.Graph.px(this.main.clientWidth);
1116     this.frame.style.height = links.Graph.px(this.main.clientHeight);
1117 
1118     this.frame.canvas.style.width = links.Graph.px(this.frame.clientWidth);
1119     this.frame.canvas.style.height = links.Graph.px(this.frame.clientHeight);
1120 };
1121 
1122 /**
1123  * Zoom the graph the given zoomfactor in or out. Start and end date will
1124  * be adjusted, and the graph will be redrawn. You can optionally give a
1125  * date around which to zoom.
1126  * For example, try zoomfactor = 0.1 or -0.1
1127  * @param {Number} zoomFactor      Zooming amount. Positive value will zoom in,
1128  *                                 negative value will zoom out
1129  * @param {Date}   zoomAroundDate  Date around which will be zoomed. Optional
1130  */
1131 links.Graph.prototype._zoom = function(zoomFactor, zoomAroundDate) {
1132     // if zoomAroundDate is not provided, take it half between start Date and end Date
1133     if (zoomAroundDate == undefined)
1134         zoomAroundDate = new Date((this.start.valueOf() + this.end.valueOf()) / 2);
1135 
1136     // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
1137     // result in a start>=end )
1138     if (zoomFactor >= 1) zoomFactor = 0.9;
1139     if (zoomFactor <= -1) zoomFactor = -0.9;
1140 
1141     // adjust a negative factor such that zooming in with 0.1 equals zooming
1142     // out with a factor -0.1
1143     if (zoomFactor < 0) {
1144         zoomFactor = zoomFactor / (1 + zoomFactor);
1145     }
1146 
1147     // zoom start Date and end Date relative to the zoomAroundDate
1148     var startDiff = parseFloat(this.start.valueOf() - zoomAroundDate.valueOf());
1149     var endDiff = parseFloat(this.end.valueOf() - zoomAroundDate.valueOf());
1150 
1151     // calculate new dates
1152     var newStart = new Date(this.start.valueOf() - startDiff * zoomFactor);
1153     var newEnd   = new Date(this.end.valueOf() - endDiff * zoomFactor);
1154 
1155     /* TODO: cleanup
1156      // prevent scale of less than 10 milliseconds
1157      // TODO: IE has problems with milliseconds
1158      if (zoomFactor > 0 && (newEnd.valueOf() - newStart.valueOf()) < 10)
1159      return;
1160 
1161      // prevent scale of mroe than than 10 thousand years
1162      if (zoomFactor < 0 && (newEnd.getFullYear() - newStart.getFullYear()) > 10000)
1163      return;
1164 
1165      // apply new dates
1166      this.start = newStart;
1167      this.end = newEnd;
1168      */
1169 
1170     var interval = (newEnd.valueOf() - newStart.valueOf());
1171     var zoomMin = Number(this.zoomMin) || 10;
1172     if (zoomMin < 10) {
1173         zoomMin = 10;
1174     }
1175     if (interval >= zoomMin) {
1176         // apply new dates
1177         this._applyRange(newStart, newEnd, zoomAroundDate);
1178 
1179         this._redrawHorizontalAxis();
1180         this._redrawData();
1181         this._redrawDataTooltip();
1182     }
1183 };
1184 
1185 /**
1186  * Move the graph the given movefactor to the left or right. Start and end
1187  * date will be adjusted, and the graph will be redrawn.
1188  * For example, try moveFactor = 0.1 or -0.1
1189  * @param {Number} moveFactor      Moving amount. Positive value will move right,
1190  *                                 negative value will move left
1191  */
1192 links.Graph.prototype._move = function(moveFactor) {
1193     // TODO: test this function again
1194     // zoom start Date and end Date relative to the zoomAroundDate
1195     var diff = parseFloat(this.end.valueOf() - this.start.valueOf());
1196 
1197     // apply new dates
1198     var newStart = new Date(this.start.valueOf() + diff * moveFactor);
1199     var newEnd   = new Date(this.end.valueOf() + diff * moveFactor);
1200 
1201     this._applyRange(newStart, newEnd);
1202 
1203     // redraw
1204     this._redrawHorizontalAxis();
1205     this._redrawData();
1206 };
1207 
1208 
1209 /**
1210  * Apply a visible range. The range is limited to feasible maximum and minimum
1211  * range.
1212  * @param {Date} start
1213  * @param {Date} end
1214  * @param {Date}   zoomAroundDate   Optional. Date around which will be zoomed
1215  *                                  When needed to satisfy a min/max zoom level
1216  *                                  or range.
1217  */
1218 links.Graph.prototype._applyRange = function (start, end, zoomAroundDate) {
1219     // calculate new start and end value
1220     var startValue = start.valueOf();
1221     var endValue = end.valueOf();
1222     var interval = (endValue - startValue);
1223 
1224     // determine maximum and minimum interval
1225     var year = 1000 * 60 * 60 * 24 * 365;
1226     var zoomMin = Number(this.zoomMin) || 10;
1227     if (zoomMin < 10) {
1228         zoomMin = 10;
1229     }
1230     var zoomMax = Number(this.zoomMax) || 10000 * year;
1231     if (zoomMax > 10000 * year) {
1232         zoomMax = 10000 * year;
1233     }
1234     if (zoomMax < zoomMin) {
1235         zoomMax = zoomMin;
1236     }
1237 
1238     // determine min and max date value
1239     var min = this.min ? this.min.valueOf() : undefined;
1240     var max = this.max ? this.max.valueOf() : undefined;
1241     if (min && max) {
1242         if (min >= max) {
1243             // empty range
1244             var day = 1000 * 60 * 60 * 24;
1245             max = min + day;
1246         }
1247         if (zoomMax > (max - min)) {
1248             zoomMax = (max - min);
1249         }
1250         if (zoomMin > (max - min)) {
1251             zoomMin = (max - min);
1252         }
1253     }
1254 
1255     // prevent empty interval
1256     if (startValue >= endValue) {
1257         endValue += 1000 * 60 * 60 * 24;
1258     }
1259 
1260     // prevent too small scale
1261     // TODO: IE has problems with milliseconds
1262     if (interval < zoomMin) {
1263         var diff = (zoomMin - interval);
1264         var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5;
1265         startValue -= Math.round(diff * f);
1266         endValue   += Math.round(diff * (1 - f));
1267     }
1268 
1269     // prevent too large scale
1270     if (interval > zoomMax) {
1271         var diff = (interval - zoomMax);
1272         var f = zoomAroundDate ? (zoomAroundDate.valueOf() - startValue) / interval : 0.5;
1273         startValue += Math.round(diff * f);
1274         endValue   -= Math.round(diff * (1 - f));
1275     }
1276 
1277     // prevent to small start date
1278     if (min) {
1279         var diff = (startValue - min);
1280         if (diff < 0) {
1281             startValue -= diff;
1282             endValue -= diff;
1283         }
1284     }
1285 
1286     // prevent to large end date
1287     if (max) {
1288         var diff = (max - endValue);
1289         if (diff < 0) {
1290             startValue += diff;
1291             endValue += diff;
1292         }
1293     }
1294 
1295     // apply new dates
1296     this.start = new Date(startValue);
1297     this.end = new Date(endValue);
1298 };
1299 
1300 
1301 /**
1302  * Zoom the graph vertically. The vertical range will be adjusted, and the graph
1303  * will be redrawn. You can optionally give a value around which to zoom.
1304  * For example, try zoomfactor = 0.1 or -0.1
1305  * @param {Number} zoomFactor      Zooming amount. Positive value will zoom in,
1306  *                                 negative value will zoom out
1307  * @param {Date}   zoomAroundValue Value around which will be zoomed. Optional
1308  */
1309 links.Graph.prototype._zoomVertical = function(zoomFactor, zoomAroundValue) {
1310     if (zoomAroundValue == undefined)
1311         zoomAroundValue = (this.vStart + this.vEnd) / 2;
1312 
1313     // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
1314     // result in a start>=end )
1315     if (zoomFactor >= 1) zoomFactor = 0.9;
1316     if (zoomFactor <= -1) zoomFactor = -0.9;
1317 
1318     // adjust a negative factor such that zooming in with 0.1 equals zooming
1319     // out with a factor -0.1
1320     if (zoomFactor < 0) {
1321         zoomFactor = zoomFactor / (1 + zoomFactor);
1322     }
1323 
1324     // zoom start Date and end Date relative to the zoomAroundDate
1325     var startDiff = (this.vStart - zoomAroundValue);
1326     var endDiff = (this.vEnd - zoomAroundValue);
1327 
1328     // calculate start and end
1329     var newStart = (this.vStart - startDiff * zoomFactor);
1330     var newEnd   = (this.vEnd - endDiff * zoomFactor);
1331 
1332     // prevent empty range
1333     if (newStart >= newEnd) {
1334         return;
1335     }
1336 
1337     // prevent range larger than the available range
1338     if (newStart < this.vMin) {
1339         newStart = this.vMin;
1340     }
1341     if (newEnd > this.vMax) {
1342         newEnd = this.vMax;
1343     }
1344     /* TODO: allow start and end larger than the value range?
1345      if (newStart < this.vMin && newStart < this.vStart) {
1346      newStart = (this.vStart > this.vMin) ? this.vMin : this.vStart;
1347      }
1348      if (newEnd > this.vMax && newEnd > this.vEnd) {
1349      newEnd = (this.vEnd < this.vMax) ? this.vMax : this.vEnd;
1350      }
1351      */
1352 
1353     // apply new range
1354     this.vStart = newStart;
1355     this.vEnd = newEnd;
1356 
1357     // redraw
1358     this._redrawVerticalAxis();
1359     this._redrawHorizontalAxis(); // -> width of the vertical axis can be changed
1360     this._redrawData();
1361     this._redrawDataTooltip();
1362 };
1363 
1364 
1365 /**
1366  * Redraw the Graph. This needs to be executed after the start and/or
1367  * end time are changed, or when data is added or removed dynamically.
1368  */
1369 links.Graph.prototype.redraw = function() {
1370     this._initSize();
1371 
1372     // Note: the order of drawing is important!
1373     this._redrawLegend();
1374     this._redrawVerticalAxis();
1375     this._redrawHorizontalAxis();
1376     this._redrawData();
1377     this._redrawDataTooltip();
1378 
1379     // store the current width and height. This is needed to detect when the frame
1380     // was resized (externally).
1381     this.lastMainWidth = this.main.clientWidth;
1382     this.lastMainHeight = this.main.clientHeight;
1383 };
1384 
1385 /**
1386  * Initialize size, range, location of axis
1387  * Execute this method when the data or options are changed, before redrawing
1388  * the graph.
1389  */
1390 links.Graph.prototype._initSize = function() {
1391     // calculate the width and height of a single character
1392     // this is used to calculate the step size, and also the positioning of the
1393     // axis
1394     var charText = document.createTextNode("0");
1395     var charDiv = document.createElement("DIV");
1396     charDiv.className = "graph-axis-text";
1397     charDiv.appendChild(charText);
1398     charDiv.style.position = "absolute";
1399     charDiv.style.visibility = "hidden";
1400     charDiv.style.padding = "0px";
1401     this.frame.canvas.axis.appendChild(charDiv);
1402     this.axisCharWidth  = parseInt(charDiv.clientWidth);
1403     this.axisCharHeight = parseInt(charDiv.clientHeight);
1404     charDiv.style.padding = "";
1405     charDiv.className = "graph-axis-text graph-axis-text-minor";
1406     this.axisTextMinorHeight = parseInt(charDiv.offsetHeight);
1407     charDiv.className = "graph-axis-text graph-axis-text-major";
1408     this.axisTextMajorHeight = parseInt(charDiv.offsetHeight);
1409     this.frame.canvas.axis.removeChild(charDiv);  // TODO: When using .redraw() via the browser event onresize, this gives an error in Chrome
1410 
1411     // calculate the position of the axis
1412     this.axisOffset = this.main.clientHeight -
1413         this.axisTextMinorHeight -
1414         this.axisTextMajorHeight -
1415         2 * this.mainPadding;
1416 
1417     // TODO: do not retrieve datarange here? -> initSize is executed during each redraw()
1418     // retrieve the data range (this can take some time for large amounts of data)
1419     //this.dataRange = this._getDataRange(this.data[0]); // TODO
1420     if (this.data.length > 0) {
1421         var verticalRange = null;
1422         for (var i = 0, imax = this.data.length; i < imax; i++) {
1423             var dataRange = this.data[i].dataRange;
1424             if (dataRange) {
1425                 if (verticalRange) {
1426                     verticalRange.min = Math.min(verticalRange.min, dataRange.min);
1427                     verticalRange.max = Math.max(verticalRange.max, dataRange.max);
1428                 }
1429                 else {
1430                     verticalRange = {
1431                         min: dataRange.min,
1432                         max: dataRange.max
1433                     };
1434                 }
1435             }
1436         }
1437         this.verticalRange = verticalRange || {"min" : -10, "max" : 10};
1438     }
1439     else {
1440         this.verticalRange = {"min" : -10, "max" : 10};
1441     }
1442 
1443     // get the minimum and maximum data values, and add 5 percent
1444     // so there is always some whitespace above and below the drawn data
1445     var range = this.verticalRange.max - this.verticalRange.min;
1446     if (range <= 0) {
1447         range = 1;
1448     }
1449     var avg = (this.verticalRange.max + this.verticalRange.min) / 2;
1450     this.vMin = this.vMinFixed != undefined ? this.vMinFixed : avg - range / 2 * 1.05;
1451     this.vMax = this.vMaxFixed != undefined ? this.vMaxFixed : avg + range / 2 * 1.05;
1452     if (this.vMax <= this.vMin) {
1453         this.vMax = this.vMin + 1;
1454     }
1455 };
1456 
1457 /**
1458  * Draw the horizontal axis in the graph, containing grid, axis, minor and
1459  * major labels
1460  */
1461 links.Graph.prototype._redrawHorizontalAxis = function () {
1462     var startTime = new Date(); // TODO: cleanup
1463 
1464     // clear any existing data
1465     while (this.frame.canvas.axis.hasChildNodes()) {
1466         this.frame.canvas.axis.removeChild(this.frame.canvas.axis.lastChild);
1467     }
1468     this.majorLabels = [];
1469 
1470     // resize the horizontal axis
1471     this.frame.style.left = links.Graph.px(this.main.axisLeft.clientWidth + this.mainPadding);
1472     this.frame.style.top = links.Graph.px(this.mainPadding);
1473     this.frame.style.height = links.Graph.px(this.main.clientHeight - 2 * this.mainPadding );
1474     this.frame.style.width = links.Graph.px(this.main.clientWidth -
1475         this.main.axisLeft.clientWidth -
1476         this.legendWidth -
1477         2 * this.mainPadding - 2);
1478 
1479     this.frame.canvas.style.width = links.Graph.px(this.frame.clientWidth);
1480     this.frame.canvas.style.height = links.Graph.px(this.axisOffset);
1481     this.frame.background.style.height = links.Graph.px(this.axisOffset);
1482 
1483     this._calcConversionFactor();
1484 
1485     // the drawn axis is more wide than the actual visual part, such that
1486     // the axis can be dragged without having to redraw it each time again.
1487     var start = this._screenToTime(-this.axisMargin);
1488     var end = this._screenToTime(this.frame.clientWidth + this.axisMargin);
1489     var width = this.frame.clientWidth + 2*this.axisMargin;
1490 
1491     var yvalueMinor = this.axisOffset;
1492     var yvalueMajor = this.axisOffset + this.axisTextMinorHeight;
1493 
1494     // calculate minimum step (in milliseconds) based on character size
1495     this.minimumStep = this._screenToTime(this.axisCharWidth * 6).valueOf() -
1496         this._screenToTime(0).valueOf();
1497 
1498     this.hStep.setRange(start, end, this.minimumStep);
1499 
1500     // create a left major label
1501     if (this.leftMajorLabel) {
1502         this.frame.canvas.removeChild(this.leftMajorLabel);
1503         this.leftMajorLabel = undefined;
1504     }
1505     var leftDate = this.hStep.getLabelMajor(this._screenToTime(0));
1506     var content = document.createTextNode(leftDate);
1507     this.leftMajorLabel = document.createElement("DIV");
1508     this.leftMajorLabel.className = "graph-axis-text graph-axis-text-major";
1509     this.leftMajorLabel.appendChild(content);
1510     this.leftMajorLabel.style.position = "absolute";
1511     this.leftMajorLabel.style.left = links.Graph.px(0);
1512     this.leftMajorLabel.style.top = links.Graph.px(yvalueMajor);
1513     this.leftMajorLabel.title = leftDate;
1514     this.frame.canvas.appendChild(this.leftMajorLabel);
1515 
1516     this.hStep.start();
1517     var count = 0;
1518     while (!this.hStep.end() && count < 200) {
1519         count++;
1520         var x = this.timeToScreen(this.hStep.getCurrent());
1521         var hvline = this.hStep.isMajor() ? this.frame.clientHeight :
1522             (this.axisOffset + this.axisTextMinorHeight);
1523 
1524         //create vertical line
1525         var vline = document.createElement("DIV");
1526         vline.className = this.hStep.isMajor() ? "graph-axis-grid graph-axis-grid-major" :
1527             "graph-axis-grid graph-axis-grid-minor";
1528         vline.style.position = "absolute";
1529         vline.style.borderLeftStyle = "solid";
1530         vline.style.top = links.Graph.px(0);
1531         vline.style.width = links.Graph.px(0);
1532         vline.style.height = links.Graph.px(hvline);
1533         vline.style.left = links.Graph.px(x - vline.offsetWidth/2);
1534         this.frame.canvas.axis.appendChild(vline);
1535 
1536         if (this.hStep.isMajor())
1537         {
1538             var content = document.createTextNode(this.hStep.getLabelMajor());
1539             var majorValue = document.createElement("DIV");
1540             this.frame.canvas.axis.appendChild(majorValue);
1541             majorValue.className = "graph-axis-text graph-axis-text-major";
1542             majorValue.appendChild(content);
1543             majorValue.style.position = "absolute";
1544             majorValue.style.width = links.Graph.px(majorValue.clientWidth);
1545             majorValue.style.left = links.Graph.px(x);
1546             majorValue.style.top = links.Graph.px(yvalueMajor);
1547             majorValue.title = this.hStep.getCurrent();
1548             majorValue.x = x;
1549             this.majorLabels.push(majorValue);
1550         }
1551 
1552         // minor label
1553         var content = document.createTextNode(this.hStep.getLabelMinor());
1554         var minorValue = document.createElement("DIV");
1555         minorValue.appendChild(content);
1556         minorValue.className = "graph-axis-text graph-axis-text-minor";
1557         minorValue.style.position = "absolute";
1558         minorValue.style.left = links.Graph.px(x);
1559         minorValue.style.top  = links.Graph.px(yvalueMinor);
1560         minorValue.title = this.hStep.getCurrent();
1561         this.frame.canvas.axis.appendChild(minorValue);
1562 
1563         this.hStep.next();
1564     }
1565 
1566     // make horizontal axis line on top
1567     var line = document.createElement("DIV");
1568     line.className = "graph-axis";
1569     line.style.position = "absolute";
1570     line.style.borderTopStyle = "solid";
1571     line.style.top = links.Graph.px(0);
1572     line.style.left = links.Graph.px(this.timeToScreen(start));
1573     line.style.width = links.Graph.px(this.timeToScreen(end) - this.timeToScreen(start));
1574     line.style.height = links.Graph.px(0);
1575     this.frame.canvas.axis.appendChild(line);
1576 
1577     // make horizontal axis line on bottom side
1578     var line = document.createElement("DIV");
1579     line.className = "graph-axis";
1580     line.style.position = "absolute";
1581     line.style.borderTopStyle = "solid";
1582     line.style.top = links.Graph.px(this.axisOffset);
1583     line.style.left = links.Graph.px(this.timeToScreen(start));
1584     line.style.width = links.Graph.px(this.timeToScreen(end) - this.timeToScreen(start));
1585     line.style.height = links.Graph.px(0);
1586     this.frame.canvas.axis.appendChild(line);
1587 
1588     // reposition the left major label
1589     this._redrawAxisLeftMajorLabel();
1590 
1591     var endTime = new Date(); // TODO: cleanup
1592     //document.title = (endTime - startTime) + " ms"; // TODO: cleanup
1593 };
1594 
1595 
1596 /**
1597  * Reposition the major labels of the horizontal axis
1598  */
1599 links.Graph.prototype._redrawAxisLeftMajorLabel = function() {
1600     var offset = parseFloat(this.frame.canvas.axis.style.left);
1601 
1602     var lastBelowZero = null;
1603     var firstAboveZero = null;
1604     var xPrev = null;
1605     for (var i in this.majorLabels) {
1606         if (this.majorLabels.hasOwnProperty(i)) {
1607             var label = this.majorLabels[i];
1608 
1609             if (label.x + offset < 0)
1610                 lastBelowZero = label;
1611 
1612             if (label.x + offset  > 0 && (xPrev == null || xPrev + offset  < 0)) {
1613                 firstAboveZero = label;
1614             }
1615 
1616             xPrev = label.x;
1617         }
1618     }
1619 
1620     if (lastBelowZero)
1621         lastBelowZero.style.visibility = "hidden";
1622 
1623     if (firstAboveZero)
1624         firstAboveZero.style.visibility = "visible";
1625 
1626     if (firstAboveZero && this.leftMajorLabel.clientWidth > firstAboveZero.x + offset ) {
1627         this.leftMajorLabel.style.visibility = "hidden";
1628     }
1629     else {
1630         var leftTime = this.hStep.getLabelMajor(this._screenToTime(-offset));
1631         this.leftMajorLabel.title = leftTime;
1632         this.leftMajorLabel.innerHTML = leftTime;
1633         if (this.leftMajorLabel.style.visibility != "visible") {
1634             this.leftMajorLabel.style.visibility = "visible";
1635         }
1636     }
1637 };
1638 
1639 /**
1640  * Draw the vertical axis in the graph
1641  */
1642 links.Graph.prototype._redrawVerticalAxis = function () {
1643     //var testStart = new Date(); // TODO: cleanup
1644     var i;
1645 
1646     if (!this.main.axisLeft) {
1647         // create the left vertical axis
1648         this.main.axisLeft = document.createElement("DIV");
1649         this.main.axisLeft.style.position = "absolute";
1650         this.main.axisLeft.className = "graph-axis graph-axis-vertical";
1651         this.main.axisLeft.style.borderRightStyle = "solid";
1652 
1653         this.main.appendChild(this.main.axisLeft);
1654     } else {
1655         // clear any existing data
1656         while (this.main.axisLeft.hasChildNodes()) {
1657             this.main.axisLeft.removeChild(this.main.axisLeft.lastChild);
1658         }
1659     }
1660 
1661     if (!this.main.axisRight) {
1662         // create the left vertical axis
1663         this.main.axisRight = document.createElement("DIV");
1664         this.main.axisRight.style.position = "absolute";
1665         this.main.axisRight.className = "graph-axis graph-axis-vertical";
1666         this.main.axisRight.style.borderRightStyle = "solid";
1667         this.main.appendChild(this.main.axisRight);
1668     } else {
1669         // do nothing
1670     }
1671 
1672     if (!this.main.zoomButtons) {
1673         // create zoom buttons for the vertical axis
1674         this.main.zoomButtons = document.createElement("DIV");
1675         this.main.zoomButtons.className = "graph-axis-button-menu";
1676         this.main.zoomButtons.style.position = "absolute";
1677 
1678         var graph = this;
1679         var zoomIn = document.createElement("DIV");
1680         zoomIn.innerHTML = "+";
1681         zoomIn.title = "Zoom in vertically (shift + scroll wheel)";
1682         zoomIn.className = "graph-axis-button";
1683         this.main.zoomButtons.appendChild(zoomIn);
1684         links.Graph.addEventListener(zoomIn, "mousedown", function (event) {
1685             graph._zoomVertical(0.2);
1686             links.Graph.preventDefault(event);
1687         });
1688 
1689         var zoomOut = document.createElement("DIV");
1690         zoomOut.innerHTML = "−";
1691         zoomOut.className = "graph-axis-button";
1692         zoomOut.title = "Zoom out vertically (shift + scroll wheel)";
1693         this.main.zoomButtons.appendChild(zoomOut);
1694         links.Graph.addEventListener(zoomOut, "mousedown", function (event) {
1695             graph._zoomVertical(-0.2);
1696             links.Graph.preventDefault(event);
1697         });
1698 
1699         this.main.appendChild(this.main.zoomButtons);
1700     }
1701 
1702     // clear any existing data from the grid
1703     while (this.frame.vgrid.hasChildNodes()) {
1704         this.frame.vgrid.removeChild(this.frame.vgrid.lastChild);
1705     }
1706 
1707     // determine the range start, end, and step
1708     this.vStart = (this.vStart != undefined && this.vStart < this.vMax) ? this.vStart : this.vMin;
1709     this.vEnd = (this.vEnd != undefined && this.vEnd > this.vMin) ? this.vEnd : this.vMax;
1710     // TODO: allow start and end larger than visible area?
1711     this.vStart = Math.max(this.vStart, this.vMin);
1712     this.vEnd = Math.min(this.vEnd, this.vMax);
1713 
1714     var start = this.vStart;
1715     var end = this.vEnd;
1716     var stepnum = parseInt(this.axisOffset / 40);
1717     var step = this.vStepSize || ((this.vEnd - this.vStart) / stepnum);
1718     var prettyStep = true;
1719     this.vStep._setRange(start, end, step, prettyStep);
1720 
1721     if (this.vEnd > this.vStart) {
1722         // calculate the conversion from y value to position on screen
1723         var graphBottom = this.axisOffset;
1724         var graphTop = 0;
1725         var yScale = (graphTop - graphBottom) / (this.vEnd - this.vStart);
1726         var yShift = graphBottom - this.vStart * yScale;
1727         this.yToScreen = function (y) {
1728             return y * yScale + yShift;
1729         };
1730         this.screenToY = function (ys) {
1731             return (ys - yShift) / yScale;
1732         };
1733         // TODO: make a more neat solution for this.yToScreen()
1734     }
1735     else {
1736         this.yToScreen = function () {
1737             return 0;
1738         };
1739         this.screenToY = function () {
1740             return 0;
1741         };
1742     }
1743 
1744     if (this.vAreas && !this.frame.background.childNodes.length) {
1745         // create vertical background areas
1746         for (i = 0; i < this.vAreas.length; i++) {
1747             var area = this.vAreas[i];
1748             var divArea = document.createElement('DIV');
1749             divArea.className = 'graph-background-area';
1750             divArea.start = (area.start != null) ? Number(area.start) : null;
1751             divArea.end = (area.end != null) ? Number(area.end) : null;
1752             if (area.className) {
1753                 divArea.className += ' ' + area.className;
1754             }
1755             if (area.color) {
1756                 divArea.style.backgroundColor = area.color;
1757             }
1758             this.frame.background.appendChild(divArea);
1759         }
1760     }
1761     if (this.frame.background.childNodes.length) {
1762         // reposition vertical background areas
1763         var childs = this.frame.background.childNodes;
1764         for (i = 0; i < childs.length; i++) {
1765             var child = childs[i];
1766             var areaStart = this.yToScreen(child.start != null ? Math.max(child.start, this.vStart) : this.vStart);
1767             var areaEnd = this.yToScreen(child.end != null ? Math.min(child.end, this.vEnd) : this.vEnd);
1768             child.style.top = areaEnd + 'px';
1769             child.style.height = Math.max(areaStart - areaEnd, 0) + 'px';
1770         }
1771     }
1772 
1773     var maxWidth = 0;
1774     var count = 0;
1775     this.vStep.start();
1776     if ( this.yToScreen(this.vStep.getCurrent()) > this.axisOffset) {
1777         this.vStep.next();
1778     }
1779     while(!this.vStep.end() && count < 100) {
1780         count++;
1781         var y = this.vStep.getCurrent();
1782         var yScreen = this.yToScreen(y);
1783 
1784         // use scientific notation when necessary
1785         if (Math.abs(y) > 1e6) {
1786             y = y.toExponential();
1787         }
1788         else if (Math.abs(y) < 1e-4) {
1789             if (Math.abs(y) > this.vStep.getStep()/2)
1790                 y = y.toExponential();
1791             else
1792                 y = 0;
1793         }
1794 
1795         // create the text of the label
1796         var content = document.createTextNode(y);
1797         var labelText = document.createElement("DIV");
1798         labelText.appendChild(content);
1799         labelText.className = "graph-axis-text graph-axis-text-vertical";
1800         labelText.style.position = "absolute";
1801         labelText.style.whiteSpace = "nowrap";
1802         labelText.style.textAlign = "right";
1803         this.main.axisLeft.appendChild(labelText);
1804 
1805         // create the label line
1806         var labelLine = document.createElement("DIV");
1807         labelLine.className = "graph-axis-grid graph-axis-grid-vertical";
1808         labelLine.style.position = "absolute";
1809         labelLine.style.borderTopStyle = "solid";
1810         labelLine.style.width = "5px";
1811         this.main.axisLeft.appendChild(labelLine);
1812 
1813         // create the grid line
1814         var labelGridLine = document.createElement("DIV");
1815         labelGridLine.className = (y != 0) ? "graph-axis-grid graph-axis-grid-minor" :
1816             "graph-axis-grid graph-axis-grid-major";
1817         labelGridLine.style.position = "absolute";
1818         labelGridLine.style.left = "0px";
1819         labelGridLine.style.width = "100%";
1820         labelGridLine.style.borderTopStyle = "solid";
1821         this.frame.vgrid.appendChild(labelGridLine);
1822 
1823         // position the label text and line vertically
1824         var h = labelText.offsetHeight;
1825         labelText.style.top  = links.Graph.px(yScreen - h/2);
1826         labelLine.style.top = links.Graph.px(yScreen);
1827         labelGridLine.style.top = links.Graph.px(yScreen);
1828 
1829         // calculate the widest label so far.
1830         maxWidth = Math.max(maxWidth, labelText.offsetWidth);
1831 
1832         this.vStep.next();
1833     }
1834 
1835     // right align all elements
1836     maxWidth += this.main.zoomButtons.clientWidth; // append width of the zoom buttons
1837     for (i = 0; i < this.main.axisLeft.childNodes.length; i++) {
1838         this.main.axisLeft.childNodes[i].style.left =
1839             links.Graph.px(maxWidth - this.main.axisLeft.childNodes[i].offsetWidth);
1840     }
1841 
1842     // resize the axis
1843     this.main.axisLeft.style.left = links.Graph.px(this.mainPadding);
1844     this.main.axisLeft.style.top = links.Graph.px(this.mainPadding);
1845     this.main.axisLeft.style.height = links.Graph.px(this.axisOffset + 1);
1846     this.main.axisLeft.style.width = links.Graph.px(maxWidth);
1847 
1848     this.main.axisRight.style.left =
1849         links.Graph.px(this.main.clientWidth - this.legendWidth - this.mainPadding - 2);
1850     this.main.axisRight.style.top = links.Graph.px(this.mainPadding);
1851     this.main.axisRight.style.height = links.Graph.px(this.axisOffset + 1);
1852 
1853     //var testEnd = new Date(); // TODO: cleanup
1854     //document.title += " v:" +(testEnd - testStart) + "ms"; // TODO: cleanup
1855 };
1856 
1857 
1858 /**
1859  * Draw all events that are provided in the data on the graph
1860  */
1861 links.Graph.prototype._redrawData = function() {
1862     this._calcConversionFactor();
1863 
1864     // determine the size of the graph
1865     var start = this._screenToTime(-this.axisMargin);
1866     var end = this._screenToTime(this.frame.clientWidth + this.axisMargin);
1867     //var width = this.frame.clientWidth + 2*this.axisMargin;
1868     /*
1869      // TODO: use axisMargin?
1870      var start = this._screenToTime(0);
1871      var end = this._screenToTime(this.frame.clientWidth);
1872      var width = this.frame.clientWidth;
1873      */
1874 
1875     var graph = this.frame.canvas.graph;
1876     var ctx = graph.getContext("2d");
1877 
1878     // clear the graph.
1879     // It is important to clear the old size of the graph (before resizing), else
1880     // Safari does not clear the whole graph.
1881     ctx.clearRect(0, 0, graph.height, graph.width);
1882 
1883     // resize the graph element
1884     var left = this.timeToScreen(start);
1885     var right = this.timeToScreen(end);
1886     var graphWidth = right - left;
1887     var height = this.axisOffset;
1888 
1889     graph.style.left = links.Graph.px(left);
1890     graph.width = graphWidth;
1891     graph.height = height;
1892 
1893     var offset = parseFloat(graph.style.left);
1894 
1895     // draw the graph(s)
1896     for (var col = 0, colCount = this.data.length; col < colCount; col++) {
1897         var style = this._getLineStyle(col);
1898         var color = this._getLineColor(col);
1899         var textColor = this._getTextColor(col);
1900         var font = this._getFont(col);
1901         var width = this._getLineWidth(col);
1902         var radius = this._getLineRadius(col);
1903         var visible = this._getLineVisible(col);
1904         var type = this.data[col].type || 'line';
1905         var data = this.data[col].data;
1906         var d;
1907 
1908         // determine the first and last row inside the visible area
1909         var rowRange = this._getVisbleRowRange(data, start, end, type,
1910             this.data[col].visibleRowRange);
1911         this.data[col].visibleRowRange = rowRange;
1912         var rowStep = this._calculateRowStep(rowRange);
1913 
1914         if (visible && rowRange) {
1915             switch (type) {
1916                 case 'line':
1917                     if (style == "line" || style == "dot-line") {
1918                         // draw line
1919                         ctx.strokeStyle = color;
1920                         ctx.lineWidth = width;
1921 
1922                         ctx.beginPath();
1923                         var row = rowRange.start;
1924                         while (row <= rowRange.end) {
1925                             // find the first data row with a non-null value
1926                             while (row <= rowRange.end && data[row].value == null) {
1927                                 row += rowStep;
1928                             }
1929                             if (row <= rowRange.end) {
1930                                 // move to the first non-null data point
1931                                 value = data[row].value;
1932                                 var x = this.timeToScreen(data[row].date) - offset;
1933                                 var y = this.yToScreen(value);
1934                                 ctx.moveTo(x, y);
1935 
1936                                 /* TODO: implement fill style
1937                                  ctx.moveTo(x, this.yToScreen(0));
1938                                  ctx.lineTo(x, y);
1939                                  */
1940                                 row += rowStep;
1941                             }
1942 
1943                             // draw lines as long as data values are not null
1944                             while (row <= rowRange.end && (value = data[row].value) != null) {
1945                                 x = this.timeToScreen(data[row].date) - offset;
1946                                 y = this.yToScreen(value);
1947                                 ctx.lineTo(x, y);
1948                                 row += rowStep;
1949                             }
1950 
1951                             /* TODO: implement fill style
1952                              ctx.lineTo(x, this.yToScreen(0));
1953                              */
1954                         }
1955 
1956                         /* TODO: implement fill style
1957                          ctx.fillStyle = "rgba(255,255,0, 0.5)";
1958                          ctx.fill();
1959                          */
1960 
1961                         ctx.stroke();
1962                     }
1963 
1964                     if (type == 'line' && (style == "dot" || style == "dot-line")) {
1965                         // draw dots
1966                         var diameter = 2 * radius;
1967                         ctx.fillStyle = color;
1968 
1969                         for (row = rowRange.start; row <= rowRange.end; row += rowStep) {
1970                             var value = data[row].value;
1971                             if (value != null) {
1972                                 x = this.timeToScreen(data[row].date) - offset;
1973                                 y = this.yToScreen(value);
1974                                 ctx.fillRect(x - radius, y - radius, diameter, diameter);
1975                             }
1976                         }
1977                     }
1978                     break;
1979 
1980                 case 'area':
1981                     // draw background area
1982                     for (row = rowRange.start; row <= rowRange.end; row += rowStep) {
1983                         d = data[row];
1984                         ctx.fillStyle = d.color || color;
1985 
1986                         var xStart = this.timeToScreen(d.start) - offset;
1987                         var yStart = this.timeToScreen(d.end) - offset;
1988                         ctx.fillRect(xStart, 0, yStart - xStart, height);
1989 
1990                         if (d.text) {
1991                             // draw text
1992                             ctx.font = d.font || font;
1993                             ctx.textAlign = 'left';
1994                             ctx.textBaseline = 'top';
1995                             ctx.fillStyle = d.textColor || textColor;
1996                             ctx.fillText(d.text, xStart + 2, 0);
1997                         }
1998                     }
1999                     break;
2000 
2001                 case 'event':
2002                     // draw event background area
2003                     for (row = rowRange.start; row <= rowRange.end; row += rowStep) {
2004                         d = data[row];
2005                         ctx.fillStyle = d.color || color;
2006 
2007                         // area with a start only
2008                         var dWidth = d.width || width;
2009                         xStart = this.timeToScreen(d.date) - offset;
2010                         ctx.fillRect(xStart - dWidth / 2, 0, dWidth, height);
2011 
2012                         if (d.text) {
2013                             // draw text
2014                             ctx.font = d.font || font;
2015                             ctx.textAlign = 'left';
2016                             ctx.textBaseline = 'top';
2017                             ctx.fillStyle = d.textColor || textColor;
2018                             ctx.fillText(d.text, xStart + dWidth / 2 + 2, 0);
2019                         }
2020                     }
2021                     break;
2022 
2023                 default:
2024                     throw new Error('Unknown type of dataset "' + type + '". ' +
2025                         'Choose "line" or "area"');
2026             }
2027         }
2028     }
2029 };
2030 
2031 /**
2032  * Calculate the row step (skipping datapoints in case of much data)
2033  * @param {Object} rowRange  Object containing parameters
2034  *                               {Date} start
2035  *                               {Date} end
2036  * @return {Number} rowStep   an integer number
2037  * @private
2038  */
2039 links.Graph.prototype._calculateRowStep = function(rowRange) {
2040     var rowStep;
2041 
2042     // choose a step size, depending on the width of the screen in pixels
2043     // and the number of data points.
2044     if ( this.autoDataStep && rowRange ) {
2045         // skip data points in case of much data
2046         var rowCount = (rowRange.end - rowRange.start);
2047         var canvasWidth = (this.frame.clientWidth + 2 * this.axisMargin);
2048         rowStep = Math.max(Math.floor(rowCount / canvasWidth), 1);
2049     }
2050     else {
2051         // draw all data points
2052         rowStep = 1;
2053     }
2054 
2055     return rowStep;
2056 };
2057 
2058 /**
2059  * Redraw the tooltip showing the currently hovered value
2060  */
2061 links.Graph.prototype._redrawDataTooltip = function () {
2062     var tooltip = this.tooltip;
2063     if (this.showTooltip && tooltip) {
2064         var dataPoint = tooltip.dataPoint;
2065         if (dataPoint) {
2066             var dot = tooltip.dot;
2067             var label = tooltip.label;
2068 
2069             var graph = this.frame.canvas.graph;
2070             var offset = parseFloat(graph.style.left) + this.axisMargin;
2071             var radius = dataPoint.radius || 4;
2072             var color = dataPoint.color || '#4d4d4d';
2073             var left = this.timeToScreen(dataPoint.date) + offset;
2074             var top = (dataPoint.value != undefined) ? this.yToScreen(dataPoint.value) : 16;
2075 
2076             if (!dot) {
2077                 dot = document.createElement('div');
2078                 dot.className = 'graph-tooltip-dot';
2079                 tooltip.dot = dot;
2080             }
2081             if (dot.style.borderColor != color && dot.parentNode) {
2082                 // note: this is a workaround for a bug in Chrome on Windows,
2083                 // which does not apply changed border color correctly
2084                 dot.parentNode.removeChild(dot);
2085             }
2086             if (!dot.parentNode) {
2087                 this.frame.canvas.appendChild(dot);
2088             }
2089 
2090             if (!label) {
2091                 // note: we could create label as a child of dot, but there
2092                 // appears to be a bug in Chrome on Windows giving issues.
2093                 label = document.createElement('div');
2094                 label.className = 'graph-tooltip-label';
2095                 tooltip.label = label;
2096             }
2097             if (!label.parentNode) {
2098                 // note: the label must be added to the DOM before changing
2099                 // its innerHTML, else you encounter a bug on IE 6-8.
2100                 this.frame.canvas.appendChild(label);
2101             }
2102 
2103             dot.style.left = left + 'px';
2104             dot.style.top = top + 'px';
2105             dot.style.borderColor = color;
2106             dot.style.borderRadius = radius + 'px';
2107             dot.style.borderWidth = radius + 'px';
2108             dot.style.marginLeft = -radius + 'px';
2109             dot.style.marginTop = -radius + 'px';
2110             dot.style.display = dataPoint.title ? 'none': '';
2111 
2112             var html;
2113             if (this.tooltipFormatter) {
2114                 // custom format function
2115                 html = this.tooltipFormatter(dataPoint);
2116             }
2117             else {
2118                 html = '<table style="color: ' + color + '">';
2119                 if (dataPoint.title) {
2120                     html += '<tr><td>' + dataPoint.title + '</td></tr>';
2121                 }
2122                 else {
2123                     html += '<tr><td>Date:</td><td>' + dataPoint.date + '</td></tr>';
2124                     if (dataPoint.value != undefined) {
2125                         html += '<tr><td>Value:</td><td>' + dataPoint.value.toPrecision(4) + '</td></tr>';
2126                     }
2127                 }
2128                 html += '</table>';
2129             }
2130             label.innerHTML = html;
2131 
2132             var width = label.clientWidth;
2133             var graphWidth = this.timeToScreen(this.end) - this.timeToScreen(this.start);
2134             var height = label.clientHeight;
2135             var margin = 10;
2136             var showAbove = (top - height - margin > 0);
2137             var showRight = (left + width + margin < graphWidth);
2138             label.style.top  = (showAbove ? (top - height - radius) : (top + radius)) + 'px';
2139             label.style.left = (showRight ? (left + radius) : (left - width - radius)) + 'px';
2140         }
2141         else {
2142             // remove the dot when visible
2143             if (tooltip.dot && tooltip.dot.parentNode) {
2144                 tooltip.dot.parentNode.removeChild(tooltip.dot);
2145                 tooltip.dot = undefined; // remove the dot, else we get issues on IE8-
2146             }
2147             if (tooltip.label && tooltip.label.parentNode) {
2148                 tooltip.label.parentNode.removeChild(tooltip.label);
2149                 tooltip.label = undefined; // remove the label, else we get issues on IE8-
2150             }
2151         }
2152     }
2153 };
2154 
2155 /**
2156  * Set a tooltip for the currently hovered data
2157  * @param {Object} dataPoint    object containing parameters:
2158  *                              {String} date
2159  *                              {String} value
2160  *                              {String} color
2161  *                              {String} radius
2162  * @private
2163  */
2164 links.Graph.prototype._setTooltip = function (dataPoint) {
2165     if (!this.tooltip) {
2166         this.tooltip = {};
2167     }
2168     this.tooltip.dataPoint = dataPoint;
2169 
2170     this._redrawDataTooltip();
2171 };
2172 
2173 
2174 /**
2175  * Find the data point closest to given date and value (euclidean distance).
2176  * If no data point is found near given position, undefined is returned.
2177  * @param {Date} date
2178  * @param {Number} value
2179  * @return {Object | undefined} dataPoint   An object containing parameters
2180  *                                            {Date} date
2181  *                                            {Number} value
2182  *                                            {String} color
2183  *                                            {Number} radius
2184  * @private
2185  */
2186 links.Graph.prototype._findClosestDataPoint = function (date, value) {
2187     var maxDistance = 30; // px
2188     var winner = undefined;
2189     var graph = this;
2190     function isVisible (dataPoint) {
2191         return dataPoint.date >= graph.start &&
2192             dataPoint.date <= graph.end &&
2193             dataPoint.value >= graph.vStart &&
2194             dataPoint.value <= graph.vEnd
2195     }
2196 
2197     for (var col = 0, colCount = this.data.length; col < colCount; col++) {
2198         var visible = this._getLineVisible(col);
2199         var rowRange = this.data[col].visibleRowRange;
2200         var data = this.data[col].data;
2201         var type = this.data[col].type;
2202 
2203         if (visible && rowRange) {
2204             var rowStep = this._calculateRowStep(rowRange);
2205             var row = rowRange.start;
2206             while (row <= rowRange.end) {
2207                 var dataPoint = data[row];
2208                 if (type == 'event') {
2209                     dataPoint = {
2210                         date: dataPoint.date,
2211                         value: this.screenToY(16), // TODO: use the real font height
2212                         text: dataPoint.text,
2213                         title: dataPoint.title
2214                     };
2215                 }
2216                 else if (type == 'area') {
2217                     dataPoint = {
2218                         date: dataPoint.start,
2219                         value: this.screenToY(16), // TODO: use the real font height
2220                         text: dataPoint.text,
2221                         title: dataPoint.title
2222                     };
2223                 }
2224 
2225                 if (dataPoint.value != null) {
2226                     // first data point found right from x.
2227                     var dateDistance = Math.abs(dataPoint.date - date) * this.ttsFactor;
2228                     if (dateDistance < maxDistance) {
2229                         var valueDistance = Math.abs(this.yToScreen(dataPoint.value) - this.yToScreen(value));
2230                         if ((valueDistance < maxDistance) && isVisible(dataPoint)) {
2231                             var distance = Math.sqrt(
2232                                     dateDistance * dateDistance +
2233                                     valueDistance * valueDistance);
2234                             if (!winner || distance < winner.distance) {
2235                                 // we have a new winner
2236                                 var color = this._getLineColor(col);
2237                                 var radius;
2238                                 if (type == 'event' || type == 'area') {
2239                                     radius = this._getLineWidth(col);
2240                                     color = this._getTextColor(col);
2241                                 }
2242                                 else if (this._getLineStyle(col) == 'line') {
2243                                     radius = this._getLineWidth(col) * 2;
2244                                 }
2245                                 else {
2246                                     radius = this._getLineRadius(col) * 2;
2247                                 }
2248                                 radius = Math.max(radius, 4);
2249 
2250                                 winner = {
2251                                     distance: distance,
2252                                     dataPoint: {
2253                                         date: dataPoint.date,
2254                                         //value: (dataPoint.value != undefined) ? dataPoint.value : this.screenToY(10),
2255                                         value: dataPoint.value,
2256                                         title: dataPoint.title,
2257                                         text: dataPoint.text,
2258                                         color: color,
2259                                         radius: radius,
2260                                         line: col
2261                                     }
2262                                 };
2263                             }
2264                         }
2265                     }
2266                     else if (dataPoint.date > date) {
2267                         // skip the rest of the data
2268                         row = rowRange.end;
2269                     }
2270                 }
2271                 row += rowStep;
2272             }
2273         }
2274     }
2275 
2276     return winner ? winner.dataPoint : undefined;
2277 };
2278 
2279 /**
2280  * Average a range of values in the given data table
2281  * @param {Array}  data    table containing objects with parameters date and value
2282  * @param {Number} start   index to start averaging
2283  * @param {Number} length  the number of values to average
2284  * @return {Object}        An object with average values for the date and value
2285  *
2286  */
2287     // TODO: this method is not used. Delete it?
2288 links.Graph.prototype._average = function(data, start, length) {
2289     var sumDate = 0;
2290     var countDate = 0;
2291     var sumValue = 0;
2292     var countValue = 0;
2293 
2294     for (var row = start, end = Math.min(start+length, data.length); row < end; row++) {
2295         var d = data[row];
2296         if (d.date != undefined) {
2297             sumDate += d.date.valueOf();
2298             countDate += 1;
2299         }
2300         if (d.value != undefined) {
2301             sumValue += d.value;
2302             countValue += 1;
2303         }
2304     }
2305 
2306     var avgDate = new Date(Math.round(sumDate / countDate));
2307     var avgValue = sumValue / countValue;
2308 
2309     return {"date": avgDate, "value": avgValue};
2310 };
2311 
2312 
2313 /**
2314  * Draw all events that are provided in the data on the graph
2315  */
2316 links.Graph.prototype._redrawLegend = function() {
2317     // Calculate the number of functions that need a legend entry
2318     var legendCount = 0;
2319     for (var col = 0, len = this.data.length; col < len; col++) {
2320         if (this._getLineLegend(col) == true)
2321             legendCount ++;
2322     }
2323 
2324     if (legendCount == 0 || (this.legend && this.legend.visible === false) ) {
2325         // no legend entries
2326         if (this.main.legend) {
2327             // remove if existing
2328             this.main.removeChild(this.main.legend);
2329             this.main.legend = undefined;
2330         }
2331 
2332         this.legendWidth = 0;
2333         return;
2334     }
2335 
2336     var scrollTop = 0;
2337     if (!this.main.legend) {
2338         // create the legend
2339         this.main.legend = document.createElement("DIV");
2340         this.main.legend.className = "graph-legend";
2341         this.main.legend.style.position = "absolute";
2342         this.main.legend.style.overflowY = "auto";
2343 
2344         this.main.appendChild(this.main.legend);
2345     } else {
2346         // clear any existing contents of the legend
2347         scrollTop = this.main.legend.scrollTop;
2348         while (this.main.legend.hasChildNodes()) {
2349             this.main.legend.removeChild(this.main.legend.lastChild);
2350         }
2351     }
2352 
2353     var maxWidth = 0;
2354     for (var col = 0, len = this.data.length; col < len; col++) {
2355         var showLegend = this._getLineLegend(col);
2356 
2357         if (showLegend) {
2358             var color = this._getLineColor(col);
2359             var label = this.data[col].label;
2360 
2361             var divLegendItem = document.createElement("DIV");
2362             divLegendItem.className = "graph-legend-item";
2363             this.main.legend.appendChild(divLegendItem);
2364 
2365             if (this.legend && this.legend.toggleVisibility) {
2366                 // show a checkbox to show/hide graph
2367                 var chkShow = document.createElement("INPUT");
2368                 chkShow.type = "checkbox";
2369                 chkShow.checked = this._getLineVisible(col);
2370                 chkShow.defaultChecked = this._getLineVisible(col);    // for IE
2371                 chkShow.style.marginRight = links.Graph.px(this.mainPadding);
2372                 chkShow.col = col; // store its column number
2373 
2374                 var me = this;
2375                 chkShow.onmousedown = function (event) {
2376                     me._setLineVisible(this.col, !this.checked );
2377                     me._checkSize();
2378                     me.redraw();
2379                 };
2380 
2381                 divLegendItem.appendChild(chkShow);
2382             }
2383 
2384             var spanColor = document.createElement("SPAN");
2385             spanColor.style.backgroundColor = color;
2386             spanColor.innerHTML = "      ";
2387             divLegendItem.appendChild(spanColor);
2388 
2389             var text = document.createTextNode(" " + label);
2390             divLegendItem.appendChild(text);
2391             // TODO: test on IE
2392 
2393             maxWidth = Math.max(maxWidth, divLegendItem.clientWidth);
2394         }
2395     }
2396 
2397     // position the legend in the upper right corner
2398     // TODO: make location customizable
2399     this.main.legend.style.top = links.Graph.px(this.mainPadding);
2400     this.main.legend.style.height = "auto";
2401     var scroll = false;
2402     if (this.main.legend.clientHeight > (this.axisOffset - 1)) {
2403         this.main.legend.style.height = links.Graph.px(this.axisOffset - 1) ;
2404         scroll = true;
2405     }
2406 
2407     if (this.legend && this.legend.width) {
2408         this.main.legend.style.width = this.legend.width;
2409     }
2410     else if (scroll) {
2411         this.main.legend.style.width = "auto";
2412         this.main.legend.style.width = links.Graph.px(this.main.legend.clientWidth + 40); // adjust for scroll bar width
2413     }
2414     else {
2415         this.main.legend.style.width = "auto";
2416         this.main.legend.style.width = links.Graph.px(this.main.legend.clientWidth + 5);  // +5 to prevent wrapping text
2417     }
2418 
2419     this.legendWidth =
2420         (this.main.legend.offsetWidth ? this.main.legend.offsetWidth : this.main.legend.clientWidth) +
2421             this.mainPadding; // TODO: test on IE6
2422 
2423     this.main.legend.style.left = links.Graph.px(this.main.clientWidth - this.legendWidth);
2424 
2425     // restore the previous scroll position
2426     if (scrollTop) {
2427         this.main.legend.scrollTop = scrollTop;
2428     }
2429 };
2430 
2431 /**
2432  * Determines
2433  * @param {Array} data       An array containing objects with parameters
2434  *                           d (datetime) and v (value)
2435  * @param {Date} start       The start date of the visible range
2436  * @param {Date} end         The end date of the visible range
2437  * @param {String} type      Type of data. 'line' (default), 'area', or 'event'
2438  * @param {Object} oldRowRange  previous row range, can serve as start
2439  *                                to find the current visible range faster.
2440  * @return {object}         Range object containing start row and end row
2441  *                            range.start {int}  row number of first visible row
2442  *                            range.end   {int}  row number of last visible row +1
2443  *                                               (this can be the rowcount +1)
2444  */
2445 links.Graph.prototype._getVisbleRowRange = function(data, start, end, type, oldRowRange) {
2446     if (!data) {
2447         data = [];
2448     }
2449     var fieldStart = 'date';
2450     var fieldEnd = 'date';
2451     if (type == 'area') {
2452         fieldStart = 'start';
2453         fieldEnd = 'end';
2454     }
2455     var rowCount = data.length;
2456 
2457     // initialize
2458     var rowRange = {
2459         start: 0,
2460         end: (rowCount-1)
2461     };
2462     if (oldRowRange != null) {
2463         rowRange.start = oldRowRange.start;
2464         rowRange.end = oldRowRange.end;
2465     }
2466 
2467     // check if the current range does not exceed the actual number of rows
2468     if (rowRange.start > rowCount - 1 && rowCount > 0) {
2469         rowRange.start = rowCount - 1;
2470     }
2471 
2472     if (rowRange.end > rowCount - 1) {
2473         rowRange.end = rowCount - 1;
2474     }
2475 
2476     // find the first visible row. Start searching at the previous first visible row
2477     while (rowRange.start > 0 &&
2478         data[rowRange.start][fieldStart].valueOf() > start.valueOf()) {
2479         rowRange.start--;
2480     }
2481     while (rowRange.start < rowCount-1 &&
2482         data[rowRange.start][fieldStart].valueOf() < start.valueOf()) {
2483         rowRange.start++;
2484     }
2485 
2486     // find the last visible row. Start searching at the previous last visible row
2487     while (rowRange.end > rowRange.start &&
2488         data[rowRange.end][fieldEnd].valueOf() > end.valueOf()) {
2489         rowRange.end--;
2490     }
2491     while (rowRange.end < rowCount-1 &&
2492         data[rowRange.end][fieldEnd].valueOf() < end.valueOf()) {
2493         rowRange.end++;
2494     }
2495 
2496     return rowRange;
2497 };
2498 
2499 
2500 /**
2501  * Determines the row range of a datatable
2502  * @param data {Array}          An array containing objects with parameters
2503  *                              d (datetime) and v (value)
2504  * @param {String[]} [fields]   Optional array with field names to be read
2505  *                              for min/max. These fields must contain Date
2506  *                              objects. If fields is undefined, the data will
2507  *                              be searched for ['date'].
2508  * @return {object}             Range object containing start row and end row
2509  *                                  range.start {Date} first date in the data
2510  *                                  range.end   {Date} last date in the data
2511  */
2512 links.Graph.prototype._getRowRange = function(data, fields) {
2513     if (!data) {
2514         data = [];
2515     }
2516     if (!fields) {
2517         fields = ['date'];
2518     }
2519 
2520     var rowRange = {
2521         min: undefined,  // number
2522         max: undefined   // number
2523     };
2524 
2525     if (data.length > 0) {
2526         for (var f = 0; f < fields.length; f++) {
2527             var field = fields[f];
2528 
2529             rowRange.min = data[0][field].valueOf();
2530             rowRange.max = data[0][field].valueOf();
2531 
2532             for (var row = 1, rows = data.length; row < rows; row++) {
2533                 var d = data[row][field];
2534                 if (d != undefined) {
2535                     rowRange.min = Math.min(d.valueOf(), rowRange.min);
2536                     rowRange.max = Math.max(d.valueOf(), rowRange.max);
2537                 }
2538             }
2539         }
2540     }
2541 
2542     if (rowRange.min != null && !isNaN(rowRange.min) &&
2543         rowRange.max != null && !isNaN(rowRange.max)) {
2544         return {
2545             min: new Date(rowRange.min),
2546             max: new Date(rowRange.max)
2547         };
2548     }
2549     return null;
2550 };
2551 
2552 /**
2553  * Calculate the maximum and minimum value of all graphs in the provided data
2554  * table.
2555  * @param data {Array}      An array containing objects with parameters
2556  *                          d (datetime) and v (value)
2557  * @return {Object}         An object with parameters min and max (both numbers)
2558  */
2559 links.Graph.prototype._getDataRange = function(data) {
2560     if (!data) {
2561         data = [];
2562     }
2563 
2564     var dataRange = null;
2565     for (var row = 0, rows = data.length; row < rows; row++) {
2566         var value = data[row].value;
2567         if (value != undefined) {
2568             if (dataRange) {
2569                 // find max/min
2570                 dataRange.min = Math.min(value, dataRange.min);
2571                 dataRange.max = Math.max(value, dataRange.max);
2572             }
2573             else {
2574                 // first defined value
2575                 dataRange = {
2576                     min: value,
2577                     max: value
2578                 }
2579             }
2580         }
2581     }
2582 
2583     if (dataRange &&
2584         dataRange.min != null && !isNaN(dataRange.min) &&
2585         dataRange.max != null && !isNaN(dataRange.max)) {
2586         return dataRange;
2587     }
2588     return null;
2589 };
2590 
2591 
2592 /**
2593  * Returns a string with the style for the given column in data.
2594  * Available styles are "dot", "line", or "dot-line"
2595  * @param {int} column    The column number
2596  * @return {string} style The style for this line
2597  */
2598 links.Graph.prototype._getLineStyle = function(column) {
2599     if (this.lines && column < this.lines.length) {
2600         var line = this.lines[column];
2601         if (line && line.style != undefined)
2602             return line.style.toLowerCase();
2603     }
2604 
2605     if (this.line && this.line.style != undefined)
2606         return this.line.style.toLowerCase();
2607 
2608     return "line";
2609 };
2610 
2611 /**
2612  * Returns a string with the color for the given column in data.
2613  * @param {int} column    The column number
2614  * @return {string} color The color for this line
2615  */
2616 links.Graph.prototype._getLineColor = function(column) {
2617     if (this.lines && column < this.lines.length) {
2618         var line = this.lines[column];
2619         if (line && line.color != undefined)
2620             return line.color;
2621     }
2622 
2623     if (this.line && this.line.color != undefined)
2624         return this.line.color;
2625 
2626     if (column < this.defaultColors.length) {
2627         return this.defaultColors[column];
2628     }
2629 
2630     return "black";
2631 };
2632 
2633 /**
2634  * Returns a string with the text color for the given column in data.
2635  * @param {int} column    The column number
2636  * @return {string} color The text color for this line
2637  */
2638 links.Graph.prototype._getTextColor = function(column) {
2639     if (this.lines && column < this.lines.length) {
2640         var line = this.lines[column];
2641         if (line && line.textColor != undefined)
2642             return line.textColor;
2643     }
2644 
2645     if (this.line && this.line.textColor != undefined)
2646         return this.line.textColor;
2647 
2648     return "#4D4D4D";
2649 };
2650 
2651 /**
2652  * Returns a string with the font the given column in data.
2653  * @param {int} column    The column number
2654  * @return {string} font  The font for this line, for example '13px arial'
2655  */
2656 links.Graph.prototype._getFont = function(column) {
2657     if (this.lines && column < this.lines.length) {
2658         var line = this.lines[column];
2659         if (line && line.font != undefined)
2660             return line.font;
2661     }
2662 
2663     if (this.line && this.line.font != undefined)
2664         return this.line.font;
2665 
2666     return "13px arial";
2667 };
2668 
2669 /**
2670  * Returns a float with the line width for the given column in data.
2671  * @param {Number} column        The column number
2672  * @return {Number} linewidthh   The width for this line
2673  */
2674 links.Graph.prototype._getLineWidth = function(column) {
2675     if (this.lines && column < this.lines.length) {
2676         var line = this.lines[column];
2677         if (line && line.width != undefined)
2678             return parseFloat(line.width);
2679     }
2680 
2681     if (this.line && this.line.width != undefined)
2682         return parseFloat(this.line.width);
2683 
2684     return 2.0;
2685 };
2686 
2687 /**
2688  * Returns a float with the line radius (radius for the dots) for the given
2689  * column in data.
2690  * @param {int} column         The column number
2691  * @return {Number} lineRadius The radius for the dots on this line
2692  */
2693 links.Graph.prototype._getLineRadius = function(column) {
2694     if (this.lines && column < this.lines.length) {
2695         var line = this.lines[column];
2696         if (line && line.radius != undefined)
2697             return parseFloat(line.radius);
2698     }
2699 
2700     if (this.line && this.line.radius != undefined)
2701         return parseFloat(this.line.radius);
2702 
2703     return 3.0;
2704 };
2705 
2706 /**
2707  * Returns whether a certain line must be displayed in the legend
2708  * @param {int} column            The column number
2709  * @return {boolean} showLegend   Whether this line must be displayed in the legend
2710  */
2711 links.Graph.prototype._getLineLegend = function(column) {
2712     if (this.lines && column < this.lines.length) {
2713         var line = this.lines[column];
2714         if (line && line.legend != undefined)
2715             return line.legend;
2716     }
2717 
2718     if (this.line && this.line.legend != undefined)
2719         return this.line.legend;
2720 
2721     return true;
2722 };
2723 
2724 /**
2725  * Returns whether a certain line is visible (and must be drawn)
2726  * @param {int} column            The column number
2727  * @return {boolean} visible      True if this line is visible
2728  */
2729 links.Graph.prototype._getLineVisible = function(column) {
2730     if (this.lines && column < this.lines.length) {
2731         var line = this.lines[column];
2732         if (line && line.visible != undefined)
2733             return line.visible;
2734     }
2735 
2736     if (this.line && this.line.visible != undefined)
2737         return this.line.visible;
2738 
2739     return true;
2740 };
2741 
2742 
2743 /**
2744  * Change the visibility of a line
2745  * @param {int} column         The column number (one based)
2746  * @param {boolean} visible    True if this line must be visible
2747  */
2748 links.Graph.prototype._setLineVisible = function(column, visible) {
2749     column = parseInt(column);
2750     if (column < 0)
2751         return;
2752 
2753     if (!this.lines)
2754         this.lines = [];
2755 
2756     if (!this.lines[column])
2757         this.lines[column] = {};
2758 
2759     this.lines[column].visible = visible;
2760 };
2761 
2762 /**
2763  * Check if the current frame size corresponds with the end Date. If the size
2764  * does not correspond, the end Date is changed to match the frame size.
2765  *
2766  * This function is used before a mousedown and scroll event, to check if
2767  * the frame size is not changed (caused by resizing events on the page).
2768  */
2769 links.Graph.prototype._checkSize = function() {
2770     if (this.lastMainWidth != this.main.clientWidth ||
2771         this.lastMainHeight != this.main.clientHeight) {
2772 
2773         var diff = this.main.clientWidth - this.lastMainWidth;
2774 
2775         // recalculate the current end Date based on the real size of the frame
2776         this.end = new Date((this.frame.clientWidth + diff) / (this.frame.clientWidth) *
2777             (this.end.valueOf() - this.start.valueOf()) +
2778             this.start.valueOf() );
2779         // startEnd is the stored end position on start of a mouse movement
2780         if (this.startEnd) {
2781             this.startEnd = new Date((this.frame.clientWidth + diff) / (this.frame.clientWidth) *
2782                 (this.startEnd.valueOf() - this.start.valueOf()) +
2783                 this.start.valueOf() );
2784         }
2785 
2786         // redraw the graph
2787         this.redraw();
2788     }
2789 };
2790 
2791 /**
2792  * Start a moving operation inside the provided parent element
2793  * @param {Event}       event         The event that occurred (required for
2794  *                                    retrieving the  mouse position)
2795  */
2796 links.Graph.prototype._onMouseDown = function(event) {
2797     event = event || window.event;
2798 
2799     if (!this.moveable)
2800         return;
2801 
2802     // check if mouse is still down (may be up when focus is lost for example
2803     // in an iframe)
2804     if (this.leftButtonDown) {
2805         this.onMouseUp(event);
2806     }
2807 
2808     // only react on left mouse button down
2809     this.leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
2810     if (!this.leftButtonDown && !this.touchDown) {
2811         return;
2812     }
2813 
2814     // check if frame is not resized (causing a mismatch with the end Date)
2815     this._checkSize();
2816 
2817     // get mouse position
2818     this.startMouseX = links.Graph._getPageX(event);
2819     this.startMouseY = links.Graph._getPageY(event);
2820 
2821     this.startStart = new Date(this.start.valueOf());
2822     this.startEnd = new Date(this.end.valueOf());
2823     this.startVStart = this.vStart;
2824     this.startVEnd = this.vEnd;
2825     this.startGraphLeft = parseFloat(this.frame.canvas.graph.style.left);
2826     this.startAxisLeft = parseFloat(this.frame.canvas.axis.style.left);
2827 
2828     this.frame.style.cursor = 'move';
2829 
2830     // add event listeners to handle moving the contents
2831     // we store the function onmousemove and onmouseup in the graph, so we can
2832     // remove the eventlisteners lateron in the function mouseUp()
2833     var me = this;
2834     if (!this.onmousemove) {
2835         this.onmousemove = function (event) {me._onMouseMove(event);};
2836         links.Graph.addEventListener(document, "mousemove", this.onmousemove);
2837     }
2838     if (!this.onmouseup) {
2839         this.onmouseup = function (event) {me._onMouseUp(event);};
2840         links.Graph.addEventListener(document, "mouseup", this.onmouseup);
2841     }
2842     links.Graph.preventDefault(event);
2843 };
2844 
2845 
2846 /**
2847  * Perform moving operating.
2848  * This function activated from within the funcion links.Graph._onMouseDown().
2849  * @param {Event}   event  Well, eehh, the event
2850  */
2851 links.Graph.prototype._onMouseMove = function (event) {
2852     event = event || window.event;
2853 
2854     var mouseX = links.Graph._getPageX(event);
2855     var mouseY = links.Graph._getPageY(event);
2856 
2857     // calculate change in mouse position
2858     var diffX = mouseX - this.startMouseX;
2859     //var diffY = mouseY - this.startMouseY;
2860     var diffY = this.screenToY(this.startMouseY) - this.screenToY(mouseY);
2861     var diffYs = mouseY - this.startMouseY;
2862 
2863     // FIXME: on millisecond scale this.start needs to be rounded to integer milliseconds.
2864     var diffMillisecs = (-diffX) / this.frame.clientWidth *
2865         (this.startEnd.valueOf() - this.startStart.valueOf());
2866 
2867     var newStart = new Date(this.startStart.valueOf() + Math.round(diffMillisecs));
2868     var newEnd = new Date(this.startEnd.valueOf() + Math.round(diffMillisecs));
2869     this._applyRange(newStart, newEnd);
2870 
2871     // if the applied range is moved due to a fixed min or max,
2872     // change the diffMillisecs and diffX accordingly
2873     var appliedDiff = (this.start.valueOf() - newStart.valueOf());
2874     if (appliedDiff) {
2875         diffMillisecs += appliedDiff;
2876         diffX = -diffMillisecs * this.frame.clientWidth /
2877             (this.startEnd.valueOf() - this.startStart.valueOf());
2878     }
2879 
2880     // adjust vertical axis setting when needed
2881     // TODO: put that in a separate method _applyVerticalRange()
2882     var vStartNew = this.startVStart + diffY;
2883     var vEndNew = this.startVEnd + diffY;
2884     var d;
2885     if (vStartNew < this.vMin) {
2886         d = (this.vMin - vStartNew);
2887         vStartNew += d;
2888         vEndNew += d;
2889     }
2890     if (vEndNew > this.vMax) {
2891         d = (vEndNew - this.vMax);
2892         vStartNew -= d;
2893         vEndNew -= d;
2894     }
2895     var epsilon = (this.vEnd - this.vStart) / 1000000;
2896     var movedVertically = (Math.abs(vStartNew - this.vStart) > epsilon ||
2897         Math.abs(vEndNew - this.vEnd) > epsilon);
2898     if (movedVertically) {
2899         this.vStart = vStartNew;
2900         this.vEnd = vEndNew;
2901     }
2902 
2903     if ((!this.redrawWhileMoving ||
2904         Math.abs(this.startAxisLeft + diffX) < this.axisMargin) &&
2905         !movedVertically) {
2906         // move the horizontal axis and data(this is fast)
2907         this.frame.canvas.axis.style.left = links.Graph.px(this.startAxisLeft + diffX);
2908         this.frame.canvas.graph.style.left = links.Graph.px(this.startGraphLeft + diffX);
2909     }
2910     else {
2911         // redraw the horizontal and vertical axis and the data (this is slow)
2912         this._redrawVerticalAxis();
2913 
2914         this.frame.canvas.axis.style.left = links.Graph.px(0);
2915         this.startAxisLeft = -diffX;
2916         this._redrawHorizontalAxis();
2917 
2918         this.frame.canvas.graph.style.left = links.Graph.px(0);
2919         this.startGraphLeft = -diffX - this.axisMargin;
2920         this._redrawData();
2921     }
2922     this._redrawAxisLeftMajorLabel(); // reposition the left major label
2923     this._redrawDataTooltip();
2924 
2925     // fire a rangechange event
2926     var properties = {
2927         'start': new Date(this.start.valueOf()),
2928         'end':   new Date(this.end.valueOf())
2929     };
2930     this.trigger('rangechange', properties);
2931 
2932     links.Graph.preventDefault(event);
2933 };
2934 
2935 
2936 /**
2937  * Perform mouse out, but simulate mouse leave.
2938  * This function force the tooltip to hide when the mouse leaves the frame.
2939  * It is also called (as an event listener) when the graph is dragged and the
2940  * mouse button is released. This way the tooltip can be hidden after a drag.
2941  * @param {Event} event
2942  */
2943 links.Graph.prototype._onMouseOut = function (event) {
2944     event = event || window.event;
2945     var me = this;
2946 
2947     // Do not hide when dragging the graph
2948     if (event.which > 0 && event.type == 'mouseout' ) {
2949         if (!this.onmouseupoutside) {
2950             this.onmouseupoutside = function (event) {me._onMouseOut(event);};
2951             links.Graph.addEventListener(document, "mouseup", this.onmouseupoutside);
2952         }
2953         return;
2954     }
2955 
2956     // Remove event listener when mouse is released outside of graph
2957     if (event.type == 'mouseup') {
2958         if (this.onmouseupoutside) {
2959             links.Graph.removeEventListener(document, "mouseup", this.onmouseupoutside);
2960             this.onmouseupoutside = undefined;
2961         }
2962     }
2963 
2964     if (links.Graph.isOutside(event, this.frame))
2965         this._setTooltip(undefined);
2966 }
2967 
2968 /**
2969  * Perform mouse hover
2970  * @param {Event} event
2971  */
2972 links.Graph.prototype._onMouseHover = function (event) {
2973     event = event || window.event;
2974 
2975     /* TODO: check target
2976      var target = event.target || event.srcElement;
2977     console.log(target == this.frame.canvas)
2978     if (target != this.frame.canvas) {
2979         return;
2980     }*/
2981 
2982     // TODO: handle touch
2983     if (this.leftButtonDown) {
2984         return;
2985     }
2986 
2987     var mouseX = links.Graph._getPageX(event);
2988     var mouseY = links.Graph._getPageY(event);
2989     var offsetX = links.Graph._getAbsoluteLeft(this.frame.canvas);
2990     var offsetY = links.Graph._getAbsoluteTop(this.frame.canvas);
2991 
2992     // calculate the timestamp from the mouse position
2993     var date = this._screenToTime(mouseX - offsetX);
2994     var value = this.screenToY(mouseY - offsetY);
2995 
2996     // find the value closest to the current date
2997     var dataPoint = this._findClosestDataPoint(date, value);
2998     this._setTooltip(dataPoint);
2999 };
3000 
3001 /**
3002  * Stop moving operating.
3003  * This function activated from within the function links.Graph._onMouseDown().
3004  * @param {event}  event   The event
3005  */
3006 links.Graph.prototype._onMouseUp = function (event) {
3007     this.frame.style.cursor = 'auto';
3008     this.leftButtonDown = false;
3009 
3010     this.frame.canvas.axis.style.left = links.Graph.px(0);
3011     this._redrawHorizontalAxis();
3012 
3013     this.frame.canvas.graph.style.left = links.Graph.px(0);
3014     this._redrawData();
3015 
3016     // fire a rangechanged event
3017     var properties = {
3018         'start': new Date(this.start.valueOf()),
3019         'end':   new Date(this.end.valueOf())
3020     };
3021     this.trigger('rangechanged', properties);
3022 
3023     // remove event listeners
3024     if (this.onmousemove) {
3025         links.Graph.removeEventListener(document, "mousemove", this.onmousemove);
3026         this.onmousemove = undefined;
3027     }
3028     if (this.onmouseup) {
3029         links.Graph.removeEventListener(document, "mouseup",   this.onmouseup);
3030         this.onmouseup = undefined;
3031     }
3032     links.Graph.preventDefault(event);
3033 };
3034 
3035 
3036 
3037 /**
3038  * Event handler for touchstart event on mobile devices
3039  */
3040 links.Graph.prototype._onTouchStart = function(event) {
3041     links.Graph.preventDefault(event);
3042 
3043     if (this.touchDown) {
3044         // if already moving, return
3045         return;
3046     }
3047     this.touchDown = true;
3048 
3049     var me = this;
3050     if (!this.ontouchmove) {
3051         this.ontouchmove = function (event) {me._onTouchMove(event);};
3052         links.Graph.addEventListener(document, "touchmove", this.ontouchmove);
3053     }
3054     if (!this.ontouchend) {
3055         this.ontouchend   = function (event) {me._onTouchEnd(event);};
3056         links.Graph.addEventListener(document, "touchend", this.ontouchend);
3057     }
3058 
3059     this._onMouseDown(event);
3060 };
3061 
3062 /**
3063  * Event handler for touchmove event on mobile devices
3064  */
3065 links.Graph.prototype._onTouchMove = function(event) {
3066     links.Graph.preventDefault(event);
3067     this._onMouseMove(event);
3068 };
3069 
3070 /**
3071  * Event handler for touchend event on mobile devices
3072  */
3073 links.Graph.prototype._onTouchEnd = function(event) {
3074     links.Graph.preventDefault(event);
3075     this.touchDown = false;
3076 
3077     if (this.ontouchmove) {
3078         links.Graph.removeEventListener(document, "touchmove", this.ontouchmove);
3079         this.ontouchmove = undefined;
3080     }
3081     if (this.ontouchend) {
3082         links.Graph.removeEventListener(document, "touchend", this.ontouchend);
3083         this.ontouchend = undefined;
3084     }
3085 
3086     this._onMouseUp(event);
3087 };
3088 
3089 
3090 /**
3091  * Event handler for mouse wheel event, used to zoom the graph
3092  * Code from http://adomas.org/javascript-mouse-wheel/
3093  * @param {event}  event   The event
3094  */
3095 links.Graph.prototype._onWheel = function(event) {
3096     event = event || window.event;
3097 
3098     if (!this.zoomable)
3099         return;
3100 
3101     // retrieve delta
3102     var delta = 0;
3103     if (event.wheelDelta) { /* IE/Opera. */
3104         delta = event.wheelDelta/120;
3105     } else if (event.detail) { /* Mozilla case. */
3106         // In Mozilla, sign of delta is different than in IE.
3107         // Also, delta is multiple of 3.
3108         delta = -event.detail/3;
3109     }
3110 
3111     // If delta is nonzero, handle it.
3112     // Basically, delta is now positive if wheel was scrolled up,
3113     // and negative, if wheel was scrolled down.
3114     if (delta) {
3115         // check if frame is not resized (causing a mismatch with the end date)
3116         this._checkSize();
3117 
3118         // perform the zoom action. Delta is normally 1 or -1
3119         var zoomFactor = delta / 5.0;
3120 
3121         if (!event.shiftKey) {
3122             // zoom horizontally
3123             var zoomAroundDate;
3124             var frameLeft = links.Graph._getAbsoluteLeft(this.frame);
3125             if (event.clientX != undefined && frameLeft != undefined ) {
3126                 var x = event.clientX - frameLeft;
3127                 zoomAroundDate = this._screenToTime(x);
3128             }
3129             else {
3130                 zoomAroundDate = undefined;
3131             }
3132             this._zoom(zoomFactor, zoomAroundDate);
3133 
3134             // fire a rangechange event
3135             var properties = {
3136                 'start': new Date(this.start.valueOf()),
3137                 'end': new Date(this.end.valueOf())
3138             };
3139             this.trigger('rangechange', properties);
3140             this.trigger('rangechanged', properties);
3141         }
3142         else {
3143             // zoom vertically
3144             var zoomAroundValue;
3145             var frameTop = links.Graph._getAbsoluteTop(this.frame);
3146             if (event.clientY != undefined && frameTop != undefined ) {
3147                 var y = event.clientY - frameTop;
3148                 zoomAroundValue = this.screenToY(y);
3149             }
3150             else {
3151                 zoomAroundValue = undefined;
3152             }
3153             this._zoomVertical (zoomFactor, zoomAroundValue);
3154 
3155         }
3156     }
3157 
3158     // Prevent default actions caused by mouse wheel.
3159     // That might be ugly, but we handle scrolls somehow
3160     // anyway, so don't bother here..
3161     if (event.preventDefault)
3162         event.preventDefault();
3163     event.returnValue = false;
3164 };
3165 
3166 /**
3167  * Retrieve the absolute left value of a DOM element
3168  * @param {Element} elem        A dom element, for example a div
3169  * @return {number} left        The absolute left position of this element
3170  *                              in the browser page.
3171  */
3172 links.Graph._getAbsoluteLeft = function(elem) {
3173     var doc = document.documentElement;
3174     var body = document.body;
3175 
3176     var left = elem.offsetLeft;
3177     var e = elem.offsetParent;
3178     while (e != null && e != body && e != doc) {
3179         left += e.offsetLeft;
3180         left -= e.scrollLeft;
3181         e = e.offsetParent;
3182     }
3183     return left;
3184 };
3185 
3186 /**
3187  * Retrieve the absolute top value of a DOM element
3188  * @param {Element} elem        A dom element, for example a div
3189  * @return {number} top        The absolute top position of this element
3190  *                              in the browser page.
3191  */
3192 links.Graph._getAbsoluteTop = function(elem) {
3193     var doc = document.documentElement;
3194     var body = document.body;
3195 
3196     var top = elem.offsetTop;
3197     var e = elem.offsetParent;
3198     while (e != null && e != body && e != doc) {
3199         top += e.offsetTop;
3200         top -= e.scrollTop;
3201         e = e.offsetParent;
3202     }
3203     return top;
3204 };
3205 
3206 /**
3207  * Get the absolute, vertical mouse position from an event.
3208  * @param {Event} event
3209  * @return {Number} pageY
3210  */
3211 links.Graph._getPageY = function (event) {
3212     if (('targetTouches' in event) && event.targetTouches.length) {
3213         event = event.targetTouches[0];
3214     }
3215 
3216     if ('pageY' in event) {
3217         return event.pageY;
3218     }
3219 
3220     // calculate pageY from clientY
3221     var clientY = event.clientY;
3222     var doc = document.documentElement;
3223     var body = document.body;
3224     return clientY +
3225         ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
3226         ( doc && doc.clientTop || body && body.clientTop || 0 );
3227 };
3228 
3229 /**
3230  * Get the absolute, horizontal mouse position from an event.
3231  * @param {Event} event
3232  * @return {Number} pageX
3233  */
3234 links.Graph._getPageX = function (event) {
3235     if (('targetTouches' in event) && event.targetTouches.length) {
3236         event = event.targetTouches[0];
3237     }
3238 
3239     if ('pageX' in event) {
3240         return event.pageX;
3241     }
3242 
3243     // calculate pageX from clientX
3244     var clientX = event.clientX;
3245     var doc = document.documentElement;
3246     var body = document.body;
3247     return clientX +
3248         ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
3249         ( doc && doc.clientLeft || body && body.clientLeft || 0 );
3250 };
3251 
3252 /**
3253  * Set a new value for the visible range int the Graph.
3254  * Set start to null to include everything from the earliest date to end.
3255  * Set end to null to include everything from start to the last date.
3256  * Example usage:
3257  *    myGraph.setVisibleChartRange(new Date("2010-08-22"),
3258  *                                 new Date("2010-09-13"));
3259  * @param {Date}   start       The start date for the graph
3260  * @param {Date}   end         The end date for the graph
3261  * @param {Boolean} redrawNow  Optional. If true (default), the graph is
3262  *                             automatically redrawn after the range is changed
3263  */
3264 links.Graph.prototype.setVisibleChartRange = function(start, end, redrawNow) {
3265     var col, cols, rowRange, d;
3266 
3267     // TODO: rewrite this method for the new data format
3268     if (start != null) {
3269         // clone the value
3270         start = new Date(start.valueOf());
3271     } else {
3272         // use earliest date from the data
3273         var startValue = null;  // number
3274         for (col = 0, cols = this.data.length; col < cols; col++) {
3275             rowRange = this.data[col].rowRange;
3276             if (rowRange) {
3277                 d = rowRange.min;
3278 
3279                 if (d != undefined) {
3280                     if (startValue != undefined) {
3281                         startValue = Math.min(startValue, d.valueOf());
3282                     }
3283                     else {
3284                         startValue = d.valueOf();
3285                     }
3286                 }
3287             }
3288         }
3289 
3290         if (startValue != undefined) {
3291             start = new Date(startValue);
3292         }
3293         else {
3294             start = new Date();
3295         }
3296     }
3297 
3298     if (end != null) {
3299         // clone the value
3300         end = new Date(end.valueOf());
3301     } else {
3302         // use lastest date from the data
3303         var endValue = null;
3304         for (col = 0, cols = this.data.length; col < cols; col++) {
3305             rowRange = this.data[col].rowRange;
3306             if (rowRange) {
3307                 d = rowRange.max;
3308 
3309                 if (d != undefined) {
3310                     if (endValue != undefined) {
3311                         endValue = Math.max(endValue, d.valueOf());
3312                     }
3313                     else {
3314                         endValue = d;
3315                     }
3316                 }
3317             }
3318         }
3319 
3320         if (endValue != undefined) {
3321             end = new Date(endValue);
3322         } else {
3323             end = new Date();
3324             end.setDate(this.end.getDate() + 20);
3325         }
3326     }
3327 
3328     // prevent start Date <= end Date
3329     if (end.valueOf() <= start.valueOf()) {
3330         end = new Date(start.valueOf());
3331         end.setDate(end.getDate() + 20);
3332     }
3333 
3334     // apply new start and end
3335     this._applyRange(start, end);
3336 
3337     this._calcConversionFactor();
3338 
3339     if (redrawNow == undefined) {
3340         redrawNow = true;
3341     }
3342     if (redrawNow) {
3343         this.redraw();
3344     }
3345 };
3346 
3347 /**
3348  * Adjust the visible chart range to fit the contents.
3349  */
3350 links.Graph.prototype.setVisibleChartRangeAuto = function() {
3351     this.setVisibleChartRange(undefined, undefined);
3352 };
3353 
3354 /**
3355  * Adjust the visible range such that the current time is located in the center
3356  * of the graph
3357  */
3358 links.Graph.prototype.setVisibleChartRangeNow = function() {
3359     var now = new Date();
3360 
3361     var diff = (this.end.valueOf() - this.start.valueOf());
3362 
3363     var startNew = new Date(now.valueOf() - diff/2);
3364     var endNew = new Date(startNew.valueOf() + diff);
3365     this.setVisibleChartRange(startNew, endNew);
3366 };
3367 
3368 /**
3369  * Retrieve the current visible range in the Graph.
3370  * @return {Object} An object with start and end properties
3371  */
3372 links.Graph.prototype.getVisibleChartRange = function() {
3373     return {
3374         'start': new Date(this.start.valueOf()),
3375         'end': new Date(this.end.valueOf())
3376     };
3377 };
3378 
3379 /**
3380  * Retrieve the current value range (range on the vertical axis)
3381  */
3382 links.Graph.prototype.getValueRange = function() {
3383     return {
3384         'start': this.vStart,
3385         'end': this.vEnd
3386     };
3387 };
3388 
3389 /**
3390  * Set vertical value range
3391  * @param {Number} start        Start of the range. If undefined, start will
3392  *                              be set to match the minimum data value
3393  * @param {Number} end          End of the range. If undefined, end will
3394  *                              be set to match the maximum data value
3395  * @param {Boolean} redrawNow   Optional. If true (default) the graph is
3396  *                              redrawn after the range has been changed
3397  */
3398 links.Graph.prototype.setValueRange = function(start, end, redrawNow) {
3399     this.vStart = start ? Number(start) : undefined;
3400     this.vEnd = end ? Number(end) : undefined;
3401 
3402     if (this.vEnd <= this.vStart) {
3403         this.vEnd = undefined;
3404     }
3405 
3406     if (redrawNow == undefined) {
3407         redrawNow = true;
3408     }
3409     if (redrawNow) {
3410         this.redraw();
3411     }
3412 };
3413 
3414 /**
3415  * Adjust the vertical range to auto fit the contents
3416  */
3417 links.Graph.prototype.setValueRangeAuto = function() {
3418     this.setValueRange(undefined, undefined);
3419 };
3420 
3421 
3422 /** ------------------------------------------------------------------------ **/
3423 
3424 
3425 /**
3426  * Event listener (singleton)
3427  */
3428 links.events = links.events || {
3429     'listeners': [],
3430 
3431     /**
3432      * Find a single listener by its object
3433      * @param {Object} object
3434      * @return {Number} index  -1 when not found
3435      */
3436     'indexOf': function (object) {
3437         var listeners = this.listeners;
3438         for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
3439             var listener = listeners[i];
3440             if (listener && listener.object == object) {
3441                 return i;
3442             }
3443         }
3444         return -1;
3445     },
3446 
3447     /**
3448      * Add an event listener
3449      * @param {Object} object
3450      * @param {String} event       The name of an event, for example 'select'
3451      * @param {function} callback  The callback method, called when the
3452      *                             event takes place
3453      */
3454     'addListener': function (object, event, callback) {
3455         var index = this.indexOf(object);
3456         var listener = this.listeners[index];
3457         if (!listener) {
3458             listener = {
3459                 'object': object,
3460                 'events': {}
3461             };
3462             this.listeners.push(listener);
3463         }
3464 
3465         var callbacks = listener.events[event];
3466         if (!callbacks) {
3467             callbacks = [];
3468             listener.events[event] = callbacks;
3469         }
3470 
3471         // add the callback if it does not yet exist
3472         if (callbacks.indexOf(callback) == -1) {
3473             callbacks.push(callback);
3474         }
3475     },
3476 
3477     /**
3478      * Remove an event listener
3479      * @param {Object} object
3480      * @param {String} event       The name of an event, for example 'select'
3481      * @param {function} callback  The registered callback method
3482      */
3483     'removeListener': function (object, event, callback) {
3484         var index = this.indexOf(object);
3485         var listener = this.listeners[index];
3486         if (listener) {
3487             var callbacks = listener.events[event];
3488             if (callbacks) {
3489                 var index = callbacks.indexOf(callback);
3490                 if (index != -1) {
3491                     callbacks.splice(index, 1);
3492                 }
3493 
3494                 // remove the array when empty
3495                 if (callbacks.length == 0) {
3496                     delete listener.events[event];
3497                 }
3498             }
3499 
3500             // count the number of registered events. remove listener when empty
3501             var count = 0;
3502             var events = listener.events;
3503             for (var event in events) {
3504                 if (events.hasOwnProperty(event)) {
3505                     count++;
3506                 }
3507             }
3508             if (count == 0) {
3509                 delete this.listeners[index];
3510             }
3511         }
3512     },
3513 
3514     /**
3515      * Remove all registered event listeners
3516      */
3517     'removeAllListeners': function () {
3518         this.listeners = [];
3519     },
3520 
3521     /**
3522      * Trigger an event. All registered event handlers will be called
3523      * @param {Object} object
3524      * @param {String} event
3525      * @param {Object} properties (optional)
3526      */
3527     'trigger': function (object, event, properties) {
3528         var index = this.indexOf(object);
3529         var listener = this.listeners[index];
3530         if (listener) {
3531             var callbacks = listener.events[event];
3532             if (callbacks) {
3533                 for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
3534                     callbacks[i](properties);
3535                 }
3536             }
3537         }
3538     }
3539 };
3540 
3541 
3542 /** ------------------------------------------------------------------------ **/
3543 
3544 
3545 /**
3546  * Add and event listener. Works for all browsers
3547  * @param {Element} element    An html element
3548  * @param {string}      action     The action, for example "click",
3549  *                                 without the prefix "on"
3550  * @param {function}    listener   The callback function to be executed
3551  * @param {boolean}     useCapture
3552  */
3553 links.Graph.addEventListener = function (element, action, listener, useCapture) {
3554     if (element.addEventListener) {
3555         if (useCapture == undefined)
3556             useCapture = false;
3557 
3558         if (action == "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
3559             action = "DOMMouseScroll";  // For Firefox
3560         }
3561 
3562         element.addEventListener(action, listener, useCapture);
3563     } else {
3564         element.attachEvent("on" + action, listener);  // IE browsers
3565     }
3566 };
3567 
3568 /**
3569  * Remove an event listener from an element
3570  * @param {Element}  element   An html dom element
3571  * @param {string}       action    The name of the event, for example "mousedown"
3572  * @param {function}     listener  The listener function
3573  * @param {boolean}      useCapture
3574  */
3575 links.Graph.removeEventListener = function(element, action, listener, useCapture) {
3576     if (element.removeEventListener) {
3577         // non-IE browsers
3578         if (useCapture == undefined)
3579             useCapture = false;
3580 
3581         if (action == "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
3582             action = "DOMMouseScroll";  // For Firefox
3583         }
3584 
3585         element.removeEventListener(action, listener, useCapture);
3586     } else {
3587         // IE browsers
3588         element.detachEvent("on" + action, listener);
3589     }
3590 };
3591 
3592 
3593 /**
3594  * Stop event propagation
3595  */
3596 links.Graph.stopPropagation = function (event) {
3597     if (!event)
3598         event = window.event;
3599 
3600     if (event.stopPropagation) {
3601         event.stopPropagation();  // non-IE browsers
3602     }
3603     else {
3604         event.cancelBubble = true;  // IE browsers
3605     }
3606 };
3607 
3608 
3609 /**
3610  * Cancels the event if it is cancelable, without stopping further propagation of the event.
3611  */
3612 links.Graph.preventDefault = function (event) {
3613     if (!event)
3614         event = window.event;
3615 
3616     if (event.preventDefault) {
3617         event.preventDefault();  // non-IE browsers
3618     }
3619     else {
3620         event.returnValue = false;  // IE browsers
3621     }
3622 };
3623 
3624 /**
3625  * Check if an event took place outside a specified parent element.
3626  * @param {Event} event A javascript (mouse) event object
3627  * @param {Element} parent The DOM element to check if event was inside it
3628  * @return {boolean}
3629  */
3630 links.Graph.isOutside = function (event, parent) {
3631     var elem = event.relatedTarget || event.toElement || event.fromElement
3632 
3633     while ( elem && elem !== parent) {
3634         elem = elem.parentNode;
3635     }
3636 
3637     return elem !== parent;
3638 }
3639