Add ProjectorScatterPlotAdapter, which turns 'hover, selected, neighbors' into
color / scale / opacity / label arrays to power the scatter plot.

I started moving the trace code to be array-driven like the rest of the render attributes, but it turns out that the easiest way to do that is to rewrite the trace visualizer as a generic "LineSegmentVisualizer" that accumulates all line segments into a single render call. Reverted but coming soon.
Change: 136351181
diff --git a/tensorflow/tensorboard/components/vz-projector/projectorScatterPlotAdapter.ts b/tensorflow/tensorboard/components/vz-projector/projectorScatterPlotAdapter.ts
new file mode 100644
index 0000000..1f5576e
--- /dev/null
+++ b/tensorflow/tensorboard/components/vz-projector/projectorScatterPlotAdapter.ts
@@ -0,0 +1,215 @@
+/* Copyright 2016 The TensorFlow Authors. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+==============================================================================*/
+
+import {DataSet} from './data';
+import {NearestEntry} from './knn';
+import {LabelRenderParams} from './renderContext';
+
+const LABEL_FONT_SIZE = 10;
+const LABEL_SCALE_DEFAULT = 1.0;
+const LABEL_SCALE_LARGE = 1.7;
+const LABEL_FILL_COLOR = 0x000000;
+const LABEL_STROKE_COLOR = 0xFFFFFF;
+
+const POINT_COLOR_UNSELECTED = 0x888888;
+const POINT_COLOR_NO_SELECTION = 0x7575D9;
+const POINT_COLOR_SELECTED = 0xFA6666;
+const POINT_COLOR_HOVER = 0x760B4F;
+
+const POINT_SCALE_DEFAULT = 1.0;
+const POINT_SCALE_SELECTED = 1.2;
+const POINT_SCALE_NEIGHBOR = 1.2;
+const POINT_SCALE_HOVER = 1.2;
+
+const LABELS_3D_COLOR_UNSELECTED = 0xFFFFFF;
+const LABELS_3D_COLOR_NO_SELECTION = 0xFFFFFF;
+
+/**
+ * Interprets projector events and assembes the arrays and commands necessary
+ * to use the ScatterPlot to render the current projected data set.
+ */
+export class ProjectorScatterPlotAdapter {
+  generateVisibleLabelRenderParams(
+      ds: DataSet, selectedPointIndices: number[],
+      neighborsOfFirstPoint: NearestEntry[],
+      hoverPointIndex: number): LabelRenderParams {
+    if (ds == null) {
+      return null;
+    }
+
+    const n = selectedPointIndices.length + neighborsOfFirstPoint.length +
+        ((hoverPointIndex != null) ? 1 : 0);
+
+    const visibleLabels = new Uint32Array(n);
+    const scale = new Float32Array(n);
+    const opacityFlags = new Int8Array(n);
+
+    scale.fill(LABEL_SCALE_DEFAULT);
+    opacityFlags.fill(1);
+
+    let dst = 0;
+
+    if (hoverPointIndex != null) {
+      visibleLabels[dst] = hoverPointIndex;
+      scale[dst] = LABEL_SCALE_LARGE;
+      opacityFlags[dst] = 0;
+      ++dst;
+    }
+
+    // Selected points
+    {
+      const n = selectedPointIndices.length;
+      for (let i = 0; i < n; ++i) {
+        visibleLabels[dst] = selectedPointIndices[i];
+        scale[dst] = LABEL_SCALE_LARGE;
+        opacityFlags[dst] = (n === 1) ? 0 : 1;
+        ++dst;
+      }
+    }
+
+    // Neighbors
+    {
+      const n = neighborsOfFirstPoint.length;
+      for (let i = 0; i < n; ++i) {
+        visibleLabels[dst++] = neighborsOfFirstPoint[i].index;
+      }
+    }
+
+    return new LabelRenderParams(
+        visibleLabels, scale, opacityFlags, LABEL_FONT_SIZE, LABEL_FILL_COLOR,
+        LABEL_STROKE_COLOR);
+  }
+
+  generatePointScaleFactorArray(
+      ds: DataSet, selectedPointIndices: number[],
+      neighborsOfFirstPoint: NearestEntry[],
+      hoverPointIndex: number): Float32Array {
+    if (ds == null) {
+      return new Float32Array(0);
+    }
+
+    const scale = new Float32Array(ds.points.length);
+    scale.fill(POINT_SCALE_DEFAULT);
+
+    // Scale up all selected points.
+    {
+      const n = selectedPointIndices.length;
+      for (let i = 0; i < n; ++i) {
+        const p = selectedPointIndices[i];
+        scale[p] = POINT_SCALE_SELECTED;
+      }
+    }
+
+    // Scale up the neighbor points.
+    {
+      const n = neighborsOfFirstPoint.length;
+      for (let i = 0; i < n; ++i) {
+        const p = neighborsOfFirstPoint[i].index;
+        scale[p] = POINT_SCALE_NEIGHBOR;
+      }
+    }
+
+    // Scale up the hover point.
+    if (hoverPointIndex != null) {
+      scale[hoverPointIndex] = POINT_SCALE_HOVER;
+    }
+
+    return scale;
+  }
+
+  generatePointColorArray(
+      ds: DataSet, legendPointColorer: (index: number) => string,
+      selectedPointIndices: number[], neighborsOfFirstPoint: NearestEntry[],
+      hoverPointIndex: number, label3dMode: boolean): Float32Array {
+    if (ds == null) {
+      return new Float32Array(0);
+    }
+
+    const colors = new Float32Array(ds.points.length * 3);
+
+    let unselectedColor = POINT_COLOR_UNSELECTED;
+    let noSelectionColor = POINT_COLOR_NO_SELECTION;
+
+    if (label3dMode) {
+      unselectedColor = LABELS_3D_COLOR_UNSELECTED;
+      noSelectionColor = LABELS_3D_COLOR_NO_SELECTION;
+    }
+
+    // Give all points the unselected color.
+    {
+      const n = ds.points.length;
+      let dst = 0;
+      if (selectedPointIndices.length > 0) {
+        const c = new THREE.Color(unselectedColor);
+        for (let i = 0; i < n; ++i) {
+          colors[dst++] = c.r;
+          colors[dst++] = c.g;
+          colors[dst++] = c.b;
+        }
+      } else {
+        if (legendPointColorer != null) {
+          for (let i = 0; i < n; ++i) {
+            const c = new THREE.Color(legendPointColorer(i));
+            colors[dst++] = c.r;
+            colors[dst++] = c.g;
+            colors[dst++] = c.b;
+          }
+        } else {
+          const c = new THREE.Color(noSelectionColor);
+          for (let i = 0; i < n; ++i) {
+            colors[dst++] = c.r;
+            colors[dst++] = c.g;
+            colors[dst++] = c.b;
+          }
+        }
+      }
+    }
+
+    // Color the selected points.
+    {
+      const n = selectedPointIndices.length;
+      const c = new THREE.Color(POINT_COLOR_SELECTED);
+      for (let i = 0; i < n; ++i) {
+        let dst = selectedPointIndices[i] * 3;
+        colors[dst++] = c.r;
+        colors[dst++] = c.g;
+        colors[dst++] = c.b;
+      }
+    }
+
+    // Color the neighbors.
+    {
+      const n = neighborsOfFirstPoint.length;
+      const c = new THREE.Color(POINT_COLOR_SELECTED);
+      for (let i = 0; i < n; ++i) {
+        let dst = neighborsOfFirstPoint[i].index * 3;
+        colors[dst++] = c.r;
+        colors[dst++] = c.g;
+        colors[dst++] = c.b;
+      }
+    }
+
+    // Color the hover point.
+    if (hoverPointIndex != null) {
+      const c = new THREE.Color(POINT_COLOR_HOVER);
+      let dst = hoverPointIndex * 3;
+      colors[dst++] = c.r;
+      colors[dst++] = c.g;
+      colors[dst++] = c.b;
+    }
+
+    return colors;
+  }
+}
diff --git a/tensorflow/tensorboard/components/vz-projector/renderContext.ts b/tensorflow/tensorboard/components/vz-projector/renderContext.ts
index 16ccf8f..3da699b 100644
--- a/tensorflow/tensorboard/components/vz-projector/renderContext.ts
+++ b/tensorflow/tensorboard/components/vz-projector/renderContext.ts
@@ -42,6 +42,9 @@
  * RenderContext contains all of the state required to color and render the data
  * set. ScatterPlot passes this to every attached visualizer as part of the
  * render callback.
