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