Source: view.js

define([
    'jquery',
    'underscore',
    'three',
    'shapes',
    'draw',
    'multi-model',
    'util'
], function($, _, THREE, shapes, draw, multiModel, util) {
  var makeArrow = draw.makeArrow;
  var makeLineCollection = draw.makeLineCollection;
/**
 *
 * @class DecompositionView
 *
 * Contains all the information on how the model is being presented to the
 * user.
 *
 * @param {MultiModel} multiModel - A multi model object with all models
 * @param {string} modelKey - The key referencing the target model
 *                            within the multiModel
 *
 * @return {DecompositionView}
 * @constructs DecompositionView
 *
 */
function DecompositionView(multiModel, modelKey, uiState) {
  /**
   * The decomposition model that the view represents.
   * @type {DecompositionModel}
   */
  this.decomp = multiModel.models[modelKey];

  /**
   * All models in the current scene and global metrics about them
   * @type {MultiModel}
   */
  this.allModels = multiModel;

  /**
   * Number of samples represented in the view.
   * @type {integer}
   */
  this.count = this.decomp.length;
  /**
   * Top visible dimensions
   * @type {integer[]}
   */
  // make sure we only use at most 3 elements for scatter and arrow plots
  this.visibleDimensions = _.range(this.decomp.dimensions).slice(0, 3);
  /**
   * Orientation of the axes, `-1` means the axis is flipped, `1` means the
   * axis is not flipped.
   * @type {integer[]}
   */
  this.axesOrientation = _.map(this.visibleDimensions, function() {
    // by default values are not flipped i.e. all elements are equal to 1
    return 1;
  });

  /**
   * Axes color.
   * @type {integer}
   * @default '#FFFFFF' (white)
   */
  this.axesColor = '#FFFFFF';
  /**
   * Background color.
   * @type {integer}
   * @default '#000000' (black)
   */
  this.backgroundColor = '#000000';
  /**
   * Static tubes objects covering an entire trajectory.
   * Can use setDrawRange on the underlying geometry to display
   * just part of the trajectory.
   * @type {THREE.Mesh[]}
   */
  this.staticTubes = [];
  /**
   * Dynamic tubes covering the final tube segment of a trajectory
   * Must be rebuilt each frame by the animations controller
   * @type {THREE.Mesh[]}
   */
  this.dynamicTubes = [];
  /**
   * Array of THREE.Mesh objects on screen (represent samples).
   * @type {THREE.Mesh[]}
   */
  this.markers = [];

  /**
   * Meshes to be swapped out of scene when markers are modified.
   * @type {THREE.Mesh[]}
   */
  this.oldMarkers = [];

  /**
   * Flag indicating old markers must be removed from the scene tree.
   * @type {boolean}
   */
  this.needsSwapMarkers = false;

  /**
   * Array of THREE.Mesh objects on screen (represent confidence intervals).
   * @type {THREE.Mesh[]}
   */
  this.ellipsoids = [];
  /**
   * Object with THREE.LineSegments for the procrustes edges. Has a left and
   * a right attribute.
   * @type {Object}
   */
  this.lines = {'left': null, 'right': null};

  /**
   * The shared state for the UI
   * @type {UIState}
   */
  this.UIState = uiState;

  //Register property changes
  //Note that declaring var scope at the local scope is absolutely critical
  //or callbacks will call into the wrong scope!
  var scope = this;
  this.UIState.registerProperty('view.viewType', function(evt) {
    scope._initGeometry();
  });
}

DecompositionView.prototype._initGeometry = function() {
  this.oldMarkers = this.markers;
  if (this.oldMarkers.length > 0)
    this.needsSwapMarkers = true;
  this.markers = [];

  //TODO FIXME HACK:  Do we need to swap lines as well?
  this.lines = {'left': null, 'right': null};

  if (this.decomp.isScatterType() &&
      (this.UIState['view.viewType'] === 'parallel-plot')) {
    this._fastInitParallelPlot();
  }
  else if (this.UIState['view.usesPointCloud']) {
    this._fastInit();
  }
  else {
    this._initBaseView();
  }
  this.needsUpdate = true;
};

/**
 * Calculate the appropriate size for a geometry based on the first dimension's
 * range.
 */
DecompositionView.prototype.getGeometryFactor = function() {
  // this is a heuristic tested on numerous plots since 2013, based off of
  // the old implementation of emperor. We select the dimensions of all the
  // geometries based on this factor.
  return (this.decomp.dimensionRanges.max[0] -
          this.decomp.dimensionRanges.min[0]) * 0.012;
};

/**
 * Retrieve a shallow copy of concatenated static and dynamic tube arrays
 * @type {THREE.Mesh[]}
 */
DecompositionView.prototype.getTubes = function() {
  return this.staticTubes.concat(this.dynamicTubes);
};

/**
 *
 * Helper method to initialize the base THREE.js objects.
 * @private
 *
 */
DecompositionView.prototype._initBaseView = function() {
  var mesh, x = this.visibleDimensions[0], y = this.visibleDimensions[1],
      z = this.visibleDimensions[2];
  var scope = this;

  // get the correctly sized geometry
  var radius = this.getGeometryFactor(), hasConfidenceIntervals;
  var geometry = shapes.getGeometry('Sphere', radius);

  hasConfidenceIntervals = this.decomp.hasConfidenceIntervals();

  if (this.decomp.isScatterType()) {
    this.decomp.apply(function(plottable) {
      mesh = new THREE.Mesh(geometry, new THREE.MeshPhongMaterial());
      mesh.name = plottable.name;

      mesh.material.color = new THREE.Color(0xff0000);
      mesh.material.transparent = false;
      mesh.material.depthWrite = true;
      mesh.material.opacity = 1;
      mesh.matrixAutoUpdate = true;

      mesh.position.set(plottable.coordinates[x], plottable.coordinates[y],
                        plottable.coordinates[z] || 0);

      mesh.userData.shape = 'Sphere';

      scope.markers.push(mesh);

      if (hasConfidenceIntervals) {
        // copy the current sphere and make it an ellipsoid
        mesh = mesh.clone();

        mesh.name = plottable.name + '_ci';
        mesh.material.transparent = true;
        mesh.material.opacity = 0.5;

        mesh.scale.set(plottable.ci[x] / geometry.parameters.radius,
                       plottable.ci[y] / geometry.parameters.radius,
                       plottable.ci[z] / geometry.parameters.radius);

        scope.ellipsoids.push(mesh);
      }
    });
  }
  else if (this.decomp.isArrowType()) {
    var arrow, zero = [0, 0, 0], point;

    this.decomp.apply(function(plottable) {
      point = [plottable.coordinates[x],
               plottable.coordinates[y],
               plottable.coordinates[z] || 0];
      arrow = makeArrow(zero, point, 0xc0c0c0, plottable.name);

      scope.markers.push(arrow);
    });
  }
  else {
    throw new Error('Unsupported decomposition type');
  }

  if (this.decomp.edges.length) {
    var left, center, right, u, v, verticesLeft = [], verticesRight = [];
    this.decomp.edges.forEach(function(edge) {
      u = edge[0];
      v = edge[1];

      // remember x, y and z
      center = [(u.coordinates[x] + v.coordinates[x]) / 2,
                (u.coordinates[y] + v.coordinates[y]) / 2,
                ((u.coordinates[z] + v.coordinates[z]) / 2) || 0];

      left = [u.coordinates[x], u.coordinates[y], u.coordinates[z] || 0];
      right = [v.coordinates[x], v.coordinates[y], v.coordinates[z] || 0];

      verticesLeft.push(left, center);
      verticesRight.push(right, center);
    });

    this.lines.left = makeLineCollection(verticesLeft, 0xffffff);
    this.lines.right = makeLineCollection(verticesRight, 0xff0000);
  }
};

DecompositionView.prototype._fastInit = function() {
  if (this.decomp.hasConfidenceIntervals()) {
    throw new Error('Ellipsoids are not supported in fast mode');
  }
  if (this.decomp.isArrowType()) {
    throw new Error('Only scatter type is supported in fast mode');
  }

  var positions, colors, scales, opacities, visibilities, emissives, geometry,
      cloud;

  var x = this.visibleDimensions[0], y = this.visibleDimensions[1],
      z = this.visibleDimensions[2];

  /**
   * In order to draw large numbers of samples we can't use full-blown
   * geometries like spheres. Instead we will use shaders to draw each sample
   * as a circle. Note that since these are programs that need to be compiled
   * for the GPU, they need to be stored as strings.
   *
   * The "vertexShader" determines the location and size of each vertex in the
   * geometry. And the "fragmentShader" determines the shape, opacity,
   * visibility and color. In addition there's some logic to smooth the circles
   * and add antialiasing.
   *
   * The source for the shaders was inspired and or modified from:
   *
   * https://www.desultoryquest.com/blog/drawing-anti-aliased-circular-points-using-opengl-slash-webgl/
   * http://jsfiddle.net/callum/x7y72k1e/10/
   * http://math.hws.edu/eck/cs424/s12/lab4/lab4-files/points.html
   * https://stackoverflow.com/q/33695202/379593
   *
   */
  var vertexShader = [
    'attribute float scale;',

    'attribute vec3 color;',
    'attribute float opacity;',
    'attribute float visible;',
    'attribute float emissive;',

    'varying vec3 vColor;',
    'varying float vOpacity;',
    'varying float vVisible;',
    'varying float vEmissive;',

    'void main() {',
      'vColor = color;',
      'vOpacity = opacity;',
      'vVisible = visible;',
      'vEmissive = emissive;',

      'vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);',
      'gl_Position = projectionMatrix * mvPosition; ',
      'gl_PointSize = kSIZE * scale * (800.0 / length(mvPosition.xyz));',
    '}'].join('\n');

  var fragmentShader = [
    'precision mediump float;',
    'varying vec3 vColor;',
    'varying float vOpacity;',
    'varying float vVisible;',
    'varying float vEmissive;',

    'void main() {',
      // remove objects when they might be "visible" but completely transparent
      'if (vVisible > 0.0 && vOpacity > 0.0) {',
        'vec2 cxy = 2.0 * gl_PointCoord - 1.0;',
        'float delta = 0.0, alpha = 1.0, r = dot(cxy, cxy);',

        // get rid of the frame around the points
        'if(r > 1.1) discard;',

        // antialiasing smoothing
        'delta = fwidth(r);',
        'alpha = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, r);',

        // if the object is selected make it white
        'if (vEmissive > 0.0) {',
        '  gl_FragColor = vec4(1, 1, 1, vOpacity) * alpha;',
        '}',
        'else {',
        '  gl_FragColor = vec4(vColor, vOpacity) * alpha;',
        '}',
      '}',
      'else {',
        'discard;',
      '}',
    '}'].join('\n');

  positions = new Float32Array(this.decomp.length * 3);
  colors = new Float32Array(this.decomp.length * 3);
  scales = new Float32Array(this.decomp.length);
  opacities = new Float32Array(this.decomp.length);
  visibilities = new Float32Array(this.decomp.length);
  emissives = new Float32Array(this.decomp.length);

  var material = new THREE.ShaderMaterial({
    vertexShader: vertexShader,
    fragmentShader: fragmentShader,
    transparent: true
  });

  // we need to define a baseline size for markers so we can control the scale
  material.defines.kSIZE = this.getGeometryFactor();

  // needed for the shader's smoothstep and fwidth functions
  material.extensions.derivatives = true;

  geometry = new THREE.BufferGeometry();
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  geometry.setAttribute('scale', new THREE.BufferAttribute(scales, 1));
  geometry.setAttribute('opacity', new THREE.BufferAttribute(opacities, 1));
  geometry.setAttribute('visible', new THREE.BufferAttribute(visibilities, 1));
  geometry.setAttribute('emissive', new THREE.BufferAttribute(emissives, 1));

  cloud = new THREE.Points(geometry, material);

  this.decomp.apply(function(plottable) {
    geometry.attributes.position.setXYZ(plottable.idx,
                                        plottable.coordinates[x],
                                        plottable.coordinates[y],
                                        plottable.coordinates[z] || 0);

    // set default to red, visible, full opacity and of scale 1
    geometry.attributes.color.setXYZ(plottable.idx, 1, 0, 0);
    geometry.attributes.visible.setX(plottable.idx, 1);
    geometry.attributes.opacity.setX(plottable.idx, 1);
    geometry.attributes.emissive.setX(plottable.idx, 0);
    geometry.attributes.scale.setX(plottable.idx, 1);
  });

  geometry.attributes.position.needsUpdate = true;
  geometry.attributes.color.needsUpdate = true;
  geometry.attributes.visible.needsUpdate = true;
  geometry.attributes.opacity.needsUpdate = true;
  geometry.attributes.scale.needsUpdate = true;
  geometry.attributes.emissive.needsUpdate = true;

  this.markers.push(cloud);
};

/**
 * Parallel plots closely mirroring the shader enabled _fastInit calls
 */
DecompositionView.prototype._fastInitParallelPlot = function()
{
  var positions, colors, opacities, visibilities, geometry, cloud;

  // We're really just drawing a bunch of line strips...
  // highly doubt shaders are necessary for this...
  var vertexShader = [
    'attribute vec3 color;',
    'attribute float opacity;',
    'attribute float visible;',
    'attribute float emissive;',

    'varying vec3 vColor;',
    'varying float vOpacity;',
    'varying float vVisible;',
    'varying float vEmissive;',

    'void main() {',
    '  vColor = color;',
    '  vOpacity = opacity;',
    '  vVisible = visible;',
    '  vEmissive = emissive;',

    '  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);',
    '}'].join('\n');

  var fragmentShader = [
    'precision mediump float;',
    'varying vec3 vColor;',
    'varying float vOpacity;',
    'varying float vVisible;',
    'varying float vEmissive;',

    'void main() {',
    ' if (vVisible <= 0.0 || vOpacity <= 0.0)',
    '   discard;',

    // if the object is selected make it white
    ' if (vEmissive > 0.0) {',
    '   gl_FragColor = vec4(1, 1, 1, vOpacity);',
    ' }',
    ' else {',
    '   gl_FragColor = vec4(vColor, vOpacity);',
    ' }',
    '}'].join('\n');

  var allDimensions = _.range(this.decomp.dimensions);

  // We'll build the line strips as GL_LINES for simplicity, at least for now,
  // by doubling up vertex positions at each of the intermediate axes.
  var numPoints = (allDimensions.length * 2 - 2) * (this.decomp.length);
  positions = new Float32Array(numPoints * 3);
  colors = new Float32Array(numPoints * 3);
  opacities = new Float32Array(numPoints);
  visibilities = new Float32Array(numPoints);
  emissives = new Float32Array(numPoints);

  var material = new THREE.ShaderMaterial({
    vertexShader: vertexShader,
    fragmentShader: fragmentShader,
    transparent: true
  });

  geometry = new THREE.BufferGeometry();
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  geometry.setAttribute('opacity', new THREE.BufferAttribute(opacities, 1));
  geometry.setAttribute('visible', new THREE.BufferAttribute(visibilities, 1));
  geometry.setAttribute('emissive', new THREE.BufferAttribute(emissives, 1));

  lines = new THREE.LineSegments(geometry, material);

  var attributeIndex = 0;

  for (var i = 0; i < this.decomp.length; i++)
  {
    var plottable = this.decomp.plottable[i];
    // Each point in the model maps to (allDimensions.length * 2 - 2)
    // positions due to the use of lines rather than line strips.
    for (var j = 0; j < allDimensions.length; j++)
    {
      //normalize by global range bounds
      var globalMin = this.allModels.dimensionRanges.min[allDimensions[j]];
      var globalMax = this.allModels.dimensionRanges.max[allDimensions[j]];
      var maxMinusMin = globalMax - globalMin;
      var interpVal = (plottable.coordinates[j] - globalMin) / (maxMinusMin);
      geometry.attributes.position.setXYZ(attributeIndex,
                                        j,
                                        interpVal,
                                        0);

      geometry.attributes.color.setXYZ(attributeIndex, 1, 0, 0);
      geometry.attributes.visible.setX(attributeIndex, 1);
      geometry.attributes.opacity.setX(attributeIndex, 1);
      attributeIndex++;

      //Because we are drawing all line strips at once using GL_LINES
      //(which seemed easier than multiple line strip calls)
      //it is necessary to duplicate the end points of each line.  But the
      //duplicate points are only necessary for points in the middle of the
      //line strip: the first point and last point of the strip are added once
      //all of the points in the middle of the line strip must be duplicated.
      if (j == 0 || j == allDimensions.length - 1)
        continue;

      geometry.attributes.position.setXYZ(attributeIndex,
                                        j,
                                        interpVal,
                                        0);
      geometry.attributes.color.setXYZ(attributeIndex, 1, 0, 0);
      geometry.attributes.visible.setX(attributeIndex, 1);
      geometry.attributes.opacity.setX(attributeIndex, 1);
      attributeIndex++;
    }
  }

  geometry.attributes.position.needsUpdate = true;
  geometry.attributes.color.needsUpdate = true;
  geometry.attributes.visible.needsUpdate = true;
  geometry.attributes.opacity.needsUpdate = true;

  this.markers.push(lines);
};

DecompositionView.prototype.getModelPointIndex = function(raytraceIndex,
                                                          viewType)
{
  var allDimensions = _.range(this.decomp.dimensions);
  var numPointsPerScatterPoint = (allDimensions.length * 2 - 2);

  if (viewType === 'scatter') {
    //Each point in the model maps to a single point in the mesh in scatter
    return raytraceIndex;
  }
  else if (viewType === 'parallel-plot') {
    return Math.floor(raytraceIndex / numPointsPerScatterPoint);
  }
};
/**
 *
 * Get the number of visible elements
 *
 * @return {Number} The number of visible elements in this view.
 *
 */
DecompositionView.prototype.getVisibleCount = function() {
  var visible = 0, attrVisible, numPoints = 0, scope = this;

  visible = _.reduce(this.markers, function(acc, marker) {
    var perMarkerCount = 0;

    // shader objects need to be counted different from meshes
    if (marker.isLineSegments || marker.isPoints) {
      attrVisible = marker.geometry.attributes.visible;

      // for line segments we need to go in jumps of dimensions*2
      if (marker.isLineSegments) {
        numPoints = (scope.decomp.dimensions * 2 - 2);
      }
      else {
        numPoints = 1;
      }

      for (var i = 0; i < attrVisible.count; i += numPoints) {
        perMarkerCount += (attrVisible.getX(i) + 0);
      }
    }
    else {
      // +0 cast bool to int
      perMarkerCount += (marker.visible + 0);
    }

    return acc + perMarkerCount;
  }, 0);

  return visible;
};

/**
 *
 * Update the position of the markers, arrows and lines.
 *
 * This method is called by flipVisibleDimension and by changeVisibleDimensions
 * and will naively change the positions even if they haven't changed.
 *
 */
DecompositionView.prototype.updatePositions = function() {
  var x = this.visibleDimensions[0], y = this.visibleDimensions[1],
      z = this.visibleDimensions[2], scope = this, hasConfidenceIntervals,
      radius = 0, is2D = (z === null || z === undefined);

  hasConfidenceIntervals = this.decomp.hasConfidenceIntervals();

  // we need the original radius to scale confidence intervals (if they exist)
  if (hasConfidenceIntervals) {
    radius = this.getGeometryFactor();
  }

  if (this.UIState['view.usesPointCloud'] &&
      (this.UIState['view.viewType'] === 'scatter')) {
    var cloud = this.markers[0];

    this.decomp.apply(function(plottable) {
      cloud.geometry.attributes.position.setXYZ(
        plottable.idx,
        plottable.coordinates[x] * scope.axesOrientation[0],
        plottable.coordinates[y] * scope.axesOrientation[1],
        is2D ? 0 : plottable.coordinates[z] * scope.axesOrientation[2]);
    });
    cloud.geometry.attributes.position.needsUpdate = true;
  }
  else if (this.decomp.isScatterType() &&
           (this.UIState['view.viewType'] === 'parallel-plot')) {
    //TODO:  Do we need to do anything when axes are changed in parallel plots?
  }
  else if (this.decomp.isScatterType()) {
    this.decomp.apply(function(plottable) {
      mesh = scope.markers[plottable.idx];

      // always use the original data plus the axis orientation
      mesh.position.set(
        plottable.coordinates[x] * scope.axesOrientation[0],
        plottable.coordinates[y] * scope.axesOrientation[1],
        is2D ? 0 : plottable.coordinates[z] * scope.axesOrientation[2]);
      mesh.updateMatrix();

      if (hasConfidenceIntervals) {
        mesh = scope.ellipsoids[plottable.idx];

        mesh.position.set(
          plottable.coordinates[x] * scope.axesOrientation[0],
          plottable.coordinates[y] * scope.axesOrientation[1],
          is2D ? 0 : plottable.coordinates[z] * scope.axesOrientation[2]);

        // flatten the ellipsoids ever so slightly
        mesh.scale.set(plottable.ci[x] / radius, plottable.ci[y] / radius,
                       is2D ? 0.01 : plottable.ci[z] / radius);

        mesh.updateMatrix();
      }
    });
  }
  else if (this.decomp.isArrowType()) {
    var target, arrow;

    this.decomp.apply(function(plottable) {
      arrow = scope.markers[plottable.idx];

      target = new THREE.Vector3(
        plottable.coordinates[x] * scope.axesOrientation[0],
        plottable.coordinates[y] * scope.axesOrientation[1],
        is2D ? 0 : plottable.coordinates[z] * scope.axesOrientation[2]);

      arrow.setPointsTo(target);
    });
  }

  // edges are made using THREE.LineSegments and a buffer geometry so updating
  // the position takes a bit more work but these objects will render faster
  if (this.decomp.edges.length) {
    this._redrawEdges();
  }
  this.needsUpdate = true;
};


/**
 *
 * Internal method to draw edges for plottables
 *
 * @param {Plottable[]} plottables An array of plottables for which the edges
 * should be redrawn. If this object is not supplied, all the edges are drawn.
 */
DecompositionView.prototype._redrawEdges = function(plottables) {
  var u, v, j = 0, left = [], right = [];
  var x = this.visibleDimensions[0], y = this.visibleDimensions[1],
      z = this.visibleDimensions[2], scope = this,
      is2D = (z === null), drawAll = (plottables === undefined);

  this.decomp.edges.forEach(function(edge) {
    u = edge[0];
    v = edge[1];

    if (drawAll ||
        (plottables.indexOf(u) !== -1 || plottables.indexOf(v) !== -1)) {

      center = [(u.coordinates[x] + v.coordinates[x]) / 2,
                (u.coordinates[y] + v.coordinates[y]) / 2,
                is2D ? 0 : (u.coordinates[z] + v.coordinates[z]) / 2];

      left = [u.coordinates[x], u.coordinates[y],
              is2D ? 0 : u.coordinates[z]];
      right = [v.coordinates[x], v.coordinates[y],
               is2D ? 0 : v.coordinates[z]];

      scope.lines.left.setLineAtIndex(j, left, center);
      scope.lines.right.setLineAtIndex(j, right, center);
    }

    j++;
  });

  // otherwise the geometry will remain unchanged
  this.lines.left.geometry.attributes.position.needsUpdate = true;
  this.lines.right.geometry.attributes.position.needsUpdate = true;

  this.needsUpdate = true;
};

/**
 *
 * Change the visible coordinates
 *
 * @param {integer[]} newDims An Array of integers in which each integer is the
 * index to the principal coordinate to show
 *
 */
DecompositionView.prototype.changeVisibleDimensions = function(newDims) {
  if (newDims.length < 2 || newDims.length > 3) {
    throw new Error('Only three dimensions can be shown at the same time');
  }

  // one by one, find and update the dimensions that are changing
  for (var i = 0; i < newDims.length; i++) {
    if (this.visibleDimensions[i] !== newDims[i]) {
      // index represents the global position of the dimension
      var index = this.visibleDimensions[i],
          orientation = this.axesOrientation[i];

      // 1.- Correct the limits of the ranges for the dimension that we are
      // moving out of the scene i.e. the old dimension
      if (this.axesOrientation[i] === -1) {
        var max = this.decomp.dimensionRanges.max[index];
        var min = this.decomp.dimensionRanges.min[index];
        this.decomp.dimensionRanges.max[index] = min * (-1);
        this.decomp.dimensionRanges.min[index] = max * (-1);
      }

      // 2.- Set the orientation of the new dimension to be 1
      this.axesOrientation[i] = 1;

      // 3.- Update the visible dimensions to include the new value
      this.visibleDimensions[i] = newDims[i];
    }
  }

  this.updatePositions();
};

/**
 *
 * Reorient one of the visible dimensions.
 *
 * @param {integer} index The index of the dimension to re-orient, if this
 * dimension is not visible i.e. not in `this.visibleDimensions`, then the
 * method will return right away.
 *
 */
DecompositionView.prototype.flipVisibleDimension = function(index) {
  var scope = this, newMin, newMax;

  // the index in the visible dimensions
  var localIndex = this.visibleDimensions.indexOf(index);

  if (localIndex !== -1) {
    // update the ranges for this decomposition
    var max = this.decomp.dimensionRanges.max[index];
    var min = this.decomp.dimensionRanges.min[index];
    this.decomp.dimensionRanges.max[index] = min * (-1);
    this.decomp.dimensionRanges.min[index] = max * (-1);

    // and update the state of the orientation
    this.axesOrientation[localIndex] *= -1;

    this.updatePositions();
  }
};

/**
 * Change the plottables attributes based on the metadata category using the
 * provided setPlottableAttributes function
 *
 * @param {object} attributes Key:value pairs of elements and values to change
 * in plottables.
 * @param {function} setPlottableAttributes Helper function to change the
 * values of plottables, in general this should be implemented in the
 * controller but it can be nullable if not needed. setPlottableAttributes
 * should receive: the scope where the plottables exist, the value to be
 * applied to the plottables and the plotables to change. For more info
 * see ColorViewController.setPlottableAttribute
 * @see ColorViewController.setPlottableAttribute
 * @param {string} category The category/column in the mapping file
 *
 * @return {object[]} Array of objects to be consumed by Slick grid.
 *
 */
DecompositionView.prototype.setCategory = function(attributes,
                                                   setPlottableAttributes,
                                                   category) {
  var scope = this, dataView = [], plottables;

  var fieldValues = util.naturalSort(_.keys(attributes));

  _.each(fieldValues, function(fieldVal, index) {
    /*
     *
     * WARNING: This is mixing attributes of the view with the model ...
     * it's a bit of a gray area though.
     *
     **/
    plottables = scope.decomp.getPlottablesByMetadataCategoryValue(category,
                                                                   fieldVal);
    if (setPlottableAttributes !== null) {
      setPlottableAttributes(scope, attributes[fieldVal], plottables);
    }

    dataView.push({id: index, category: fieldVal, value: attributes[fieldVal],
                   plottables: plottables});
  });
  this.needsUpdate = true;

  return dataView;
};

/**
 *
 * Hide edges where plottables are present.
 *
 * @param {Plottable[]} plottables An array of plottables for which the edges
 * should be hidden. If this object is not supplied, all the edges are hidden.
 */
DecompositionView.prototype.hideEdgesForPlottables = function(plottables) {
  // no edges to hide
  if (this.decomp.edges.length === 0) {
    return;
  }

  var u, v, j = 0, hideAll, scope = this;

  hideAll = plottables === undefined;

  this.decomp.edges.forEach(function(edge) {
    u = edge[0];
    v = edge[1];

    if (hideAll ||
        (plottables.indexOf(u) !== -1 || plottables.indexOf(v) !== -1)) {

      scope.lines.left.setLineAtIndex(j, [0, 0, 0], [0, 0, 0]);
      scope.lines.right.setLineAtIndex(j, [0, 0, 0], [0, 0, 0]);
    }
    j++;
  });

  // otherwise the geometry will remain unchanged
  this.lines.left.geometry.attributes.position.needsUpdate = true;
  this.lines.right.geometry.attributes.position.needsUpdate = true;
};

/**
 *
 * Hide edges where plottables are present.
 *
 * @param {Plottable[]} plottables An array of plottables for which the edges
 * should be hidden. If this object is not supplied, all the edges are hidden.
 */
DecompositionView.prototype.showEdgesForPlottables = function(plottables) {
  // no edges to show
  if (this.decomp.edges.length === 0) {
    return;
  }

  this._redrawEdges(plottables);
};

/**
 * Set the color for a group of plottables.
 *
 * @param {Object} color An object that can be interpreted as a color by the
 * THREE.Color class. Can be either a string like '#ff0000' or a number like
 * 0xff0000, or a CSS color name like 'red', etc.
 * @param {Plottable[]} group An array of plottables for which the color should
 * be set. If this object is not provided, all the plottables in the view will
 * have the color set.
 */
DecompositionView.prototype.setColor = function(color, group) {
  var idx, hasConfidenceIntervals, scope = this;

  group = group || this.decomp.plottable;
  hasConfidenceIntervals = this.decomp.hasConfidenceIntervals();

  if (this.UIState['view.usesPointCloud'] &&
      (this.UIState['view.viewType'] === 'scatter')) {
    var cloud = this.markers[0];
    color = new THREE.Color(color);

    group.forEach(function(plottable) {
      cloud.geometry.attributes.color.setXYZ(plottable.idx,
                                             color.r, color.g, color.b);
    });
    cloud.geometry.attributes.color.needsUpdate = true;
  }
  else if (this.UIState['view.viewType'] == 'parallel-plot' &&
           this.decomp.isScatterType()) {
    var lines = this.markers[0];
    color = new THREE.Color(color);
    var numPoints = (this.decomp.dimensions * 2 - 2);
    group.forEach(function(plottable) {
      var startIndex = plottable.idx * numPoints;
      var endIndex = (plottable.idx + 1) * numPoints;
      for (var i = startIndex; i < endIndex; i++)
        lines.geometry.attributes.color.setXYZ(i, color.r, color.g, color.b);
    });
    lines.geometry.attributes.color.needsUpdate = true;
  }
  else if (this.decomp.isScatterType()) {
    group.forEach(function(plottable) {
      idx = plottable.idx;
      scope.markers[idx].material.color = new THREE.Color(color);

      if (hasConfidenceIntervals) {
        scope.ellipsoids[idx].material.color = new THREE.Color(color);
      }
    });
  }
  else if (this.decomp.isArrowType()) {
    group.forEach(function(plottable) {
      scope.markers[plottable.idx].setColor(new THREE.Color(color));
    });
  }
  this.needsUpdate = true;
};

/**
 * Set the visibility for a group of plottables.
 *
 * @param {Bool} visible Whether or not the objects should be visible.
 * @param {Plottable[]} group An array of plottables for which the visibility
 * should be set. If this object is not provided, all the plottables in the
 * view will be have the visibility set.
 */
DecompositionView.prototype.setVisibility = function(visible, group) {
  var hasConfidenceIntervals, scope = this;

  group = group || this.decomp.plottable;

  hasConfidenceIntervals = this.decomp.hasConfidenceIntervals();

  if (this.UIState['view.usesPointCloud'] &&
      (this.UIState['view.viewType'] === 'scatter')) {
    var cloud = this.markers[0];

    _.each(group, function(plottable) {
      cloud.geometry.attributes.visible.setX(plottable.idx, visible * 1);
    });
    cloud.geometry.attributes.visible.needsUpdate = true;
  }
  else if (this.UIState['view.viewType'] == 'parallel-plot' &&
           this.decomp.isScatterType()) {
    var lines = this.markers[0];
    var numPoints = (this.decomp.dimensions * 2 - 2);
    _.each(group, function(plottable) {
      var startIndex = plottable.idx * numPoints;
      var endIndex = (plottable.idx + 1) * (numPoints);
      for (i = startIndex; i < endIndex; i++)
        lines.geometry.attributes.visible.setX(i, visible * 1);
    });
    lines.geometry.attributes.visible.needsUpdate = true;
  }
  else {
    _.each(group, function(plottable) {
      scope.markers[plottable.idx].visible = visible;

      if (hasConfidenceIntervals) {
        scope.ellipsoids[plottable.idx].visible = visible;
      }
    });
  }

  if (visible === true) {
    this.showEdgesForPlottables(group);
  }
  else {
    this.hideEdgesForPlottables(group);
  }

  this.needsUpdate = true;
};

/**
 * Set the scale for a group of plottables.
 *
 * @param {Float} scale The scale to set for the objects, relative to the
 * original size. Should be a positive and non-zero value.
 * @param {Plottable[]} group An array of plottables for which the scale
 * should be set. If this object is not provided, all the plottables in the
 * view will be have the scale set.
 */
DecompositionView.prototype.setScale = function(scale, group) {
  var scope = this;

  if (this.decomp.isArrowType()) {
    throw Error('Cannot change the scale of an arrow.');
  }

  group = group || this.decomp.plottable;

  if (this.UIState['view.usesPointCloud'] &&
      (this.UIState['view.viewType'] === 'scatter')) {
    var cloud = this.markers[0];

    _.each(group, function(plottable) {
      cloud.geometry.attributes.scale.setX(plottable.idx, scale);
    });
    cloud.geometry.attributes.scale.needsUpdate = true;
  }
  else if (this.UIState['view.viewType'] == 'parallel-plot' &&
           this.decomp.isScatterType()) {
    //Nothing to do for parallel plots.
  }
  else {
    _.each(group, function(element) {
      scope.markers[element.idx].scale.set(scale, scale, scale);
    });
  }
  this.needsUpdate = true;
};

/**
 * Set the opacity for a group of plottables.
 *
 * @param {Float} opacity The opacity value (from 0 to 1) for the selected
 * objects.
 * @param {Plottable[]} group An array of plottables for which the opacity
 * should be set. If this object is not provided, all the plottables in the
 * view will be have the opacity set.
 */
DecompositionView.prototype.setOpacity = function(opacity, group) {
  // webgl acts up with transparent objects, so we only set them to be
  // explicitly transparent if the opacity is not at full
  var transparent = opacity !== 1, funk, scope = this;

  group = group || this.decomp.plottable;

  if (this.UIState['view.usesPointCloud'] &&
      (this.UIState['view.viewType'] === 'scatter')) {
    var cloud = this.markers[0];

    _.each(group, function(plottable) {
      cloud.geometry.attributes.opacity.setX(plottable.idx, opacity);
    });
    cloud.geometry.attributes.opacity.needsUpdate = true;
  }
  else if (this.UIState['view.viewType'] == 'parallel-plot' &&
           this.decomp.isScatterType()) {
    var lines = this.markers[0];
    var numPoints = (this.decomp.dimensions * 2 - 2);
    _.each(group, function(plottable) {
      var startIndex = plottable.idx * numPoints;
      var endIndex = (plottable.idx + 1) * (numPoints);
      for (var i = startIndex; i < endIndex; i++)
        lines.geometry.attributes.opacity.setX(i, opacity);
    });
    lines.geometry.attributes.opacity.needsUpdate = true;
  }
  else {
    if (this.decomp.isScatterType()) {
      funk = _changeMeshOpacity;
    }
    else if (this.decomp.isArrowType()) {
      funk = _changeArrowOpacity;
    }

    _.each(group, function(plottable) {
      funk(scope.markers[plottable.idx], opacity, transparent);
    });
  }
  this.needsUpdate = true;
};

/**
 * Toggles the visibility of arrow labels
 *
 * @throws {Error} if this method is called on a scatter type.
 */
DecompositionView.prototype.toggleLabelVisibility = function() {
  if (this.decomp.isScatterType()) {
    throw new Error('Cannot hide labels of scatter types');
  }
  var scope = this;

  this.decomp.apply(function(plottable) {
    arrow = scope.markers[plottable.idx];
    arrow.label.visible = Boolean(arrow.label.visible ^ true);
  });
  this.needsUpdate = true;
};


/**
 * Set the emissive attribute of the markers
 *
 * @param {Bool} emissive Whether the object should be emissive.
 * @param {Plottable[]} group An array of plottables for which the emissive
 * attribute will be set. If this object is not provided, all the plottables in the
 * view will be have the scale set.
 */
DecompositionView.prototype.setEmissive = function(emissive, group) {
  group = group || this.decomp.plottable;

  if (this.decomp.isArrowType()) {
    throw new Error('Cannot set emissive attribute of arrows');
  }

  var i = 0, j = 0;

  if (this.UIState.getProperty('view.usesPointCloud') ||
      this.UIState.getProperty('view.viewType') === 'parallel-plot') {
    var emissives = this.markers[0].geometry.attributes.emissive;

    // the emissive attribute is a boolean one
    emissive = (emissive > 0) * 1;

    if (this.markers[0].isPoints) {
      for (i = 0; i < group.length; i++) {
        emissives.setX(group[i].idx, emissive);
      }
    }
    else if (this.markers[0].isLineSegments) {
      // line segments need to be repeated one per dimension
      for (i = 0; i < group.length; i++) {
        var numPoints = (this.decomp.dimensions * 2 - 2);
        var startIndex = group[i].idx * numPoints;
        var endIndex = (group[i].idx + 1) * (numPoints);

        for (j = startIndex; j < endIndex; j++) {
          emissives.setX(j, emissive);
        }
      }
    }
    emissives.needsUpdate = true;
  }
  else {
    for (i = 0; i < group.length; i++) {
      var material = this.markers[group[i].idx].material;
      material.emissive.set(emissive);
    }
  }

  this.needsUpdate = true;
};

/**
 * Group by color
 *
 * @param {Array} names An array of strings with the sample names.
 * @return {Object} Mapping of colors to objects.
 */
DecompositionView.prototype.groupByColor = function(names) {

  var colorGroups = {}, groupping, markers = this.markers;
  var plottables = this.decomp.getPlottableByIDs(names);

  // we need to retrieve colors in a very different way
  if (this.UIState['view.viewType'] === 'parallel-plot' ||
      this.UIState['view.usesPointCloud']) {
    var colors = this.markers[0].geometry.attributes.color;
    var numPoints = 1;

    if (this.markers[0].isLineSegments) {
        numPoints = (this.decomp.dimensions * 2 - 2);
    }

    groupping = function(plottable) {
      // taken from Color.getHexString in THREE.js
      r = (colors.getX(plottable.idx * numPoints) * 255) << 16;
      g = (colors.getY(plottable.idx * numPoints) * 255) << 8;
      b = (colors.getZ(plottable.idx * numPoints) * 255) << 0;
      return ('000000' + (r ^ g ^ b).toString(16)).slice(-6);
    };
  }
  else {
    if (this.decomp.isScatterType()) {
      groupping = function(plottable) {
        return markers[plottable.idx].material.color.getHexString();
      };
    }
    else {
      // check that this getColor method works
      groupping = function(plottable) {
        return markers[plottable.idx].getColor().getHexString();
      };
    }
  }

  return _.groupBy(plottables, groupping);
};

/**
 *
 * Helper that builds a vega specification off of the current view state
 *
 * @private
 */
DecompositionView.prototype._buildVegaSpec = function() {
  function rgbColor(colorObj) {
    var r = colorObj.r * 255;
    var g = colorObj.g * 255;
    var b = colorObj.b * 255;
    return 'rgb(' + r + ',' + g + ',' + b + ')';
  }

  // Maps THREE.js geometries to vega shapes
  var getShape = {
    Sphere: 'circle',
    Diamond: 'diamond',
    Cone: 'triangle-down',
    Cylinder: 'square',
    Ring: 'circle',
    Square: 'square',
    Icosahedron: 'cross',
    Star: 'cross'
  };

  function viewMarkersAsVegaDataset(markers) {
    var points = [], marker, i;
    for (i = 0; i < markers.length; i++) {
      marker = markers[i];
      if (marker.visible) {
        points.push({
          id: marker.name,
          x: marker.position.x,
          y: marker.position.y,
          color: rgbColor(marker.material.color),
          originalShape: marker.userData.shape,
          shape: getShape[marker.userData.shape],
          scale: { x: marker.scale.x, y: marker.scale.y },
          opacity: marker.material.opacity
        });
      }
    }
    return points;
  };

  // This is probably horribly slow on QIITA-scale MD files, probably needs
  // some attention
  function plottablesAsMetadata(points, header) {
    var md = [], point, row, i, j;
    for (i = 0; i < points.length; i++) {
      point = points[i];
      row = {};
      for (j = 0; j < header.length; j++) {
        row[header[j]] = point.metadata[j];
      }
      md.push(row);
    }
    return md;
  }

  var scope = this;
  var model = scope.decomp;

  var axisX = scope.visibleDimensions[0];
  var axisY = scope.visibleDimensions[1];

  var dimRanges = model.dimensionRanges;
  var rangeX = [dimRanges.min[axisX], dimRanges.max[axisX]];
  var rangeY = [dimRanges.min[axisY], dimRanges.max[axisY]];

  var baseWidth = 800;

  return {
    '$schema': 'https://vega.github.io/schema/vega/v5.json',
    padding: 5,
    background: scope.backgroundColor,
    config: {
      axis: { labelColor: scope.axesColor, titleColor: scope.axesColor },
      title: { color: scope.axesColor }
    },
    title: 'Emperor PCoA',
    data: [
      {
        name: 'metadata',
        values: plottablesAsMetadata(model.plottable, model.md_headers)
      },
      {
        name: 'points', values: viewMarkersAsVegaDataset(scope.markers),
        transform: [
          {
            type: 'lookup',
            from: 'metadata',
            key: model.md_headers[0],
            fields: ['id'],
            as: ['metadata']
          }
        ]
      }
    ],
    signals: [
      {
        name: 'width',
        update: baseWidth + ' * ((' + rangeX[1] + ') - (' + rangeX[0] + '))'
      },
      {
        name: 'height',
        update: baseWidth + ' * ((' + rangeY[1] + ') - (' + rangeY[0] + '))'
      }
    ],
    scales: [
      { name: 'xScale', range: 'width', domain: [rangeX[0], rangeX[1]] },
      { name: 'yScale', range: 'height', domain: [rangeY[0], rangeY[1]] }
    ],
    axes: [
      { orient: 'bottom', scale: 'xScale', title: model.axesLabels[axisX] },
      { orient: 'left', scale: 'yScale', title: model.axesLabels[axisY] }
    ],
    marks: [
      {
        type: 'symbol',
        from: {data: 'points'},
        encode: {
          enter: {
            fill: { field: 'color' },
            x: { scale: 'xScale', field: 'x' },
            y: { scale: 'yScale', field: 'y' },
            shape: { field: 'shape' },
            size: { signal: 'datum.scale.x * datum.scale.y * 100' },
            opacity: { field: 'opacity' }
          },
          update: {
            tooltip: { signal: 'datum.metadata' }
          }
        }
      }
    ]
  };
};

/**
 * Called as part of the swap operation to change out objects in the scene,
 * this function atomically clears the swap flag, clears the old markers,
 * and returns what the old markers were.
 */
DecompositionView.prototype.getAndClearOldMarkers = function() {
  this.needsSwapMarkers = false;
  var oldMarkers = this.oldMarkers;
  this.oldMarkers = [];
  return oldMarkers;
};

/**
 * Helper function to change the opacity of an arrow object.
 *
 * @private
 */
function _changeArrowOpacity(arrow, value, transparent) {
  arrow.line.material.transparent = transparent;
  arrow.line.material.opacity = value;

  arrow.cone.material.transparent = transparent;
  arrow.cone.material.opacity = value;
}

/**
 * Helper function to change the opacity of a mesh object.
 *
 * @private
 */
function _changeMeshOpacity(mesh, value, transparent) {
  mesh.material.transparent = transparent;
  mesh.material.opacity = value;
}

  return DecompositionView;
});