+ * TODO(nicholsonc): This should only contain the data that's changed between
+ * each frame. Data like colors / scale factors / labels should be recomputed
+ * only when they change.
  */
 export class RenderContext {
   camera: THREE.Camera;
@@ -58,9 +61,9 @@
   constructor(
       camera: THREE.Camera, cameraTarget: THREE.Vector3, screenWidth: number,
       screenHeight: number, nearestCameraSpacePointZ: number,
-      farthestCameraSpacePointZ: number,
-      labelAccessor: (index: number) => string, pointColors: Float32Array,
-      pointScaleFactors: Float32Array, labels: LabelRenderParams) {
+      farthestCameraSpacePointZ: number, pointColors: Float32Array,
+      pointScaleFactors: Float32Array, labelAccessor: (index: number) => string,
+      labels: LabelRenderParams) {
     this.camera = camera;
     this.cameraTarget = cameraTarget;
     this.screenWidth = screenWidth;
diff --git a/tensorflow/tensorboard/components/vz-projector/scatterPlot.ts b/tensorflow/tensorboard/components/vz-projector/scatterPlot.ts
index fbc68e1..397b8ee 100644
--- a/tensorflow/tensorboard/components/vz-projector/scatterPlot.ts
+++ b/tensorflow/tensorboard/components/vz-projector/scatterPlot.ts
@@ -609,7 +609,7 @@
   update() {
     this.getPointsCoordinates();
     this.visualizers.forEach(v => {
-      v.onUpdate();
+      v.onUpdate(this.dataSet);
     });
     this.render();
   }
@@ -633,7 +633,7 @@
     const rc = new RenderContext(
         this.camera, this.orbitCameraControls.target, this.width, this.height,
         cameraSpacePointExtents[0], cameraSpacePointExtents[1],
-        this.labelAccessor, this.pointColors, this.pointScaleFactors,
+        this.pointColors, this.pointScaleFactors, this.labelAccessor,
         this.labels);
 
     // Render first pass to picking target. This render fills pickingTexture
diff --git a/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizer.ts b/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizer.ts
index eaa1fae..bac536e 100644
--- a/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizer.ts
+++ b/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizer.ts
@@ -61,7 +61,7 @@
    * Called when the projector updates application state (projection style,
    * etc). Generally followed by a render.
    */
-  onUpdate();
+  onUpdate(dataSet: DataSet);
   /**
    * Called when the canvas size changes.
    */
diff --git a/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizer3DLabels.ts b/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizer3DLabels.ts
index ae59a5b..92bbd32 100644
--- a/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizer3DLabels.ts
+++ b/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizer3DLabels.ts
@@ -194,7 +194,7 @@
     }
   }
 
