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