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