-  private createLabelGeometry() {
+  private createLabelGeometry(dataSet: DataSet) {
     this.processLabelVerts();
     this.createColorBuffers();
 
@@ -220,7 +220,7 @@
     this.geometry.addAttribute('color', colors);
 
     let lettersSoFar = 0;
-    for (let i = 0; i < this.dataSet.points.length; i++) {
+    for (let i = 0; i < dataSet.points.length; i++) {
       let label: string = this.labelAccessor(i).toString();
       let leftOffset = 0;
       // Determine length of word in pixels.
@@ -267,8 +267,8 @@
       }
     }
 
-    for (let i = 0; i < this.dataSet.points.length; i++) {
-      let pp = this.dataSet.points[i].projectedPoint;
+    for (let i = 0; i < dataSet.points.length; i++) {
+      let pp = dataSet.points[i].projectedPoint;
       this.labelVertexMap[i].forEach((j) => {
         this.positions.setXYZ(j, pp[0], pp[1], pp[2]);
       });
@@ -287,10 +287,10 @@
     }
   }
 
-  private createLabels() {
+  private createLabels(dataSet: DataSet) {
     this.destroyLabels();
     if (this.labelAccessor) {
-      this.createLabelGeometry();
+      this.createLabelGeometry(dataSet);
     }
   }
 
@@ -320,7 +320,7 @@
       scene: THREE.Scene, sceneIs3D: boolean, backgroundColor: number) {
     this.scene = scene;
     if (this.labelsMesh == null) {
-      this.createLabels();
+      this.createLabels(this.dataSet);
     }
     if (this.labelsMesh) {
       scene.add(this.labelsMesh);
@@ -333,7 +333,7 @@
 
   onSetLabelAccessor(labelAccessor: (index: number) => string) {
     this.labelAccessor = labelAccessor;
-    this.onUpdate();
+    this.onUpdate(this.dataSet);
   }
 
   onDataSet(dataSet: DataSet, spriteImage: HTMLImageElement) {
@@ -361,8 +361,8 @@
     colors.needsUpdate = true;
   }
 
-  onUpdate() {
-    this.createLabels();
+  onUpdate(dataSet: DataSet) {
+    this.createLabels(dataSet);
     if (this.labelsMesh && this.scene) {
       this.scene.add(this.labelsMesh);
     }
diff --git a/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizerAxes.ts b/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizerAxes.ts
index 34197f1..b3c7825 100644
--- a/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizerAxes.ts
+++ b/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizerAxes.ts
@@ -83,7 +83,7 @@
 
   onPickingRender(renderContext: RenderContext) {}
   onRender(renderContext: RenderContext) {}
-  onUpdate() {}
+  onUpdate(dataSet: DataSet) {}
   onResize(newWidth: number, newHeight: number) {}
   onSetLabelAccessor(labelAccessor: (index: number) => string) {}
 }
diff --git a/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizerSprites.ts b/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizerSprites.ts
index a350b32..d79718b 100644
--- a/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizerSprites.ts
+++ b/tensorflow/tensorboard/components/vz-projector/scatterPlotVisualizerSprites.ts
@@ -272,16 +272,16 @@
     this.geometry.addAttribute('scaleFactor', scaleFactors);
   }
 
-  private updatePositionsArray() {
+  private updatePositionsArray(dataSet: DataSet) {
     if (this.geometry == null) {
       return;
     }
-    const n = this.dataSet.points.length;
+    const n = dataSet.points.length;
     const positions =
         this.geometry.getAttribute('position') as THREE.BufferAttribute;
     positions.array = new Float32Array(n * XYZ_NUM_ELEMENTS);
     for (let i = 0; i < n; i++) {
-      let pp = this.dataSet.points[i].projectedPoint;
+      let pp = dataSet.points[i].projectedPoint;
       positions.setXYZ(i, pp[0], pp[1], pp[2]);
     }
     positions.needsUpdate = true;
@@ -309,12 +309,12 @@
     scene.fog = this.fog;
     if (this.dataSet) {
       this.addSprites(scene);
-      this.updatePositionsArray();
+      this.updatePositionsArray(this.dataSet);
     }
   }
 
-  onUpdate() {
-    this.updatePositionsArray();
+  onUpdate(dataSet: DataSet) {
+    this.updatePositionsArray(dataSet);
   }
 
   onResize(newWidth: number, newHeight: number) {}
diff --git a/tensorflow/tensorboard/components/vz-projector/vz-projector.ts b/tensorflow/tensorboard/components/vz-projector/vz-projector.ts
index 2556108..0d36fca 100644
--- a/tensorflow/tensorboard/components/vz-projector/vz-projector.ts
+++ b/tensorflow/tensorboard/components/vz-projector/vz-projector.ts
@@ -14,11 +14,11 @@
 ==============================================================================*/
 
 import {ColorOption, DataProto, DataSet, MetadataInfo, Projection, State} from './data';
-import * as logging from './logging';
 import {DataProvider, getDataProvider, ServingMode, TensorInfo} from './data-loader';
 import {HoverContext, HoverListener} from './hoverContext';
 import * as knn from './knn';
-import {LabelRenderParams} from './renderContext';
+import * as logging from './logging';
+import {ProjectorScatterPlotAdapter} from './projectorScatterPlotAdapter';
 import {Mode, ScatterPlot} from './scatterPlot';
 import {ScatterPlotVisualizer3DLabels} from './scatterPlotVisualizer3DLabels';
 import {ScatterPlotVisualizerCanvasLabels} from './scatterPlotVisualizerCanvasLabels';
@@ -33,31 +33,12 @@
 // tslint:disable-next-line:no-unused-variable
 import {PolymerElement, PolymerHTMLElement} from './vz-projector-util';
 
-const LABEL_FONT_SIZE = 10;
-const LABEL_SCALE_DEFAULT = 1.0;
-const LABEL_SCALE_LARGE = 1.7;
-const LABEL_FILL_COLOR = 0x000000;
-const LABEL_STROKE_COLOR = 0xFFFFFF;
-
-const POINT_COLOR_UNSELECTED = 0x888888;
-const POINT_COLOR_NO_SELECTION = 0x7575D9;
-const POINT_COLOR_SELECTED = 0xFA6666;
-const POINT_COLOR_HOVER = 0x760B4F;
-const POINT_COLOR_MISSING = 'black';
-
-const LABELS_3D_COLOR_UNSELECTED = 0xFFFFFF;
-const LABELS_3D_COLOR_NO_SELECTION = 0xFFFFFF;
-
-const POINT_SCALE_DEFAULT = 1.0;
-const POINT_SCALE_SELECTED = 1.2;
-const POINT_SCALE_NEIGHBOR = 1.2;
-const POINT_SCALE_HOVER = 1.2;
-
 /**
  * The minimum number of dimensions the data should have to automatically
  * decide to normalize the data.
  */
 const THRESHOLD_DIM_NORMALIZE = 50;
+const POINT_COLOR_MISSING = 'black';
 
 export let ProjectorPolymer = PolymerElement({
   is: 'vz-projector',
@@ -79,6 +60,7 @@
 
   private dataSet: DataSet;
   private dom: d3.Selection<any>;
+  private projectorScatterPlotAdapter: ProjectorScatterPlotAdapter;
   private scatterPlot: ScatterPlot;
   private dim: number;
 
@@ -256,92 +238,6 @@
     return colorer;
   }
 
-  private generateVisibleLabelRenderParams(
-      selectedPointIndices: number[], neighborsOfFirstPoint: knn.NearestEntry[],
-      hoverPointIndex: number): LabelRenderParams {
-    if (this.currentDataSet == null) {
-      return null;
-    }
-
-    const n = selectedPointIndices.length + neighborsOfFirstPoint.length +
-        ((hoverPointIndex != null) ? 1 : 0);
-
-    const visibleLabels = new Uint32Array(n);
-    const scale = new Float32Array(n);
-    const opacityFlags = new Int8Array(n);
-
-    scale.fill(LABEL_SCALE_DEFAULT);
-    opacityFlags.fill(1);
-
-    let dst = 0;
-
-    if (hoverPointIndex != null) {
-      visibleLabels[dst] = hoverPointIndex;
-      scale[dst] = LABEL_SCALE_LARGE;
-      opacityFlags[dst] = 0;
-      ++dst;
-    }
-
-    // Selected points
-    {
-      const n = selectedPointIndices.length;
-      for (let i = 0; i < n; ++i) {
-        visibleLabels[dst] = selectedPointIndices[i];
-        scale[dst] = LABEL_SCALE_LARGE;
-        opacityFlags[dst] = (n === 1) ? 0 : 1;
-        ++dst;
-      }
-    }
-
-    // Neighbors
-    {
-      const n = neighborsOfFirstPoint.length;
-      for (let i = 0; i < n; ++i) {
-        visibleLabels[dst++] = neighborsOfFirstPoint[i].index;
-      }
-    }
-
-    return new LabelRenderParams(
-        visibleLabels, scale, opacityFlags, LABEL_FONT_SIZE, LABEL_FILL_COLOR,
-        LABEL_STROKE_COLOR);
-  }
-
-  private generateScatterPlotScaleFactorArray(
-      selectedPointIndices: number[], neighborsOfFirstPoint: knn.NearestEntry[],
-      hoverPointIndex: number): Float32Array {
-    if (this.currentDataSet == null) {
-      return new Float32Array(0);
-    }
-
-    const scale = new Float32Array(this.currentDataSet.points.length);
-    scale.fill(POINT_SCALE_DEFAULT);
-
-    // Scale up all selected points.
-    {
-      const n = selectedPointIndices.length;
-      for (let i = 0; i < n; ++i) {
-        const p = selectedPointIndices[i];
-        scale[p] = POINT_SCALE_SELECTED;
-      }
-    }
-
-    // Scale up the neighbor points.
-    {
-      const n = neighborsOfFirstPoint.length;
-      for (let i = 0; i < n; ++i) {
-        const p = neighborsOfFirstPoint[i].index;
-        scale[p] = POINT_SCALE_NEIGHBOR;
-      }
-    }
-
-    // Scale up the hover point.
-    if (hoverPointIndex != null) {
-      scale[hoverPointIndex] = POINT_SCALE_HOVER;
-    }
-
-    return scale;
-  }
-
   private get3DLabelModeButton(): any {
     return this.querySelector('#labels3DMode');
   }
@@ -351,90 +247,6 @@
     return (label3DModeButton as any).active;
   }
 
-  private generateScatterPlotColorArray(
-      legendPointColorer: (index: number) => string,
-      selectedPointIndices: number[], neighborsOfFirstPoint: knn.NearestEntry[],
-      hoverPointIndex: number): Float32Array {
-    if (this.currentDataSet == null) {
-      return new Float32Array(0);
-    }
-
-    const colors = new Float32Array(this.currentDataSet.points.length * 3);
-
-    let unselectedColor = POINT_COLOR_UNSELECTED;
-    let noSelectionColor = POINT_COLOR_NO_SELECTION;
-
-    if (this.get3DLabelMode()) {
-      unselectedColor = LABELS_3D_COLOR_UNSELECTED;
-      noSelectionColor = LABELS_3D_COLOR_NO_SELECTION;
-    }
-
-    // Give all points the unselected color.
-    {
-      const n = this.currentDataSet.points.length;
-      let dst = 0;
-      if (selectedPointIndices.length > 0) {
-        const c = new THREE.Color(unselectedColor);
-        for (let i = 0; i < n; ++i) {
-          colors[dst++] = c.r;
-          colors[dst++] = c.g;
-          colors[dst++] = c.b;
-        }
-      } else {
-        if (legendPointColorer != null) {
-          for (let i = 0; i < n; ++i) {
-            const c = new THREE.Color(legendPointColorer(i));
-            colors[dst++] = c.r;
-            colors[dst++] = c.g;
-            colors[dst++] = c.b;
-          }
-        } else {
-          const c = new THREE.Color(noSelectionColor);
-          for (let i = 0; i < n; ++i) {
-            colors[dst++] = c.r;
-            colors[dst++] = c.g;
-            colors[dst++] = c.b;
-          }
-        }
-      }
-    }
-
-    // Color the selected points.
-    {
-      const n = selectedPointIndices.length;
-      const c = new THREE.Color(POINT_COLOR_SELECTED);
-      for (let i = 0; i < n; ++i) {
-        let dst = selectedPointIndices[i] * 3;
-        colors[dst++] = c.r;
-        colors[dst++] = c.g;
-        colors[dst++] = c.b;
-      }
-    }
-
-    // Color the neighbors.
-    {
-      const n = neighborsOfFirstPoint.length;
-      const c = new THREE.Color(POINT_COLOR_SELECTED);
-      for (let i = 0; i < n; ++i) {
-        let dst = neighborsOfFirstPoint[i].index * 3;
-        colors[dst++] = c.r;
-        colors[dst++] = c.g;
-        colors[dst++] = c.b;
-      }
-    }
-
-    // Color the hover point.
-    if (hoverPointIndex != null) {
-      const c = new THREE.Color(POINT_COLOR_HOVER);
-      let dst = hoverPointIndex * 3;
-      colors[dst++] = c.r;
-      colors[dst++] = c.g;
-      colors[dst++] = c.b;
-    }
-
-    return colors;
-  }
-
   clearSelectionAndHover() {
     this.notifySelectionChanged([]);
     this.notifyHoverOverPoint(null);
@@ -497,6 +309,8 @@
       this.scatterPlot.resize();
     });
 
+    this.projectorScatterPlotAdapter = new ProjectorScatterPlotAdapter();
+
     this.scatterPlot = new ScatterPlot(
         this.getScatterContainer(), i => '' +
             this.currentDataSet.points[i].metadata[this.selectedLabelOption],
@@ -534,16 +348,22 @@
   }
 
   private updateScatterPlot() {
-    const pointColors = this.generateScatterPlotColorArray(
-        this.getLegendPointColorer(this.selectedColorOption),
-        this.selectedPointIndices, this.neighborsOfFirstPoint,
-        this.hoverPointIndex);
-    const pointScaleFactors = this.generateScatterPlotScaleFactorArray(
-        this.selectedPointIndices, this.neighborsOfFirstPoint,
-        this.hoverPointIndex);
-    const labels = this.generateVisibleLabelRenderParams(
-        this.selectedPointIndices, this.neighborsOfFirstPoint,
-        this.hoverPointIndex);
+    const dataSet = this.currentDataSet;
+    const selectedSet = this.selectedPointIndices;
+    const hoverIndex = this.hoverPointIndex;
+    const neighbors = this.neighborsOfFirstPoint;
+    const pointColorer = this.getLegendPointColorer(this.selectedColorOption);
+
+    const pointColors =
+        this.projectorScatterPlotAdapter.generatePointColorArray(
+            dataSet, pointColorer, selectedSet, neighbors, hoverIndex,
+            this.get3DLabelMode());
+    const pointScaleFactors =
+        this.projectorScatterPlotAdapter.generatePointScaleFactorArray(
+            dataSet, selectedSet, neighbors, hoverIndex);
+    const labels =
+        this.projectorScatterPlotAdapter.generateVisibleLabelRenderParams(
+            dataSet, selectedSet, neighbors, hoverIndex);
 
     this.scatterPlot.setPointColors(pointColors);
     this.scatterPlot.setPointScaleFactors(pointScaleFactors);
@@ -564,10 +384,8 @@
       scatterPlot.addVisualizer(new ScatterPlotVisualizer3DLabels());
     } else {
       scatterPlot.addVisualizer(new ScatterPlotVisualizerSprites());
-
       scatterPlot.addVisualizer(
           new ScatterPlotVisualizerTraces(selectionContext));
-
       scatterPlot.addVisualizer(
           new ScatterPlotVisualizerCanvasLabels(this.getScatterContainer()));
     }