      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