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