1 define([ 2 'jquery', 3 'underscore', 4 'three', 5 'shapes', 6 'draw', 7 'multi-model', 8 'util' 9 ], function($, _, THREE, shapes, draw, multiModel, util) { 10 var makeArrow = draw.makeArrow; 11 var makeLineCollection = draw.makeLineCollection; 12 /** 13 * 14 * @class DecompositionView 15 * 16 * Contains all the information on how the model is being presented to the 17 * user. 18 * 19 * @param {MultiModel} multiModel - A multi model object with all models 20 * @param {string} modelKey - The key referencing the target model 21 * within the multiModel 22 * 23 * @return {DecompositionView} 24 * @constructs DecompositionView 25 * 26 */ 27 function DecompositionView(multiModel, modelKey, uiState) { 28 /** 29 * The decomposition model that the view represents. 30 * @type {DecompositionModel} 31 */ 32 this.decomp = multiModel.models[modelKey]; 33 34 /** 35 * All models in the current scene and global metrics about them 36 * @type {MultiModel} 37 */ 38 this.allModels = multiModel; 39 40 /** 41 * Number of samples represented in the view. 42 * @type {integer} 43 */ 44 this.count = this.decomp.length; 45 /** 46 * Top visible dimensions 47 * @type {integer[]} 48 */ 49 // make sure we only use at most 3 elements for scatter and arrow plots 50 this.visibleDimensions = _.range(this.decomp.dimensions).slice(0, 3); 51 /** 52 * Orientation of the axes, `-1` means the axis is flipped, `1` means the 53 * axis is not flipped. 54 * @type {integer[]} 55 */ 56 this.axesOrientation = _.map(this.visibleDimensions, function() { 57 // by default values are not flipped i.e. all elements are equal to 1 58 return 1; 59 }); 60 61 /** 62 * Axes color. 63 * @type {integer} 64 * @default '#FFFFFF' (white) 65 */ 66 this.axesColor = '#FFFFFF'; 67 /** 68 * Background color. 69 * @type {integer} 70 * @default '#000000' (black) 71 */ 72 this.backgroundColor = '#000000'; 73 /** 74 * Static tubes objects covering an entire trajectory. 75 * Can use setDrawRange on the underlying geometry to display 76 * just part of the trajectory. 77 * @type {THREE.Mesh[]} 78 */ 79 this.staticTubes = []; 80 /** 81 * Dynamic tubes covering the final tube segment of a trajectory 82 * Must be rebuilt each frame by the animations controller 83 * @type {THREE.Mesh[]} 84 */ 85 this.dynamicTubes = []; 86 /** 87 * Array of THREE.Mesh objects on screen (represent samples). 88 * @type {THREE.Mesh[]} 89 */ 90 this.markers = []; 91 92 /** 93 * Meshes to be swapped out of scene when markers are modified. 94 * @type {THREE.Mesh[]} 95 */ 96 this.oldMarkers = []; 97 98 /** 99 * Flag indicating old markers must be removed from the scene tree. 100 * @type {boolean} 101 */ 102 this.needsSwapMarkers = false; 103 104 /** 105 * Array of THREE.Mesh objects on screen (represent confidence intervals). 106 * @type {THREE.Mesh[]} 107 */ 108 this.ellipsoids = []; 109 /** 110 * Object with THREE.LineSegments for the procrustes edges. Has a left and 111 * a right attribute. 112 * @type {Object} 113 */ 114 this.lines = {'left': null, 'right': null}; 115 116 /** 117 * The shared state for the UI 118 * @type {UIState} 119 */ 120 this.UIState = uiState; 121 122 //Register property changes 123 //Note that declaring var scope at the local scope is absolutely critical 124 //or callbacks will call into the wrong scope! 125 var scope = this; 126 this.UIState.registerProperty('view.viewType', function(evt) { 127 scope._initGeometry(); 128 }); 129 } 130 131 DecompositionView.prototype._initGeometry = function() { 132 this.oldMarkers = this.markers; 133 if (this.oldMarkers.length > 0) 134 this.needsSwapMarkers = true; 135 this.markers = []; 136 137 //TODO FIXME HACK: Do we need to swap lines as well? 138 this.lines = {'left': null, 'right': null}; 139 140 if (this.decomp.isScatterType() && 141 (this.UIState['view.viewType'] === 'parallel-plot')) { 142 this._fastInitParallelPlot(); 143 } 144 else if (this.UIState['view.usesPointCloud']) { 145 this._fastInit(); 146 } 147 else { 148 this._initBaseView(); 149 } 150 this.needsUpdate = true; 151 }; 152 153 /** 154 * Calculate the appropriate size for a geometry based on the first dimension's 155 * range. 156 */ 157 DecompositionView.prototype.getGeometryFactor = function() { 158 // this is a heuristic tested on numerous plots since 2013, based off of 159 // the old implementation of emperor. We select the dimensions of all the 160 // geometries based on this factor. 161 return (this.decomp.dimensionRanges.max[0] - 162 this.decomp.dimensionRanges.min[0]) * 0.012; 163 }; 164 165 /** 166 * Retrieve a shallow copy of concatenated static and dynamic tube arrays 167 * @type {THREE.Mesh[]} 168 */ 169 DecompositionView.prototype.getTubes = function() { 170 return this.staticTubes.concat(this.dynamicTubes); 171 }; 172 173 /** 174 * 175 * Helper method to initialize the base THREE.js objects. 176 * @private 177 * 178 */ 179 DecompositionView.prototype._initBaseView = function() { 180 var mesh, x = this.visibleDimensions[0], y = this.visibleDimensions[1], 181 z = this.visibleDimensions[2]; 182 var scope = this; 183 184 // get the correctly sized geometry 185 var radius = this.getGeometryFactor(), hasConfidenceIntervals; 186 var geometry = shapes.getGeometry('Sphere', radius); 187 188 hasConfidenceIntervals = this.decomp.hasConfidenceIntervals(); 189 190 if (this.decomp.isScatterType()) { 191 this.decomp.apply(function(plottable) { 192 mesh = new THREE.Mesh(geometry, new THREE.MeshPhongMaterial()); 193 mesh.name = plottable.name; 194 195 mesh.material.color = new THREE.Color(0xff0000); 196 mesh.material.transparent = false; 197 mesh.material.depthWrite = true; 198 mesh.material.opacity = 1; 199 mesh.matrixAutoUpdate = true; 200 201 mesh.position.set(plottable.coordinates[x], plottable.coordinates[y], 202 plottable.coordinates[z] || 0); 203 204 mesh.userData.shape = 'Sphere'; 205 206 scope.markers.push(mesh); 207 208 if (hasConfidenceIntervals) { 209 // copy the current sphere and make it an ellipsoid 210 mesh = mesh.clone(); 211 212 mesh.name = plottable.name + '_ci'; 213 mesh.material.transparent = true; 214 mesh.material.opacity = 0.5; 215 216 mesh.scale.set(plottable.ci[x] / geometry.parameters.radius, 217 plottable.ci[y] / geometry.parameters.radius, 218 plottable.ci[z] / geometry.parameters.radius); 219 220 scope.ellipsoids.push(mesh); 221 } 222 }); 223 } 224 else if (this.decomp.isArrowType()) { 225 var arrow, zero = [0, 0, 0], point; 226 227 this.decomp.apply(function(plottable) { 228 point = [plottable.coordinates[x], 229 plottable.coordinates[y], 230 plottable.coordinates[z] || 0]; 231 arrow = makeArrow(zero, point, 0xc0c0c0, plottable.name); 232 233 scope.markers.push(arrow); 234 }); 235 } 236 else { 237 throw new Error('Unsupported decomposition type'); 238 } 239 240 if (this.decomp.edges.length) { 241 var left, center, right, u, v, verticesLeft = [], verticesRight = []; 242 this.decomp.edges.forEach(function(edge) { 243 u = edge[0]; 244 v = edge[1]; 245 246 // remember x, y and z 247 center = [(u.coordinates[x] + v.coordinates[x]) / 2, 248 (u.coordinates[y] + v.coordinates[y]) / 2, 249 ((u.coordinates[z] + v.coordinates[z]) / 2) || 0]; 250 251 left = [u.coordinates[x], u.coordinates[y], u.coordinates[z] || 0]; 252 right = [v.coordinates[x], v.coordinates[y], v.coordinates[z] || 0]; 253 254 verticesLeft.push(left, center); 255 verticesRight.push(right, center); 256 }); 257 258 this.lines.left = makeLineCollection(verticesLeft, 0xffffff); 259 this.lines.right = makeLineCollection(verticesRight, 0xff0000); 260 } 261 }; 262 263 DecompositionView.prototype._fastInit = function() { 264 if (this.decomp.hasConfidenceIntervals()) { 265 throw new Error('Ellipsoids are not supported in fast mode'); 266 } 267 if (this.decomp.isArrowType()) { 268 throw new Error('Only scatter type is supported in fast mode'); 269 } 270 271 var positions, colors, scales, opacities, visibilities, emissives, geometry, 272 cloud; 273 274 var x = this.visibleDimensions[0], y = this.visibleDimensions[1], 275 z = this.visibleDimensions[2]; 276 277 /** 278 * In order to draw large numbers of samples we can't use full-blown 279 * geometries like spheres. Instead we will use shaders to draw each sample 280 * as a circle. Note that since these are programs that need to be compiled 281 * for the GPU, they need to be stored as strings. 282 * 283 * The "vertexShader" determines the location and size of each vertex in the 284 * geometry. And the "fragmentShader" determines the shape, opacity, 285 * visibility and color. In addition there's some logic to smooth the circles 286 * and add antialiasing. 287 * 288 * The source for the shaders was inspired and or modified from: 289 * 290 * https://www.desultoryquest.com/blog/drawing-anti-aliased-circular-points-using-opengl-slash-webgl/ 291 * http://jsfiddle.net/callum/x7y72k1e/10/ 292 * http://math.hws.edu/eck/cs424/s12/lab4/lab4-files/points.html 293 * https://stackoverflow.com/q/33695202/379593 294 * 295 */ 296 var vertexShader = [ 297 'attribute float scale;', 298 299 'attribute vec3 color;', 300 'attribute float opacity;', 301 'attribute float visible;', 302 'attribute float emissive;', 303 304 'varying vec3 vColor;', 305 'varying float vOpacity;', 306 'varying float vVisible;', 307 'varying float vEmissive;', 308 309 'void main() {', 310 'vColor = color;', 311 'vOpacity = opacity;', 312 'vVisible = visible;', 313 'vEmissive = emissive;', 314 315 'vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);', 316 'gl_Position = projectionMatrix * mvPosition; ', 317 'gl_PointSize = kSIZE * scale * (800.0 / length(mvPosition.xyz));', 318 '}'].join('\n'); 319 320 var fragmentShader = [ 321 'precision mediump float;', 322 'varying vec3 vColor;', 323 'varying float vOpacity;', 324 'varying float vVisible;', 325 'varying float vEmissive;', 326 327 'void main() {', 328 // remove objects when they might be "visible" but completely transparent 329 'if (vVisible > 0.0 && vOpacity > 0.0) {', 330 'vec2 cxy = 2.0 * gl_PointCoord - 1.0;', 331 'float delta = 0.0, alpha = 1.0, r = dot(cxy, cxy);', 332 333 // get rid of the frame around the points 334 'if(r > 1.1) discard;', 335 336 // antialiasing smoothing 337 'delta = fwidth(r);', 338 'alpha = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, r);', 339 340 // if the object is selected make it white 341 'if (vEmissive > 0.0) {', 342 ' gl_FragColor = vec4(1, 1, 1, vOpacity) * alpha;', 343 '}', 344 'else {', 345 ' gl_FragColor = vec4(vColor, vOpacity) * alpha;', 346 '}', 347 '}', 348 'else {', 349 'discard;', 350 '}', 351 '}'].join('\n'); 352 353 positions = new Float32Array(this.decomp.length * 3); 354 colors = new Float32Array(this.decomp.length * 3); 355 scales = new Float32Array(this.decomp.length); 356 opacities = new Float32Array(this.decomp.length); 357 visibilities = new Float32Array(this.decomp.length); 358 emissives = new Float32Array(this.decomp.length); 359 360 var material = new THREE.ShaderMaterial({ 361 vertexShader: vertexShader, 362 fragmentShader: fragmentShader, 363 transparent: true 364 }); 365 366 // we need to define a baseline size for markers so we can control the scale 367 material.defines.kSIZE = this.getGeometryFactor(); 368 369 // needed for the shader's smoothstep and fwidth functions 370 material.extensions.derivatives = true; 371 372 geometry = new THREE.BufferGeometry(); 373 geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); 374 geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); 375 geometry.setAttribute('scale', new THREE.BufferAttribute(scales, 1)); 376 geometry.setAttribute('opacity', new THREE.BufferAttribute(opacities, 1)); 377 geometry.setAttribute('visible', new THREE.BufferAttribute(visibilities, 1)); 378 geometry.setAttribute('emissive', new THREE.BufferAttribute(emissives, 1)); 379 380 cloud = new THREE.Points(geometry, material); 381 382 this.decomp.apply(function(plottable) { 383 geometry.attributes.position.setXYZ(plottable.idx, 384 plottable.coordinates[x], 385 plottable.coordinates[y], 386 plottable.coordinates[z] || 0); 387 388 // set default to red, visible, full opacity and of scale 1 389 geometry.attributes.color.setXYZ(plottable.idx, 1, 0, 0); 390 geometry.attributes.visible.setX(plottable.idx, 1); 391 geometry.attributes.opacity.setX(plottable.idx, 1); 392 geometry.attributes.emissive.setX(plottable.idx, 0); 393 geometry.attributes.scale.setX(plottable.idx, 1); 394 }); 395 396 geometry.attributes.position.needsUpdate = true; 397 geometry.attributes.color.needsUpdate = true; 398 geometry.attributes.visible.needsUpdate = true; 399 geometry.attributes.opacity.needsUpdate = true; 400 geometry.attributes.scale.needsUpdate = true; 401 geometry.attributes.emissive.needsUpdate = true; 402 403 this.markers.push(cloud); 404 }; 405 406 /** 407 * Parallel plots closely mirroring the shader enabled _fastInit calls 408 */ 409 DecompositionView.prototype._fastInitParallelPlot = function() 410 { 411 var positions, colors, opacities, visibilities, geometry, cloud; 412 413 // We're really just drawing a bunch of line strips... 414 // highly doubt shaders are necessary for this... 415 var vertexShader = [ 416 'attribute vec3 color;', 417 'attribute float opacity;', 418 'attribute float visible;', 419 'attribute float emissive;', 420 421 'varying vec3 vColor;', 422 'varying float vOpacity;', 423 'varying float vVisible;', 424 'varying float vEmissive;', 425 426 'void main() {', 427 ' vColor = color;', 428 ' vOpacity = opacity;', 429 ' vVisible = visible;', 430 ' vEmissive = emissive;', 431 432 ' gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);', 433 '}'].join('\n'); 434 435 var fragmentShader = [ 436 'precision mediump float;', 437 'varying vec3 vColor;', 438 'varying float vOpacity;', 439 'varying float vVisible;', 440 'varying float vEmissive;', 441 442 'void main() {', 443 ' if (vVisible <= 0.0 || vOpacity <= 0.0)', 444 ' discard;', 445 446 // if the object is selected make it white 447 ' if (vEmissive > 0.0) {', 448 ' gl_FragColor = vec4(1, 1, 1, vOpacity);', 449 ' }', 450 ' else {', 451 ' gl_FragColor = vec4(vColor, vOpacity);', 452 ' }', 453 '}'].join('\n'); 454 455 var allDimensions = _.range(this.decomp.dimensions); 456 457 // We'll build the line strips as GL_LINES for simplicity, at least for now, 458 // by doubling up vertex positions at each of the intermediate axes. 459 var numPoints = (allDimensions.length * 2 - 2) * (this.decomp.length); 460 positions = new Float32Array(numPoints * 3); 461 colors = new Float32Array(numPoints * 3); 462 opacities = new Float32Array(numPoints); 463 visibilities = new Float32Array(numPoints); 464 emissives = new Float32Array(numPoints); 465 466 var material = new THREE.ShaderMaterial({ 467 vertexShader: vertexShader, 468 fragmentShader: fragmentShader, 469 transparent: true 470 }); 471 472 geometry = new THREE.BufferGeometry(); 473 geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); 474 geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); 475 geometry.setAttribute('opacity', new THREE.BufferAttribute(opacities, 1)); 476 geometry.setAttribute('visible', new THREE.BufferAttribute(visibilities, 1)); 477 geometry.setAttribute('emissive', new THREE.BufferAttribute(emissives, 1)); 478 479 lines = new THREE.LineSegments(geometry, material); 480 481 var attributeIndex = 0; 482 483 for (var i = 0; i < this.decomp.length; i++) 484 { 485 var plottable = this.decomp.plottable[i]; 486 // Each point in the model maps to (allDimensions.length * 2 - 2) 487 // positions due to the use of lines rather than line strips. 488 for (var j = 0; j < allDimensions.length; j++) 489 { 490 //normalize by global range bounds 491 var globalMin = this.allModels.dimensionRanges.min[allDimensions[j]]; 492 var globalMax = this.allModels.dimensionRanges.max[allDimensions[j]]; 493 var maxMinusMin = globalMax - globalMin; 494 var interpVal = (plottable.coordinates[j] - globalMin) / (maxMinusMin); 495 geometry.attributes.position.setXYZ(attributeIndex, 496 j, 497 interpVal, 498 0); 499 500 geometry.attributes.color.setXYZ(attributeIndex, 1, 0, 0); 501 geometry.attributes.visible.setX(attributeIndex, 1); 502 geometry.attributes.opacity.setX(attributeIndex, 1); 503 attributeIndex++; 504 505 //Because we are drawing all line strips at once using GL_LINES 506 //(which seemed easier than multiple line strip calls) 507 //it is necessary to duplicate the end points of each line. But the 508 //duplicate points are only necessary for points in the middle of the 509 //line strip: the first point and last point of the strip are added once 510 //all of the points in the middle of the line strip must be duplicated. 511 if (j == 0 || j == allDimensions.length - 1) 512 continue; 513 514 geometry.attributes.position.setXYZ(attributeIndex, 515 j, 516 interpVal, 517 0); 518 geometry.attributes.color.setXYZ(attributeIndex, 1, 0, 0); 519 geometry.attributes.visible.setX(attributeIndex, 1); 520 geometry.attributes.opacity.setX(attributeIndex, 1); 521 attributeIndex++; 522 } 523 } 524 525 geometry.attributes.position.needsUpdate = true; 526 geometry.attributes.color.needsUpdate = true; 527 geometry.attributes.visible.needsUpdate = true; 528 geometry.attributes.opacity.needsUpdate = true; 529 530 this.markers.push(lines); 531 }; 532 533 DecompositionView.prototype.getModelPointIndex = function(raytraceIndex, 534 viewType) 535 { 536 var allDimensions = _.range(this.decomp.dimensions); 537 var numPointsPerScatterPoint = (allDimensions.length * 2 - 2); 538 539 if (viewType === 'scatter') { 540 //Each point in the model maps to a single point in the mesh in scatter 541 return raytraceIndex; 542 } 543 else if (viewType === 'parallel-plot') { 544 return Math.floor(raytraceIndex / numPointsPerScatterPoint); 545 } 546 }; 547 /** 548 * 549 * Get the number of visible elements 550 * 551 * @return {Number} The number of visible elements in this view. 552 * 553 */ 554 DecompositionView.prototype.getVisibleCount = function() { 555 var visible = 0, attrVisible, numPoints = 0, scope = this; 556 557 visible = _.reduce(this.markers, function(acc, marker) { 558 var perMarkerCount = 0; 559 560 // shader objects need to be counted different from meshes 561 if (marker.isLineSegments || marker.isPoints) { 562 attrVisible = marker.geometry.attributes.visible; 563 564 // for line segments we need to go in jumps of dimensions*2 565 if (marker.isLineSegments) { 566 numPoints = (scope.decomp.dimensions * 2 - 2); 567 } 568 else { 569 numPoints = 1; 570 } 571 572 for (var i = 0; i < attrVisible.count; i += numPoints) { 573 perMarkerCount += (attrVisible.getX(i) + 0); 574 } 575 } 576 else { 577 // +0 cast bool to int 578 perMarkerCount += (marker.visible + 0); 579 } 580 581 return acc + perMarkerCount; 582 }, 0); 583 584 return visible; 585 }; 586 587 /** 588 * 589 * Update the position of the markers, arrows and lines. 590 * 591 * This method is called by flipVisibleDimension and by changeVisibleDimensions 592 * and will naively change the positions even if they haven't changed. 593 * 594 */ 595 DecompositionView.prototype.updatePositions = function() { 596 var x = this.visibleDimensions[0], y = this.visibleDimensions[1], 597 z = this.visibleDimensions[2], scope = this, hasConfidenceIntervals, 598 radius = 0, is2D = (z === null || z === undefined); 599 600 hasConfidenceIntervals = this.decomp.hasConfidenceIntervals(); 601 602 // we need the original radius to scale confidence intervals (if they exist) 603 if (hasConfidenceIntervals) { 604 radius = this.getGeometryFactor(); 605 } 606 607 if (this.UIState['view.usesPointCloud'] && 608 (this.UIState['view.viewType'] === 'scatter')) { 609 var cloud = this.markers[0]; 610 611 this.decomp.apply(function(plottable) { 612 cloud.geometry.attributes.position.setXYZ( 613 plottable.idx, 614 plottable.coordinates[x] * scope.axesOrientation[0], 615 plottable.coordinates[y] * scope.axesOrientation[1], 616 is2D ? 0 : plottable.coordinates[z] * scope.axesOrientation[2]); 617 }); 618 cloud.geometry.attributes.position.needsUpdate = true; 619 } 620 else if (this.decomp.isScatterType() && 621 (this.UIState['view.viewType'] === 'parallel-plot')) { 622 //TODO: Do we need to do anything when axes are changed in parallel plots? 623 } 624 else if (this.decomp.isScatterType()) { 625 this.decomp.apply(function(plottable) { 626 mesh = scope.markers[plottable.idx]; 627 628 // always use the original data plus the axis orientation 629 mesh.position.set( 630 plottable.coordinates[x] * scope.axesOrientation[0], 631 plottable.coordinates[y] * scope.axesOrientation[1], 632 is2D ? 0 : plottable.coordinates[z] * scope.axesOrientation[2]); 633 mesh.updateMatrix(); 634 635 if (hasConfidenceIntervals) { 636 mesh = scope.ellipsoids[plottable.idx]; 637 638 mesh.position.set( 639 plottable.coordinates[x] * scope.axesOrientation[0], 640 plottable.coordinates[y] * scope.axesOrientation[1], 641 is2D ? 0 : plottable.coordinates[z] * scope.axesOrientation[2]); 642 643 // flatten the ellipsoids ever so slightly 644 mesh.scale.set(plottable.ci[x] / radius, plottable.ci[y] / radius, 645 is2D ? 0.01 : plottable.ci[z] / radius); 646 647 mesh.updateMatrix(); 648 } 649 }); 650 } 651 else if (this.decomp.isArrowType()) { 652 var target, arrow; 653 654 this.decomp.apply(function(plottable) { 655 arrow = scope.markers[plottable.idx]; 656 657 target = new THREE.Vector3( 658 plottable.coordinates[x] * scope.axesOrientation[0], 659 plottable.coordinates[y] * scope.axesOrientation[1], 660 is2D ? 0 : plottable.coordinates[z] * scope.axesOrientation[2]); 661 662 arrow.setPointsTo(target); 663 }); 664 } 665 666 // edges are made using THREE.LineSegments and a buffer geometry so updating 667 // the position takes a bit more work but these objects will render faster 668 if (this.decomp.edges.length) { 669 this._redrawEdges(); 670 } 671 this.needsUpdate = true; 672 }; 673 674 675 /** 676 * 677 * Internal method to draw edges for plottables 678 * 679 * @param {Plottable[]} plottables An array of plottables for which the edges 680 * should be redrawn. If this object is not supplied, all the edges are drawn. 681 */ 682 DecompositionView.prototype._redrawEdges = function(plottables) { 683 var u, v, j = 0, left = [], right = []; 684 var x = this.visibleDimensions[0], y = this.visibleDimensions[1], 685 z = this.visibleDimensions[2], scope = this, 686 is2D = (z === null), drawAll = (plottables === undefined); 687 688 this.decomp.edges.forEach(function(edge) { 689 u = edge[0]; 690 v = edge[1]; 691 692 if (drawAll || 693 (plottables.indexOf(u) !== -1 || plottables.indexOf(v) !== -1)) { 694 695 center = [(u.coordinates[x] + v.coordinates[x]) / 2, 696 (u.coordinates[y] + v.coordinates[y]) / 2, 697 is2D ? 0 : (u.coordinates[z] + v.coordinates[z]) / 2]; 698 699 left = [u.coordinates[x], u.coordinates[y], 700 is2D ? 0 : u.coordinates[z]]; 701 right = [v.coordinates[x], v.coordinates[y], 702 is2D ? 0 : v.coordinates[z]]; 703 704 scope.lines.left.setLineAtIndex(j, left, center); 705 scope.lines.right.setLineAtIndex(j, right, center); 706 } 707 708 j++; 709 }); 710 711 // otherwise the geometry will remain unchanged 712 this.lines.left.geometry.attributes.position.needsUpdate = true; 713 this.lines.right.geometry.attributes.position.needsUpdate = true; 714 715 this.needsUpdate = true; 716 }; 717 718 /** 719 * 720 * Change the visible coordinates 721 * 722 * @param {integer[]} newDims An Array of integers in which each integer is the 723 * index to the principal coordinate to show 724 * 725 */ 726 DecompositionView.prototype.changeVisibleDimensions = function(newDims) { 727 if (newDims.length < 2 || newDims.length > 3) { 728 throw new Error('Only three dimensions can be shown at the same time'); 729 } 730 731 // one by one, find and update the dimensions that are changing 732 for (var i = 0; i < newDims.length; i++) { 733 if (this.visibleDimensions[i] !== newDims[i]) { 734 // index represents the global position of the dimension 735 var index = this.visibleDimensions[i], 736 orientation = this.axesOrientation[i]; 737 738 // 1.- Correct the limits of the ranges for the dimension that we are 739 // moving out of the scene i.e. the old dimension 740 if (this.axesOrientation[i] === -1) { 741 var max = this.decomp.dimensionRanges.max[index]; 742 var min = this.decomp.dimensionRanges.min[index]; 743 this.decomp.dimensionRanges.max[index] = min * (-1); 744 this.decomp.dimensionRanges.min[index] = max * (-1); 745 } 746 747 // 2.- Set the orientation of the new dimension to be 1 748 this.axesOrientation[i] = 1; 749 750 // 3.- Update the visible dimensions to include the new value 751 this.visibleDimensions[i] = newDims[i]; 752 } 753 } 754 755 this.updatePositions(); 756 }; 757 758 /** 759 * 760 * Reorient one of the visible dimensions. 761 * 762 * @param {integer} index The index of the dimension to re-orient, if this 763 * dimension is not visible i.e. not in `this.visibleDimensions`, then the 764 * method will return right away. 765 * 766 */ 767 DecompositionView.prototype.flipVisibleDimension = function(index) { 768 var scope = this, newMin, newMax; 769 770 // the index in the visible dimensions 771 var localIndex = this.visibleDimensions.indexOf(index); 772 773 if (localIndex !== -1) { 774 // update the ranges for this decomposition 775 var max = this.decomp.dimensionRanges.max[index]; 776 var min = this.decomp.dimensionRanges.min[index]; 777 this.decomp.dimensionRanges.max[index] = min * (-1); 778 this.decomp.dimensionRanges.min[index] = max * (-1); 779 780 // and update the state of the orientation 781 this.axesOrientation[localIndex] *= -1; 782 783 this.updatePositions(); 784 } 785 }; 786 787 /** 788 * Change the plottables attributes based on the metadata category using the 789 * provided setPlottableAttributes function 790 * 791 * @param {object} attributes Key:value pairs of elements and values to change 792 * in plottables. 793 * @param {function} setPlottableAttributes Helper function to change the 794 * values of plottables, in general this should be implemented in the 795 * controller but it can be nullable if not needed. setPlottableAttributes 796 * should receive: the scope where the plottables exist, the value to be 797 * applied to the plottables and the plotables to change. For more info 798 * see ColorViewController.setPlottableAttribute 799 * @see ColorViewController.setPlottableAttribute 800 * @param {string} category The category/column in the mapping file 801 * 802 * @return {object[]} Array of objects to be consumed by Slick grid. 803 * 804 */ 805 DecompositionView.prototype.setCategory = function(attributes, 806 setPlottableAttributes, 807 category) { 808 var scope = this, dataView = [], plottables; 809 810 var fieldValues = util.naturalSort(_.keys(attributes)); 811 812 _.each(fieldValues, function(fieldVal, index) { 813 /* 814 * 815 * WARNING: This is mixing attributes of the view with the model ... 816 * it's a bit of a gray area though. 817 * 818 **/ 819 plottables = scope.decomp.getPlottablesByMetadataCategoryValue(category, 820 fieldVal); 821 if (setPlottableAttributes !== null) { 822 setPlottableAttributes(scope, attributes[fieldVal], plottables); 823 } 824 825 dataView.push({id: index, category: fieldVal, value: attributes[fieldVal], 826 plottables: plottables}); 827 }); 828 this.needsUpdate = true; 829 830 return dataView; 831 }; 832 833 /** 834 * 835 * Hide edges where plottables are present. 836 * 837 * @param {Plottable[]} plottables An array of plottables for which the edges 838 * should be hidden. If this object is not supplied, all the edges are hidden. 839 */ 840 DecompositionView.prototype.hideEdgesForPlottables = function(plottables) { 841 // no edges to hide 842 if (this.decomp.edges.length === 0) { 843 return; 844 } 845 846 var u, v, j = 0, hideAll, scope = this; 847 848 hideAll = plottables === undefined; 849 850 this.decomp.edges.forEach(function(edge) { 851 u = edge[0]; 852 v = edge[1]; 853 854 if (hideAll || 855 (plottables.indexOf(u) !== -1 || plottables.indexOf(v) !== -1)) { 856 857 scope.lines.left.setLineAtIndex(j, [0, 0, 0], [0, 0, 0]); 858 scope.lines.right.setLineAtIndex(j, [0, 0, 0], [0, 0, 0]); 859 } 860 j++; 861 }); 862 863 // otherwise the geometry will remain unchanged 864 this.lines.left.geometry.attributes.position.needsUpdate = true; 865 this.lines.right.geometry.attributes.position.needsUpdate = true; 866 }; 867 868 /** 869 * 870 * Hide edges where plottables are present. 871 * 872 * @param {Plottable[]} plottables An array of plottables for which the edges 873 * should be hidden. If this object is not supplied, all the edges are hidden. 874 */ 875 DecompositionView.prototype.showEdgesForPlottables = function(plottables) { 876 // no edges to show 877 if (this.decomp.edges.length === 0) { 878 return; 879 } 880 881 this._redrawEdges(plottables); 882 }; 883 884 /** 885 * Set the color for a group of plottables. 886 * 887 * @param {Object} color An object that can be interpreted as a color by the 888 * THREE.Color class. Can be either a string like '#ff0000' or a number like 889 * 0xff0000, or a CSS color name like 'red', etc. 890 * @param {Plottable[]} group An array of plottables for which the color should 891 * be set. If this object is not provided, all the plottables in the view will 892 * have the color set. 893 */ 894 DecompositionView.prototype.setColor = function(color, group) { 895 var idx, hasConfidenceIntervals, scope = this; 896 897 group = group || this.decomp.plottable; 898 hasConfidenceIntervals = this.decomp.hasConfidenceIntervals(); 899 900 if (this.UIState['view.usesPointCloud'] && 901 (this.UIState['view.viewType'] === 'scatter')) { 902 var cloud = this.markers[0]; 903 color = new THREE.Color(color); 904 905 group.forEach(function(plottable) { 906 cloud.geometry.attributes.color.setXYZ(plottable.idx, 907 color.r, color.g, color.b); 908 }); 909 cloud.geometry.attributes.color.needsUpdate = true; 910 } 911 else if (this.UIState['view.viewType'] == 'parallel-plot' && 912 this.decomp.isScatterType()) { 913 var lines = this.markers[0]; 914 color = new THREE.Color(color); 915 var numPoints = (this.decomp.dimensions * 2 - 2); 916 group.forEach(function(plottable) { 917 var startIndex = plottable.idx * numPoints; 918 var endIndex = (plottable.idx + 1) * numPoints; 919 for (var i = startIndex; i < endIndex; i++) 920 lines.geometry.attributes.color.setXYZ(i, color.r, color.g, color.b); 921 }); 922 lines.geometry.attributes.color.needsUpdate = true; 923 } 924 else if (this.decomp.isScatterType()) { 925 group.forEach(function(plottable) { 926 idx = plottable.idx; 927 scope.markers[idx].material.color = new THREE.Color(color); 928 929 if (hasConfidenceIntervals) { 930 scope.ellipsoids[idx].material.color = new THREE.Color(color); 931 } 932 }); 933 } 934 else if (this.decomp.isArrowType()) { 935 group.forEach(function(plottable) { 936 scope.markers[plottable.idx].setColor(new THREE.Color(color)); 937 }); 938 } 939 this.needsUpdate = true; 940 }; 941 942 /** 943 * Set the visibility for a group of plottables. 944 * 945 * @param {Bool} visible Whether or not the objects should be visible. 946 * @param {Plottable[]} group An array of plottables for which the visibility 947 * should be set. If this object is not provided, all the plottables in the 948 * view will be have the visibility set. 949 */ 950 DecompositionView.prototype.setVisibility = function(visible, group) { 951 var hasConfidenceIntervals, scope = this; 952 953 group = group || this.decomp.plottable; 954 955 hasConfidenceIntervals = this.decomp.hasConfidenceIntervals(); 956 957 if (this.UIState['view.usesPointCloud'] && 958 (this.UIState['view.viewType'] === 'scatter')) { 959 var cloud = this.markers[0]; 960 961 _.each(group, function(plottable) { 962 cloud.geometry.attributes.visible.setX(plottable.idx, visible * 1); 963 }); 964 cloud.geometry.attributes.visible.needsUpdate = true; 965 } 966 else if (this.UIState['view.viewType'] == 'parallel-plot' && 967 this.decomp.isScatterType()) { 968 var lines = this.markers[0]; 969 var numPoints = (this.decomp.dimensions * 2 - 2); 970 _.each(group, function(plottable) { 971 var startIndex = plottable.idx * numPoints; 972 var endIndex = (plottable.idx + 1) * (numPoints); 973 for (i = startIndex; i < endIndex; i++) 974 lines.geometry.attributes.visible.setX(i, visible * 1); 975 }); 976 lines.geometry.attributes.visible.needsUpdate = true; 977 } 978 else { 979 _.each(group, function(plottable) { 980 scope.markers[plottable.idx].visible = visible; 981 982 if (hasConfidenceIntervals) { 983 scope.ellipsoids[plottable.idx].visible = visible; 984 } 985 }); 986 } 987 988 if (visible === true) { 989 this.showEdgesForPlottables(group); 990 } 991 else { 992 this.hideEdgesForPlottables(group); 993 } 994 995 this.needsUpdate = true; 996 }; 997 998 /** 999 * Set the scale for a group of plottables. 1000 * 1001 * @param {Float} scale The scale to set for the objects, relative to the 1002 * original size. Should be a positive and non-zero value. 1003 * @param {Plottable[]} group An array of plottables for which the scale 1004 * should be set. If this object is not provided, all the plottables in the 1005 * view will be have the scale set. 1006 */ 1007 DecompositionView.prototype.setScale = function(scale, group) { 1008 var scope = this; 1009 1010 if (this.decomp.isArrowType()) { 1011 throw Error('Cannot change the scale of an arrow.'); 1012 } 1013 1014 group = group || this.decomp.plottable; 1015 1016 if (this.UIState['view.usesPointCloud'] && 1017 (this.UIState['view.viewType'] === 'scatter')) { 1018 var cloud = this.markers[0]; 1019 1020 _.each(group, function(plottable) { 1021 cloud.geometry.attributes.scale.setX(plottable.idx, scale); 1022 }); 1023 cloud.geometry.attributes.scale.needsUpdate = true; 1024 } 1025 else if (this.UIState['view.viewType'] == 'parallel-plot' && 1026 this.decomp.isScatterType()) { 1027 //Nothing to do for parallel plots. 1028 } 1029 else { 1030 _.each(group, function(element) { 1031 scope.markers[element.idx].scale.set(scale, scale, scale); 1032 }); 1033 } 1034 this.needsUpdate = true; 1035 }; 1036 1037 /** 1038 * Set the opacity for a group of plottables. 1039 * 1040 * @param {Float} opacity The opacity value (from 0 to 1) for the selected 1041 * objects. 1042 * @param {Plottable[]} group An array of plottables for which the opacity 1043 * should be set. If this object is not provided, all the plottables in the 1044 * view will be have the opacity set. 1045 */ 1046 DecompositionView.prototype.setOpacity = function(opacity, group) { 1047 // webgl acts up with transparent objects, so we only set them to be 1048 // explicitly transparent if the opacity is not at full 1049 var transparent = opacity !== 1, funk, scope = this; 1050 1051 group = group || this.decomp.plottable; 1052 1053 if (this.UIState['view.usesPointCloud'] && 1054 (this.UIState['view.viewType'] === 'scatter')) { 1055 var cloud = this.markers[0]; 1056 1057 _.each(group, function(plottable) { 1058 cloud.geometry.attributes.opacity.setX(plottable.idx, opacity); 1059 }); 1060 cloud.geometry.attributes.opacity.needsUpdate = true; 1061 } 1062 else if (this.UIState['view.viewType'] == 'parallel-plot' && 1063 this.decomp.isScatterType()) { 1064 var lines = this.markers[0]; 1065 var numPoints = (this.decomp.dimensions * 2 - 2); 1066 _.each(group, function(plottable) { 1067 var startIndex = plottable.idx * numPoints; 1068 var endIndex = (plottable.idx + 1) * (numPoints); 1069 for (var i = startIndex; i < endIndex; i++) 1070 lines.geometry.attributes.opacity.setX(i, opacity); 1071 }); 1072 lines.geometry.attributes.opacity.needsUpdate = true; 1073 } 1074 else { 1075 if (this.decomp.isScatterType()) { 1076 funk = _changeMeshOpacity; 1077 } 1078 else if (this.decomp.isArrowType()) { 1079 funk = _changeArrowOpacity; 1080 } 1081 1082 _.each(group, function(plottable) { 1083 funk(scope.markers[plottable.idx], opacity, transparent); 1084 }); 1085 } 1086 this.needsUpdate = true; 1087 }; 1088 1089 /** 1090 * Toggles the visibility of arrow labels 1091 * 1092 * @throws {Error} if this method is called on a scatter type. 1093 */ 1094 DecompositionView.prototype.toggleLabelVisibility = function() { 1095 if (this.decomp.isScatterType()) { 1096 throw new Error('Cannot hide labels of scatter types'); 1097 } 1098 var scope = this; 1099 1100 this.decomp.apply(function(plottable) { 1101 arrow = scope.markers[plottable.idx]; 1102 arrow.label.visible = Boolean(arrow.label.visible ^ true); 1103 }); 1104 this.needsUpdate = true; 1105 }; 1106 1107 1108 /** 1109 * Set the emissive attribute of the markers 1110 * 1111 * @param {Bool} emissive Whether the object should be emissive. 1112 * @param {Plottable[]} group An array of plottables for which the emissive 1113 * attribute will be set. If this object is not provided, all the plottables in 1114 * the view will be have the scale set. 1115 */ 1116 DecompositionView.prototype.setEmissive = function(emissive, group) { 1117 group = group || this.decomp.plottable; 1118 1119 if (this.decomp.isArrowType()) { 1120 throw new Error('Cannot set emissive attribute of arrows'); 1121 } 1122 1123 var i = 0, j = 0; 1124 1125 if (this.UIState.getProperty('view.usesPointCloud') || 1126 this.UIState.getProperty('view.viewType') === 'parallel-plot') { 1127 var emissives = this.markers[0].geometry.attributes.emissive; 1128 1129 // the emissive attribute is a boolean one 1130 emissive = (emissive > 0) * 1; 1131 1132 if (this.markers[0].isPoints) { 1133 for (i = 0; i < group.length; i++) { 1134 emissives.setX(group[i].idx, emissive); 1135 } 1136 } 1137 else if (this.markers[0].isLineSegments) { 1138 // line segments need to be repeated one per dimension 1139 for (i = 0; i < group.length; i++) { 1140 var numPoints = (this.decomp.dimensions * 2 - 2); 1141 var startIndex = group[i].idx * numPoints; 1142 var endIndex = (group[i].idx + 1) * (numPoints); 1143 1144 for (j = startIndex; j < endIndex; j++) { 1145 emissives.setX(j, emissive); 1146 } 1147 } 1148 } 1149 emissives.needsUpdate = true; 1150 } 1151 else { 1152 for (i = 0; i < group.length; i++) { 1153 var material = this.markers[group[i].idx].material; 1154 material.emissive.set(emissive); 1155 } 1156 } 1157 1158 this.needsUpdate = true; 1159 }; 1160 1161 /** 1162 * Group by color 1163 * 1164 * @param {Array} names An array of strings with the sample names. 1165 * @return {Object} Mapping of colors to objects. 1166 */ 1167 DecompositionView.prototype.groupByColor = function(names) { 1168 1169 var colorGroups = {}, groupping, markers = this.markers; 1170 var plottables = this.decomp.getPlottableByIDs(names); 1171 1172 // we need to retrieve colors in a very different way 1173 if (this.UIState['view.viewType'] === 'parallel-plot' || 1174 this.UIState['view.usesPointCloud']) { 1175 var colors = this.markers[0].geometry.attributes.color; 1176 var numPoints = 1; 1177 1178 if (this.markers[0].isLineSegments) { 1179 numPoints = (this.decomp.dimensions * 2 - 2); 1180 } 1181 1182 groupping = function(plottable) { 1183 // taken from Color.getHexString in THREE.js 1184 r = (colors.getX(plottable.idx * numPoints) * 255) << 16; 1185 g = (colors.getY(plottable.idx * numPoints) * 255) << 8; 1186 b = (colors.getZ(plottable.idx * numPoints) * 255) << 0; 1187 return ('000000' + (r ^ g ^ b).toString(16)).slice(-6); 1188 }; 1189 } 1190 else { 1191 if (this.decomp.isScatterType()) { 1192 groupping = function(plottable) { 1193 return markers[plottable.idx].material.color.getHexString(); 1194 }; 1195 } 1196 else { 1197 // check that this getColor method works 1198 groupping = function(plottable) { 1199 return markers[plottable.idx].getColor().getHexString(); 1200 }; 1201 } 1202 } 1203 1204 return _.groupBy(plottables, groupping); 1205 }; 1206 1207 /** 1208 * 1209 * Helper that builds a vega specification off of the current view state 1210 * 1211 * @private 1212 */ 1213 DecompositionView.prototype._buildVegaSpec = function() { 1214 function rgbColor(colorObj) { 1215 var r = colorObj.r * 255; 1216 var g = colorObj.g * 255; 1217 var b = colorObj.b * 255; 1218 return 'rgb(' + r + ',' + g + ',' + b + ')'; 1219 } 1220 1221 // Maps THREE.js geometries to vega shapes 1222 var getShape = { 1223 Sphere: 'circle', 1224 Diamond: 'diamond', 1225 Cone: 'triangle-down', 1226 Cylinder: 'square', 1227 Ring: 'circle', 1228 Square: 'square', 1229 Icosahedron: 'cross', 1230 Star: 'cross' 1231 }; 1232 1233 function viewMarkersAsVegaDataset(markers) { 1234 var points = [], marker, i; 1235 for (i = 0; i < markers.length; i++) { 1236 marker = markers[i]; 1237 if (marker.visible) { 1238 points.push({ 1239 id: marker.name, 1240 x: marker.position.x, 1241 y: marker.position.y, 1242 color: rgbColor(marker.material.color), 1243 originalShape: marker.userData.shape, 1244 shape: getShape[marker.userData.shape], 1245 scale: { x: marker.scale.x, y: marker.scale.y }, 1246 opacity: marker.material.opacity 1247 }); 1248 } 1249 } 1250 return points; 1251 }; 1252 1253 // This is probably horribly slow on QIITA-scale MD files, probably needs 1254 // some attention 1255 function plottablesAsMetadata(points, header) { 1256 var md = [], point, row, i, j; 1257 for (i = 0; i < points.length; i++) { 1258 point = points[i]; 1259 row = {}; 1260 for (j = 0; j < header.length; j++) { 1261 row[header[j]] = point.metadata[j]; 1262 } 1263 md.push(row); 1264 } 1265 return md; 1266 } 1267 1268 var scope = this; 1269 var model = scope.decomp; 1270 1271 var axisX = scope.visibleDimensions[0]; 1272 var axisY = scope.visibleDimensions[1]; 1273 1274 var dimRanges = model.dimensionRanges; 1275 var rangeX = [dimRanges.min[axisX], dimRanges.max[axisX]]; 1276 var rangeY = [dimRanges.min[axisY], dimRanges.max[axisY]]; 1277 1278 var baseWidth = 800; 1279 1280 return { 1281 '$schema': 'https://vega.github.io/schema/vega/v5.json', 1282 padding: 5, 1283 background: scope.backgroundColor, 1284 config: { 1285 axis: { labelColor: scope.axesColor, titleColor: scope.axesColor }, 1286 title: { color: scope.axesColor } 1287 }, 1288 title: 'Emperor PCoA', 1289 data: [ 1290 { 1291 name: 'metadata', 1292 values: plottablesAsMetadata(model.plottable, model.md_headers) 1293 }, 1294 { 1295 name: 'points', values: viewMarkersAsVegaDataset(scope.markers), 1296 transform: [ 1297 { 1298 type: 'lookup', 1299 from: 'metadata', 1300 key: model.md_headers[0], 1301 fields: ['id'], 1302 as: ['metadata'] 1303 } 1304 ] 1305 } 1306 ], 1307 signals: [ 1308 { 1309 name: 'width', 1310 update: baseWidth + ' * ((' + rangeX[1] + ') - (' + rangeX[0] + '))' 1311 }, 1312 { 1313 name: 'height', 1314 update: baseWidth + ' * ((' + rangeY[1] + ') - (' + rangeY[0] + '))' 1315 } 1316 ], 1317 scales: [ 1318 { name: 'xScale', range: 'width', domain: [rangeX[0], rangeX[1]] }, 1319 { name: 'yScale', range: 'height', domain: [rangeY[0], rangeY[1]] } 1320 ], 1321 axes: [ 1322 { orient: 'bottom', scale: 'xScale', title: model.axesLabels[axisX] }, 1323 { orient: 'left', scale: 'yScale', title: model.axesLabels[axisY] } 1324 ], 1325 marks: [ 1326 { 1327 type: 'symbol', 1328 from: {data: 'points'}, 1329 encode: { 1330 enter: { 1331 fill: { field: 'color' }, 1332 x: { scale: 'xScale', field: 'x' }, 1333 y: { scale: 'yScale', field: 'y' }, 1334 shape: { field: 'shape' }, 1335 size: { signal: 'datum.scale.x * datum.scale.y * 100' }, 1336 opacity: { field: 'opacity' } 1337 }, 1338 update: { 1339 tooltip: { signal: 'datum.metadata' } 1340 } 1341 } 1342 } 1343 ] 1344 }; 1345 }; 1346 1347 /** 1348 * Called as part of the swap operation to change out objects in the scene, 1349 * this function atomically clears the swap flag, clears the old markers, 1350 * and returns what the old markers were. 1351 */ 1352 DecompositionView.prototype.getAndClearOldMarkers = function() { 1353 this.needsSwapMarkers = false; 1354 var oldMarkers = this.oldMarkers; 1355 this.oldMarkers = []; 1356 return oldMarkers; 1357 }; 1358 1359 /** 1360 * Helper function to change the opacity of an arrow object. 1361 * 1362 * @private 1363 */ 1364 function _changeArrowOpacity(arrow, value, transparent) { 1365 arrow.line.material.transparent = transparent; 1366 arrow.line.material.opacity = value; 1367 1368 arrow.cone.material.transparent = transparent; 1369 arrow.cone.material.opacity = value; 1370 } 1371 1372 /** 1373 * Helper function to change the opacity of a mesh object. 1374 * 1375 * @private 1376 */ 1377 function _changeMeshOpacity(mesh, value, transparent) { 1378 mesh.material.transparent = transparent; 1379 mesh.material.opacity = value; 1380 } 1381 1382 return DecompositionView; 1383 }); 1384