1 /**
  2  * @file graph3d.js
  3  *
  4  * @brief
  5  * Graph3d is an interactive google visualization chart to draw data in a
  6  * three dimensional graph. You can freely move and zoom in the graph by
  7  * dragging and scrolling in the window. Graph3d also supports animation.
  8  *
  9  * Graph3d is part of the CHAP Links library.
 10  *
 11  * Graph3d is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and
 12  * Internet Explorer 9+.
 13  *
 14  * @license
 15  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 16  * use this file except in compliance with the License. You may obtain a copy
 17  * of the License at
 18  *
 19  * http://www.apache.org/licenses/LICENSE-2.0
 20  *
 21  * Unless required by applicable law or agreed to in writing, software
 22  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 23  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 24  * License for the specific language governing permissions and limitations under
 25  * the License.
 26  *
 27  * Copyright (C) 2010-2012 Almende B.V.
 28  *
 29  * @author  Jos de Jong, jos@almende.org
 30  * @date    2012-10-24
 31  * @version 1.2
 32  */
 33 
 34 /*
 35  TODO
 36  - add options to add text besides the circles/dots
 37 
 38  - add methods getAnimationIndex, getAnimationCount, setAnimationIndex, setAnimationNext, setAnimationPrev, ...
 39  - add extra examples to the playground
 40  - make default dot color customizable, and also the size, min size and max size of the dots
 41  - calculating size of a dot with style dot-size is not created well.
 42  - problem when animating and there is only one group
 43  - enable gray bottom side of the graph
 44  - add options to customize the color and with of the lines (when style:"line")
 45  - add an option to draw multiple lines in 3d
 46  - add options to draw dots in 3d, with a value represented by a radius or color
 47  - create a function to export as png
 48  window.open(graph.frame.canvas.toDataURL("image/png"));
 49  http://www.nihilogic.dk/labs/canvas2image/
 50  - option to show network: dots connected by a line. The width or color of a line
 51  can represent a value
 52 
 53  BUGS
 54  - when playing, and you change the data, something goes wrong and the animation starts playing 2x, and cannot be stopped
 55  - opera: right aligning the text on the axis does not work
 56 
 57  DOCUMENTATION
 58  http://en.wikipedia.org/wiki/3D_projection
 59 
 60  */
 61 
 62 
 63 /**
 64  * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library,
 65  * "links"
 66  */
 67 if (typeof links === 'undefined') {
 68     links = {};
 69     // important: do not use var, as "var links = {};" will overwrite
 70     //            the existing links variable value with undefined in IE8, IE7.
 71 }
 72 
 73 /**
 74  * @constructor links.Graph3d
 75  * The Graph is a visualization Graphs on a time line
 76  *
 77  * Graph is developed in javascript as a Google Visualization Chart.
 78  *
 79  * @param {Element} container   The DOM element in which the Graph will
 80  *                                  be created. Normally a div element.
 81  */
 82 links.Graph3d = function (container) {
 83     // create variables and set default values
 84     this.containerElement = container;
 85     this.width = "400px";
 86     this.height = "400px";
 87     this.margin = 10; // px
 88     this.defaultXCenter = "55%";
 89     this.defaultYCenter = "50%";
 90 
 91     this.style = links.Graph3d.STYLE.DOT;
 92     this.showPerspective = true;
 93     this.showGrid = true;
 94     this.keepAspectRatio = true;
 95     this.showShadow = false;
 96     this.showGrayBottom = false; // TODO: this does not work correctly
 97     this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a "cube"
 98 
 99     this.animationInterval = 1000; // milliseconds
100     this.animationPreload = false;
101     this.animationAutoPlay = false;
102 
103     this.camera = new links.Graph3d.Camera();
104     this.eye = new links.Point3d(0, 0, -1);  // TODO: set eye.z about 3/4 of the width of the window?
105 
106     this.dataTable = null;  // The original data table
107     this.dataPoints = null; // The table with point objects
108 
109     // the column indexes
110     this.colX = undefined;
111     this.colY = undefined;
112     this.colZ = undefined;
113     this.colValue = undefined;
114     this.colFilter = undefined;
115 
116     this.xMin = 0;
117     this.xStep = undefined; // auto by default
118     this.xMax = 1;
119     this.yMin = 0;
120     this.yStep = undefined; // auto by default
121     this.yMax = 1;
122     this.zMin = 0;
123     this.zStep = undefined; // auto by default
124     this.zMax = 1;
125     this.valueMin = 0;
126     this.valueMax = 1;
127     // TODO: customize axis range
128 
129     // constants
130     this.colorAxis = "#4D4D4D";
131     this.colorGrid = "#D3D3D3";
132     this.colorDot = "#7DC1FF";
133     this.colorDotBorder = "#3267D2";
134 
135     // create a frame and canvas
136     this.create();
137 };
138 
139 /**
140  * @class Camera
141  * The camera is mounted on a (virtual) camera arm. The camera arm can rotate
142  * The camera is always looking in the direction of the origin of the arm.
143  * This way, the camera always rotates around one fixed point, the location
144  * of the camera arm.
145  *
146  * Documentation:
147  *   http://en.wikipedia.org/wiki/3D_projection
148  */
149 links.Graph3d.Camera = function () {
150     this.armLocation = new links.Point3d();
151     this.armRotation = {};
152     this.armRotation.horizontal = 0;
153     this.armRotation.vertical = 0;
154     this.armLength = 1.7;
155 
156     this.cameraLocation = new links.Point3d();
157     this.cameraRotation =  new links.Point3d(Math.PI/2, 0, 0);
158 
159     this.calculateCameraOrientation();
160 };
161 
162 
163 /**
164  * Set the location (origin) of the arm
165  * @param {number} x    Normalized value of x
166  * @param {number} y    Normalized value of y
167  * @param {number} z    Normalized value of z
168  */
169 links.Graph3d.Camera.prototype.setArmLocation = function(x, y, z) {
170     this.armLocation.x = x;
171     this.armLocation.y = y;
172     this.armLocation.z = z;
173 
174     this.calculateCameraOrientation();
175 };
176 
177 /**
178  * Set the rotation of the camera arm
179  * @param {number} horizontal   The horizontal rotation, between 0 and 2*PI.
180  *                              Optional, can be left undefined.
181  * @param {number} vertical     The vertical rotation, between 0 and 0.5*PI
182  *                              if vertical=0.5*PI, the graph is shown from the
183  *                              top. Optional, can be left undefined.
184  */
185 links.Graph3d.Camera.prototype.setArmRotation = function(horizontal, vertical) {
186     if (horizontal !== undefined) {
187         this.armRotation.horizontal = horizontal;
188     }
189 
190     if (vertical !== undefined) {
191         this.armRotation.vertical = vertical;
192         if (this.armRotation.vertical < 0) this.armRotation.vertical = 0;
193         if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI;
194     }
195 
196     if (horizontal !== undefined || vertical !== undefined) {
197         this.calculateCameraOrientation();
198     }
199 };
200 
201 /**
202  * Retrieve the current arm rotation
203  * @return {object}   An object with parameters horizontal and vertical
204  */
205 links.Graph3d.Camera.prototype.getArmRotation = function() {
206     var rot = {};
207     rot.horizontal = this.armRotation.horizontal;
208     rot.vertical = this.armRotation.vertical;
209 
210     return rot;
211 };
212 
213 /**
214  * Set the (normalized) length of the camera arm.
215  * @param {number} length A length between 0.71 and 5.0
216  */
217 links.Graph3d.Camera.prototype.setArmLength = function(length) {
218     if (length === undefined)
219         return;
220 
221     this.armLength = length;
222 
223     // Radius must be larger than the corner of the graph,
224     // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the
225     // graph
226     if (this.armLength < 0.71) this.armLength = 0.71;
227     if (this.armLength > 5.0) this.armLength = 5.0;
228 
229     this.calculateCameraOrientation();
230 };
231 
232 /**
233  * Retrieve the arm length
234  * @return {number} length
235  */
236 links.Graph3d.Camera.prototype.getArmLength = function() {
237     return this.armLength;
238 };
239 
240 /**
241  * Retrieve the camera location
242  * @return {links.Point3d} cameraLocation
243  */
244 links.Graph3d.Camera.prototype.getCameraLocation = function() {
245     return this.cameraLocation;
246 };
247 
248 /**
249  * Retrieve the camera rotation
250  * @return {links.Point3d} cameraRotation
251  */
252 links.Graph3d.Camera.prototype.getCameraRotation = function() {
253     return this.cameraRotation;
254 };
255 
256 /**
257  * Calculate the location and rotation of the camera based on the
258  * position and orientation of the camera arm
259  */
260 links.Graph3d.Camera.prototype.calculateCameraOrientation = function() {
261     // calculate location of the camera
262     this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
263     this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
264     this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical);
265 
266     // calculate rotation of the camera
267     this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical;
268     this.cameraRotation.y = 0.0;
269     this.cameraRotation.z = -this.armRotation.horizontal;
270 };
271 
272 /**
273  * Calculate the scaling values, dependent on the range in x, y, and z direction
274  */
275 links.Graph3d.prototype._setScale = function() {
276     this.scale = new links.Point3d(1 / (this.xMax - this.xMin),
277         1 / (this.yMax - this.yMin),
278         1 / (this.zMax - this.zMin));
279 
280     // keep aspect ration between x and y scale if desired
281     if (this.keepAspectRatio) {
282         if (this.scale.x < this.scale.y) {
283             this.scale.y = this.scale.x;
284         }
285         else {
286             this.scale.x = this.scale.y;
287         }
288     }
289 
290     // scale the vertical axis
291     this.scale.z *= this.verticalRatio;
292     // TODO: can this be automated? verticalRatio?
293 
294     // determine scale for (optional) value
295     this.scale.value = 1 / (this.valueMax - this.valueMin);
296 
297     // position the camera arm
298     var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x;
299     var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y;
300     var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z;
301     this.camera.setArmLocation(xCenter, yCenter, zCenter);
302 };
303 
304 
305 /**
306  * Convert a 3D location to a 2D location on screen
307  * http://en.wikipedia.org/wiki/3D_projection
308  * @param {links.Point3d} point3d   A 3D point with parameters x, y, z
309  * @return {links.Point2d} point2d  A 2D point with parameters x, y
310  */
311 links.Graph3d.prototype._convert3Dto2D = function(point3d)
312 {
313     var translation = this._convertPointToTranslation(point3d);
314     return this._convertTranslationToScreen(translation);
315 };
316 
317 /**
318  * Convert a 3D location its translation seen from the camera
319  * http://en.wikipedia.org/wiki/3D_projection
320  * @param {links.Point3d} point3d      A 3D point with parameters x, y, z
321  * @return {links.Point3d} translation A 3D point with parameters x, y, z This is
322  *                                     the translation of the point, seen from the
323  *                                     camera
324  */
325 links.Graph3d.prototype._convertPointToTranslation = function(point3d)
326 {
327     var ax = point3d.x * this.scale.x,
328         ay = point3d.y * this.scale.y,
329         az = point3d.z * this.scale.z,
330 
331         cx = this.camera.getCameraLocation().x,
332         cy = this.camera.getCameraLocation().y,
333         cz = this.camera.getCameraLocation().z,
334 
335     // calculate angles
336         sinTx = Math.sin(this.camera.getCameraRotation().x),
337         cosTx = Math.cos(this.camera.getCameraRotation().x),
338         sinTy = Math.sin(this.camera.getCameraRotation().y),
339         cosTy = Math.cos(this.camera.getCameraRotation().y),
340         sinTz = Math.sin(this.camera.getCameraRotation().z),
341         cosTz = Math.cos(this.camera.getCameraRotation().z),
342 
343     // calculate translation
344         dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz),
345         dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax-cx)),
346         dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax-cx));
347 
348     return new links.Point3d(dx, dy, dz);
349 };
350 
351 /**
352  * Convert a translation point to a point on the screen
353  * @param {links.Point3d} translation   A 3D point with parameters x, y, z This is
354  *                                      the translation of the point, seen from the
355  *                                      camera
356  * @return {links.Point2d} point2d      A 2D point with parameters x, y
357  */
358 links.Graph3d.prototype._convertTranslationToScreen = function(translation) {
359     var ex = this.eye.x,
360         ey = this.eye.y,
361         ez = this.eye.z,
362         dx = translation.x,
363         dy = translation.y,
364         dz = translation.z;
365 
366     // calculate position on screen from translation
367     var bx;
368     var by;
369     if (this.showPerspective) {
370         bx = (dx - ex) * (ez / dz);
371         by = (dy - ey) * (ez / dz);
372     }
373     else {
374         bx = dx * -(ez / this.camera.getArmLength());
375         by = dy * -(ez / this.camera.getArmLength());
376     }
377 
378     // shift and scale the point to the center of the screen
379     // use the width of the graph to scale both horizontally and vertically.
380     var point2d = new links.Point2d(
381         this.xcenter + bx * this.frame.canvas.clientWidth,
382         this.ycenter - by * this.frame.canvas.clientWidth);
383 
384     return point2d;
385 };
386 
387 /**
388  * Main drawing logic. This is the function that needs to be called
389  * in the html page, to draw the Graph.
390  *
391  * A data table with the events must be provided, and an options table.
392  * @param {google.visualization.DataTable} data The data containing the events
393  *                                              for the Graph.
394  * @param {Object} options A name/value map containing settings for the Graph.
395  */
396 links.Graph3d.prototype.draw = function(data, options) {
397     var cameraPosition = undefined;
398 
399     if (options !== undefined) {
400         // retrieve parameter values
401         if (options.width !== undefined)           this.width = options.width;
402         if (options.height !== undefined)          this.height = options.height;
403 
404         if (options.xCenter !== undefined)         this.defaultXCenter = options.xCenter;
405         if (options.yCenter !== undefined)         this.defaultYCenter = options.yCenter;
406 
407         if (options.style !== undefined) {
408             var styleNumber = this._getStyleNumber(options.style);
409             if (styleNumber !== -1) {
410                 this.style = styleNumber;
411             }
412         }
413         if (options.showGrid !== undefined)          this.showGrid = options.showGrid;
414         if (options.showPerspective !== undefined)   this.showPerspective = options.showPerspective;
415         if (options.showShadow !== undefined)        this.showShadow = options.showShadow;
416         if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls;
417         if (options.keepAspectRatio !== undefined)   this.keepAspectRatio = options.keepAspectRatio;
418         if (options.verticalRatio !== undefined)     this.verticalRatio = options.verticalRatio;
419 
420         if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval;
421         if (options.animationPreload !== undefined)  this.animationPreload = options.animationPreload;
422         if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart;
423 
424         if (options.xMin !== undefined) this.defaultXMin = options.xMin;
425         if (options.xStep !== undefined) this.defaultXStep = options.xStep;
426         if (options.xMax !== undefined) this.defaultXMax = options.xMax;
427         if (options.yMin !== undefined) this.defaultYMin = options.yMin;
428         if (options.yStep !== undefined) this.defaultYStep = options.yStep;
429         if (options.yMax !== undefined) this.defaultYMax = options.yMax;
430         if (options.zMin !== undefined) this.defaultZMin = options.zMin;
431         if (options.zStep !== undefined) this.defaultZStep = options.zStep;
432         if (options.zMax !== undefined) this.defaultZMax = options.zMax;
433         if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin;
434         if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax;
435 
436         if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition;
437     }
438 
439     this._setBackgroundColor(options.backgroundColor);
440 
441     this.setSize(this.width, this.height);
442 
443     if (cameraPosition !== undefined) {
444         this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical);
445         this.camera.setArmLength(cameraPosition.distance);
446     }
447     else {
448         this.camera.setArmRotation(1.0, 0.5);
449         this.camera.setArmLength(1.7);
450     }
451 
452     // draw the Graph
453     this.redraw(data);
454 
455     // start animation when option is true
456     if (this.animationAutoStart && this.dataFilter) {
457         this.animationStart();
458     }
459 
460     // fire the ready event
461     google.visualization.events.trigger(this, 'ready', null);
462 };
463 
464 
465 /**
466  * Set the background styling for the graph
467  * @param {string | Object} backgroundColor
468  */
469 links.Graph3d.prototype._setBackgroundColor = function(backgroundColor) {
470     var fill = "white";
471     var stroke = "gray";
472     var strokeWidth = 1;
473 
474     if (typeof(backgroundColor) === "string") {
475         fill = backgroundColor;
476         stroke = "none";
477         strokeWidth = 0;
478     }
479     else if (typeof(backgroundColor) === "object") {
480         if (backgroundColor.fill !== undefined)        fill = backgroundColor.fill;
481         if (backgroundColor.stroke !== undefined)      stroke = backgroundColor.stroke;
482         if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth;
483     }
484     else if  (backgroundColor === undefined) {
485         // use use defaults
486     }
487     else {
488         throw "Unsupported type of backgroundColor";
489     }
490 
491     this.frame.style.backgroundColor = fill;
492     this.frame.style.borderColor = stroke;
493     this.frame.style.borderWidth = strokeWidth + "px";
494     this.frame.style.borderStyle = "solid";
495 };
496 
497 
498 /// enumerate the available styles
499 links.Graph3d.STYLE = { DOT : 0,
500     DOTLINE : 1,
501     DOTCOLOR: 2,
502     DOTSIZE: 3,
503     LINE: 4,
504     GRID : 5,
505     SURFACE : 6};
506 
507 /**
508  * Retrieve the style index from given styleName
509  * @param styleName    {string} Style name such as "dot", "grid", "dot-line"
510  * @return styleNumber {number} Enumeration value representing the style, or -1
511  *                              when not found
512  */
513 links.Graph3d.prototype._getStyleNumber = function(styleName) {
514     switch (styleName) {
515         case "dot":       return links.Graph3d.STYLE.DOT;
516         case "dot-line":  return links.Graph3d.STYLE.DOTLINE;
517         case "dot-color": return links.Graph3d.STYLE.DOTCOLOR;
518         case "dot-size":  return links.Graph3d.STYLE.DOTSIZE;
519         case "line":      return links.Graph3d.STYLE.LINE;
520         case "grid":      return links.Graph3d.STYLE.GRID;
521         case "surface":   return links.Graph3d.STYLE.SURFACE;
522     }
523 
524     return -1;
525 };
526 
527 /**
528  * Retrieve the style name from given number
529  * @param styleNumber  {number} A style number
530  * @return styleName   {string} the name of this style number, or an empty
531  *                              string when number is out of range.
532  */
533 links.Graph3d.prototype._getStyleName = function(styleNumber) {
534     switch (styleNumber) {
535         case links.Graph3d.STYLE.DOT:     return "dot";
536         case links.Graph3d.STYLE.DOTLINE: return "dot-line";
537         case links.Graph3d.STYLE.DOTCOLOR:return "dot-color";
538         case links.Graph3d.STYLE.DOTSIZE: return "dot-size";
539         case links.Graph3d.STYLE.LINE:    return "line";
540         case links.Graph3d.STYLE.GRID:    return "grid";
541         case links.Graph3d.STYLE.SURFACE: return "surface";
542     }
543 
544     return "";
545 };
546 
547 /**
548  * Determine the indexes of the data columns, based on the given style and data
549  * @param {google.visualization.DataTable} data
550  * @param {number}  style
551  */
552 links.Graph3d.prototype._determineColumnIndexes = function(data, style) {
553     if (this.style === links.Graph3d.STYLE.DOT ||
554         this.style === links.Graph3d.STYLE.DOTLINE ||
555         this.style === links.Graph3d.STYLE.LINE ||
556         this.style === links.Graph3d.STYLE.GRID ||
557         this.style === links.Graph3d.STYLE.SURFACE) {
558         // 3 columns expected, and optionally a 4th with filter values
559         this.colX = 0;
560         this.colY = 1;
561         this.colZ = 2;
562         this.colValue = undefined;
563 
564         if (data.getNumberOfColumns() > 3) {
565             this.colFilter = 3;
566         }
567     }
568     else if (this.style === links.Graph3d.STYLE.DOTCOLOR||
569         this.style === links.Graph3d.STYLE.DOTSIZE) {
570         // 4 columns expected, and optionally a 5th with filter values
571         this.colX = 0;
572         this.colY = 1;
573         this.colZ = 2;
574         this.colValue = 3;
575 
576         if (data.getNumberOfColumns() > 4) {
577             this.colFilter = 4;
578         }
579     }
580     else {
581         throw "Unknown style '" + this.style + "'";
582     }
583 };
584 
585 /**
586  * Initialize the data from the data table. Calculate minimum and maximum values
587  * and column index values
588  * @param {google.visualization.DataTable} data   The data containing the events
589  *                                                for the Graph.
590  * @param {number}         style   Style number
591  */
592 links.Graph3d.prototype._dataInitialize = function (data, style) {
593     if (data === undefined || data.getNumberOfRows === undefined)
594         return;
595 
596     // determine the location of x,y,z,value,filter columns
597     this._determineColumnIndexes(data, style);
598 
599     this.dataTable = data;
600     this.dataFilter = undefined;
601 
602     // check if a filter column is provided
603     if (this.colFilter && data.getNumberOfColumns() >= this.colFilter) {
604         if (this.dataFilter === undefined) {
605             this.dataFilter = new links.Filter(data, this.colFilter, this);
606 
607             var me = this;
608             this.dataFilter.setOnLoadCallback(function() {me.redraw();});
609         }
610     }
611 
612     // calculate minimums and maximums
613     var xRange = data.getColumnRange(this.colX);
614     this.xMin = (this.defaultXMin !== undefined) ? this.defaultXMin : xRange.min;
615     this.xMax = (this.defaultXMax !== undefined) ? this.defaultXMax : xRange.max;
616     if (this.xMax <= this.xMin) this.xMax = this.xMin + 1;
617     this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : (this.xMax-this.xMin)/5;
618 
619     var yRange = data.getColumnRange(this.colY);
620     this.yMin = (this.defaultYMin !== undefined) ? this.defaultYMin : yRange.min;
621     this.yMax = (this.defaultYMax !== undefined) ? this.defaultYMax : yRange.max;
622     if (this.yMax <= this.yMin) this.yMax = this.yMin + 1;
623     this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : (this.yMax-this.yMin)/5;
624 
625     var zRange = data.getColumnRange(this.colZ);
626     this.zMin = (this.defaultZMin !== undefined) ? this.defaultZMin : zRange.min;
627     this.zMax = (this.defaultZMax !== undefined) ? this.defaultZMax : zRange.max;
628     if (this.zMax <= this.zMin) this.zMax = this.zMin + 1;
629     this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5;
630 
631     if (this.colValue !== undefined) {
632         var valueRange = data.getColumnRange(this.colValue);
633         this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min;
634         this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max;
635         if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1;
636     }
637 
638     // set the scale dependent on the ranges.
639     this._setScale();
640 };
641 
642 
643 
644 /**
645  * Filter the data based on the current filter
646  * @param {google.visualization.DataTable} data
647  * @return {Array} dataPoints   Array with point objects which can be drawn on screen
648  */
649 links.Graph3d.prototype._getDataPoints = function (data) {
650     // TODO: store the created matrix dataPoints in the filters instead of reloading each time
651     var start = new Date();
652 
653     var dataPoints = [];
654 
655     var middle = new Date();
656 
657     if (this.style === links.Graph3d.STYLE.GRID ||
658         this.style === links.Graph3d.STYLE.SURFACE) {
659         // copy all values from the google data table to a matrix
660         // the provided values are supposed to form a grid of (x,y) positions
661 
662         // create two lists with all present x and y values
663         var dataX = [];
664         var dataY = [];
665         for (var i = 0; i < data.getNumberOfRows(); i++) {
666             var x = data.getValue(i, this.colX) || 0;
667             var y = data.getValue(i, this.colY) || 0;
668 
669             if (dataX.indexOf(x) === -1) {
670                 dataX.push(x);
671             }
672             if (dataY.indexOf(y) === -1) {
673                 dataY.push(y);
674             }
675         }
676 
677         function sortNumber(a, b) {
678             return a - b;
679         }
680         dataX.sort(sortNumber);
681         dataY.sort(sortNumber);
682 
683         // create a grid, a 2d matrix, with all values.
684         var dataMatrix = [];     // temporary data matrix
685         for (var i = 0; i < data.getNumberOfRows(); i++) {
686             var x = data.getValue(i, this.colX) || 0;
687             var y = data.getValue(i, this.colY) || 0;
688             var z = data.getValue(i, this.colZ) || 0;
689 
690             var xIndex = dataX.indexOf(x);  // TODO: implement Array().indexOf() for Internet Explorer
691             var yIndex = dataY.indexOf(y);
692 
693             if (dataMatrix[xIndex] === undefined) {
694                 dataMatrix[xIndex] = [];
695             }
696 
697             var point3d = new links.Point3d();
698             point3d.x = x;
699             point3d.y = y;
700             point3d.z = z;
701 
702             var obj = {};
703             obj.point = point3d;
704             obj.trans = undefined;
705             obj.screen = undefined;
706 
707             dataMatrix[xIndex][yIndex] = obj;
708 
709             dataPoints.push(obj);
710         }
711 
712         // fill in the pointers to the neigbors.
713         for (var x = 0; x < dataMatrix.length; x++) {
714             for (var y = 0; y < dataMatrix[x].length; y++) {
715                 if (dataMatrix[x][y]) {
716                     dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined;
717                     dataMatrix[x][y].pointTop   = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined;
718                     dataMatrix[x][y].pointCross =
719                         (x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ?
720                             dataMatrix[x+1][y+1] :
721                             undefined;
722                 }
723             }
724         }
725     }
726     else {  // "dot" or "dot-line"
727         // copy all values from the google data table to a list with Point3d objects
728         for (var i = 0; i < data.getNumberOfRows(); i++) {
729             var point = new links.Point3d();
730             point.x = data.getValue(i, this.colX) || 0;
731             point.y = data.getValue(i, this.colY) || 0;
732             point.z = data.getValue(i, this.colZ) || 0;
733 
734             if (this.colValue !== undefined) {
735                 point.value = data.getValue(i, this.colValue) || 0;
736             }
737 
738             var obj = {};
739             obj.point = point;
740             obj.trans = undefined;
741             obj.screen = undefined;
742 
743             dataPoints.push(obj);
744         }
745     }
746 
747     // create a bottom point, used for sorting on depth
748     for (var i = 0; i < dataPoints.length; i++) {
749         var point = dataPoints[i].point;
750         dataPoints[i].bottom = new links.Point3d(point.x, point.y, 0.0);
751     }
752 
753     var end = new Date();
754     //document.title = (end - start) + " " + (end - middle) + " "; // TODO
755 
756     return dataPoints;
757 };
758 
759 
760 
761 
762 /**
763  * Append suffix "px" to provided value x
764  * @param {int}     x  An integer value
765  * @return {string} the string value of x, followed by the suffix "px"
766  */
767 links.Graph3d.px = function(x) {
768     return x + "px";
769 };
770 
771 
772 /**
773  * Create the main frame for the Graph3d.
774  * This function is executed once when a Graph3d object is created. The frame
775  * contains a canvas, and this canvas contains all objects like the axis and
776  * nodes.
777  */
778 links.Graph3d.prototype.create = function () {
779     // remove all elements from the container element.
780     while (this.containerElement.hasChildNodes()) {
781         this.containerElement.removeChild(this.containerElement.firstChild);
782     }
783 
784     this.frame = document.createElement("div");
785     this.frame.style.position = "relative";
786 
787     // create the graph canvas (HTML canvas element)
788     this.frame.canvas = document.createElement( "canvas" );
789     this.frame.canvas.style.position = "relative";
790     this.frame.appendChild(this.frame.canvas);
791     //if (!this.frame.canvas.getContext) {
792     {
793         var noCanvas = document.createElement( "DIV" );
794         noCanvas.style.color = "red";
795         noCanvas.style.fontWeight =  "bold" ;
796         noCanvas.style.padding =  "10px";
797         noCanvas.innerHTML =  "Error: your browser does not support HTML canvas";
798         this.frame.canvas.appendChild(noCanvas);
799     }
800 
801     this.frame.filter = document.createElement( "div" );
802     this.frame.filter.style.position = "absolute";
803     this.frame.filter.style.bottom = "0px";
804     this.frame.filter.style.left = "0px";
805     this.frame.filter.style.width = "100%";
806     this.frame.appendChild(this.frame.filter);
807 
808     // add event listeners to handle moving and zooming the contents
809     var me = this;
810     var onkeydown = function (event) {me._onKeyDown(event);};
811     var onmousedown = function (event) {me._onMouseDown(event);};
812     var ontouchstart = function (event) {me._onTouchStart(event);};
813     var onmousewheel = function (event) {me._onWheel(event);};
814     // TODO: these events are never cleaned up... can give a "memory leakage"
815 
816     links.addEventListener(this.frame.canvas, "keydown", onkeydown);
817     links.addEventListener(this.frame.canvas, "mousedown", onmousedown);
818     links.addEventListener(this.frame.canvas, "touchstart", ontouchstart);
819     links.addEventListener(this.frame.canvas, "mousewheel", onmousewheel);
820 
821     // add the new graph to the container element
822     this.containerElement.appendChild(this.frame);
823 };
824 
825 
826 /**
827  * Set a new size for the graph
828  * @param {string} width   Width in pixels or percentage (for example "800px"
829  *                         or "50%")
830  * @param {string} height  Height in pixels or percentage  (for example "400px"
831  *                         or "30%")
832  */
833 links.Graph3d.prototype.setSize = function(width, height) {
834     this.frame.style.width = width;
835     this.frame.style.height = height;
836 
837     this._resizeCanvas();
838 };
839 
840 /**
841  * Resize the canvas to the current size of the frame
842  */
843 links.Graph3d.prototype._resizeCanvas = function() {
844     this.frame.canvas.style.width = "100%";
845     this.frame.canvas.style.height = "100%";
846 
847     this.frame.canvas.width = this.frame.canvas.clientWidth;
848     this.frame.canvas.height = this.frame.canvas.clientHeight;
849 
850     // adjust with for margin
851     this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + "px";
852 };
853 
854 /**
855  * Start animation
856  */
857 links.Graph3d.prototype.animationStart = function() {
858     if (!this.frame.filter || !this.frame.filter.slider)
859         throw "No animation available";
860 
861     this.frame.filter.slider.play();
862 };
863 
864 
865 /**
866  * Stop animation
867  */
868 links.Graph3d.prototype.animationStop = function() {
869     if (!this.frame.filter || !this.frame.filter.slider)
870         throw "No animation available";
871 
872     this.frame.filter.slider.stop();
873 };
874 
875 
876 /**
877  * Resize the center position based on the current values in this.defaultXCenter
878  * and this.defaultYCenter (which are strings with a percentage or a value
879  * in pixels). The center positions are the variables this.xCenter
880  * and this.yCenter
881  */
882 links.Graph3d.prototype._resizeCenter = function() {
883     // calculate the horizontal center position
884     if (this.defaultXCenter.charAt(this.defaultXCenter.length-1) === "%") {
885         this.xcenter =
886             parseFloat(this.defaultXCenter) / 100 *
887                 this.frame.canvas.clientWidth;
888     }
889     else {
890         this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px
891     }
892 
893     // calculate the vertical center position
894     if (this.defaultYCenter.charAt(this.defaultYCenter.length-1) === "%") {
895         this.ycenter =
896             parseFloat(this.defaultYCenter) / 100 *
897                 (this.frame.canvas.clientHeight - this.frame.filter.clientHeight);
898     }
899     else {
900         this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px
901     }
902 };
903 
904 /**
905  * Set the rotation and distance of the camera
906  * @param {Object} pos   An object with the camera position. The object
907  *                       contains three parameters:
908  *                       - horizontal {number}
909  *                         The horizontal rotation, between 0 and 2*PI.
910  *                         Optional, can be left undefined.
911  *                       - vertical {number}
912  *                         The vertical rotation, between 0 and 0.5*PI
913  *                         if vertical=0.5*PI, the graph is shown from the
914  *                         top. Optional, can be left undefined.
915  *                       - distance {number}
916  *                         The (normalized) distance of the camera to the
917  *                         center of the graph, a value between 0.71 and 5.0.
918  *                         Optional, can be left undefined.
919  */
920 links.Graph3d.prototype.setCameraPosition = function(pos) {
921     if (pos === undefined)
922         return;
923 
924     if (pos.horizontal !== undefined && pos.vertical !== undefined)
925         this.camera.setArmRotation(pos.horizontal, pos.vertical);
926 
927     if (pos.distance !== undefined)
928         this.camera.setArmLength(pos.distance);
929 
930     this.redraw();
931 };
932 
933 
934 /**
935  * Retrieve the current camera rotation
936  * @return {object}   An object with parameters horizontal, vertical, and
937  *                    distance
938  */
939 links.Graph3d.prototype.getCameraPosition = function() {
940     var pos = this.camera.getArmRotation();
941     pos.distance = this.camera.getArmLength();
942     return pos;
943 };
944 
945 /**
946  * Load data into the 3D Graph
947  */
948 links.Graph3d.prototype._readData = function(data) {
949     // read the data
950     this._dataInitialize(data, this.style);
951 
952     if (this.dataFilter) {
953         // apply filtering
954         this.dataPoints = this.dataFilter._getDataPoints();
955     }
956     else {
957         // no filtering. load all data
958         this.dataPoints = this._getDataPoints(this.dataTable);
959     }
960 
961     // draw the filter
962     this._redrawFilter();
963 };
964 
965 
966 /**
967  * Redraw the Graph. This needs to be executed after the start and/or
968  * end time are changed, or when data is added or removed dynamically.
969  * @param {google.visualization.DataTable} data    Optional, new data table
970  */
971 links.Graph3d.prototype.redraw = function(data) {
972     // load the data if needed
973     if (data !== undefined) {
974         this._readData(data);
975     }
976 
977     var start = new Date(); // TODO: cleanup
978     if (this.dataPoints === undefined) {
979         throw "Error: graph data not initialized";
980     }
981 
982     this._resizeCanvas();
983     this._resizeCenter();
984     this._redrawSlider();
985     this._redrawClear();
986     this._redrawAxis();
987 
988     if (this.style === links.Graph3d.STYLE.GRID ||
989         this.style === links.Graph3d.STYLE.SURFACE) {
990         this._redrawDataGrid();
991     }
992     else if (this.style === links.Graph3d.STYLE.LINE) {
993         this._redrawDataLine();
994     }
995     else {
996         // style is DOT, DOTLINE, DOTCOLOR, or DOTSIZE
997         this._redrawDataDot();
998     }
999 
1000     this._redrawInfo();
1001     this._redrawLegend();
1002 
1003     var end = new Date();
1004     //document.title = " " + (end - start) // TODO: cleanup
1005 };
1006 
1007 /**
1008  * Clear the canvas before redrawing
1009  */
1010 links.Graph3d.prototype._redrawClear = function() {
1011     var canvas = this.frame.canvas;
1012     var ctx = canvas.getContext("2d");
1013 
1014     ctx.clearRect(0, 0, canvas.width, canvas.height);
1015 };
1016 
1017 
1018 /**
1019  * Redraw the legend showing the colors
1020  */
1021 links.Graph3d.prototype._redrawLegend = function() {
1022     if (this.style === links.Graph3d.STYLE.DOTCOLOR ||
1023         this.style === links.Graph3d.STYLE.DOTSIZE) {
1024 
1025         var dotSize = this.frame.clientWidth * 0.02;
1026 
1027         if (this.style === links.Graph3d.STYLE.DOTSIZE) {
1028             var widthMin = dotSize / 2; // px
1029             var widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function
1030         }
1031         else {
1032             var widthMin = 20; // px
1033             var widthMax = 20; // px
1034         }
1035 
1036         var height = Math.max(this.frame.clientHeight * 0.25, 100);
1037         var top = this.margin;
1038         var right = this.frame.clientWidth - this.margin;
1039         var left = right - widthMax;
1040         var bottom = top + height;
1041     }
1042 
1043     var canvas = this.frame.canvas;
1044     var ctx = canvas.getContext("2d");
1045     ctx.lineWidth = 1;
1046     ctx.font = "14px arial"; // TODO: put in options
1047 
1048     if (this.style === links.Graph3d.STYLE.DOTCOLOR) {
1049         // draw the color bar
1050         var ymin = 0;
1051         var ymax = height; // Todo: make height customizable
1052         for (var y = ymin; y < ymax; y++) {
1053             var f = (y - ymin) / (ymax - ymin);
1054 
1055             //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function
1056             var hue = f * 240;
1057             var color = this._hsv2rgb(hue, 1, 1);
1058 
1059             ctx.strokeStyle = color;
1060             ctx.beginPath();
1061             ctx.moveTo(left, top + y);
1062             ctx.lineTo(right, top + y);
1063             ctx.stroke();
1064         }
1065 
1066         ctx.strokeStyle =  this.colorAxis;
1067         ctx.strokeRect(left, top, widthMax, height);
1068     }
1069 
1070     if (this.style === links.Graph3d.STYLE.DOTSIZE) {
1071         // draw border around color bar
1072         ctx.strokeStyle =  this.colorAxis;
1073         ctx.fillStyle =  this.colorDot;
1074         ctx.beginPath();
1075         ctx.moveTo(left, top);
1076         ctx.lineTo(right, top);
1077         ctx.lineTo(right - widthMax + widthMin, bottom);
1078         ctx.lineTo(left, bottom);
1079         ctx.closePath();
1080         ctx.fill();
1081         ctx.stroke();
1082     }
1083 
1084     if (this.style === links.Graph3d.STYLE.DOTCOLOR ||
1085         this.style === links.Graph3d.STYLE.DOTSIZE) {
1086         // print values along the color bar
1087         var gridLineLen = 5; // px
1088         var step = new links.StepNumber(this.valueMin, this.valueMax, (this.valueMax-this.valueMin)/5, true);
1089         step.start();
1090         if (step.getCurrent() < this.valueMin) {
1091             step.next();
1092         }
1093         while (!step.end()) {
1094             var y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height;
1095 
1096             ctx.beginPath();
1097             ctx.moveTo(left - gridLineLen, y);
1098             ctx.lineTo(left, y);
1099             ctx.stroke();
1100 
1101             ctx.textAlign = "right";
1102             ctx.textBaseline = "middle";
1103             ctx.fillStyle = this.colorAxis;
1104             ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y);
1105 
1106             step.next();
1107         }
1108 
1109         ctx.textAlign = "right";
1110         ctx.textBaseline = "top";
1111         var label = this.dataTable.getColumnLabel(this.colValue);
1112         ctx.fillText(label, right, bottom + this.margin);
1113     }
1114 };
1115 
1116 /**
1117  * Redraw the filter
1118  */
1119 links.Graph3d.prototype._redrawFilter = function() {
1120     this.frame.filter.innerHTML = "";
1121 
1122     if (this.dataFilter) {
1123         var options = {
1124             'visible': this.showAnimationControls
1125         };
1126         var slider = new links.Slider(this.frame.filter, options);
1127         this.frame.filter.slider = slider;
1128 
1129         // TODO: css here is not nice here...
1130         this.frame.filter.style.padding = "10px";
1131         //this.frame.filter.style.backgroundColor = "#EFEFEF";
1132 
1133         slider.setValues(this.dataFilter.values);
1134         slider.setPlayInterval(this.animationInterval);
1135 
1136         // create an event handler
1137         var me = this;
1138         var onchange = function () {
1139             var index = slider.getIndex();
1140 
1141             me.dataFilter.selectValue(index);
1142             me.dataPoints = me.dataFilter._getDataPoints();
1143 
1144             me.redraw();
1145         };
1146         slider.setOnChangeCallback(onchange);
1147     }
1148     else {
1149         this.frame.filter.slider = undefined;
1150     }
1151 };
1152 
1153 /**
1154  * Redraw the slider
1155  */
1156 links.Graph3d.prototype._redrawSlider = function() {
1157     if ( this.frame.filter.slider !== undefined) {
1158         this.frame.filter.slider.redraw();
1159     }
1160 };
1161 
1162 
1163 /**
1164  * Redraw common information
1165  */
1166 links.Graph3d.prototype._redrawInfo = function() {
1167     if (this.dataFilter) {
1168         var canvas = this.frame.canvas;
1169         var ctx = canvas.getContext("2d");
1170 
1171         ctx.font = "14px arial"; // TODO: put in options
1172         ctx.lineStyle = "gray";
1173         ctx.fillStyle = "gray";
1174         ctx.textAlign = "left";
1175         ctx.textBaseline = "top";
1176 
1177         var x = this.margin;
1178         var y = this.margin;
1179         ctx.fillText(this.dataFilter.getLabel() + ": " + this.dataFilter.getSelectedValue(), x, y);
1180     }
1181 };
1182 
1183 
1184 /**
1185  * Redraw the axis
1186  */
1187 links.Graph3d.prototype._redrawAxis = function() {
1188     var canvas = this.frame.canvas;
1189     var ctx = canvas.getContext("2d");
1190 
1191     // TODO: get the actual rendered style of the containerElement
1192     //ctx.font = this.containerElement.style.font;
1193     ctx.font = 24 / this.camera.getArmLength() + "px arial";
1194 
1195     // calculate the length for the short grid lines
1196     var gridLenX = 0.025 / this.scale.x;
1197     var gridLenY = 0.025 / this.scale.y;
1198     var textMargin = 5 / this.camera.getArmLength(); // px
1199     var armAngle = this.camera.getArmRotation().horizontal;
1200 
1201     // draw x-grid lines
1202     ctx.lineWidth = 1;
1203     var prettyStep = (this.defaultXStep === undefined);
1204     var step = new links.StepNumber(this.xMin, this.xMax, this.xStep, prettyStep);
1205     step.start();
1206     if (step.getCurrent() < this.xMin) {
1207         step.next();
1208     }
1209     while (!step.end()) {
1210         var x = step.getCurrent();
1211 
1212         if (this.showGrid) {
1213             var from = this._convert3Dto2D(new links.Point3d(x, this.yMin, this.zMin));
1214             var to = this._convert3Dto2D(new links.Point3d(x, this.yMax, this.zMin));
1215             ctx.strokeStyle = this.colorGrid;
1216             ctx.beginPath();
1217             ctx.moveTo(from.x, from.y);
1218             ctx.lineTo(to.x, to.y);
1219             ctx.stroke();
1220         }
1221         else {
1222             var from = this._convert3Dto2D(new links.Point3d(x, this.yMin, this.zMin));
1223             var to = this._convert3Dto2D(new links.Point3d(x, this.yMin+gridLenX, this.zMin));
1224             ctx.strokeStyle = this.colorAxis;
1225             ctx.beginPath();
1226             ctx.moveTo(from.x, from.y);
1227             ctx.lineTo(to.x, to.y);
1228             ctx.stroke();
1229 
1230             var from = this._convert3Dto2D(new links.Point3d(x, this.yMax, this.zMin));
1231             var to = this._convert3Dto2D(new links.Point3d(x, this.yMax-gridLenX, this.zMin));
1232             ctx.strokeStyle = this.colorAxis;
1233             ctx.beginPath();
1234             ctx.moveTo(from.x, from.y);
1235             ctx.lineTo(to.x, to.y);
1236             ctx.stroke();
1237         }
1238 
1239         var yText = (Math.cos(armAngle) > 0) ? this.yMin : this.yMax;
1240         var text = this._convert3Dto2D(new links.Point3d(x, yText, this.zMin));
1241         if (Math.cos(armAngle * 2) > 0) {
1242             ctx.textAlign = "center";
1243             ctx.textBaseline = "top";
1244             text.y += textMargin;
1245         }
1246         else if (Math.sin(armAngle * 2) < 0){
1247             ctx.textAlign = "right";
1248             ctx.textBaseline = "middle";
1249         }
1250         else {
1251             ctx.textAlign = "left";
1252             ctx.textBaseline = "middle";
1253         }
1254         ctx.fillStyle = this.colorAxis;
1255         ctx.fillText("  " + step.getCurrent() + "  ", text.x, text.y);
1256 
1257         step.next();
1258     }
1259 
1260     // draw y-grid lines
1261     ctx.lineWidth = 1;
1262     var prettyStep = (this.defaultYStep === undefined);
1263     var step = new links.StepNumber(this.yMin, this.yMax, this.yStep, prettyStep);
1264     step.start();
1265     if (step.getCurrent() < this.yMin) {
1266         step.next();
1267     }
1268     while (!step.end()) {
1269         if (this.showGrid) {
1270             var from = this._convert3Dto2D(new links.Point3d(this.xMin, step.getCurrent(), this.zMin));
1271             var to = this._convert3Dto2D(new links.Point3d(this.xMax, step.getCurrent(), this.zMin));
1272             ctx.strokeStyle = this.colorGrid;
1273             ctx.beginPath();
1274             ctx.moveTo(from.x, from.y);
1275             ctx.lineTo(to.x, to.y);
1276             ctx.stroke();
1277         }
1278         else {
1279             var from = this._convert3Dto2D(new links.Point3d(this.xMin, step.getCurrent(), this.zMin));
1280             var to = this._convert3Dto2D(new links.Point3d(this.xMin+gridLenY, step.getCurrent(), this.zMin));
1281             ctx.strokeStyle = this.colorAxis;
1282             ctx.beginPath();
1283             ctx.moveTo(from.x, from.y);
1284             ctx.lineTo(to.x, to.y);
1285             ctx.stroke();
1286 
1287             var from = this._convert3Dto2D(new links.Point3d(this.xMax, step.getCurrent(), this.zMin));
1288             var to = this._convert3Dto2D(new links.Point3d(this.xMax-gridLenY, step.getCurrent(), this.zMin));
1289             ctx.strokeStyle = this.colorAxis;
1290             ctx.beginPath();
1291             ctx.moveTo(from.x, from.y);
1292             ctx.lineTo(to.x, to.y);
1293             ctx.stroke();
1294         }
1295 
1296         var xText = (Math.sin(armAngle ) > 0) ? this.xMin : this.xMax;
1297         var text = this._convert3Dto2D(new links.Point3d(xText, step.getCurrent(), this.zMin));
1298         if (Math.cos(armAngle * 2) < 0) {
1299             ctx.textAlign = "center";
1300             ctx.textBaseline = "top";
1301             text.y += textMargin;
1302         }
1303         else if (Math.sin(armAngle * 2) > 0){
1304             ctx.textAlign = "right";
1305             ctx.textBaseline = "middle";
1306         }
1307         else {
1308             ctx.textAlign = "left";
1309             ctx.textBaseline = "middle";
1310         }
1311         ctx.fillStyle = this.colorAxis;
1312         ctx.fillText("  " + step.getCurrent() + "  ", text.x, text.y);
1313 
1314         step.next();
1315     }
1316 
1317     // draw z-grid lines and axis
1318     ctx.lineWidth = 1;
1319     var prettyStep = (this.defaultZStep === undefined);
1320     var step = new links.StepNumber(this.zMin, this.zMax, this.zStep, prettyStep);
1321     step.start();
1322     if (step.getCurrent() < this.zMin) {
1323         step.next();
1324     }
1325     var xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
1326     var yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
1327     while (!step.end()) {
1328         // TODO: make z-grid lines really 3d?
1329         var from = this._convert3Dto2D(new links.Point3d(xText, yText, step.getCurrent()));
1330         ctx.strokeStyle = this.colorAxis;
1331         ctx.beginPath();
1332         ctx.moveTo(from.x, from.y);
1333         ctx.lineTo(from.x - textMargin, from.y);
1334         ctx.stroke();
1335 
1336         ctx.textAlign = "right";
1337         ctx.textBaseline = "middle";
1338         ctx.fillStyle = this.colorAxis;
1339         ctx.fillText(step.getCurrent() + " ", from.x - 5, from.y);
1340 
1341         step.next();
1342     }
1343     ctx.lineWidth = 1;
1344     var from = this._convert3Dto2D(new links.Point3d(xText, yText, this.zMin));
1345     var to = this._convert3Dto2D(new links.Point3d(xText, yText, this.zMax));
1346     ctx.strokeStyle = this.colorAxis;
1347     ctx.beginPath();
1348     ctx.moveTo(from.x, from.y);
1349     ctx.lineTo(to.x, to.y);
1350     ctx.stroke();
1351 
1352     // draw x-axis
1353     ctx.lineWidth = 1;
1354     // line at yMin
1355     var xMin2d = this._convert3Dto2D(new links.Point3d(this.xMin, this.yMin, this.zMin));
1356     var xMax2d = this._convert3Dto2D(new links.Point3d(this.xMax, this.yMin, this.zMin));
1357     ctx.strokeStyle = this.colorAxis;
1358     ctx.beginPath();
1359     ctx.moveTo(xMin2d.x, xMin2d.y);
1360     ctx.lineTo(xMax2d.x, xMax2d.y);
1361     ctx.stroke();
1362     // line at ymax
1363     var xMin2d = this._convert3Dto2D(new links.Point3d(this.xMin, this.yMax, this.zMin));
1364     var xMax2d = this._convert3Dto2D(new links.Point3d(this.xMax, this.yMax, this.zMin));
1365     ctx.strokeStyle = this.colorAxis;
1366     ctx.beginPath();
1367     ctx.moveTo(xMin2d.x, xMin2d.y);
1368     ctx.lineTo(xMax2d.x, xMax2d.y);
1369     ctx.stroke();
1370 
1371     // draw y-axis
1372     ctx.lineWidth = 1;
1373     // line at xMin
1374     var from = this._convert3Dto2D(new links.Point3d(this.xMin, this.yMin, this.zMin));
1375     var to = this._convert3Dto2D(new links.Point3d(this.xMin, this.yMax, this.zMin));
1376     ctx.strokeStyle = this.colorAxis;
1377     ctx.beginPath();
1378     ctx.moveTo(from.x, from.y);
1379     ctx.lineTo(to.x, to.y);
1380     ctx.stroke();
1381     // line at xMax
1382     var from = this._convert3Dto2D(new links.Point3d(this.xMax, this.yMin, this.zMin));
1383     var to = this._convert3Dto2D(new links.Point3d(this.xMax, this.yMax, this.zMin));
1384     ctx.strokeStyle = this.colorAxis;
1385     ctx.beginPath();
1386     ctx.moveTo(from.x, from.y);
1387     ctx.lineTo(to.x, to.y);
1388     ctx.stroke();
1389 
1390     // draw x-label
1391     var xLabel = this.dataTable.getColumnLabel(this.colX);
1392     if (xLabel.length > 0) {
1393         var yOffset = 0.1 / this.scale.y;
1394         var xText = (this.xMin + this.xMax) / 2;
1395         var yText = (Math.cos(armAngle) > 0) ? this.yMin - yOffset: this.yMax + yOffset;
1396         var text = this._convert3Dto2D(new links.Point3d(xText, yText, this.zMin));
1397         if (Math.cos(armAngle * 2) > 0) {
1398             ctx.textAlign = "center";
1399             ctx.textBaseline = "top";
1400         }
1401         else if (Math.sin(armAngle * 2) < 0){
1402             ctx.textAlign = "right";
1403             ctx.textBaseline = "middle";
1404         }
1405         else {
1406             ctx.textAlign = "left";
1407             ctx.textBaseline = "middle";
1408         }
1409         ctx.fillStyle = this.colorAxis;
1410         ctx.fillText(xLabel, text.x, text.y);
1411     }
1412 
1413     // draw y-label
1414     var yLabel = this.dataTable.getColumnLabel(this.colY);
1415     if (yLabel.length > 0) {
1416         var xOffset = 0.1 / this.scale.x;
1417         var xText = (Math.sin(armAngle ) > 0) ? this.xMin - xOffset : this.xMax + xOffset;
1418         var yText = (this.yMin + this.yMax) / 2;
1419         var text = this._convert3Dto2D(new links.Point3d(xText, yText, this.zMin));
1420         if (Math.cos(armAngle * 2) < 0) {
1421             ctx.textAlign = "center";
1422             ctx.textBaseline = "top";
1423         }
1424         else if (Math.sin(armAngle * 2) > 0){
1425             ctx.textAlign = "right";
1426             ctx.textBaseline = "middle";
1427         }
1428         else {
1429             ctx.textAlign = "left";
1430             ctx.textBaseline = "middle";
1431         }
1432         ctx.fillStyle = this.colorAxis;
1433         ctx.fillText(yLabel, text.x, text.y);
1434     }
1435 
1436     // draw z-label
1437     var zLabel = this.dataTable.getColumnLabel(this.colZ);
1438     if (zLabel.length > 0) {
1439         var offset = 30;  // pixels.  // TODO: relate to the max width of the values on the z axis?
1440         var xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
1441         var yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
1442         var zText = (this.zMin + this.zMax) / 2;
1443         var text = this._convert3Dto2D(new links.Point3d(xText, yText, zText));
1444         ctx.textAlign = "right";
1445         ctx.textBaseline = "middle";
1446         ctx.fillStyle = this.colorAxis;
1447         ctx.fillText(zLabel, text.x - offset, text.y);
1448     }
1449 };
1450 
1451 /**
1452  * Calculate the color based on the given value.
1453  * @param {number} H   Hue, a value be between 0 and 360
1454  * @param {number} S   Saturation, a value between 0 and 1
1455  * @param {number} V   Value, a value between 0 and 1
1456  */
1457 links.Graph3d.prototype._hsv2rgb = function(H, S, V) {
1458     var R, G, B, C, Hi, X;
1459 
1460     C = V * S;
1461     Hi = Math.floor(H/60);  // hi = 0,1,2,3,4,5
1462     X = C * (1 - Math.abs(((H/60) % 2) - 1));
1463 
1464     switch (Hi) {
1465         case 0: R = C; G = X; B = 0; break;
1466         case 1: R = X; G = C; B = 0; break;
1467         case 2: R = 0; G = C; B = X; break;
1468         case 3: R = 0; G = X; B = C; break;
1469         case 4: R = X; G = 0; B = C; break;
1470         case 5: R = C; G = 0; B = X; break;
1471 
1472         default: R = 0; G = 0; B = 0; break;
1473     }
1474 
1475     return "RGB(" + parseInt(R*255) + "," + parseInt(G*255) + "," + parseInt(B*255) + ")";
1476 };
1477 
1478 
1479 /**
1480  * Draw all datapoints as a grid
1481  * This function can be used when the style is "grid"
1482  */
1483 links.Graph3d.prototype._redrawDataGrid = function() {
1484     var canvas = this.frame.canvas;
1485     var ctx = canvas.getContext("2d");
1486 
1487     if (this.dataPoints === undefined || this.dataPoints.length <= 0)
1488         return; // TODO: throw exception?
1489 
1490     // calculate the translations and screen position of all points
1491     for (var i = 0; i < this.dataPoints.length; i++) {
1492         var trans = this._convertPointToTranslation(this.dataPoints[i].point);
1493         var screen = this._convertTranslationToScreen(trans);
1494 
1495         this.dataPoints[i].trans = trans;
1496         this.dataPoints[i].screen = screen;
1497 
1498         // calculate the translation of the point at the bottom (needed for sorting)
1499         var transbottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
1500         this.dataPoints[i].transbottom = transbottom;
1501     }
1502 
1503     // sort the points on depth of their (x,y) position (not on z)
1504     var sortDepth = function (a, b) {
1505         return a.transbottom.z - b.transbottom.z;
1506     };
1507     this.dataPoints.sort(sortDepth);
1508 
1509     if (this.style === links.Graph3d.STYLE.SURFACE) {
1510         for (var i = 0; i < this.dataPoints.length; i++) {
1511             var me    = this.dataPoints[i];
1512             var right = this.dataPoints[i].pointRight;
1513             var top   = this.dataPoints[i].pointTop;
1514             var cross = this.dataPoints[i].pointCross;
1515 
1516             if (me !== undefined && right !== undefined && top !== undefined && cross !== undefined) {
1517 
1518                 if (this.showGrayBottom || this.showShadow) {
1519                     // calculate the cross product of the two vectors from center
1520                     // to left and right, in order to know whether we are looking at the
1521                     // bottom or at the top side. We can also use the cross product
1522                     // for calculating light intensity
1523                     var aDiff = links.Point3d.subtract(cross.trans, me.trans);
1524                     var bDiff = links.Point3d.subtract(top.trans, right.trans);
1525                     var crossproduct = links.Point3d.crossProduct(aDiff, bDiff);
1526                     var len = crossproduct.length();
1527 
1528                     var topSideVisible = (crossproduct.z > 0);
1529                 }
1530                 else {
1531                     var topSideVisible = true;
1532                 }
1533 
1534                 if (topSideVisible) {
1535                     // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
1536                     var zAvg = (me.point.z + right.point.z + top.point.z + cross.point.z) / 4;
1537                     var h = (1 - (zAvg - this.zMin) * this.scale.z  / this.verticalRatio) * 240;
1538                     var s = 1; // saturation
1539 
1540                     if (this.showShadow) {
1541                         var v = Math.min(1 + (crossproduct.x / len) / 2, 1);  // value. TODO: scale
1542                         var fillStyle = this._hsv2rgb(h, s, v);
1543                         var strokeStyle = this.colorAxis;
1544                         var strokeStyle = fillStyle;
1545                     }
1546                     else  {
1547                         var v = 1;
1548                         var fillStyle = this._hsv2rgb(h, s, v);
1549                         var strokeStyle = this.colorAxis;
1550                     }
1551                 }
1552                 else {
1553                     var fillStyle = "gray";
1554                     var strokeStyle = this.colorAxis;
1555                 }
1556                 var lineWidth = 0.5;
1557                 /*
1558                  // fill two triangles.
1559                  ctx.lineWidth = lineWidth;
1560                  ctx.fillStyle = fillStyle;
1561                  ctx.strokeStyle = fillStyle;
1562 
1563                  // first triangle
1564                  ctx.beginPath();
1565                  ctx.moveTo(me.screen.x, me.screen.y);
1566                  ctx.lineTo(cross.screen.x, cross.screen.y);
1567                  ctx.lineTo(right.screen.x, right.screen.y);
1568                  ctx.closePath();
1569                  ctx.fill();
1570                  ctx.stroke();
1571 
1572                  // second triangle
1573                  ctx.beginPath();
1574                  ctx.moveTo(me.screen.x, me.screen.y);
1575                  ctx.lineTo(cross.screen.x, cross.screen.y);
1576                  ctx.lineTo(top.screen.x, top.screen.y);
1577                  ctx.closePath();
1578                  ctx.fill();
1579                  ctx.stroke();
1580 
1581                  // line around the square
1582                  ctx.strokeStyle = strokeStyle;
1583                  ctx.beginPath();
1584                  ctx.moveTo(me.screen.x, me.screen.y);
1585                  ctx.lineTo(right.screen.x, right.screen.y);
1586                  ctx.lineTo(cross.screen.x, cross.screen.y);
1587                  ctx.lineTo(top.screen.x, top.screen.y);
1588                  ctx.closePath();
1589                  ctx.stroke();
1590                  //*/
1591 
1592                 //* TODO: cleanup
1593                 ctx.lineWidth = lineWidth;
1594                 ctx.fillStyle = fillStyle;
1595                 ctx.strokeStyle = strokeStyle;
1596                 ctx.beginPath();
1597                 ctx.moveTo(me.screen.x, me.screen.y);
1598                 ctx.lineTo(right.screen.x, right.screen.y);
1599                 ctx.lineTo(cross.screen.x, cross.screen.y);
1600                 ctx.lineTo(top.screen.x, top.screen.y);
1601                 ctx.closePath();
1602                 ctx.fill();
1603                 ctx.stroke();
1604                 //*/
1605             }
1606         }
1607     }
1608     else { // grid style
1609         for (var i = 0; i < this.dataPoints.length; i++) {
1610             var me    = this.dataPoints[i];
1611             var right = this.dataPoints[i].pointRight;
1612             var top   = this.dataPoints[i].pointTop;
1613 
1614             if (me !== undefined) {
1615                 if (this.showPerspective) {
1616                     var lineWidth = 2 / -me.trans.z;
1617                 }
1618                 else {
1619                     var lineWidth = 2 * -(this.eye.z / this.camera.getArmLength());
1620                 }
1621             }
1622 
1623             if (me !== undefined && right !== undefined) {
1624                 // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
1625                 var zAvg = (me.point.z + right.point.z) / 2;
1626                 var h = (1 - (zAvg - this.zMin) * this.scale.z  / this.verticalRatio) * 240;
1627 
1628                 ctx.lineWidth = lineWidth;
1629                 ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
1630                 ctx.beginPath();
1631                 ctx.moveTo(me.screen.x, me.screen.y);
1632                 ctx.lineTo(right.screen.x, right.screen.y);
1633                 ctx.stroke();
1634             }
1635 
1636             if (me !== undefined && top !== undefined) {
1637                 // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
1638                 var zAvg = (me.point.z + top.point.z) / 2;
1639                 var h = (1 - (zAvg - this.zMin) * this.scale.z  / this.verticalRatio) * 240;
1640 
1641                 ctx.lineWidth = lineWidth;
1642                 ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
1643                 ctx.beginPath();
1644                 ctx.moveTo(me.screen.x, me.screen.y);
1645                 ctx.lineTo(top.screen.x, top.screen.y);
1646                 ctx.stroke();
1647             }
1648         }
1649     }
1650 };
1651 
1652 
1653 /**
1654  * Draw all datapoints as dots.
1655  * This function can be used when the style is "dot" or "dot-line"
1656  */
1657 links.Graph3d.prototype._redrawDataDot = function() {
1658     var canvas = this.frame.canvas;
1659     var ctx = canvas.getContext("2d");
1660 
1661     if (this.dataPoints === undefined || this.dataPoints.length <= 0)
1662         return;  // TODO: throw exception?
1663 
1664     // calculate the translations of all points
1665     for (var i = 0; i < this.dataPoints.length; i++) {
1666         var trans = this._convertPointToTranslation(this.dataPoints[i].point);
1667         var screen = this._convertTranslationToScreen(trans);
1668 
1669         this.dataPoints[i].trans = trans;
1670         this.dataPoints[i].screen = screen;
1671     }
1672 
1673     // order the translated points by depth
1674     var sortDepth = function (a, b)
1675     {
1676         return a.trans.z - b.trans.z;
1677     };
1678     this.dataPoints.sort(sortDepth);
1679 
1680     // draw the datapoints as colored circles
1681     var dotSize = this.frame.clientWidth * 0.02;  // px
1682     for (var i = 0; i < this.dataPoints.length; i++) {
1683         var point = this.dataPoints[i];
1684 
1685         if (this.style === links.Graph3d.STYLE.DOTLINE) {
1686             // draw a vertical line from the bottom to the graph value
1687             var from = this._convert3Dto2D(new links.Point3d(point.point.x, point.point.y, this.zMin));
1688             ctx.lineWidth = 1;
1689             ctx.strokeStyle = this.colorGrid;
1690             ctx.beginPath();
1691             ctx.moveTo(from.x, from.y);
1692             ctx.lineTo(point.screen.x, point.screen.y);
1693             ctx.stroke();
1694         }
1695 
1696         // calculate radius for the circle
1697         var size;
1698         if (this.style === links.Graph3d.STYLE.DOTSIZE) {
1699             size = dotSize/2 + 2*dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin);
1700         }
1701         else {
1702             size = dotSize;
1703         }
1704 
1705         var radius;
1706         if (this.showPerspective) {
1707             radius = size / -point.trans.z;
1708         }
1709         else {
1710             radius = size * -(this.eye.z / this.camera.getArmLength());
1711         }
1712         if (radius < 0) {
1713             radius = 0;
1714         }
1715 
1716         if (this.style === links.Graph3d.STYLE.DOTCOLOR ) {
1717             // calculate the color based on the value
1718             var hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
1719             var color = this._hsv2rgb(hue, 1, 1);
1720             var borderColor = this._hsv2rgb(hue, 1, 0.8);
1721         }
1722         else if (this.style === links.Graph3d.STYLE.DOTSIZE) {
1723             var color = this.colorDot;
1724             var borderColor = this.colorDotBorder;
1725         }
1726         else {
1727             // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
1728             var hue = (1 - (point.point.z - this.zMin) * this.scale.z  / this.verticalRatio) * 240;
1729             var color = this._hsv2rgb(hue, 1, 1);
1730             var borderColor = this._hsv2rgb(hue, 1, 0.8);
1731         }
1732 
1733         // draw the circle
1734         ctx.lineWidth = 1.0;
1735         ctx.strokeStyle = borderColor;
1736         ctx.fillStyle = color;
1737         ctx.beginPath();
1738         ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true);
1739         ctx.fill();
1740         ctx.stroke();
1741     }
1742 };
1743 
1744 
1745 /**
1746  * Draw a line through all datapoints.
1747  * This function can be used when the style is "line"
1748  */
1749 links.Graph3d.prototype._redrawDataLine = function() {
1750     var canvas = this.frame.canvas;
1751     var ctx = canvas.getContext("2d");
1752 
1753     if (this.dataPoints === undefined || this.dataPoints.length <= 0)
1754         return;  // TODO: throw exception?
1755 
1756     // calculate the translations of all points
1757     for (var i = 0; i < this.dataPoints.length; i++) {
1758         var trans = this._convertPointToTranslation(this.dataPoints[i].point);
1759         var screen = this._convertTranslationToScreen(trans);
1760 
1761         this.dataPoints[i].trans = trans;
1762         this.dataPoints[i].screen = screen;
1763     }
1764 
1765     // start the line
1766     if (this.dataPoints.length > 0) {
1767         var point = this.dataPoints[0];
1768 
1769         ctx.lineWidth = 1;        // TODO: make customizable
1770         ctx.strokeStyle = "blue"; // TODO: make customizable
1771         ctx.beginPath();
1772         ctx.moveTo(point.screen.x, point.screen.y);
1773     }
1774 
1775     // draw the datapoints as colored circles
1776     for (var i = 1; i < this.dataPoints.length; i++) {
1777         var point = this.dataPoints[i];
1778         ctx.lineTo(point.screen.x, point.screen.y);
1779     }
1780 
1781     // finish the line
1782     if (this.dataPoints.length > 0) {
1783         ctx.stroke();
1784     }
1785 };
1786 
1787 /**
1788  * Start a moving operation inside the provided parent element
1789  * @param {Event}       event         The event that occurred (required for
1790  *                                    retrieving the  mouse position)
1791  */
1792 links.Graph3d.prototype._onMouseDown = function(event) {
1793     event = event || window.event;
1794 
1795     // check if mouse is still down (may be up when focus is lost for example
1796     // in an iframe)
1797     if (this.leftButtonDown) {
1798         this._onMouseUp(event);
1799     }
1800 
1801     // only react on left mouse button down
1802     this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
1803     if (!this.leftButtonDown && !this.touchDown) return;
1804 
1805     // get mouse position (different code for IE and all other browsers)
1806     this.startMouseX = event.clientX || event.targetTouches[0].clientX;
1807     this.startMouseY = event.clientY || event.targetTouches[0].clientY;
1808 
1809     this.startStart = new Date(this.start);
1810     this.startEnd = new Date(this.end);
1811     this.startArmRotation = this.camera.getArmRotation();
1812 
1813     this.frame.style.cursor = 'move';
1814 
1815     // add event listeners to handle moving the contents
1816     // we store the function onmousemove and onmouseup in the graph, so we can
1817     // remove the eventlisteners lateron in the function mouseUp()
1818     var me = this;
1819     this.onmousemove = function (event) {me._onMouseMove(event);};
1820     this.onmouseup   = function (event) {me._onMouseUp(event);};
1821     links.addEventListener(document, "mousemove", me.onmousemove);
1822     links.addEventListener(document, "mouseup", me.onmouseup);
1823     links.preventDefault(event);
1824 };
1825 
1826 
1827 /**
1828  * Perform moving operating.
1829  * This function activated from within the funcion links.Graph.mouseDown().
1830  * @param {event}   event  Well, eehh, the event
1831  */
1832 links.Graph3d.prototype._onMouseMove = function (event) {
1833     event = event || window.event;
1834 
1835     // calculate change in mouse position
1836     var diffX = parseFloat(event.clientX || event.targetTouches[0].clientX) - this.startMouseX;
1837     var diffY = parseFloat(event.clientY || event.targetTouches[0].clientY) - this.startMouseY;
1838 
1839     var horizontalNew = this.startArmRotation.horizontal + diffX / 200;
1840     var verticalNew = this.startArmRotation.vertical + diffY / 200;
1841 
1842     var snapAngle = 4; // degrees
1843     var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI);
1844 
1845     // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc...
1846     // the -0.001 is to take care that the vertical axis is always drawn at the left front corner
1847     if (Math.abs(Math.sin(horizontalNew)) < snapValue) {
1848         horizontalNew = Math.round((horizontalNew / Math.PI)) * Math.PI - 0.001;
1849     }
1850     if (Math.abs(Math.cos(horizontalNew)) < snapValue) {
1851         horizontalNew = (Math.round((horizontalNew/ Math.PI - 0.5)) + 0.5) * Math.PI - 0.001;
1852     }
1853 
1854     // snap vertically to nice angles
1855     if (Math.abs(Math.sin(verticalNew)) < snapValue) {
1856         verticalNew = Math.round((verticalNew / Math.PI)) * Math.PI;
1857     }
1858     if (Math.abs(Math.cos(verticalNew)) < snapValue) {
1859         verticalNew = (Math.round((verticalNew/ Math.PI - 0.5)) + 0.5) * Math.PI;
1860     }
1861 
1862     this.camera.setArmRotation(horizontalNew, verticalNew);
1863     this.redraw();
1864 
1865     // fire an oncamerapositionchange event
1866     var parameters = this.getCameraPosition();
1867     google.visualization.events.trigger(this, 'camerapositionchange', parameters);
1868 
1869     links.preventDefault(event);
1870 };
1871 
1872 
1873 /**
1874  * Stop moving operating.
1875  * This function activated from within the funcion links.Graph.mouseDown().
1876  * @param {event}  event   The event
1877  */
1878 links.Graph3d.prototype._onMouseUp = function (event) {
1879     this.frame.style.cursor = 'auto';
1880     this.leftButtonDown = false;
1881 
1882     // remove event listeners here
1883     links.removeEventListener(document, "mousemove", this.onmousemove);
1884     links.removeEventListener(document, "mouseup",   this.onmouseup);
1885     links.preventDefault(event);
1886 };
1887 
1888 /**
1889  * Event handler for touchstart event on mobile devices
1890  */
1891 links.Graph3d.prototype._onTouchStart = function(event) {
1892     this.touchDown = true;
1893 
1894     var me = this;
1895     this.ontouchmove = function (event) {me._onTouchMove(event);};
1896     this.ontouchend   = function (event) {me._onTouchEnd(event);};
1897     links.addEventListener(document, "touchmove", me.ontouchmove);
1898     links.addEventListener(document, "touchend", me.ontouchend);
1899 
1900     this._onMouseDown(event);
1901 };
1902 
1903 /**
1904  * Event handler for touchmove event on mobile devices
1905  */
1906 links.Graph3d.prototype._onTouchMove = function(event) {
1907     this._onMouseMove(event);
1908 };
1909 
1910 /**
1911  * Event handler for touchend event on mobile devices
1912  */
1913 links.Graph3d.prototype._onTouchEnd = function(event) {
1914     this.touchDown = false;
1915 
1916     links.removeEventListener(document, "touchmove", this.ontouchmove);
1917     links.removeEventListener(document, "touchend",   this.ontouchend);
1918 
1919     this._onMouseUp(event);
1920 };
1921 
1922 
1923 /**
1924  * Event handler for mouse wheel event, used to zoom the graph
1925  * Code from http://adomas.org/javascript-mouse-wheel/
1926  * @param {event}  event   The event
1927  */
1928 links.Graph3d.prototype._onWheel = function(event) {
1929     if (!event) /* For IE. */
1930         event = window.event;
1931 
1932     // retrieve delta
1933     var delta = 0;
1934     if (event.wheelDelta) { /* IE/Opera. */
1935         delta = event.wheelDelta/120;
1936     } else if (event.detail) { /* Mozilla case. */
1937         // In Mozilla, sign of delta is different than in IE.
1938         // Also, delta is multiple of 3.
1939         delta = -event.detail/3;
1940     }
1941 
1942     // If delta is nonzero, handle it.
1943     // Basically, delta is now positive if wheel was scrolled up,
1944     // and negative, if wheel was scrolled down.
1945     if (delta) {
1946         var oldLength = this.camera.getArmLength();
1947         var newLength = oldLength * (1 - delta / 10);
1948 
1949         this.camera.setArmLength(newLength);
1950         this.redraw();
1951     }
1952 
1953     // fire an oncamerapositionchange event
1954     var parameters = this.getCameraPosition();
1955     google.visualization.events.trigger(this, 'camerapositionchange', parameters);
1956 
1957     // Prevent default actions caused by mouse wheel.
1958     // That might be ugly, but we handle scrolls somehow
1959     // anyway, so don't bother here..
1960     links.preventDefault(event);
1961 };
1962 
1963 
1964 /**
1965  * @prototype Point3d
1966  * @param {Number} x
1967  * @param {Number} y
1968  * @param {Number} z
1969  */
1970 links.Point3d = function (x, y, z) {
1971     this.x = x !== undefined ? x : 0;
1972     this.y = y !== undefined ? y : 0;
1973     this.z = z !== undefined ? z : 0;
1974 };
1975 
1976 /**
1977  * Subtract the two provided points, returns a-b
1978  * @param {links.Point3d} a
1979  * @param {links.Point3d} b
1980  * @return {links.Point3d} a-b
1981  */
1982 links.Point3d.subtract = function(a, b) {
1983     var sub = new links.Point3d();
1984     sub.x = a.x - b.x;
1985     sub.y = a.y - b.y;
1986     sub.z = a.z - b.z;
1987     return sub;
1988 };
1989 
1990 /**
1991  * Add the two provided points, returns a+b
1992  * @param {links.Point3d} a
1993  * @param {links.Point3d} b
1994  * @return {links.Point3d} a+b
1995  */
1996 links.Point3d.add = function(a, b) {
1997     var sum = new links.Point3d();
1998     sum.x = a.x + b.x;
1999     sum.y = a.y + b.y;
2000     sum.z = a.z + b.z;
2001     return sum;
2002 };
2003 
2004 /**
2005  * Calculate the cross producto of the two provided points, returns axb
2006  * Documentation: http://en.wikipedia.org/wiki/Cross_product
2007  * @param {links.Point3d} a
2008  * @param {links.Point3d} b
2009  * @return {links.Point3d} cross product axb
2010  */
2011 links.Point3d.crossProduct = function(a, b) {
2012     var crossproduct = new links.Point3d();
2013 
2014     crossproduct.x = a.y * b.z - a.z * b.y;
2015     crossproduct.y = a.z * b.x - a.x * b.z;
2016     crossproduct.z = a.x * b.y - a.y * b.x;
2017 
2018     return crossproduct;
2019 };
2020 
2021 
2022 /**
2023  * Rtrieve the length of the vector (or the distance from this point to the origin
2024  * @return {Number}  length
2025  */
2026 links.Point3d.prototype.length = function() {
2027     return Math.sqrt(this.x * this.x +
2028         this.y * this.y +
2029         this.z * this.z);
2030 };
2031 
2032 /**
2033  * @prototype links.Point2d
2034  */
2035 links.Point2d = function (x, y) {
2036     this.x = x !== undefined ? x : 0;
2037     this.y = y !== undefined ? y : 0;
2038 };
2039 
2040 
2041 /**
2042  * @class Filter
2043  *
2044  * @param {google.visualization.DataTable} data The google data table
2045  * @param {number} column                       The index of the column to be filtered
2046  * @param {links.Graph} graph                   The graph
2047  */
2048 links.Filter = function (data, column, graph) {
2049     this.data = data;
2050     this.column = column;
2051     this.graph = graph; // the parent graph
2052 
2053     this.index = undefined;
2054     this.value = undefined;
2055 
2056     // read all distinct values and select the first one
2057     this.values = data.getDistinctValues(this.column);
2058     if (this.values.length) {
2059         this.selectValue(0);
2060     }
2061 
2062     // create an array with the filtered datapoints. this will be loaded afterwards
2063     this.dataPoints = [];
2064 
2065     this.loaded = false;
2066     this.onLoadCallback = undefined;
2067 
2068     if (graph.animationPreload) {
2069         this.loaded = false;
2070         this.loadInBackground();
2071     }
2072     else {
2073         this.loaded = true;
2074     }
2075 };
2076 
2077 
2078 /**
2079  * Return the label
2080  * @return {string} label
2081  */
2082 links.Filter.prototype.isLoaded = function() {
2083     return this.loaded;
2084 };
2085 
2086 
2087 /**
2088  * Return the loaded progress
2089  * @return {number} percentage between 0 and 100
2090  */
2091 links.Filter.prototype.getLoadedProgress = function() {
2092     var len = this.values.length;
2093 
2094     var i = 0;
2095     while (this.dataPoints[i]) {
2096         i++;
2097     }
2098 
2099     return Math.round(i / len * 100);
2100 };
2101 
2102 
2103 /**
2104  * Return the label
2105  * @return {string} label
2106  */
2107 links.Filter.prototype.getLabel = function() {
2108     return this.data.getColumnLabel(this.column);
2109 };
2110 
2111 
2112 /**
2113  * Return the columnIndex of the filter
2114  * @return {number} columnIndex
2115  */
2116 links.Filter.prototype.getColumn = function() {
2117     return this.column;
2118 };
2119 
2120 /**
2121  * Return the currently selected value. Returns undefined if there is no selection
2122  * @return {*} value
2123  */
2124 links.Filter.prototype.getSelectedValue = function() {
2125     if (this.index === undefined)
2126         return undefined;
2127 
2128     return this.values[this.index];
2129 };
2130 
2131 /**
2132  * Retrieve all values of the filter
2133  * @return {Array} values
2134  */
2135 links.Filter.prototype.getValues = function() {
2136     return this.values;
2137 };
2138 
2139 /**
2140  * Retrieve one value of the filter
2141  * @param {number}    index
2142  * @return {*} value
2143  */
2144 links.Filter.prototype.getValue = function(index) {
2145     if (index >= this.values.length)
2146         throw "Error: index out of range";
2147 
2148     return this.values[index];
2149 };
2150 
2151 
2152 /**
2153  * Retrieve the (filtered) dataPoints for the currently selected filter index
2154  * @param {number} index (optional)
2155  * @return {Array} dataPoints
2156  */
2157 links.Filter.prototype._getDataPoints = function(index) {
2158     if (index === undefined)
2159         index = this.index;
2160 
2161     if (index === undefined)
2162         return [];
2163 
2164     var dataPoints;
2165     if (this.dataPoints[index]) {
2166         dataPoints = this.dataPoints[index];
2167     }
2168     else {
2169         var dataView = new google.visualization.DataView(this.data);
2170 
2171         var f = {};
2172         f.column = this.column;
2173         f.value = this.values[index];
2174         var filteredRows = this.data.getFilteredRows([f]);
2175         dataView.setRows(filteredRows);
2176 
2177         dataPoints = this.graph._getDataPoints(dataView);
2178 
2179         this.dataPoints[index] = dataPoints;
2180     }
2181 
2182     return dataPoints;
2183 };
2184 
2185 
2186 
2187 /**
2188  * Set a callback function when the filter is fully loaded.
2189  */
2190 links.Filter.prototype.setOnLoadCallback = function(callback) {
2191     this.onLoadCallback = callback;
2192 };
2193 
2194 
2195 /**
2196  * Add a value to the list with available values for this filter
2197  * No double entries will be created.
2198  * @param {number} index
2199  */
2200 links.Filter.prototype.selectValue = function(index) {
2201     if (index >= this.values.length)
2202         throw "Error: index out of range";
2203 
2204     this.index = index;
2205     this.value = this.values[index];
2206 };
2207 
2208 /**
2209  * Load all filtered rows in the background one by one
2210  * Start this method without providing an index!
2211  */
2212 links.Filter.prototype.loadInBackground = function(index) {
2213     if (index === undefined)
2214         index = 0;
2215 
2216     var frame = this.graph.frame;
2217 
2218     if (index < this.values.length) {
2219         var dataPointsTemp = this._getDataPoints(index);
2220         //this.graph.redrawInfo(); // TODO: not neat
2221 
2222         // create a progress box
2223         if (frame.progress === undefined) {
2224             frame.progress = document.createElement("DIV");
2225             frame.progress.style.position = "absolute";
2226             frame.progress.style.color = "gray";
2227             frame.appendChild(frame.progress);
2228         }
2229         var progress = this.getLoadedProgress();
2230         frame.progress.innerHTML = "Loading animation... " + progress + "%";
2231         // TODO: this is no nice solution...
2232         frame.progress.style.bottom = links.Graph3d.px(60); // TODO: use height of slider
2233         frame.progress.style.left = links.Graph3d.px(10);
2234 
2235         var me = this;
2236         setTimeout(function() {me.loadInBackground(index+1);}, 10);
2237         this.loaded = false;
2238     }
2239     else {
2240         this.loaded = true;
2241 
2242         // remove the progress box
2243         if (frame.progress !== undefined) {
2244             frame.removeChild(frame.progress);
2245             frame.progress = undefined;
2246         }
2247 
2248         if (this.onLoadCallback)
2249             this.onLoadCallback();
2250     }
2251 };
2252 
2253 
2254 
2255 /**
2256  * @prototype links.StepNumber
2257  * The class StepNumber is an iterator for numbers. You provide a start and end
2258  * value, and a best step size. StepNumber itself rounds to fixed values and
2259  * a finds the step that best fits the provided step.
2260  *
2261  * If prettyStep is true, the step size is chosen as close as possible to the
2262  * provided step, but being a round value like 1, 2, 5, 10, 20, 50, ....
2263  *
2264  * Example usage:
2265  *   var step = new links.StepNumber(0, 10, 2.5, true);
2266  *   step.start();
2267  *   while (!step.end()) {
2268  *     alert(step.getCurrent());
2269  *     step.next();
2270  *   }
2271  *
2272  * Version: 1.0
2273  *
2274  * @param {number} start       The start value
2275  * @param {number} end         The end value
2276  * @param {number} step        Optional. Step size. Must be a positive value.
2277  * @param {boolean} prettyStep Optional. If true, the step size is rounded
2278  *                             To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
2279  */
2280 links.StepNumber = function (start, end, step, prettyStep) {
2281     // set default values
2282     this.start_ = 0;
2283     this.end_ = 0;
2284     this.step_ = 1;
2285     this.prettyStep = true;
2286     this.precision = 5;
2287 
2288     this.current_ = 0;
2289     this.setRange(start, end, step, prettyStep);
2290 };
2291 
2292 /**
2293  * Set a new range: start, end and step.
2294  *
2295  * @param {number} start       The start value
2296  * @param {number} end         The end value
2297  * @param {number} step        Optional. Step size. Must be a positive value.
2298  * @param {boolean} prettyStep Optional. If true, the step size is rounded
2299  *                             To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
2300  */
2301 links.StepNumber.prototype.setRange = function(start, end, step, prettyStep) {
2302     this.start_ = start ? start : 0;
2303     this.end_ = end ? end : 0;
2304 
2305     this.setStep(step, prettyStep);
2306 };
2307 
2308 /**
2309  * Set a new step size
2310  * @param {number} step        New step size. Must be a positive value
2311  * @param {boolean} prettyStep Optional. If true, the provided step is rounded
2312  *                             to a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
2313  */
2314 links.StepNumber.prototype.setStep = function(step, prettyStep) {
2315     if (step === undefined || step <= 0)
2316         return;
2317 
2318     if (prettyStep !== undefined)
2319         this.prettyStep = prettyStep;
2320 
2321     if (this.prettyStep === true)
2322         this.step_ = links.StepNumber.calculatePrettyStep(step);
2323     else
2324         this.step_ = step;
2325 };
2326 
2327 /**
2328  * Calculate a nice step size, closest to the desired step size.
2329  * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an
2330  * integer number. For example 1, 2, 5, 10, 20, 50, etc...
2331  * @param {number}  step  Desired step size
2332  * @return {number}       Nice step size
2333  */
2334 links.StepNumber.calculatePrettyStep = function (step) {
2335     var log10 = function (x) {return Math.log(x) / Math.LN10;};
2336 
2337     // try three steps (multiple of 1, 2, or 5
2338     var step1 = 1 * Math.pow(10, Math.round(log10(step / 1))),
2339         step2 = 2 * Math.pow(10, Math.round(log10(step / 2))),
2340         step5 = 5 * Math.pow(10, Math.round(log10(step / 5)));
2341 
2342     // choose the best step (closest to minimum step)
2343     var prettyStep = step1;
2344     if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2;
2345     if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5;
2346 
2347     // for safety
2348     if (prettyStep <= 0) {
2349         prettyStep = 1;
2350     }
2351 
2352     return prettyStep;
2353 };
2354 
2355 /**
2356  * returns the current value of the step
2357  * @return {number} current value
2358  */
2359 links.StepNumber.prototype.getCurrent = function () {
2360     var currentRounded = (this.current_).toPrecision(this.precision);
2361     if (this.current_ < 100000) {
2362         currentRounded *= 1; // remove zeros at the tail, behind the comma
2363     }
2364     return currentRounded;
2365 };
2366 
2367 /**
2368  * returns the current step size
2369  * @return {number} current step size
2370  */
2371 links.StepNumber.prototype.getStep = function () {
2372     return this.step_;
2373 };
2374 
2375 /**
2376  * Set the current value to the largest value smaller than start, which
2377  * is a multiple of the step size
2378  */
2379 links.StepNumber.prototype.start = function() {
2380     this.current_ = this.start_ - this.start_ % this.step_;
2381 
2382     /* TODO: cleanup
2383      if (this.prettyStep)
2384      this.current_ = this.start_ - this.start_ % this.step_;
2385      else
2386      this.current_ = this.start_;
2387      //*/
2388 };
2389 
2390 /**
2391  * Do a step, add the step size to the current value
2392  */
2393 links.StepNumber.prototype.next = function () {
2394     this.current_ += this.step_;
2395 };
2396 
2397 /**
2398  * Returns true whether the end is reached
2399  * @return {boolean}  True if the current value has passed the end value.
2400  */
2401 links.StepNumber.prototype.end = function () {
2402     return (this.current_ > this.end_);
2403 };
2404 
2405 
2406 /**
2407  * @constructor links.Slider
2408  *
2409  * An html slider control with start/stop/prev/next buttons
2410  * @param {Element} container  The element where the slider will be created
2411  * @param {Object} options     Available options:
2412  *                                 {boolean} visible   If true (default) the
2413  *                                                     slider is visible.
2414  */
2415 links.Slider = function(container, options) {
2416     if (container === undefined) {
2417         throw "Error: No container element defined";
2418     }
2419     this.container = container;
2420     this.visible = (options && options.visible != undefined) ? options.visible : true;
2421 
2422     if (this.visible) {
2423         this.frame = document.createElement("DIV");
2424         //this.frame.style.backgroundColor = "#E5E5E5";
2425         this.frame.style.width = "100%";
2426         this.frame.style.position = "relative";
2427         this.container.appendChild(this.frame);
2428 
2429         this.frame.prev = document.createElement("INPUT");
2430         this.frame.prev.type = "BUTTON";
2431         this.frame.prev.value = "Prev";
2432         this.frame.appendChild(this.frame.prev);
2433 
2434         this.frame.play = document.createElement("INPUT");
2435         this.frame.play.type = "BUTTON";
2436         this.frame.play.value = "Play";
2437         this.frame.appendChild(this.frame.play);
2438 
2439         this.frame.next = document.createElement("INPUT");
2440         this.frame.next.type = "BUTTON";
2441         this.frame.next.value = "Next";
2442         this.frame.appendChild(this.frame.next);
2443 
2444         this.frame.bar = document.createElement("INPUT");
2445         this.frame.bar.type = "BUTTON";
2446         this.frame.bar.style.position = "absolute";
2447         this.frame.bar.style.border = "1px solid red";
2448         this.frame.bar.style.width = "100px";
2449         this.frame.bar.style.height = "6px";
2450         this.frame.bar.style.borderRadius = "2px";
2451         this.frame.bar.style.MozBorderRadius = "2px";
2452         this.frame.bar.style.border = "1px solid #7F7F7F";
2453         this.frame.bar.style.backgroundColor = "#E5E5E5";
2454         this.frame.appendChild(this.frame.bar);
2455 
2456         this.frame.slide = document.createElement("INPUT");
2457         this.frame.slide.type = "BUTTON";
2458         this.frame.slide.style.margin = "0px";
2459         this.frame.slide.value = " ";
2460         this.frame.slide.style.position = "relative";
2461         this.frame.slide.style.left = "-100px";
2462         this.frame.appendChild(this.frame.slide);
2463 
2464         // create events
2465         var me = this;
2466         this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);};
2467         this.frame.prev.onclick = function (event) {me.prev(event);};
2468         this.frame.play.onclick = function (event) {me.togglePlay(event);};
2469         this.frame.next.onclick = function (event) {me.next(event);};
2470     }
2471 
2472     this.onChangeCallback = undefined;
2473 
2474     this.values = [];
2475     this.index = undefined;
2476 
2477     this.playTimeout = undefined;
2478     this.playInterval = 1000; // milliseconds
2479     this.playLoop = true;
2480 };
2481 
2482 /**
2483  * Select the previous index
2484  */
2485 links.Slider.prototype.prev = function() {
2486     var index = this.getIndex();
2487     if (index > 0) {
2488         index--;
2489         this.setIndex(index);
2490     }
2491 };
2492 
2493 /**
2494  * Select the next index
2495  */
2496 links.Slider.prototype.next = function() {
2497     var index = this.getIndex();
2498     if (index < this.values.length - 1) {
2499         index++;
2500         this.setIndex(index);
2501     }
2502 };
2503 
2504 /**
2505  * Select the next index
2506  */
2507 links.Slider.prototype.playNext = function() {
2508     var start = new Date();
2509 
2510     var index = this.getIndex();
2511     if (index < this.values.length - 1) {
2512         index++;
2513         this.setIndex(index);
2514     }
2515     else if (this.playLoop) {
2516         // jump to the start
2517         index = 0;
2518         this.setIndex(index);
2519     }
2520 
2521     var end = new Date();
2522     var diff = (end - start);
2523 
2524     // calculate how much time it to to set the index and to execute the callback
2525     // function.
2526     var interval = Math.max(this.playInterval - diff, 0);
2527     // document.title = diff // TODO: cleanup
2528 
2529     var me = this;
2530     this.playTimeout = setTimeout(function() {me.playNext();}, interval);
2531 };
2532 
2533 /**
2534  * Toggle start or stop playing
2535  */
2536 links.Slider.prototype.togglePlay = function() {
2537     if (this.playTimeout === undefined) {
2538         this.play();
2539     } else {
2540         this.stop();
2541     }
2542 };
2543 
2544 /**
2545  * Start playing
2546  */
2547 links.Slider.prototype.play = function() {
2548     this.playNext();
2549 
2550     if (this.frame) {
2551         this.frame.play.value = "Stop";
2552     }
2553 };
2554 
2555 /**
2556  * Stop playing
2557  */
2558 links.Slider.prototype.stop = function() {
2559     clearInterval(this.playTimeout);
2560     this.playTimeout = undefined;
2561 
2562     if (this.frame) {
2563         this.frame.play.value = "Play";
2564     }
2565 };
2566 
2567 /**
2568  * Set a callback function which will be triggered when the value of the
2569  * slider bar has changed.
2570  */
2571 links.Slider.prototype.setOnChangeCallback = function(callback) {
2572     this.onChangeCallback = callback;
2573 };
2574 
2575 /**
2576  * Set the interval for playing the list
2577  * @param {number} interval   The interval in milliseconds
2578  */
2579 links.Slider.prototype.setPlayInterval = function(interval) {
2580     this.playInterval = interval;
2581 };
2582 
2583 /**
2584  * Retrieve the current play interval
2585  * @return {number} interval   The interval in milliseconds
2586  */
2587 links.Slider.prototype.getPlayInterval = function(interval) {
2588     return this.playInterval;
2589 };
2590 
2591 /**
2592  * Set looping on or off
2593  * @pararm {boolean} doLoop    If true, the slider will jump to the start when
2594  *                             the end is passed, and will jump to the end
2595  *                             when the start is passed.
2596  */
2597 links.Slider.prototype.setPlayLoop = function(doLoop) {
2598     this.playLoop = doLoop;
2599 };
2600 
2601 
2602 /**
2603  * Execute the onchange callback function
2604  */
2605 links.Slider.prototype.onChange = function() {
2606     if (this.onChangeCallback !== undefined) {
2607         this.onChangeCallback();
2608     }
2609 };
2610 
2611 /**
2612  * redraw the slider on the correct place
2613  */
2614 links.Slider.prototype.redraw = function() {
2615     if (this.frame) {
2616         // resize the bar
2617         this.frame.bar.style.top = (this.frame.clientHeight/2 -
2618             this.frame.bar.offsetHeight/2) + "px";
2619         this.frame.bar.style.width = (this.frame.clientWidth -
2620             this.frame.prev.clientWidth -
2621             this.frame.play.clientWidth -
2622             this.frame.next.clientWidth - 30)  + "px";
2623 
2624         // position the slider button
2625         var left = this.indexToLeft(this.index);
2626         this.frame.slide.style.left = (left) + "px";
2627     }
2628 };
2629 
2630 
2631 /**
2632  * Set the list with values for the slider
2633  * @param {Array} values   A javascript array with values (any type)
2634  */
2635 links.Slider.prototype.setValues = function(values) {
2636     this.values = values;
2637 
2638     if (this.values.length > 0)
2639         this.setIndex(0);
2640     else
2641         this.index = undefined;
2642 };
2643 
2644 /**
2645  * Select a value by its index
2646  * @param {number} index
2647  */
2648 links.Slider.prototype.setIndex = function(index) {
2649     if (index < this.values.length) {
2650         this.index = index;
2651 
2652         this.redraw();
2653         this.onChange();
2654     }
2655     else {
2656         throw "Error: index out of range";
2657     }
2658 };
2659 
2660 /**
2661  * retrieve the index of the currently selected vaue
2662  * @return {number} index
2663  */
2664 links.Slider.prototype.getIndex = function() {
2665     return this.index;
2666 };
2667 
2668 
2669 /**
2670  * retrieve the currently selected value
2671  * @return {*} value
2672  */
2673 links.Slider.prototype.get = function() {
2674     return this.values[this.index];
2675 };
2676 
2677 
2678 links.Slider.prototype._onMouseDown = function(event) {
2679     // only react on left mouse button down
2680     var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
2681     if (!leftButtonDown) return;
2682 
2683     this.startClientX = event.clientX;
2684     this.startSlideX = parseFloat(this.frame.slide.style.left);
2685 
2686     this.frame.style.cursor = 'move';
2687 
2688     // add event listeners to handle moving the contents
2689     // we store the function onmousemove and onmouseup in the graph, so we can
2690     // remove the eventlisteners lateron in the function mouseUp()
2691     var me = this;
2692     this.onmousemove = function (event) {me._onMouseMove(event);};
2693     this.onmouseup   = function (event) {me._onMouseUp(event);};
2694     links.addEventListener(document, "mousemove", this.onmousemove);
2695     links.addEventListener(document, "mouseup",   this.onmouseup);
2696     links.preventDefault(event);
2697 };
2698 
2699 
2700 links.Slider.prototype.leftToIndex = function (left) {
2701     var width = parseFloat(this.frame.bar.style.width) -
2702         this.frame.slide.clientWidth - 10;
2703     var x = left - 3;
2704 
2705     var index = Math.round(x / width * (this.values.length-1));
2706     if (index < 0) index = 0;
2707     if (index > this.values.length-1) index = this.values.length-1;
2708 
2709     return index;
2710 };
2711 
2712 links.Slider.prototype.indexToLeft = function (index) {
2713     var width = parseFloat(this.frame.bar.style.width) -
2714         this.frame.slide.clientWidth - 10;
2715 
2716     var x = index / (this.values.length-1) * width;
2717     var left = x + 3;
2718 
2719     return left;
2720 };
2721 
2722 
2723 
2724 links.Slider.prototype._onMouseMove = function (event) {
2725     var diff = event.clientX - this.startClientX;
2726     var x = this.startSlideX + diff;
2727 
2728     var index = this.leftToIndex(x);
2729 
2730     this.setIndex(index);
2731 
2732     links.preventDefault();
2733 };
2734 
2735 
2736 links.Slider.prototype._onMouseUp = function (event) {
2737     this.frame.style.cursor = 'auto';
2738 
2739     // remove event listeners
2740     links.removeEventListener(document, "mousemove", this.onmousemove);
2741     links.removeEventListener(document, "mouseup", this.onmouseup);
2742 
2743     links.preventDefault();
2744 };
2745 
2746 
2747 
2748 /**--------------------------------------------------------------------------**/
2749 
2750 
2751 
2752 /**
2753  * Add and event listener. Works for all browsers
2754  * @param {Element}     element    An html element
2755  * @param {string}      action     The action, for example "click",
2756  *                                 without the prefix "on"
2757  * @param {function}    listener   The callback function to be executed
2758  * @param {boolean}     useCapture
2759  */
2760 links.addEventListener = function (element, action, listener, useCapture) {
2761     if (element.addEventListener) {
2762         if (useCapture === undefined)
2763             useCapture = false;
2764 
2765         if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
2766             action = "DOMMouseScroll";  // For Firefox
2767         }
2768 
2769         element.addEventListener(action, listener, useCapture);
2770     } else {
2771         element.attachEvent("on" + action, listener);  // IE browsers
2772     }
2773 };
2774 
2775 /**
2776  * Remove an event listener from an element
2777  * @param {Element}      element   An html dom element
2778  * @param {string}       action    The name of the event, for example "mousedown"
2779  * @param {function}     listener  The listener function
2780  * @param {boolean}      useCapture
2781  */
2782 links.removeEventListener = function(element, action, listener, useCapture) {
2783     if (element.removeEventListener) {
2784         // non-IE browsers
2785         if (useCapture === undefined)
2786             useCapture = false;
2787 
2788         if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
2789             action = "DOMMouseScroll";  // For Firefox
2790         }
2791 
2792         element.removeEventListener(action, listener, useCapture);
2793     } else {
2794         // IE browsers
2795         element.detachEvent("on" + action, listener);
2796     }
2797 };
2798 
2799 /**
2800  * Stop event propagation
2801  */
2802 links.stopPropagation = function (event) {
2803     if (!event)
2804         event = window.event;
2805 
2806     if (event.stopPropagation) {
2807         event.stopPropagation();  // non-IE browsers
2808     }
2809     else {
2810         event.cancelBubble = true;  // IE browsers
2811     }
2812 };
2813 
2814 
2815 /**
2816  * Cancels the event if it is cancelable, without stopping further propagation of the event.
2817  */
2818 links.preventDefault = function (event) {
2819     if (!event)
2820         event = window.event;
2821 
2822     if (event.preventDefault) {
2823         event.preventDefault();  // non-IE browsers
2824     }
2825     else {
2826         event.returnValue = false;  // IE browsers
2827     }
2828 };
2829 
2830 /**
2831  * Retrieve the absolute left value of a DOM element
2832  * @param {Element} elem    A dom element, for example a div
2833  * @return {number} left        The absolute left position of this element
2834  *                              in the browser page.
2835  */
2836 links.getAbsoluteLeft = function(elem) {
2837     var left = 0;
2838     while( elem !== null ) {
2839         left += elem.offsetLeft;
2840         left -= elem.scrollLeft;
2841         elem = elem.offsetParent;
2842     }
2843     return left;
2844 };
2845 
2846 /**
2847  * Retrieve the absolute top value of a DOM element
2848  * @param {Element} elem    A dom element, for example a div
2849  * @return {number} top         The absolute top position of this element
2850  *                              in the browser page.
2851  */
2852 links.getAbsoluteTop = function(elem) {
2853     var top = 0;
2854     while( elem !== null ) {
2855         top += elem.offsetTop;
2856         top -= elem.scrollTop;
2857         elem = elem.offsetParent;
2858     }
2859     return top;
2860 };
2861 
2862