From 2b7c04fce1c5f3429b1fbc7cf5dd19e7d8074499 Mon Sep 17 00:00:00 2001
From: James Hadfield <jh22@sanger.ac.uk>
Date: Fri, 2 Feb 2018 14:26:36 -0800
Subject: [PATCH 01/10] clean up treeHelpers.js (eslint, remove unused
 functions etc)

---
 src/components/tree/processNodes.js |   4 +-
 src/components/tree/treeHelpers.js  | 186 +++++++++-------------------
 2 files changed, 57 insertions(+), 133 deletions(-)

diff --git a/src/components/tree/processNodes.js b/src/components/tree/processNodes.js
index deda1508d..2e224cda7 100644
--- a/src/components/tree/processNodes.js
+++ b/src/components/tree/processNodes.js
@@ -1,11 +1,9 @@
-import { calcFullTipCounts, calcBranchLength, calcDates } from "./treeHelpers";
+import { calcFullTipCounts } from "./treeHelpers";
 
 export const processNodes = (nodes) => {
   const rootNode = nodes[0];
   nodes.forEach((d) => {if (typeof d.attr === "undefined") {d.attr = {};} });
   calcFullTipCounts(rootNode);
-  calcBranchLength(rootNode);
-  calcDates(nodes);
   nodes.forEach((d) => {d.hasChildren = typeof d.children !== "undefined";});
   /* set an index so that we can access visibility / nodeColors if needed */
   nodes.map((d, idx) => {d.arrayIdx = idx;});
diff --git a/src/components/tree/treeHelpers.js b/src/components/tree/treeHelpers.js
index 040534fd8..9e7cc45e6 100644
--- a/src/components/tree/treeHelpers.js
+++ b/src/components/tree/treeHelpers.js
@@ -1,4 +1,3 @@
-/* eslint-disable */
 
 import { scalePow } from "d3-scale";
 import { tipRadius, freqScale, tipRadiusOnLegendMatch } from "../../util/globals";
@@ -70,69 +69,11 @@ export const appendParentsToTree = (root) => {
 
 };
 
-export const gatherTips = (node, tips) => {
-
-  if (typeof node.children !== "undefined") {
-    for (let i = 0, c = node.children.length; i < c; i++) {
-      gatherTips(node.children[i], tips);
-    }
-  } else {
-    tips.push(node);
-  }
-  return tips;
-};
-
-export const getVaccines = (tips) => {
-  const v = [];
-  tips.forEach((tip) => {
-    if (vaccineStrains.indexOf(tip.strain) !== -1) {
-      tip.choice = vaccineChoice[tip.strain];
-      v.push(tip);
-    }
-  });
-  return v;
-};
-
-export const calcDates = (nodes) => {
-  nodes.forEach((d) => {
-    d.dateval = new Date(d.date);
-  });
-};
-
-export const minimumAttribute = (node, attr, min) => {
-  if (typeof node.children !== "undefined") {
-    for (let i = 0, c = node.children.length; i < c; i++) {
-      min = minimumAttribute(node.children[i], attr, min);
-    }
-  } else if (node[attr] < min) {
-    min = node[attr];
-  }
-  return min;
-};
-
-export const maximumAttribute = (node, attr, max) => {
-  if (typeof node.children !== "undefined") {
-    for (let i = 0, c = node.children.length; i < c; i++) {
-      max = maximumAttribute(node.children[i], attr, max);
-    }
-  } else if (node[attr] > max) {
-    max = node[attr];
-  }
-  return max;
-};
-
-export const calcBranchLength = (node) => {
-  if (typeof node.children !== "undefined") {
-    for (let i = 0, c = node.children.length; i < c; i++) {
-      calcBranchLength(node.children[i]);
-      node.children[i].branch_length = node.children[i].xvalue - node.xvalue;
-    }
-  }
-};
-
 /**
- * for each node, calculate the number of subtending tips (alive or dead)
-**/
+* for each node, calculate the number of subtending tips (alive or dead)
+* side effects: n.fullTipCount for each node
+*  @param root - deserialized JSON root to begin traversal
+*/
 export const calcFullTipCounts = (node) => {
   node.fullTipCount = 0;
   if (typeof node.children !== "undefined") {
@@ -145,10 +86,11 @@ export const calcFullTipCounts = (node) => {
   }
 };
 
-
 /**
- * for each node, calculate the number of tips in view.
-**/
+* for each node, calculate the number of subtending tips which are visible
+* side effects: n.tipCount for each node
+*  @param root - deserialized JSON root to begin traversal
+*/
 export const calcTipCounts = (node, visibility) => {
   node.tipCount = 0;
   if (typeof node.children !== "undefined") {
@@ -162,41 +104,15 @@ export const calcTipCounts = (node, visibility) => {
 };
 
 /**
-sets each node in the tree to alive=true if it has at least one descendent with current=true
-**/
-export const setNodeAlive = (node) => {
-  if (typeof node.children !== "undefined") {
-    let aliveChildren = false;
-    for (let i = 0, c = node.children.length; i < c; i++) {
-      setNodeAlive(node.children[i]);
-      aliveChildren = aliveChildren || node.children[i].alive;
-    }
-    node.alive = aliveChildren;
-  } else {
-    node.alive = node.current;
-  }
-};
-
-
-export const adjust_freq_by_date = (nodes, rootNode) => {
-  // console.log("all nodes and root node", nodes, rootNode)
-  return nodes.map((d) => {
-    // console.log("tipcount & rootnodeTipcount", d.tipCount, rootNode.tipCount)
-    // d.frequency = (d.tipCount) / rootNode.tipCount;
-  });
-};
-
-// export const arrayInEquality = function(a,b) {
-//   if (a&&b){
-//     const eq = a.map((d,i)=>d!==b[i]);
-//     return eq.some((d)=>d);
-//   }else{
-//     return true;
-//   }
-// };
-
-// branch thickness is from clade frequencies
-export const calcBranchThickness = function (nodes, visibility, rootIdx) {
+* calculates (and returns) an array of node (branch) thicknesses.
+* If the node isn't visible, the thickness is 1.
+* No side effects.
+* @param nodes - JSON nodes
+* @param visibility - visibility array (1-1 with nodes)
+* @param rootIdx - nodes index of the currently in-view root
+* @returns array of thicknesses (numeric)
+*/
+export const calcBranchThickness = (nodes, visibility, rootIdx) => {
   let maxTipCount = nodes[rootIdx].tipCount;
   /* edge case: no tips selected */
   if (!maxTipCount) {
@@ -207,6 +123,8 @@ export const calcBranchThickness = function (nodes, visibility, rootIdx) {
   ));
 };
 
+/* a getter for the value of the colour attribute of the node provided for the currently set colour
+note this is not the colour HEX */
 export const getTipColorAttribute = (node, colorScale) => {
   if (colorScale.colorBy.slice(0, 3) === "gt-" && colorScale.genotype) {
     return node.currentGt;
@@ -214,6 +132,8 @@ export const getTipColorAttribute = (node, colorScale) => {
   return node.attr[colorScale.colorBy];
 };
 
+/* generates and returns an array of colours (HEXs) for the nodes under the given colorScale */
+/* takes around 2ms on a 2000 tip tree */
 export const calcNodeColor = (tree, colorScale) => {
   if (tree && tree.nodes && colorScale && colorScale.colorBy) {
     const nodeColorAttr = tree.nodes.map((n) => getTipColorAttribute(n, colorScale));
@@ -223,11 +143,18 @@ export const calcNodeColor = (tree, colorScale) => {
   return null;
 };
 
+/**
+* equates a single tip and a legend element
+* exact match is required for categorical qunantities such as genotypes, regions
+* continuous variables need to fall into the interal (lower_bound[leg], leg]
+* @param selectedLegendItem - value of the selected tip attribute (numeric or string)
+* @param node - node (tip) in question
+* @param legendBoundsMap - if falsey, then exact match required. Else contains bounds for match.
+* @param colorScale - used to get the value of the attribute being used for colouring
+* @returns bool
+*/
 const determineLegendMatch = (selectedLegendItem, node, legendBoundsMap, colorScale) => {
   const nodeAttr = getTipColorAttribute(node, colorScale);
-  // equates a tip and a legend element
-  // exact match is required for categorical qunantities such as genotypes, regions
-  // continuous variables need to fall into the interal (lower_bound[leg], leg]
   if (legendBoundsMap) {
     return (nodeAttr <= legendBoundsMap.upper_bound[selectedLegendItem]) &&
            (nodeAttr > legendBoundsMap.lower_bound[selectedLegendItem]);
@@ -235,27 +162,27 @@ const determineLegendMatch = (selectedLegendItem, node, legendBoundsMap, colorSc
   return nodeAttr === selectedLegendItem;
 };
 
+/**
+* produces the array of tip radii - if nothing's selected this is the hardcoded tipRadius
+* if there's a selectedLegendItem, then values will be small (like normal) or big (for those tips selected)
+* @param selectedLegendItem - value of the selected tip attribute (numeric or string)
+* @param colorScale - node (tip) in question
+* @param tree
+* @returns null (if data not ready) or array of tip radii
+*/
 export const calcTipRadii = (selectedLegendItem, colorScale, tree) => {
   if (selectedLegendItem && tree && tree.nodes) {
     const legendMap = colorScale.continuous ? colorScale.legendBoundsMap : false;
     return tree.nodes.map((d) => determineLegendMatch(selectedLegendItem, d, legendMap, colorScale) ? tipRadiusOnLegendMatch : tipRadius);
   } else if (tree && tree.nodes) {
-    return tree.nodes.map((d) => tipRadius);
+    return tree.nodes.map(() => tipRadius);
   }
   return null; // fallthrough
 };
 
-const parseFilterQuery = function (query) {
-  const tmp = query.split("-").map((d) => d.split("."));
-  return {
-    "fields": tmp.map((d) => d[0]),
-    "filters": tmp.map((d) => d[d.length - 1].split(","))
-  };
-};
-
 /* recursively mark the parents of a given node active
 by setting the node idx to true in the param visArray */
-const makeParentVisible = function (visArray, node) {
+const makeParentVisible = (visArray, node) => {
   if (node.arrayIdx === 0 || visArray[node.parent.arrayIdx]) {
     return; // this is the root of the tree or the parent was already visibile
   }
@@ -300,22 +227,21 @@ FILTERS:
  - filterPairs is a list of lists. Each list defines the filtering to do.
    i.e. [ [ region, [...values]], [authors, [...values]]]
 */
-export const calcVisibility = function (tree, controls, dates) {
+export const calcVisibility = (tree, controls, dates) => {
   if (tree.nodes) {
     /* reset visibility */
-    let visibility = tree.nodes.map((d) => {
-      return true;
-    });
+    let visibility = tree.nodes.map(() => true);
 
     // if we have an analysis slider active, then we must filter on that as well
     // note that min date for analyis doesnt apply
-    if (controls.analysisSlider && controls.analysisSlider.valid) {
-      /* extra slider is numerical rounded to 2dp */
-      const valid = tree.nodes.map((d) =>
-        d.attr[controls.analysisSlider.key] ? Math.round(d.attr[controls.analysisSlider.key] * 100) / 100 <= controls.analysisSlider.value : true
-      );
-      visibility = visibility.map((cv, idx) => (cv && valid[idx]));
-    }
+    // commented out as analysis slider will probably be removed soon!
+    // if (controls.analysisSlider && controls.analysisSlider.valid) {
+    //   /* extra slider is numerical rounded to 2dp */
+    //   const valid = tree.nodes.map((d) =>
+    //     d.attr[controls.analysisSlider.key] ? Math.round(d.attr[controls.analysisSlider.key] * 100) / 100 <= controls.analysisSlider.value : true
+    //   );
+    //   visibility = visibility.map((cv, idx) => (cv && valid[idx]));
+    // }
 
     // IN VIEW FILTERING (internal + terminal nodes)
     /* edge case: this fn may be called before the shell structure of the nodes
@@ -324,7 +250,7 @@ export const calcVisibility = function (tree, controls, dates) {
     let inView;
     try {
       inView = tree.nodes.map((d) => d.shell.inView);
-    } catch(e) {
+    } catch (e) {
       inView = tree.nodes.map(() => true);
     }
     /* intersect visibility and inView */
@@ -332,7 +258,7 @@ export const calcVisibility = function (tree, controls, dates) {
 
     // FILTERS
     const filterPairs = [];
-    Object.keys(controls.filters).map((key) => {
+    Object.keys(controls.filters).forEach((key) => {
       if (controls.filters[key].length) {
         filterPairs.push([key, controls.filters[key]]);
       }
@@ -355,7 +281,7 @@ export const calcVisibility = function (tree, controls, dates) {
     }
 
     // TIME FILTERING (internal + terminal nodes)
-    const timeFiltered = tree.nodes.map((d, idx) => {
+    const timeFiltered = tree.nodes.map((d) => {
       return !(d.attr.num_date < dates.dateMinNumeric || d.parent.attr.num_date > dates.dateMaxNumeric);
     });
     visibility = visibility.map((cv, idx) => (cv && timeFiltered[idx]));
@@ -388,6 +314,6 @@ export const processVaccines = (nodes, vaccineChoices) => {
   if (!vaccineChoices) {return false;}
   const names = Object.keys(vaccineChoices);
   const vaccines = nodes.filter((d) => names.indexOf(d.strain) !== -1);
-  vaccines.forEach((d) => d.vaccineDate = vaccineChoices[d.strain]);
+  vaccines.forEach((d) => {d.vaccineDate = vaccineChoices[d.strain];});
   return vaccines;
-}
+};

From 0a2ac5aae8426e7603f3bb84badbdffb470dbd7d Mon Sep 17 00:00:00 2001
From: James Hadfield <jh22@sanger.ac.uk>
Date: Fri, 2 Feb 2018 15:12:25 -0800
Subject: [PATCH 02/10] remove processNodes.js

---
 src/components/tree/processNodes.js | 78 -----------------------------
 src/components/tree/treeHelpers.js  | 20 +++++++-
 src/reducers/tree.js                |  4 +-
 3 files changed, 20 insertions(+), 82 deletions(-)
 delete mode 100644 src/components/tree/processNodes.js

diff --git a/src/components/tree/processNodes.js b/src/components/tree/processNodes.js
deleted file mode 100644
index 2e224cda7..000000000
--- a/src/components/tree/processNodes.js
+++ /dev/null
@@ -1,78 +0,0 @@
-import { calcFullTipCounts } from "./treeHelpers";
-
-export const processNodes = (nodes) => {
-  const rootNode = nodes[0];
-  nodes.forEach((d) => {if (typeof d.attr === "undefined") {d.attr = {};} });
-  calcFullTipCounts(rootNode);
-  nodes.forEach((d) => {d.hasChildren = typeof d.children !== "undefined";});
-  /* set an index so that we can access visibility / nodeColors if needed */
-  nodes.map((d, idx) => {d.arrayIdx = idx;});
-  return nodes;
-};
-
-const rectangularLayout = (node, distanceMeasure) => {
-    return {'xVal':(distanceMeasure=='div')?node.xvalue:node.attr[distanceMeasure],
-            'yVal':node.yvalue,
-            'xValMidpoint':(distanceMeasure=='div')?node.parent.xvalue:node.parent.attr[distanceMeasure],
-            'yValMidpoint':node.yvalue
-            };
-};
-
-const vsDateLayout = (node, distanceMeasure) => {
-    return {'xVal':node.attr['num_date'], 'yVal':node.attr[distanceMeasure],
-            'xValMidpoint':node.attr['num_date'], 'yValMidpoint':node.attr[distanceMeasure]
-           };
-};
-
-const radialLayout = (node, distanceMeasure, nTips, rootVal) => {
-    const circleFraction = -0.9;
-    const circleStart = Math.PI;
-    const radius = (distanceMeasure=='div')?node.xvalue:(node.attr[distanceMeasure]-rootVal);
-    const parentRadius = (distanceMeasure=='div')?node.parent.xvalue:(node.parent.attr[distanceMeasure]-rootVal);
-    const angle = circleStart + circleFraction*2.0*Math.PI*(nTips-node.yvalue)/nTips;
-    const parentAngle = circleStart + circleFraction*2.0*Math.PI*(nTips-node.parent.yvalue)/nTips;
-    const leftRight = node.yvalue>node.parent.yvalue;
-    const smallBigArc = Math.abs(angle - parentAngle)>Math.Pi*0.5;
-    return {'xVal':radius*Math.sin(angle), 'yVal':radius*Math.cos(angle),
-            'xValMidpoint':parentRadius*Math.sin(angle),
-            'yValMidpoint':parentRadius*Math.cos(angle),
-            'radius':radius,'radiusInner':parentRadius,
-            'angle':angle, 'smallBigArc':smallBigArc, 'leftRight':leftRight};
-};
-
-/* Calculate layout geometry for radial and rectangular layouts
- * nodes: array of nodes for which x/y coordinates are to be calculated
- * nTips: total number of tips  (optional)
- * distanceMeasures: the different types of distances used to measure
-                     distances on the tree (date, mutations, etc)
-*/
-export const calcLayouts = (nodes, distanceMeasures, nTips) => {
-    if (typeof nTips==='undefined'){
-        nTips = nodes.filter((d) => {return !d.hasChildren;} ).length;
-    }
-    nodes.forEach( (node, ni) => {
-        node.geometry = {};
-        distanceMeasures.forEach((distanceMeasure, di) => {
-            const rootVal = (distanceMeasure === "div") ? nodes[0].xvalue : nodes[0].attr[distanceMeasure];
-            node.geometry[distanceMeasure]={};
-            node.geometry[distanceMeasure]["rect"] = rectangularLayout(node, distanceMeasure);
-            node.geometry[distanceMeasure]['radial'] = radialLayout(node, distanceMeasure, nTips, rootVal);
-            node.geometry[distanceMeasure]['vsDate'] = vsDateLayout(node, distanceMeasure);
-        });
-    });
-};
-
-
-/* Map the precomputed geometries to the coordinates in the SVG
- * nodes: array of nodes for which x/y coordinates are to be mapped
- * xScale: map of tree layout to coordinate space
- * yScale: map of tree layout to coordinate space
- * layout: type of layout to use (radial vs rectangular)
- * distanceMeasure: data type used to determine tree distances (date, mutations, etc)
-*/
-export const mapToCoordinates = (nodes, xScale, yScale, layout, distanceMeasure) => {
-    nodes.forEach( (node, ni) => {
-        node.geometry[distanceMeasure][layout]['x'] = xScales(node.geometry[distanceMeasure][layout]['xVal']);
-        node.geometry[distanceMeasure][layout]['y'] = yScales(node.geometry[distanceMeasure][layout]['yVal']);
-    });
-}
diff --git a/src/components/tree/treeHelpers.js b/src/components/tree/treeHelpers.js
index 9e7cc45e6..64df0a62b 100644
--- a/src/components/tree/treeHelpers.js
+++ b/src/components/tree/treeHelpers.js
@@ -1,4 +1,3 @@
-
 import { scalePow } from "d3-scale";
 import { tipRadius, freqScale, tipRadiusOnLegendMatch } from "../../util/globals";
 
@@ -317,3 +316,22 @@ export const processVaccines = (nodes, vaccineChoices) => {
   vaccines.forEach((d) => {d.vaccineDate = vaccineChoices[d.strain];});
   return vaccines;
 };
+
+/**
+ * Adds certain properties to the nodes array - for each node in nodes it adds
+ * node.fullTipCount - see calcFullTipCounts() description
+ * node.hasChildren {bool}
+ * node.arrayIdx  {integer} - the index of the node in the nodes array
+ * @param  {array} nodes redux tree nodes
+ * @return {array} input array (kinda unneccessary)
+ * side-effects: node.hasChildren (bool) and node.arrayIdx (INT) for each node in nodes
+ */
+export const processNodes = (nodes) => {
+  const rootNode = nodes[0];
+  nodes.forEach((d) => {if (typeof d.attr === "undefined") {d.attr = {};} });
+  calcFullTipCounts(rootNode);
+  nodes.forEach((d) => {d.hasChildren = typeof d.children !== "undefined";});
+  /* set an index so that we can access visibility / nodeColors if needed */
+  nodes.forEach((d, idx) => {d.arrayIdx = idx;});
+  return nodes;
+};
diff --git a/src/reducers/tree.js b/src/reducers/tree.js
index 0a1a62a91..ca9241f8f 100644
--- a/src/reducers/tree.js
+++ b/src/reducers/tree.js
@@ -1,5 +1,4 @@
-import { flattenTree, appendParentsToTree, processVaccines } from "../components/tree/treeHelpers";
-import { processNodes, calcLayouts } from "../components/tree/processNodes";
+import { flattenTree, appendParentsToTree, processVaccines, processNodes } from "../components/tree/treeHelpers";
 import { getValuesAndCountsOfVisibleTraitsFromTree, getAllValuesAndCountsOfTraitsFromTree } from "../util/treeTraversals";
 import * as types from "../actions/types";
 
@@ -45,7 +44,6 @@ const Tree = (state = getDefaultState(), action) => {
       const nodesArray = flattenTree(action.tree);
       const nodes = processNodes(nodesArray);
       const vaccines = processVaccines(nodes, action.meta.vaccine_choices);
-      calcLayouts(nodes, ["div", "num_date"]);
       return Object.assign({}, getDefaultState(), {
         nodes,
         vaccines,

From 3233167b5bb2a787f73442f65dd9a94a50023b4c Mon Sep 17 00:00:00 2001
From: James Hadfield <jh22@sanger.ac.uk>
Date: Fri, 2 Feb 2018 15:40:24 -0800
Subject: [PATCH 03/10] move & clean functions from treeViewFunctions.js

---
 .../tree/reactD3Interface/callbacks.js        | 227 +++++++++++
 src/components/tree/reactD3Interface/index.js | 129 ++++++
 src/components/tree/treeHelpers.js            |  29 +-
 src/components/tree/treeView.js               |  39 +-
 src/components/tree/treeViewFunctions.js      | 378 ------------------
 5 files changed, 402 insertions(+), 400 deletions(-)
 create mode 100644 src/components/tree/reactD3Interface/callbacks.js
 create mode 100644 src/components/tree/reactD3Interface/index.js
 delete mode 100644 src/components/tree/treeViewFunctions.js

diff --git a/src/components/tree/reactD3Interface/callbacks.js b/src/components/tree/reactD3Interface/callbacks.js
new file mode 100644
index 000000000..3a5146134
--- /dev/null
+++ b/src/components/tree/reactD3Interface/callbacks.js
@@ -0,0 +1,227 @@
+import { rgb } from "d3-color";
+import { interpolateRgb } from "d3-interpolate";
+import { updateVisibleTipsAndBranchThicknesses} from "../../../actions/treeProperties";
+import { mediumTransitionDuration } from "../../../util/globals";
+import { branchOpacityFunction } from "../treeHelpers";
+
+/* Callbacks used by the tips / branches when hovered / selected */
+
+export const onTipHover = function onTipHover(d, x, y) {
+  this.state.tree.svg.select("#tip_" + d.n.clade)
+    .attr("r", (e) => e["r"] + 4);
+  this.setState({
+    hovered: {d, type: ".tip", x, y}
+  });
+};
+
+export const onTipClick = function onTipClick(d) {
+  // console.log("tip click", d)
+  this.setState({
+    hovered: null,
+    selectedTip: d
+  });
+  this.props.dispatch(updateVisibleTipsAndBranchThicknesses({tipSelectedIdx: d.n.arrayIdx}));
+};
+
+
+export const onBranchHover = function onBranchHover(d, x, y) {
+  /* emphasize the color of the branch */
+  for (const id of ["#branch_S_" + d.n.clade, "#branch_T_" + d.n.clade]) {
+    if (this.props.colorByConfidence) {
+      this.state.tree.svg.select(id)
+        .style("stroke", (el) => { // eslint-disable-line no-loop-func
+          const ramp = branchOpacityFunction(this.props.tree.nodes[el.n.arrayIdx].attr[this.props.colorBy + "_entropy"]);
+          const raw = this.props.tree.nodeColors[el.n.arrayIdx];
+          const base = el["stroke"];
+          return rgb(interpolateRgb(raw, base)(ramp)).toString();
+        });
+    } else {
+      this.state.tree.svg.select(id)
+        .style("stroke", (el) => this.props.tree.nodeColors[el.n.arrayIdx]);
+    }
+  }
+  if (this.props.temporalConfidence.exists && this.props.temporalConfidence.display && !this.props.temporalConfidence.on) {
+    this.state.tree.svg.append("g").selectAll(".conf")
+      .data([d])
+      .enter()
+      .call((sel) => this.state.tree.drawSingleCI(sel, 0.5));
+  }
+  this.setState({
+    hovered: {d, type: ".branch", x, y}
+  });
+};
+
+export const onBranchClick = function onBranchClick(d) {
+  this.Viewer.fitToViewer();
+  this.state.tree.zoomIntoClade(d, mediumTransitionDuration);
+  /* to stop multiple phyloTree updates potentially clashing,
+  we change tipVis after geometry update + transition */
+  window.setTimeout(
+    () => this.props.dispatch(updateVisibleTipsAndBranchThicknesses({idxOfInViewRootNode: d.n.arrayIdx})),
+    mediumTransitionDuration
+  );
+  this.setState({
+    hovered: null,
+    selectedBranch: d
+  });
+};
+
+/* onBranchLeave called when mouse-off, i.e. anti-hover */
+export const onBranchLeave = function onBranchLeave(d) {
+  for (const id of ["#branch_T_" + d.n.clade, "#branch_S_" + d.n.clade]) {
+    this.state.tree.svg.select(id)
+      .style("stroke", (el) => el["stroke"]);
+  }
+  if (this.props.temporalConfidence.exists && this.props.temporalConfidence.display && !this.props.temporalConfidence.on) {
+    this.state.tree.removeConfidence(mediumTransitionDuration);
+  }
+  if (this.state.hovered) {
+    this.setState({hovered: null});
+  }
+};
+
+export const onTipLeave = function onTipLeave(d) {
+  if (!this.state.selectedTip) {
+    this.state.tree.svg.select("#tip_" + d.n.clade)
+      .attr("r", (dd) => dd["r"]);
+  }
+  if (this.state.hovered) {
+    this.setState({hovered: null});
+  }
+};
+
+
+/* clearSelectedTip when clicking to go away */
+export const clearSelectedTip = function clearSelectedTip(d) {
+  this.state.tree.svg.select("#tip_" + d.n.clade)
+    .attr("r", (dd) => dd["r"]);
+  this.setState({selectedTip: null, hovered: null});
+  /* restore the tip visibility! */
+  this.props.dispatch(updateVisibleTipsAndBranchThicknesses());
+};
+
+
+const visibleArea = function visibleArea(Viewer) {
+  const V = Viewer.getValue();
+  return {
+    left: -V.e / V.a,
+    top: -V.f / V.d,
+    right: (V.viewerWidth - V.e) / V.a,
+    bottom: (V.viewerHeight - V.f) / V.d
+  };
+};
+
+const resetGrid = function resetGrid() {
+  const layout = this.props.layout;
+  if (this.props.layout !== "unrooted") {
+    const tree = this.state.tree;
+    // const visibleArea = .visibleArea;
+    const viewer = this.Viewer;
+    window.setTimeout(() => {
+      const view = visibleArea(viewer);
+      tree.addGrid(layout, view.bottom, view.top);
+    }, 200);
+  }
+};
+
+
+export const onViewerChange = function onViewerChange() {
+  if (this.Viewer && this.state.tree) {
+    const V = this.Viewer.getValue();
+    if (V.mode === "panning") {
+      resetGrid.bind(this)();
+    } else if (V.mode === "idle") {
+      resetGrid.bind(this);
+    }
+  }
+};
+
+export const resetView = function resetView() {
+  this.Viewer.fitToViewer();
+};
+
+
+/* viewEntireTree: go back to the root! */
+export const viewEntireTree = function viewEntireTree() {
+  /* reset the SVGPanZoom */
+  this.Viewer.fitToViewer();
+  /* imperitively manipulate SVG tree elements */
+  this.state.tree.zoomIntoClade(this.state.tree.nodes[0], mediumTransitionDuration);
+  /* update branch thicknesses / tip vis after SVG tree elemtents have moved */
+  window.setTimeout(
+    () => this.props.dispatch(updateVisibleTipsAndBranchThicknesses({idxOfInViewRootNode: 0})),
+    mediumTransitionDuration
+  );
+  this.setState({selectedBranch: null, selectedTip: null});
+};
+
+export const handleIconClick = function handleIconClick(tool) {
+  return () => {
+    const V = this.Viewer.getValue();
+    if (tool === "zoom-in") {
+      this.Viewer.zoomOnViewerCenter(1.4);
+    } else if (V.a > 1.0) { // if there is room to zoom out via the SVGPanZoom, do
+      this.Viewer.zoomOnViewerCenter(0.71);
+    } else { // otherwise reset view to have SVG fit the viewer
+      resetView.bind(this)();
+      // if we have clade zoom, zoom out to the parent clade
+      if (this.state.selectedBranch && this.state.selectedBranch.n.arrayIdx) {
+        const dispatch = this.props.dispatch;
+        const arrayIdx = this.state.tree.zoomNode.parent.n.arrayIdx;
+        // reset the "clicked" branch, unset if we zoomed out all the way to the root
+        this.setState({
+          hovered: null,
+          selectedBranch: (arrayIdx) ? this.state.tree.zoomNode.parent : null
+        });
+        // clear previous timeout bc they potentially mess with the geometry update
+        if (this.timeout) {
+          clearTimeout(this.timeout);
+        }
+        // call phyloTree to zoom out, this rerenders the geometry
+        this.state.tree.zoomToParent(mediumTransitionDuration);
+        // wait and reset visibility
+        this.timeout = setTimeout(() => {
+          dispatch(updateVisibleTipsAndBranchThicknesses());
+        }, mediumTransitionDuration);
+      }
+    }
+    resetGrid.bind(this)();
+  };
+};
+
+/**
+ * @param  {node} d tree node object
+ * @return {string} displayed as label on the branch corresponding to the node
+ */
+export const branchLabel = function branchLabel(d) {
+  if (d.n.muts) {
+    if (d.n.muts.length > 5) {
+      return d.n.muts.slice(0, 5).join(", ") + "...";
+    }
+    return d.n.muts.join(", ");
+  }
+  return "";
+};
+
+/**
+ * @param  {node} d tree node object
+ * @param  {int} n total number of nodes in current view
+ * @return {int} font size of the branch label
+ */
+export const branchLabelSize = (d, n) =>
+  d.leafCount > n / 10.0 ? 12 : 0;
+
+/**
+ * @param  {node} d tree node object
+ * @param  {int} n total number of nodes in current view
+ * @return {int} font size of the tip label
+ */
+export const tipLabelSize = (d, n) => {
+  if (n > 70) {
+    return 0;
+  } else if (n < 20) {
+    return 14;
+  }
+  const fs = 6 + 8 * (70 - n) / (70 - 20);
+  return fs;
+};
diff --git a/src/components/tree/reactD3Interface/index.js b/src/components/tree/reactD3Interface/index.js
new file mode 100644
index 000000000..abc0fb134
--- /dev/null
+++ b/src/components/tree/reactD3Interface/index.js
@@ -0,0 +1,129 @@
+import { rgb } from "d3-color";
+import { mediumTransitionDuration } from "../../../util/globals";
+import { calcStrokeCols } from "../treeHelpers";
+import { viewEntireTree } from "./callbacks";
+/**
+ * function to help determine what parts of phylotree should update
+ * @param {obj} props redux props
+ * @param {obj} nextProps next redux props
+ * @param {obj} tree phyloTree object (stored in the state of treeView)
+ * @return {obj} values are mostly bools, but not always
+ */
+export const salientPropChanges = (props, nextProps, tree) => {
+  const dataInFlux = !nextProps.tree.loaded;
+  const newData = tree === null && nextProps.tree.loaded;
+  const visibility = !!nextProps.tree.visibilityVersion && props.tree.visibilityVersion !== nextProps.tree.visibilityVersion;
+  const tipRadii = !!nextProps.tree.tipRadiiVersion && props.tree.tipRadiiVersion !== nextProps.tree.tipRadiiVersion;
+  const colorBy = !!nextProps.tree.nodeColorsVersion &&
+      (props.tree.nodeColorsVersion !== nextProps.tree.nodeColorsVersion ||
+      nextProps.colorByConfidence !== props.colorByConfidence);
+  const branchThickness = props.tree.branchThicknessVersion !== nextProps.tree.branchThicknessVersion;
+  const layout = props.layout !== nextProps.layout;
+  const distanceMeasure = props.distanceMeasure !== nextProps.distanceMeasure;
+  const rerenderAllElements = nextProps.quickdraw === false && props.quickdraw === true;
+  const resetViewToRoot = props.tree.idxOfInViewRootNode !== 0 && nextProps.tree.idxOfInViewRootNode === 0;
+  /* branch labels & confidence use 0: no change, 1: turn off, 2: turn on */
+  const branchLabels = props.showBranchLabels === nextProps.showBranchLabels ? 0 : nextProps.showBranchLabels ? 2 : 1;
+  const confidence = props.temporalConfidence.on === nextProps.temporalConfidence.on && props.temporalConfidence.display === nextProps.temporalConfidence.display ? 0 :
+    (props.temporalConfidence.on === false && nextProps.temporalConfidence.on === false) ? 0 :
+      (nextProps.temporalConfidence.display === false || nextProps.temporalConfidence.on === false) ? 1 :
+        (nextProps.temporalConfidence.display === true && nextProps.temporalConfidence.on === true) ? 2 : 0;
+
+  /* sometimes we may want smooth transitions */
+  let branchTransitionTime = false; /* false = no transition. Use when speed is critical */
+  const tipTransitionTime = false;
+  if (nextProps.colorByConfidence !== props.colorByConfidence) {
+    branchTransitionTime = mediumTransitionDuration;
+  }
+
+  return {
+    dataInFlux,
+    newData,
+    visibility,
+    tipRadii,
+    colorBy,
+    layout,
+    distanceMeasure,
+    branchThickness,
+    branchTransitionTime,
+    tipTransitionTime,
+    branchLabels,
+    resetViewToRoot,
+    confidence,
+    quickdraw: nextProps.quickdraw,
+    rerenderAllElements
+  };
+};
+
+/**
+ * effect (in phyloTree) the necessary style + attr updates
+ * @param {obj} changes see salientPropChanges above
+ * @param {obj} nextProps next redux props
+ * @param {obj} tree phyloTree object
+ * @return {null} causes side-effects via phyloTree object
+ */
+export const updateStylesAndAttrs = (that, changes, nextProps, tree) => {
+  /* the objects storing the changes to make to the tree */
+  const tipAttrToUpdate = {};
+  const tipStyleToUpdate = {};
+  const branchAttrToUpdate = {};
+  const branchStyleToUpdate = {};
+
+  if (changes.visibility) {
+    tipStyleToUpdate["visibility"] = nextProps.tree.visibility;
+  }
+  if (changes.tipRadii) {
+    tipAttrToUpdate["r"] = nextProps.tree.tipRadii;
+  }
+  if (changes.colorBy) {
+    tipStyleToUpdate["fill"] = nextProps.tree.nodeColors.map((col) => {
+      return rgb(col).brighter([0.65]).toString();
+    });
+    const branchStrokes = calcStrokeCols(nextProps.tree, nextProps.colorByConfidence, nextProps.colorBy);
+    branchStyleToUpdate["stroke"] = branchStrokes;
+    tipStyleToUpdate["stroke"] = branchStrokes;
+  }
+  if (changes.branchThickness) {
+    // console.log("branch width change detected - update branch stroke-widths")
+    branchStyleToUpdate["stroke-width"] = nextProps.tree.branchThickness;
+  }
+  /* implement style * attr changes */
+  if (Object.keys(branchAttrToUpdate).length || Object.keys(branchStyleToUpdate).length) {
+    // console.log("applying branch attr", Object.keys(branchAttrToUpdate), "branch style changes", Object.keys(branchStyleToUpdate))
+    tree.updateMultipleArray(".branch", branchAttrToUpdate, branchStyleToUpdate, changes.branchTransitionTime, changes.quickdraw);
+  }
+  if (Object.keys(tipAttrToUpdate).length || Object.keys(tipStyleToUpdate).length) {
+    // console.log("applying tip attr", Object.keys(tipAttrToUpdate), "tip style changes", Object.keys(tipStyleToUpdate))
+    tree.updateMultipleArray(".tip", tipAttrToUpdate, tipStyleToUpdate, changes.tipTransitionTime, changes.quickdraw);
+  }
+
+  if (changes.layout) { /* swap layouts */
+    tree.updateLayout(nextProps.layout, mediumTransitionDuration);
+  }
+  if (changes.distanceMeasure) { /* change distance metrics */
+    tree.updateDistance(nextProps.distanceMeasure, mediumTransitionDuration);
+  }
+  if (changes.branchLabels === 2) {
+    tree.showBranchLabels();
+  } else if (changes.branchLabels === 1) {
+    tree.hideBranchLabels();
+  }
+  if (changes.confidence === 1) {
+    tree.removeConfidence(mediumTransitionDuration);
+  } else if (changes.confidence === 2) {
+    if (changes.layout) { /* setTimeout else they come back in before the branches have transitioned */
+      setTimeout(() => tree.drawConfidence(mediumTransitionDuration), mediumTransitionDuration * 1.5);
+    } else {
+      tree.drawConfidence(mediumTransitionDuration);
+    }
+  } else if (nextProps.temporalConfidence.on && (changes.branchThickness || changes.colorBy)) {
+    /* some updates may necessitate an updating of the CIs (e.g. ∆ branch thicknesses) */
+    tree.updateConfidence(changes.tipTransitionTime);
+  }
+  if (changes.resetViewToRoot) {
+    viewEntireTree.bind(that)();
+  }
+  if (changes.rerenderAllElements) {
+    tree.rerenderAllElements();
+  }
+};
diff --git a/src/components/tree/treeHelpers.js b/src/components/tree/treeHelpers.js
index 64df0a62b..81f068f28 100644
--- a/src/components/tree/treeHelpers.js
+++ b/src/components/tree/treeHelpers.js
@@ -1,3 +1,5 @@
+import { rgb } from "d3-color";
+import { interpolateRgb } from "d3-interpolate";
 import { scalePow } from "d3-scale";
 import { tipRadius, freqScale, tipRadiusOnLegendMatch } from "../../util/globals";
 
@@ -291,9 +293,9 @@ export const calcVisibility = (tree, controls, dates) => {
   return "visible";
 };
 
-export const branchInterpolateColour = "#BBB";
-export const branchOpacityConstant = 0.4;
-export const branchOpacityFunction = scalePow()
+const branchInterpolateColour = "#BBB";
+const branchOpacityConstant = 0.4;
+const branchOpacityFunction = scalePow()
   .exponent([0.3])
   .domain([0, 1])
   .range([branchOpacityConstant, 1])
@@ -302,6 +304,27 @@ export const branchOpacityFunction = scalePow()
 // export const calcEntropyOfValues = (vals) =>
 //   vals.map((v) => v * Math.log(v + 1E-10)).reduce((a, b) => a + b, 0) * -1 / Math.log(vals.length);
 
+
+/**
+ * calculate array of HEXs to actually be displayed.
+ * (colorBy) confidences manifest as opacity ramps
+ * @param {obj} tree phyloTree object
+ * @param {bool} confidence enabled?
+ * @return {array} array of hex's. 1-1 with nodes.
+ */
+export const calcStrokeCols = (tree, confidence, colorBy) => {
+  if (confidence === true) {
+    return tree.nodeColors.map((col, idx) => {
+      const entropy = tree.nodes[idx].attr[colorBy + "_entropy"];
+      return rgb(interpolateRgb(col, branchInterpolateColour)(branchOpacityFunction(entropy))).toString();
+    });
+  }
+  return tree.nodeColors.map((col) => {
+    return rgb(interpolateRgb(col, branchInterpolateColour)(branchOpacityConstant)).toString();
+  });
+};
+
+
 /**
 *  if the metadata JSON defines vaccine strains then create an array of the nodes
 *  @param nodes - nodes
diff --git a/src/components/tree/treeView.js b/src/components/tree/treeView.js
index 0f16a8a42..c08716f1a 100644
--- a/src/components/tree/treeView.js
+++ b/src/components/tree/treeView.js
@@ -12,7 +12,8 @@ import { mediumTransitionDuration } from "../../util/globals";
 import InfoPanel from "./infoPanel";
 import TipSelectedPanel from "./tipSelectedPanel";
 import computeResponsive from "../../util/computeResponsive";
-import * as funcs from "./treeViewFunctions";
+import { updateStylesAndAttrs, salientPropChanges } from "./reactD3Interface";
+import * as callbacks from "./reactD3Interface/callbacks";
 
 /*
 this.props.tree contains the nodes etc used to build the PhyloTree
@@ -58,7 +59,7 @@ class TreeView extends React.Component {
     /* This both creates the tree (when it's loaded into redux) and
     works out what to update, based upon changes to redux.control */
     let tree = this.state.tree;
-    const changes = funcs.salientPropChanges(this.props, nextProps, tree);
+    const changes = salientPropChanges(this.props, nextProps, tree);
     /* usefull for debugging: */
     // console.log("CWRP Changes:",
     //    Object.keys(changes).filter((k) => !!changes[k]).reduce((o, k) => {
@@ -75,7 +76,7 @@ class TreeView extends React.Component {
         changes[k] = false;
       }
       changes.colorBy = true;
-      funcs.updateStylesAndAttrs(this, changes, nextProps, tree);
+      updateStylesAndAttrs(this, changes, nextProps, tree);
       this.setState({tree});
       if (this.Viewer) {
         this.Viewer.fitToViewer();
@@ -83,14 +84,14 @@ class TreeView extends React.Component {
       return null; /* return to avoid an unnecessary updateStylesAndAttrs call */
     }
     if (tree) {
-      funcs.updateStylesAndAttrs(this, changes, nextProps, tree);
+      updateStylesAndAttrs(this, changes, nextProps, tree);
     }
     return null;
   }
 
   componentDidMount() {
     const tree = this.makeTree(this.props);
-    funcs.updateStylesAndAttrs(this, {colorBy: true}, this.props, tree);
+    updateStylesAndAttrs(this, {colorBy: true}, this.props, tree);
     this.setState({tree});
     if (this.Viewer) {
       this.Viewer.fitToViewer();
@@ -137,16 +138,16 @@ class TreeView extends React.Component {
           showTipLabels: true   //show
         },
         { /* callbacks */
-          onTipHover: funcs.onTipHover.bind(this),
-          onTipClick: funcs.onTipClick.bind(this),
-          onBranchHover: funcs.onBranchHover.bind(this),
-          onBranchClick: funcs.onBranchClick.bind(this),
-          onBranchLeave: funcs.onBranchLeave.bind(this),
-          onTipLeave: funcs.onTipLeave.bind(this),
-          branchLabel: funcs.branchLabel,
-          branchLabelSize: funcs.branchLabelSize,
+          onTipHover: callbacks.onTipHover.bind(this),
+          onTipClick: callbacks.onTipClick.bind(this),
+          onBranchHover: callbacks.onBranchHover.bind(this),
+          onBranchClick: callbacks.onBranchClick.bind(this),
+          onBranchLeave: callbacks.onBranchLeave.bind(this),
+          onTipLeave: callbacks.onTipLeave.bind(this),
+          branchLabel: callbacks.branchLabel,
+          branchLabelSize: callbacks.branchLabelSize,
           tipLabel: (d) => d.n.strain,
-          tipLabelSize: funcs.tipLabelSize.bind(this)
+          tipLabelSize: callbacks.tipLabelSize.bind(this)
         },
         nextProps.tree.branchThickness, /* guarenteed to be in redux by now */
         nextProps.tree.visibility,
@@ -184,7 +185,7 @@ class TreeView extends React.Component {
           colorScale={this.props.colorScale}
         />
         <TipSelectedPanel
-          goAwayCallback={(d) => funcs.clearSelectedTip.bind(this)(d)}
+          goAwayCallback={(d) => callbacks.clearSelectedTip.bind(this)(d)}
           tip={this.state.selectedTip}
           metadata={this.props.metadata}
         />
@@ -203,9 +204,9 @@ class TreeView extends React.Component {
           background={"#FFF"}
           miniaturePosition={"none"}
           // onMouseDown={this.startPan.bind(this)}
-          onDoubleClick={funcs.resetView.bind(this)}
+          onDoubleClick={callbacks.resetView.bind(this)}
           //onMouseUp={this.endPan.bind(this)}
-          onChangeValue={ funcs.onViewerChange.bind(this) }
+          onChangeValue={callbacks.onViewerChange.bind(this)}
         >
           <svg style={{pointerEvents: "auto"}}
             width={responsive.width}
@@ -238,13 +239,13 @@ class TreeView extends React.Component {
               </filter>
             </defs>
           <ZoomInIcon
-            handleClick={funcs.handleIconClick.bind(this)("zoom-in")}
+            handleClick={callbacks.handleIconClick.bind(this)("zoom-in")}
             active
             x={10}
             y={50}
           />
           <ZoomOutIcon
-            handleClick={funcs.handleIconClick.bind(this)("zoom-out")}
+            handleClick={callbacks.handleIconClick.bind(this)("zoom-out")}
             active
             x={10}
             y={90}
diff --git a/src/components/tree/treeViewFunctions.js b/src/components/tree/treeViewFunctions.js
deleted file mode 100644
index d51945b62..000000000
--- a/src/components/tree/treeViewFunctions.js
+++ /dev/null
@@ -1,378 +0,0 @@
-import { rgb } from "d3-color";
-import { interpolateRgb } from "d3-interpolate";
-import { updateVisibleTipsAndBranchThicknesses} from "../../actions/treeProperties";
-import { branchOpacityConstant,
-  branchOpacityFunction,
-  branchInterpolateColour } from "./treeHelpers";
-import { mediumTransitionDuration } from "../../util/globals";
-
-export const visibleArea = function (Viewer) {
-  const V = Viewer.getValue();
-  return {
-    left: -V.e / V.a,
-    top: -V.f / V.d,
-    right: (V.viewerWidth - V.e) / V.a,
-    bottom: (V.viewerHeight - V.f) / V.d
-  };
-};
-
-export const resetGrid = function () {
-  const layout = this.props.layout;
-  if (this.props.layout !== "unrooted") {
-    const tree = this.state.tree;
-    // const visibleArea = .visibleArea;
-    const viewer = this.Viewer;
-    const delayedRedraw = function () {
-      return function () {
-        const view = visibleArea(viewer);
-        tree.addGrid(layout, view.bottom, view.top);
-      };
-    };
-    window.setTimeout(delayedRedraw(), 200);
-  }
-};
-
-
-export const onViewerChange = function () {
-  if (this.Viewer && this.state.tree) {
-    const V = this.Viewer.getValue();
-    if (V.mode === "panning") {
-      resetGrid.bind(this)();
-    }else if (V.mode === "idle") {
-      resetGrid.bind(this);
-    }
-  }
-};
-
-export const resetView = function () {
-  this.Viewer.fitToViewer();
-};
-
-/* Callbacks used by the tips / branches when hovered / selected */
-export const onTipHover = function (d, x, y) {
-  this.state.tree.svg.select("#tip_" + d.n.clade)
-    .attr("r", (e) => e["r"] + 4);
-  this.setState({
-    hovered: {d, type: ".tip", x, y}
-  });
-};
-
-export const onTipClick = function (d) {
-  // console.log("tip click", d)
-  this.setState({
-    hovered: null,
-    selectedTip: d
-  });
-  this.props.dispatch(updateVisibleTipsAndBranchThicknesses({tipSelectedIdx: d.n.arrayIdx}));
-};
-
-export const onBranchHover = function (d, x, y) {
-  /* emphasize the color of the branch */
-  for (const id of ["#branch_S_" + d.n.clade, "#branch_T_" + d.n.clade]) {
-    if (this.props.colorByConfidence) {
-      this.state.tree.svg.select(id)
-        .style("stroke", (el) => {
-          const ramp = branchOpacityFunction(this.props.tree.nodes[el.n.arrayIdx].attr[this.props.colorBy + "_entropy"]);
-          const raw = this.props.tree.nodeColors[el.n.arrayIdx];
-          const base = el["stroke"];
-          return rgb(interpolateRgb(raw, base)(ramp)).toString();
-        });
-    } else {
-      this.state.tree.svg.select(id)
-        .style("stroke", (el) => this.props.tree.nodeColors[el.n.arrayIdx]);
-    }
-  }
-  if (this.props.temporalConfidence.exists && this.props.temporalConfidence.display && !this.props.temporalConfidence.on) {
-    this.state.tree.svg.append("g").selectAll(".conf")
-      .data([d])
-      .enter()
-        .call((sel) => this.state.tree.drawSingleCI(sel, 0.5));
-  }
-  this.setState({
-    hovered: {d, type: ".branch", x, y}
-  });
-};
-
-export const onBranchClick = function (d) {
-  this.Viewer.fitToViewer();
-  this.state.tree.zoomIntoClade(d, mediumTransitionDuration);
-  /* to stop multiple phyloTree updates potentially clashing,
-  we change tipVis after geometry update + transition */
-  window.setTimeout(() =>
-    this.props.dispatch(updateVisibleTipsAndBranchThicknesses({idxOfInViewRootNode: d.n.arrayIdx})),
-    mediumTransitionDuration
-  );
-  this.setState({
-    hovered: null,
-    selectedBranch: d
-  });
-};
-
-/* onBranchLeave called when mouse-off, i.e. anti-hover */
-export const onBranchLeave = function (d) {
-  for (const id of ["#branch_T_" + d.n.clade, "#branch_S_" + d.n.clade]) {
-    this.state.tree.svg.select(id)
-      .style("stroke", (el) => el["stroke"]);
-  }
-  if (this.props.temporalConfidence.exists && this.props.temporalConfidence.display && !this.props.temporalConfidence.on) {
-    this.state.tree.removeConfidence(mediumTransitionDuration);
-  }
-  if (this.state.hovered) {
-    this.setState({hovered: null});
-  }
-};
-
-export const onTipLeave = function (d) {
-  if (!this.state.selectedTip) {
-    this.state.tree.svg.select("#tip_" + d.n.clade)
-      .attr("r", (dd) => dd["r"]);
-  }
-  if (this.state.hovered) {
-    this.setState({hovered: null});
-  }
-};
-
-/* viewEntireTree: go back to the root! */
-const viewEntireTree = function () {
-  /* reset the SVGPanZoom */
-  this.Viewer.fitToViewer();
-  /* imperitively manipulate SVG tree elements */
-  this.state.tree.zoomIntoClade(this.state.tree.nodes[0], mediumTransitionDuration);
-  /* update branch thicknesses / tip vis after SVG tree elemtents have moved */
-  window.setTimeout(
-    () => this.props.dispatch(updateVisibleTipsAndBranchThicknesses({idxOfInViewRootNode: 0})),
-    mediumTransitionDuration
-  );
-  this.setState({selectedBranch: null, selectedTip: null});
-};
-
-/* clearSelectedTip when clicking to go away */
-export const clearSelectedTip = function (d) {
-  this.state.tree.svg.select("#tip_" + d.n.clade)
-    .attr("r", (dd) => dd["r"]);
-  this.setState({selectedTip: null, hovered: null});
-  /* restore the tip visibility! */
-  this.props.dispatch(updateVisibleTipsAndBranchThicknesses());
-};
-
-export const handleIconClick = function (tool) {
-  return () => {
-    const V = this.Viewer.getValue();
-    if (tool === "zoom-in") {
-      this.Viewer.zoomOnViewerCenter(1.4);
-    } else if (V.a > 1.0) { // if there is room to zoom out via the SVGPanZoom, do
-      this.Viewer.zoomOnViewerCenter(0.71);
-    } else {                // otherwise reset view to have SVG fit the viewer
-      resetView.bind(this)();
-      // if we have clade zoom, zoom out to the parent clade
-      if (this.state.selectedBranch && this.state.selectedBranch.n.arrayIdx) {
-        const dp = this.props.dispatch;
-        const arrayIdx = this.state.tree.zoomNode.parent.n.arrayIdx;
-        // reset the "clicked" branch, unset if we zoomed out all the way to the root
-        this.setState({
-          hovered: null,
-          selectedBranch: (arrayIdx) ? this.state.tree.zoomNode.parent : null
-        });
-        const makeCallBack = function () {
-          return function () {
-            dp(updateVisibleTipsAndBranchThicknesses());
-          };
-        };
-        // clear previous timeout bc they potentially mess with the geometry update
-        if (this.timeout) {
-          clearTimeout(this.timeout);
-        }
-        // call phyloTree to zoom out, this rerenders the geometry
-        this.state.tree.zoomToParent(mediumTransitionDuration);
-        // wait and reset visibility
-        this.timeout = setTimeout(makeCallBack(), mediumTransitionDuration);
-      }
-    }
-    resetGrid.bind(this)();
-  };
-};
-
-
-/* functions to do with tip / branch labels */
-
-/**
- * @param  {node} d tree node object
- * @return {string} displayed as label on the branch corresponding to the node
- */
-export const branchLabel = function (d) {
-  if (d.n.muts) {
-    if (d.n.muts.length > 5) {
-      return d.n.muts.slice(0, 5).join(", ") + "...";
-    }
-    return d.n.muts.join(", ");
-  }
-  return "";
-};
-
-/**
- * @param  {node} d tree node object
- * @param  {int} n total number of nodes in current view
- * @return {int} font size of the branch label
- */
-export const branchLabelSize = (d, n) =>
-  d.leafCount > n / 10.0 ? 12 : 0;
-
-/**
- * @param  {node} d tree node object
- * @param  {int} n total number of nodes in current view
- * @return {int} font size of the tip label
- */
-export const tipLabelSize = function (d, n) {
-  if (n > 70) {
-    return 0;
-  } else if (n < 20) {
-    return 14;
-  }
-  const fs = 6 + 8 * (70 - n) / (70 - 20);
-  return fs;
-};
-
-/**
- * calculate array of HEXs to actually be displayed.
- * (colorBy) confidences manifest as opacity ramps
- * @param {obj} tree phyloTree object
- * @param {bool} confidence enabled?
- * @return {array} array of hex's. 1-1 with nodes.
- */
-const calcStrokeCols = (tree, confidence, colorBy) => {
-  if (confidence === true) {
-    return tree.nodeColors.map((col, idx) => {
-      const entropy = tree.nodes[idx].attr[colorBy + "_entropy"];
-      return rgb(interpolateRgb(col, branchInterpolateColour)(branchOpacityFunction(entropy))).toString();
-    });
-  }
-  return tree.nodeColors.map((col) => {
-    return rgb(interpolateRgb(col, branchInterpolateColour)(branchOpacityConstant)).toString();
-  });
-};
-
-/**
- * function to help determine what parts of phylotree should update
- * @param {obj} props redux props
- * @param {obj} nextProps next redux props
- * @param {obj} tree phyloTree object (stored in the state of treeView)
- * @return {obj} values are mostly bools, but not always
- */
-export const salientPropChanges = (props, nextProps, tree) => {
-  const dataInFlux = !nextProps.tree.loaded;
-  const newData = tree === null && nextProps.tree.loaded;
-  const visibility = !!nextProps.tree.visibilityVersion && props.tree.visibilityVersion !== nextProps.tree.visibilityVersion
-  const tipRadii = !!nextProps.tree.tipRadiiVersion && props.tree.tipRadiiVersion !== nextProps.tree.tipRadiiVersion;
-  const colorBy = !!nextProps.tree.nodeColorsVersion &&
-      (props.tree.nodeColorsVersion !== nextProps.tree.nodeColorsVersion ||
-      nextProps.colorByConfidence !== props.colorByConfidence);
-  const branchThickness = props.tree.branchThicknessVersion !== nextProps.tree.branchThicknessVersion;
-  const layout = props.layout !== nextProps.layout;
-  const distanceMeasure = props.distanceMeasure !== nextProps.distanceMeasure;
-  const rerenderAllElements = nextProps.quickdraw === false && props.quickdraw === true;
-  const resetViewToRoot = props.tree.idxOfInViewRootNode !== 0 && nextProps.tree.idxOfInViewRootNode === 0;
-  /* branch labels & confidence use 0: no change, 1: turn off, 2: turn on */
-  const branchLabels = props.showBranchLabels === nextProps.showBranchLabels ? 0 : nextProps.showBranchLabels ? 2 : 1;
-  const confidence = props.temporalConfidence.on === nextProps.temporalConfidence.on && props.temporalConfidence.display === nextProps.temporalConfidence.display ? 0 :
-    (props.temporalConfidence.on === false && nextProps.temporalConfidence.on === false) ? 0 :
-    (nextProps.temporalConfidence.display === false || nextProps.temporalConfidence.on === false) ? 1 :
-    (nextProps.temporalConfidence.display === true && nextProps.temporalConfidence.on === true) ? 2 : 0;
-
-  /* sometimes we may want smooth transitions */
-  let branchTransitionTime = false; /* false = no transition. Use when speed is critical */
-  let tipTransitionTime = false;
-  if (nextProps.colorByConfidence !== props.colorByConfidence) {
-    branchTransitionTime = mediumTransitionDuration;
-  }
-
-  return {
-    dataInFlux,
-    newData,
-    visibility,
-    tipRadii,
-    colorBy,
-    layout,
-    distanceMeasure,
-    branchThickness,
-    branchTransitionTime,
-    tipTransitionTime,
-    branchLabels,
-    resetViewToRoot,
-    confidence,
-    quickdraw: nextProps.quickdraw,
-    rerenderAllElements
-  };
-};
-
-/**
- * effect (in phyloTree) the necessary style + attr updates
- * @param {obj} changes see salientPropChanges above
- * @param {obj} nextProps next redux props
- * @param {obj} tree phyloTree object
- * @return {null} causes side-effects via phyloTree object
- */
-export const updateStylesAndAttrs = (that, changes, nextProps, tree) => {
-  /* the objects storing the changes to make to the tree */
-  const tipAttrToUpdate = {};
-  const tipStyleToUpdate = {};
-  const branchAttrToUpdate = {};
-  const branchStyleToUpdate = {};
-
-  if (changes.visibility) {
-    tipStyleToUpdate["visibility"] = nextProps.tree.visibility;
-  }
-  if (changes.tipRadii) {
-    tipAttrToUpdate["r"] = nextProps.tree.tipRadii;
-  }
-  if (changes.colorBy) {
-    tipStyleToUpdate["fill"] = nextProps.tree.nodeColors.map((col) => {
-      return rgb(col).brighter([0.65]).toString();
-    });
-    const branchStrokes = calcStrokeCols(nextProps.tree, nextProps.colorByConfidence, nextProps.colorBy);
-    branchStyleToUpdate["stroke"] = branchStrokes;
-    tipStyleToUpdate["stroke"] = branchStrokes;
-  }
-  if (changes.branchThickness) {
-    // console.log("branch width change detected - update branch stroke-widths")
-    branchStyleToUpdate["stroke-width"] = nextProps.tree.branchThickness;
-  }
-  /* implement style * attr changes */
-  if (Object.keys(branchAttrToUpdate).length || Object.keys(branchStyleToUpdate).length) {
-    // console.log("applying branch attr", Object.keys(branchAttrToUpdate), "branch style changes", Object.keys(branchStyleToUpdate))
-    tree.updateMultipleArray(".branch", branchAttrToUpdate, branchStyleToUpdate, changes.branchTransitionTime, changes.quickdraw);
-  }
-  if (Object.keys(tipAttrToUpdate).length || Object.keys(tipStyleToUpdate).length) {
-    // console.log("applying tip attr", Object.keys(tipAttrToUpdate), "tip style changes", Object.keys(tipStyleToUpdate))
-    tree.updateMultipleArray(".tip", tipAttrToUpdate, tipStyleToUpdate, changes.tipTransitionTime, changes.quickdraw);
-  }
-
-  if (changes.layout) { /* swap layouts */
-    tree.updateLayout(nextProps.layout, mediumTransitionDuration);
-  }
-  if (changes.distanceMeasure) { /* change distance metrics */
-    tree.updateDistance(nextProps.distanceMeasure, mediumTransitionDuration);
-  }
-  if (changes.branchLabels === 2) {
-    tree.showBranchLabels();
-  } else if (changes.branchLabels === 1) {
-    tree.hideBranchLabels();
-  }
-  if (changes.confidence === 1) {
-    tree.removeConfidence(mediumTransitionDuration);
-  } else if (changes.confidence === 2) {
-    if (changes.layout) { /* setTimeout else they come back in before the branches have transitioned */
-      setTimeout(() => tree.drawConfidence(mediumTransitionDuration), mediumTransitionDuration * 1.5);
-    } else {
-      tree.drawConfidence(mediumTransitionDuration);
-    }
-  } else if (nextProps.temporalConfidence.on && (changes.branchThickness || changes.colorBy)) {
-     /* some updates may necessitate an updating of the CIs (e.g. ∆ branch thicknesses) */
-    tree.updateConfidence(changes.tipTransitionTime);
-  }
-  if (changes.resetViewToRoot) {
-    viewEntireTree.bind(that)();
-  }
-  if (changes.rerenderAllElements) {
-    tree.rerenderAllElements();
-  }
-};

From e22c0887359a9350bf4f983d393f2121274a502e Mon Sep 17 00:00:00 2001
From: James Hadfield <jh22@sanger.ac.uk>
Date: Fri, 2 Feb 2018 16:31:09 -0800
Subject: [PATCH 04/10] create folders, rename files

---
 src/components/app.js                         |  4 ++--
 src/components/download/downloadModal.js      |  2 +-
 src/components/tree/{treeView.js => index.js} | 16 ++++++++--------
 .../click.js}                                 | 10 +++++-----
 .../{infoPanel.js => infoPanels/hover.js}     | 12 ++++++------
 .../tree/{legend-item.js => legend/item.js}   | 19 ++++++++++++-------
 src/components/tree/{ => legend}/legend.js    |  8 ++++----
 .../tree/{ => phyloTree}/phyloTree.js         |  4 ++--
 src/components/tree/reactD3Interface/index.js |  2 +-
 9 files changed, 41 insertions(+), 36 deletions(-)
 rename src/components/tree/{treeView.js => index.js} (96%)
 rename src/components/tree/{tipSelectedPanel.js => infoPanels/click.js} (93%)
 rename src/components/tree/{infoPanel.js => infoPanels/hover.js} (96%)
 rename src/components/tree/{legend-item.js => legend/item.js} (68%)
 rename src/components/tree/{ => legend}/legend.js (97%)
 rename src/components/tree/{ => phyloTree}/phyloTree.js (99%)

diff --git a/src/components/app.js b/src/components/app.js
index 8e58d674d..6dcfc9226 100644
--- a/src/components/app.js
+++ b/src/components/app.js
@@ -11,7 +11,7 @@ import Controls from "./controls/controls";
 import { Entropy } from "./charts/entropy";
 import Map from "./map/map";
 import Info from "./info/info";
-import TreeView from "./tree/treeView";
+import Tree from "./tree";
 import { controlsHiddenWidth, narrativeWidth, controlsWidth } from "../util/globals";
 import { sidebarColor } from "../globalStyles";
 import TitleBar from "./framework/title-bar";
@@ -106,7 +106,7 @@ class App extends React.Component {
                 <Background>
                   <Info padding={padding} />
                   {this.props.metadata.panels.indexOf("tree") === -1 ? null : (
-                    <TreeView padding={padding} />
+                    <Tree padding={padding} />
                   )}
                   {this.props.metadata.panels.indexOf("map") === -1 ? null : (
                     <Map padding={padding} justGotNewDatasetRenderNewMap={false} />
diff --git a/src/components/download/downloadModal.js b/src/components/download/downloadModal.js
index 4cd02c502..605506023 100644
--- a/src/components/download/downloadModal.js
+++ b/src/components/download/downloadModal.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { connect } from "react-redux";
 import { DISMISS_DOWNLOAD_MODAL } from "../../actions/types";
 import { materialButton, medGrey, infoPanelStyles } from "../../globalStyles";
-import { stopProp } from "../tree/tipSelectedPanel";
+import { stopProp } from "../tree/infoPanels/click";
 import { authorString } from "../../util/stringHelpers";
 import * as helpers from "./helperFunctions";
 import * as icons from "../framework/svg-icons";
diff --git a/src/components/tree/treeView.js b/src/components/tree/index.js
similarity index 96%
rename from src/components/tree/treeView.js
rename to src/components/tree/index.js
index c08716f1a..3f68d67fc 100644
--- a/src/components/tree/treeView.js
+++ b/src/components/tree/index.js
@@ -4,13 +4,13 @@ import { connect } from "react-redux";
 import { select } from "d3-selection";
 import { ReactSVGPanZoom } from "react-svg-pan-zoom";
 import Card from "../framework/card";
-import Legend from "./legend";
+import Legend from "./legend/legend";
 import ZoomOutIcon from "../framework/zoom-out-icon";
 import ZoomInIcon from "../framework/zoom-in-icon";
-import PhyloTree from "./phyloTree";
+import PhyloTree from "./phyloTree/phyloTree";
 import { mediumTransitionDuration } from "../../util/globals";
-import InfoPanel from "./infoPanel";
-import TipSelectedPanel from "./tipSelectedPanel";
+import HoverInfoPanel from "./infoPanels/hover";
+import TipClickedPanel from "./infoPanels/click";
 import computeResponsive from "../../util/computeResponsive";
 import { updateStylesAndAttrs, salientPropChanges } from "./reactD3Interface";
 import * as callbacks from "./reactD3Interface/callbacks";
@@ -39,7 +39,7 @@ there are actually backlinks from the phylotree tree
     panelLayout: state.controls.panelLayout
   };
 })
-class TreeView extends React.Component {
+class Tree extends React.Component {
   constructor(props) {
     super(props);
     this.Viewer = null;
@@ -173,7 +173,7 @@ class TreeView extends React.Component {
     return (
       <Card center title={cardTitle}>
         <Legend padding={this.props.padding}/>
-        <InfoPanel
+        <HoverInfoPanel
           tree={this.state.tree}
           mutType={this.props.mutType}
           temporalConfidence={this.props.temporalConfidence.display}
@@ -184,7 +184,7 @@ class TreeView extends React.Component {
           colorByConfidence={this.props.colorByConfidence}
           colorScale={this.props.colorScale}
         />
-        <TipSelectedPanel
+        <TipClickedPanel
           goAwayCallback={(d) => callbacks.clearSelectedTip.bind(this)(d)}
           tip={this.state.selectedTip}
           metadata={this.props.metadata}
@@ -256,4 +256,4 @@ class TreeView extends React.Component {
   }
 }
 
-export default TreeView;
+export default Tree;
diff --git a/src/components/tree/tipSelectedPanel.js b/src/components/tree/infoPanels/click.js
similarity index 93%
rename from src/components/tree/tipSelectedPanel.js
rename to src/components/tree/infoPanels/click.js
index 7c73730e2..fd67d986a 100644
--- a/src/components/tree/tipSelectedPanel.js
+++ b/src/components/tree/infoPanels/click.js
@@ -1,7 +1,7 @@
 import React from "react";
-import { infoPanelStyles } from "../../globalStyles";
-import { prettyString, authorString } from "../../util/stringHelpers";
-import { numericToCalendar } from "../../util/dateHelpers";
+import { infoPanelStyles } from "../../../globalStyles";
+import { prettyString, authorString } from "../../../util/stringHelpers";
+import { numericToCalendar } from "../../../util/dateHelpers";
 // import { getAuthor } from "../download/helperFunctions";
 
 const styles = {
@@ -79,7 +79,7 @@ const displayVaccineInfo = (d) => {
 const validValue = (value) => value !== "?" && value !== undefined && value !== "undefined";
 const validAttr = (attrs, key) => key in attrs && validValue(attrs[key]);
 
-const TipSelectedPanel = ({tip, goAwayCallback, metadata}) => {
+const TipClickedPanel = ({tip, goAwayCallback, metadata}) => {
   if (!tip) {return null;}
   const url = validAttr(tip.n.attr, "url") ? formatURL(tip.n.attr.url) : false;
   const uncertainty = "num_date_confidence" in tip.n.attr && tip.n.attr.num_date_confidence[0] !== tip.n.attr.num_date_confidence[1];
@@ -121,4 +121,4 @@ const TipSelectedPanel = ({tip, goAwayCallback, metadata}) => {
   );
 };
 
-export default TipSelectedPanel;
+export default TipClickedPanel;
diff --git a/src/components/tree/infoPanel.js b/src/components/tree/infoPanels/hover.js
similarity index 96%
rename from src/components/tree/infoPanel.js
rename to src/components/tree/infoPanels/hover.js
index 46e92a01d..51e6e1bb7 100644
--- a/src/components/tree/infoPanel.js
+++ b/src/components/tree/infoPanels/hover.js
@@ -1,8 +1,8 @@
 import React from "react";
-import { infoPanelStyles } from "../../globalStyles";
-import { prettyString } from "../../util/stringHelpers";
-import { numericToCalendar } from "../../util/dateHelpers";
-import { getTipColorAttribute } from "./treeHelpers";
+import { infoPanelStyles } from "../../../globalStyles";
+import { prettyString } from "../../../util/stringHelpers";
+import { numericToCalendar } from "../../../util/dateHelpers";
+import { getTipColorAttribute } from "../treeHelpers";
 
 const infoLineJSX = (item, value) => (
   <g>
@@ -236,7 +236,7 @@ const displayVaccineInfo = (d) => {
 };
 
 /* the actual component - a pure function, so we can return early if needed */
-const InfoPanel = ({tree, mutType, temporalConfidence, distanceMeasure,
+const HoverInfoPanel = ({tree, mutType, temporalConfidence, distanceMeasure,
   hovered, viewer, colorBy, colorByConfidence, colorScale}) => {
   if (!(tree && hovered)) {
     return null;
@@ -280,4 +280,4 @@ const InfoPanel = ({tree, mutType, temporalConfidence, distanceMeasure,
   );
 };
 
-export default InfoPanel;
+export default HoverInfoPanel;
diff --git a/src/components/tree/legend-item.js b/src/components/tree/legend/item.js
similarity index 68%
rename from src/components/tree/legend-item.js
rename to src/components/tree/legend/item.js
index 46eb4038b..a8ba2defa 100644
--- a/src/components/tree/legend-item.js
+++ b/src/components/tree/legend/item.js
@@ -1,13 +1,18 @@
 import React from "react";
-import { legendMouseEnterExit } from "../../actions/treeProperties";
-import { dataFont, darkGrey } from "../../globalStyles";
-import { prettyString } from "../../util/stringHelpers";
+import { legendMouseEnterExit } from "../../../actions/treeProperties";
+import { dataFont, darkGrey } from "../../../globalStyles";
+import { prettyString } from "../../../util/stringHelpers";
 
 const LegendItem = ({
-  dispatch, transform,
-  legendRectSize, legendSpacing,
-  rectStroke, rectFill,
-  label, dFreq}) => (
+  dispatch,
+  transform,
+  legendRectSize,
+  legendSpacing,
+  rectStroke,
+  rectFill,
+  label,
+  dFreq
+}) => (
   <g
     transform={transform}
     onMouseEnter={() => {
diff --git a/src/components/tree/legend.js b/src/components/tree/legend/legend.js
similarity index 97%
rename from src/components/tree/legend.js
rename to src/components/tree/legend/legend.js
index 5cd0394cc..247c6e0f2 100644
--- a/src/components/tree/legend.js
+++ b/src/components/tree/legend/legend.js
@@ -2,10 +2,10 @@ import React from "react";
 import PropTypes from 'prop-types';
 import { connect } from "react-redux";
 import { rgb } from "d3-color";
-import LegendItem from "./legend-item";
-import { headerFont, darkGrey } from "../../globalStyles";
-import { legendRectSize, legendSpacing, fastTransitionDuration } from "../../util/globals";
-import { determineColorByGenotypeType } from "../../util/colorHelpers";
+import LegendItem from "./item";
+import { headerFont, darkGrey } from "../../../globalStyles";
+import { legendRectSize, legendSpacing, fastTransitionDuration } from "../../../util/globals";
+import { determineColorByGenotypeType } from "../../../util/colorHelpers";
 
 
 @connect((state) => {
diff --git a/src/components/tree/phyloTree.js b/src/components/tree/phyloTree/phyloTree.js
similarity index 99%
rename from src/components/tree/phyloTree.js
rename to src/components/tree/phyloTree/phyloTree.js
index a1e20e9ab..433b5c38c 100644
--- a/src/components/tree/phyloTree.js
+++ b/src/components/tree/phyloTree/phyloTree.js
@@ -3,8 +3,8 @@ import _debounce from "lodash/debounce";
 import { event } from "d3-selection";
 import { min, max, sum } from "d3-array";
 import { scaleLinear } from "d3-scale";
-import { flattenTree, appendParentsToTree } from "./treeHelpers";
-import { dataFont, darkGrey } from "../../globalStyles";
+import { flattenTree, appendParentsToTree } from "../treeHelpers";
+import { dataFont, darkGrey } from "../../../globalStyles";
 
 /*
  * adds the total number of descendant leaves to each node in the tree
diff --git a/src/components/tree/reactD3Interface/index.js b/src/components/tree/reactD3Interface/index.js
index abc0fb134..b9323add0 100644
--- a/src/components/tree/reactD3Interface/index.js
+++ b/src/components/tree/reactD3Interface/index.js
@@ -6,7 +6,7 @@ import { viewEntireTree } from "./callbacks";
  * function to help determine what parts of phylotree should update
  * @param {obj} props redux props
  * @param {obj} nextProps next redux props
- * @param {obj} tree phyloTree object (stored in the state of treeView)
+ * @param {obj} tree phyloTree object (stored in the state of the Tree component)
  * @return {obj} values are mostly bools, but not always
  */
 export const salientPropChanges = (props, nextProps, tree) => {

From ad5d0cd98dbc27ab22f70b172c9a81c431d773a8 Mon Sep 17 00:00:00 2001
From: James Hadfield <jh22@sanger.ac.uk>
Date: Fri, 2 Feb 2018 16:42:24 -0800
Subject: [PATCH 05/10] break phyloTree into files I

---
 .../tree/phyloTree/defaultParams.js           |  40 +++
 src/components/tree/phyloTree/helpers.js      |  18 ++
 src/components/tree/phyloTree/layouts.js      | 160 ++++++++++
 src/components/tree/phyloTree/phyloTree.js    | 295 +-----------------
 src/components/tree/phyloTree/renderers.js    |  58 ++++
 5 files changed, 292 insertions(+), 279 deletions(-)
 create mode 100644 src/components/tree/phyloTree/defaultParams.js
 create mode 100644 src/components/tree/phyloTree/helpers.js
 create mode 100644 src/components/tree/phyloTree/layouts.js
 create mode 100644 src/components/tree/phyloTree/renderers.js

diff --git a/src/components/tree/phyloTree/defaultParams.js b/src/components/tree/phyloTree/defaultParams.js
new file mode 100644
index 000000000..27a437659
--- /dev/null
+++ b/src/components/tree/phyloTree/defaultParams.js
@@ -0,0 +1,40 @@
+import { dataFont, darkGrey } from "../../../globalStyles";
+
+export const defaultParams = {
+  regressionStroke: darkGrey,
+  regressionWidth: 6,
+  majorGridStroke: "#CCC",
+  majorGridWidth: 2,
+  minorGridStroke: "#DDD",
+  minorGridWidth: 1,
+  tickLabelSize: 12,
+  tickLabelFill: darkGrey,
+  minorTicksTimeTree: 3,
+  minorTicks: 4,
+  orientation: [1, 1],
+  margins: {left: 25, right: 15, top: 5, bottom: 25},
+  showGrid: true,
+  fillSelected: "#A73",
+  radiusSelected: 5,
+  branchStroke: "#AAA",
+  branchStrokeWidth: 2,
+  tipStroke: "#AAA",
+  tipFill: "#CCC",
+  tipStrokeWidth: 1,
+  tipRadius: 4,
+  fontFamily: dataFont,
+  branchLabels: false,
+  showBranchLabels: false,
+  branchLabelFont: dataFont,
+  branchLabelFill: "#555",
+  branchLabelPadX: 8,
+  branchLabelPadY: 5,
+  tipLabels: true,
+  // showTipLabels:true,
+  tipLabelFont: dataFont,
+  tipLabelFill: "#555",
+  tipLabelPadX: 8,
+  tipLabelPadY: 2,
+  showVaccines: false,
+  mapToScreenDebounceTime: 500
+};
diff --git a/src/components/tree/phyloTree/helpers.js b/src/components/tree/phyloTree/helpers.js
new file mode 100644
index 000000000..f00d4bc76
--- /dev/null
+++ b/src/components/tree/phyloTree/helpers.js
@@ -0,0 +1,18 @@
+
+/*
+ * adds the total number of descendant leaves to each node in the tree
+ * the functions works recursively.
+ * @params:
+ *   node -- root node of the tree.
+ */
+export const addLeafCount = (node) => {
+  if (node.terminal) {
+    node.leafCount = 1;
+  } else {
+    node.leafCount = 0;
+    for (let i = 0; i < node.children.length; i++) {
+      addLeafCount(node.children[i]);
+      node.leafCount += node.children[i].leafCount;
+    }
+  }
+};
diff --git a/src/components/tree/phyloTree/layouts.js b/src/components/tree/phyloTree/layouts.js
new file mode 100644
index 000000000..b84487c91
--- /dev/null
+++ b/src/components/tree/phyloTree/layouts.js
@@ -0,0 +1,160 @@
+/* eslint-disable no-multi-spaces */
+import { sum } from "d3-array";
+import { addLeafCount } from "./helpers";
+
+/**
+ * assigns the attribute this.layout and calls the function that
+ * calculates the x,y coordinates for the respective layouts
+ * @param layout -- the layout to be used, has to be one of
+ *                  ["rect", "radial", "unrooted", "clock"]
+ */
+export const setLayout = function setLayout(layout) {
+  if (typeof layout === "undefined" || layout !== this.layout) {
+    this.nodes.forEach((d) => {d.update = true;});
+  }
+  if (typeof layout === "undefined") {
+    this.layout = "rect";
+  } else {
+    this.layout = layout;
+  }
+  if (this.layout === "rect") {
+    this.rectangularLayout();
+  } else if (this.layout === "clock") {
+    this.timeVsRootToTip();
+  } else if (this.layout === "radial") {
+    this.radialLayout();
+  } else if (this.layout === "unrooted") {
+    this.unrootedLayout();
+  }
+};
+
+
+/**
+ * assignes x,y coordinates for a rectancular layout
+ * @return {null}
+ */
+export const rectangularLayout = function rectangularLayout() {
+  this.nodes.forEach((d) => {
+    d.y = d.n.yvalue; // precomputed y-values
+    d.x = d.depth;    // depth according to current distance
+    d.px = d.pDepth;  // parent positions
+    d.py = d.y;
+    d.x_conf = d.conf; // assign confidence intervals
+  });
+};
+
+/**
+ * assign x,y coordinates fro the root-to-tip regression layout
+ * this requires a time tree with attr["num_date"] set
+ * in addition, this function calculates a regression between
+ * num_date and div which is saved as this.regression
+ * @return {null}
+ */
+export const timeVsRootToTip = function timeVsRootToTip() {
+  this.nodes.forEach((d) => {
+    d.y = d.n.attr["div"];
+    d.x = d.n.attr["num_date"];
+    d.px = d.n.parent.attr["num_date"];
+    d.py = d.n.parent.attr["div"];
+  });
+  const nTips = this.numberOfTips;
+  // REGRESSION WITH FREE INTERCEPT
+  // const meanDiv = d3.sum(this.nodes.filter((d)=>d.terminal).map((d)=>d.y))/nTips;
+  // const meanTime = d3.sum(this.nodes.filter((d)=>d.terminal).map((d)=>d.depth))/nTips;
+  // const covarTimeDiv = d3.sum(this.nodes.filter((d)=>d.terminal).map((d)=>(d.y-meanDiv)*(d.depth-meanTime)))/nTips;
+  // const varTime = d3.sum(this.nodes.filter((d)=>d.terminal).map((d)=>(d.depth-meanTime)*(d.depth-meanTime)))/nTips;
+  // const slope = covarTimeDiv/varTime;
+  // const intercept = meanDiv-meanTime*slope;
+  // REGRESSION THROUGH ROOT
+  const offset = this.nodes[0].depth;
+  const XY = sum(
+    this.nodes.filter((d) => d.terminal)
+      .map((d) => (d.y) * (d.depth - offset))
+  ) / nTips;
+  const secondMomentTime = sum(
+    this.nodes.filter((d) => d.terminal)
+      .map((d) => (d.depth - offset) * (d.depth - offset))
+  ) / nTips;
+  const slope = XY / secondMomentTime;
+  const intercept = -offset * slope;
+  this.regression = {slope: slope, intercept: intercept};
+};
+
+/*
+ * Utility function for the unrooted tree layout.
+ * assigns x,y coordinates to the subtree starting in node
+ * @params:
+ *   node -- root of the subtree.
+ *   nTips -- total number of tips in the tree.
+ */
+const unrootedPlaceSubtree = (node, nTips) => {
+  node.x = node.px + node.branchLength * Math.cos(node.tau + node.w * 0.5);
+  node.y = node.py + node.branchLength * Math.sin(node.tau + node.w * 0.5);
+  let eta = node.tau; // eta is the cumulative angle for the wedges in the layout
+  if (!node.terminal) {
+    for (let i = 0; i < node.children.length; i++) {
+      const ch = node.children[i];
+      ch.w = 2 * Math.PI * ch.leafCount / nTips;
+      ch.tau = eta;
+      eta += ch.w;
+      ch.px = node.x;
+      ch.py = node.y;
+      unrootedPlaceSubtree(ch, nTips);
+    }
+  }
+};
+
+
+/**
+ * calculates x,y coordinates for the unrooted layout. this is
+ * done recursively via a the function unrootedPlaceSubtree
+ * @return {null}
+ */
+export const unrootedLayout = function unrootedLayout() {
+  const nTips = this.numberOfTips;
+  // postorder iteration to determine leaf count of every node
+  addLeafCount(this.nodes[0]);
+  // calculate branch length from depth
+  this.nodes.forEach((d) => {d.branchLength = d.depth - d.pDepth;});
+  // preorder iteration to layout nodes
+  this.nodes[0].x = 0;
+  this.nodes[0].y = 0;
+  this.nodes[0].px = 0;
+  this.nodes[0].py = 0;
+  this.nodes[0].w = 2 * Math.PI;
+  this.nodes[0].tau = 0;
+  let eta = 1.5 * Math.PI;
+  for (let i = 0; i < this.nodes[0].children.length; i++) {
+    this.nodes[0].children[i].px = 0;
+    this.nodes[0].children[i].py = 0;
+    this.nodes[0].children[i].w = 2.0 * Math.PI * this.nodes[0].children[i].leafCount / nTips;
+    this.nodes[0].children[i].tau = eta;
+    eta += this.nodes[0].children[i].w;
+    unrootedPlaceSubtree(this.nodes[0].children[i], nTips);
+  }
+};
+
+/**
+ * calculates and assigns x,y coordinates for the radial layout.
+ * in addition to x,y, this calculates the end-points of the radial
+ * arcs and whether that arc is more than pi or not
+ * @return {null}
+ */
+export const radialLayout = function radialLayout() {
+  const nTips = this.numberOfTips;
+  const offset = this.nodes[0].depth;
+  this.nodes.forEach((d) => {
+    const angleCBar1 = 2.0 * 0.95 * Math.PI * d.yRange[0] / nTips;
+    const angleCBar2 = 2.0 * 0.95 * Math.PI * d.yRange[1] / nTips;
+    d.angle = 2.0 * 0.95 * Math.PI * d.n.yvalue / nTips;
+    d.y = (d.depth - offset) * Math.cos(d.angle);
+    d.x = (d.depth - offset) * Math.sin(d.angle);
+    d.py = d.y * (d.pDepth - offset) / (d.depth - offset + 1e-15);
+    d.px = d.x * (d.pDepth - offset) / (d.depth - offset + 1e-15);
+    d.yCBarStart = (d.depth - offset) * Math.cos(angleCBar1);
+    d.xCBarStart = (d.depth - offset) * Math.sin(angleCBar1);
+    d.yCBarEnd = (d.depth - offset) * Math.cos(angleCBar2);
+    d.xCBarEnd = (d.depth - offset) * Math.sin(angleCBar2);
+    d.smallBigArc = Math.abs(angleCBar2 - angleCBar1) > Math.PI * 1.0;
+  });
+};
diff --git a/src/components/tree/phyloTree/phyloTree.js b/src/components/tree/phyloTree/phyloTree.js
index 433b5c38c..34f8b3d95 100644
--- a/src/components/tree/phyloTree/phyloTree.js
+++ b/src/components/tree/phyloTree/phyloTree.js
@@ -5,55 +5,18 @@ import { min, max, sum } from "d3-array";
 import { scaleLinear } from "d3-scale";
 import { flattenTree, appendParentsToTree } from "../treeHelpers";
 import { dataFont, darkGrey } from "../../../globalStyles";
+import { defaultParams } from "./defaultParams";
+import { addLeafCount } from "./helpers";
 
-/*
- * adds the total number of descendant leaves to each node in the tree
- * the functions works recursively.
- * @params:
- *   node -- root node of the tree.
- */
-const addLeafCount = function (node) {
-    if (node.terminal) {
-        node.leafCount=1;
-    }else{
-        node.leafCount=0;
-        for (var i=0; i<node.children.length; i++){
-            addLeafCount(node.children[i]);
-            node.leafCount += node.children[i].leafCount;
-        }
-    }
-};
+/* PROTOTYPES */
+import { render } from "./renderers";
+import * as layouts from "./layouts";
 
 
 const contains = function(array, elem){
   return array.some(function (d){return d===elem;});
 }
 
-/*
- * Utility function for the unrooted tree layout.
- * assigns x,y coordinates to the subtree starting in node
- * @params:
- *   node -- root of the subtree.
- *   nTips -- total number of tips in the tree.
- */
-const unrootedPlaceSubtree = function(node, nTips){
-  node.x = node.px+node.branchLength*Math.cos(node.tau + node.w*0.5);
-  node.y = node.py+node.branchLength*Math.sin(node.tau + node.w*0.5);
-  var eta = node.tau; //eta is the cumulative angle for the wedges in the layout
-  if (!node.terminal){
-      for (var i=0; i<node.children.length; i++){
-          var ch = node.children[i];
-          ch.w = 2*Math.PI*ch.leafCount/nTips;
-          ch.tau = eta;
-          eta += ch.w;
-          ch.px = node.x;
-          ch.py = node.y;
-          unrootedPlaceSubtree(ch, nTips);
-      }
-  }
-};
-
-
 /*
  * this function takes a call back and applies it recursively
  * to all child nodes, including internal nodes
@@ -80,7 +43,9 @@ const applyToChildren = function(node,func){
  */
 var PhyloTree = function(treeJson) {
   this.grid = false;
-  this.setDefaults();
+  this.grid = false;
+  this.attributes = ['r', 'cx', 'cy', 'id', 'class', 'd'];
+  this.params = defaultParams;
   appendParentsToTree(treeJson); // add reference to .parent to each node in tree
   const nodesArray = flattenTree(treeJson); // convert the tree json into a flat list of nodes
   // wrap each node in a shell structure to avoid mutating the input data
@@ -127,117 +92,11 @@ var PhyloTree = function(treeJson) {
     {leading: false, trailing: true, maxWait: this.params.mapToScreenDebounceTime});
 };
 
-/*
- * set default values.
- */
-PhyloTree.prototype.setDefaults = function () {
-    this.grid = false;
-    this.attributes = ['r', 'cx', 'cy', 'id', 'class', 'd'];
-    this.params = {
-        regressionStroke: darkGrey,
-        regressionWidth: 6,
-        majorGridStroke: "#CCC",
-        majorGridWidth: 2,
-        minorGridStroke: "#DDD",
-        minorGridWidth: 1,
-        tickLabelSize: 12,
-        tickLabelFill: darkGrey,
-        minorTicksTimeTree: 3,
-        minorTicks: 4,
-        orientation: [1,1],
-        margins: {left:25, right:15, top:5, bottom:25},
-        showGrid: true,
-        fillSelected:"#A73",
-        radiusSelected:5,
-        branchStroke: "#AAA",
-        branchStrokeWidth: 2,
-        tipStroke: "#AAA",
-        tipFill: "#CCC",
-        tipStrokeWidth: 1,
-        tipRadius: 4,
-        fontFamily: dataFont,
-        branchLabels:false,
-        showBranchLabels:false,
-        branchLabelFont: dataFont,
-        branchLabelFill: "#555",
-        branchLabelPadX: 8,
-        branchLabelPadY:5,
-        tipLabels:true,
-        // showTipLabels:true,
-        tipLabelFont: dataFont,
-        tipLabelFill: "#555",
-        tipLabelPadX: 8,
-        tipLabelPadY: 2,
-        showVaccines: false,
-        mapToScreenDebounceTime: 500
-    };
-};
-
+/* DEFINE THE PROTOTYPES */
+PhyloTree.prototype.render = render;
 
-/**
- * @param  svg    -- the svg into which the tree is drawn
- * @param  layout -- the layout to be used, e.g. "rect"
- * @param  distance   -- the property used as branch length, e.g. div or num_date
- * @param  options    -- an object that contains options that will be added to this.params
- * @param  callbacks  -- an object with call back function defining mouse behavior
- * @param  branchThickness (OPTIONAL) -- array of branch thicknesses
- * @param  visibility (OPTIONAL) -- array of "visible" or "hidden"
- * @return {null}
- */
-PhyloTree.prototype.render = function(svg, layout, distance, options, callbacks, branchThickness, visibility, drawConfidence, vaccines) {
-  if (branchThickness) {
-    this.nodes.forEach(function(d, i) {
-      d["stroke-width"] = branchThickness[i];
-    });
-  }
-  this.svg = svg;
-  this.params = Object.assign(this.params, options);
-  this.callbacks = callbacks;
-  this.vaccines = vaccines ? vaccines.map((d) => d.shell) : undefined;
-
-  this.clearSVG();
-  this.setDistance(distance);
-  this.setLayout(layout);
-  this.mapToScreen();
-  if (this.params.showGrid){
-      this.addGrid();
-  }
-  if (this.params.branchLabels){
-    this.drawBranches();
-  }
-  this.drawTips();
-  if (this.params.showVaccines) {
-    this.drawVaccines();
-  }
-  this.drawCladeLabels();
-  if (visibility) {
-    this.nodes.forEach(function(d, i) {
-      d["visibility"] = visibility[i];
-    });
-    this.svg.selectAll(".tip").style("visibility", (d) => d["visibility"]);
-  }
-
-  // setting branchLabels and tipLabels to false above in params is not working for some react-dimensions
-  // hence the commenting here
-  // if (this.params.branchLabels){
-  //   this.drawBranchLabels();
-  // }
-  /* don't even bother initially - there will be too many! */
-  // if (this.params.tipLabels){
-  //   this.updateTipLabels(100);
-  // }
 
-  this.updateGeometry(10);
 
-  this.svg.selectAll(".regression").remove();
-  if (this.layout === "clock" && this.distance === "num_date") {
-    this.drawRegression();
-  }
-  this.removeConfidence();
-  if (drawConfidence) {
-    this.drawConfidence();
-  }
-};
 
 /*
  * update branchThicknesses without modifying the SVG
@@ -277,83 +136,14 @@ PhyloTree.prototype.setDistance = function(distanceAttribute) {
   });
 };
 
+/* LAYOUT PROTOTYPES */
+PhyloTree.prototype.setLayout = layouts.setLayout;
+PhyloTree.prototype.rectangularLayout = layouts.rectangularLayout;
+PhyloTree.prototype.timeVsRootToTip = layouts.timeVsRootToTip;
+PhyloTree.prototype.unrootedLayout = layouts.unrootedLayout;
+PhyloTree.prototype.radialLayout = layouts.radialLayout;
 
 
-/**
- * assigns the attribute this.layout and calls the function that
- * calculates the x,y coordinates for the respective layouts
- * @param layout -- the layout to be used, has to be one of
- *                  ["rect", "radial", "unrooted", "clock"]
- */
-PhyloTree.prototype.setLayout = function(layout){
-    if (typeof layout==="undefined" || layout!==this.layout){
-        this.nodes.forEach(function(d){d.update=true});
-    }
-    if (typeof layout==="undefined"){
-        this.layout = "rect";
-    }else {
-        this.layout = layout;
-    }
-    if (this.layout==="rect"){
-        this.rectangularLayout();
-    } else if (this.layout==="clock"){
-        this.timeVsRootToTip();
-    } else if (this.layout==="radial"){
-        this.radialLayout();
-    } else if (this.layout==="unrooted"){
-        this.unrootedLayout();
-    }
-};
-
-
-
-/// ASSIGN XY COORDINATES FOR DIFFERENCE LAYOUTS
-
-/**
- * assignes x,y coordinates for a rectancular layout
- * @return {null}
- */
-PhyloTree.prototype.rectangularLayout = function() {
-  this.nodes.forEach(function(d) {
-    d.y = d.n.yvalue; // precomputed y-values
-    d.x = d.depth;    // depth according to current distance
-    d.px = d.pDepth;  // parent positions
-    d.py = d.y;
-    d.x_conf = d.conf; // assign confidence intervals
-  });
-};
-
-/**
- * assign x,y coordinates fro the root-to-tip regression layout
- * this requires a time tree with attr["num_date"] set
- * in addition, this function calculates a regression between
- * num_date and div which is saved as this.regression
- * @return {null}
- */
-PhyloTree.prototype.timeVsRootToTip = function(){
-  this.nodes.forEach(function (d) {
-    d.y = d.n.attr["div"];
-    d.x = d.n.attr["num_date"];
-    d.px = d.n.parent.attr["num_date"];
-    d.py = d.n.parent.attr["div"];
-  });
-  const nTips = this.numberOfTips;
-  // REGRESSION WITH FREE INTERCEPT
-  // const meanDiv = d3.sum(this.nodes.filter((d)=>d.terminal).map((d)=>d.y))/nTips;
-  // const meanTime = d3.sum(this.nodes.filter((d)=>d.terminal).map((d)=>d.depth))/nTips;
-  // const covarTimeDiv = d3.sum(this.nodes.filter((d)=>d.terminal).map((d)=>(d.y-meanDiv)*(d.depth-meanTime)))/nTips;
-  // const varTime = d3.sum(this.nodes.filter((d)=>d.terminal).map((d)=>(d.depth-meanTime)*(d.depth-meanTime)))/nTips;
-  //const slope = covarTimeDiv/varTime;
-  //const intercept = meanDiv-meanTime*slope;
-  // REGRESSION THROUGH ROOT
-  const offset = this.nodes[0].depth;
-  const XY = sum(this.nodes.filter((d)=>d.terminal).map((d)=>(d.y)*(d.depth-offset)))/nTips;
-  const secondMomentTime = sum(this.nodes.filter((d)=>d.terminal).map((d)=>(d.depth-offset)*(d.depth-offset)))/nTips;
-  const slope = XY/secondMomentTime;
-  const intercept = -offset*slope;
-  this.regression = {slope:slope, intercept: intercept};
-};
-
 /**
  * draws the regression line in the svg and adds a text with the rate estimate
  * @return {null}
@@ -384,59 +174,6 @@ PhyloTree.prototype.drawRegression = function(){
         .style("font-family",this.params.fontFamily);
 };
 
-/**
- * calculates and assigns x,y coordinates for the radial layout.
- * in addition to x,y, this calculates the end-points of the radial
- * arcs and whether that arc is more than pi or not
- * @return {null}
- */
-PhyloTree.prototype.radialLayout = function() {
-  const nTips = this.numberOfTips;
-  const offset = this.nodes[0].depth;
-  this.nodes.forEach(function(d) {
-    const angleCBar1 = 2.0 * 0.95 * Math.PI * d.yRange[0] / nTips;
-    const angleCBar2 = 2.0 * 0.95 * Math.PI * d.yRange[1] / nTips;
-    d.angle = 2.0 * 0.95 * Math.PI * d.n.yvalue / nTips;
-    d.y = (d.depth - offset) * Math.cos(d.angle);
-    d.x = (d.depth - offset) * Math.sin(d.angle);
-    d.py = d.y * (d.pDepth - offset) / (d.depth - offset + 1e-15);
-    d.px = d.x * (d.pDepth - offset) / (d.depth - offset + 1e-15);
-    d.yCBarStart = (d.depth - offset) * Math.cos(angleCBar1);
-    d.xCBarStart = (d.depth - offset) * Math.sin(angleCBar1);
-    d.yCBarEnd = (d.depth - offset) * Math.cos(angleCBar2);
-    d.xCBarEnd = (d.depth - offset) * Math.sin(angleCBar2);
-    d.smallBigArc = Math.abs(angleCBar2 - angleCBar1) > Math.PI * 1.0;
-  });
-};
-
-/**
- * calculates x,y coordinates for the unrooted layout. this is
- * done recursively via a the function unrootedPlaceSubtree
- * @return {null}
- */
-PhyloTree.prototype.unrootedLayout = function(){
-  const nTips=this.numberOfTips;
-  //postorder iteration to determine leaf count of every node
-  addLeafCount(this.nodes[0]);
-  //calculate branch length from depth
-  this.nodes.forEach(function(d){d.branchLength = d.depth - d.pDepth;});
-  //preorder iteration to layout nodes
-  this.nodes[0].x = 0;
-  this.nodes[0].y = 0;
-  this.nodes[0].px = 0;
-  this.nodes[0].py = 0;
-  this.nodes[0].w = 2*Math.PI;
-  this.nodes[0].tau = 0;
-  var eta = 1.5*Math.PI;
-  for (var i=0; i<this.nodes[0].children.length; i++){
-    this.nodes[0].children[i].px=0;
-    this.nodes[0].children[i].py=0;
-    this.nodes[0].children[i].w = 2.0*Math.PI*this.nodes[0].children[i].leafCount/nTips;
-    this.nodes[0].children[i].tau = eta;
-    eta += this.nodes[0].children[i].w;
-    unrootedPlaceSubtree(this.nodes[0].children[i], nTips);
-  }
-};
 
 ///****************************************************************
 
diff --git a/src/components/tree/phyloTree/renderers.js b/src/components/tree/phyloTree/renderers.js
new file mode 100644
index 000000000..e0401ac26
--- /dev/null
+++ b/src/components/tree/phyloTree/renderers.js
@@ -0,0 +1,58 @@
+/**
+ * @param  svg    -- the svg into which the tree is drawn
+ * @param  layout -- the layout to be used, e.g. "rect"
+ * @param  distance   -- the property used as branch length, e.g. div or num_date
+ * @param  options    -- an object that contains options that will be added to this.params
+ * @param  callbacks  -- an object with call back function defining mouse behavior
+ * @param  branchThickness (OPTIONAL) -- array of branch thicknesses
+ * @param  visibility (OPTIONAL) -- array of "visible" or "hidden"
+ * @return {null}
+ */
+export const render = function render(svg, layout, distance, options, callbacks, branchThickness, visibility, drawConfidence, vaccines) {
+  if (branchThickness) {
+    this.nodes.forEach((d, i) => {
+      d["stroke-width"] = branchThickness[i];
+    });
+  }
+  this.svg = svg;
+  this.params = Object.assign(this.params, options);
+  this.callbacks = callbacks;
+  this.vaccines = vaccines ? vaccines.map((d) => d.shell) : undefined;
+
+  this.clearSVG();
+  this.setDistance(distance);
+  this.setLayout(layout);
+  this.mapToScreen();
+  if (this.params.showGrid) {this.addGrid();}
+  if (this.params.branchLabels) {this.drawBranches();}
+  this.drawTips();
+  if (this.params.showVaccines) {this.drawVaccines();}
+  this.drawCladeLabels();
+  if (visibility) {
+    this.nodes.forEach((d, i) => {
+      d["visibility"] = visibility[i];
+    });
+    this.svg.selectAll(".tip").style("visibility", (d) => d["visibility"]);
+  }
+
+  // setting branchLabels and tipLabels to false above in params is not working for some react-dimensions
+  // hence the commenting here
+  // if (this.params.branchLabels){
+  //   this.drawBranchLabels();
+  // }
+  /* don't even bother initially - there will be too many! */
+  // if (this.params.tipLabels){
+  //   this.updateTipLabels(100);
+  // }
+
+  this.updateGeometry(10);
+
+  this.svg.selectAll(".regression").remove();
+  if (this.layout === "clock" && this.distance === "num_date") {
+    this.drawRegression();
+  }
+  this.removeConfidence();
+  if (drawConfidence) {
+    this.drawConfidence();
+  }
+};

From b7fb9ebde46770e54420c8245143a00cadcf8a55 Mon Sep 17 00:00:00 2001
From: James Hadfield <jh22@sanger.ac.uk>
Date: Fri, 2 Feb 2018 17:26:35 -0800
Subject: [PATCH 06/10] break phyloTree into files II

---
 src/components/tree/phyloTree/confidence.js |  65 +++
 src/components/tree/phyloTree/grid.js       | 175 ++++++++
 src/components/tree/phyloTree/helpers.js    |  18 +
 src/components/tree/phyloTree/layouts.js    |  25 ++
 src/components/tree/phyloTree/phyloTree.js  | 437 +-------------------
 src/components/tree/phyloTree/zoom.js       | 135 ++++++
 6 files changed, 433 insertions(+), 422 deletions(-)
 create mode 100644 src/components/tree/phyloTree/confidence.js
 create mode 100644 src/components/tree/phyloTree/grid.js
 create mode 100644 src/components/tree/phyloTree/zoom.js

diff --git a/src/components/tree/phyloTree/confidence.js b/src/components/tree/phyloTree/confidence.js
new file mode 100644
index 000000000..1b1f40357
--- /dev/null
+++ b/src/components/tree/phyloTree/confidence.js
@@ -0,0 +1,65 @@
+/* eslint-disable */
+
+export const removeConfidence = function removeConfidence(dt) {
+  if (dt) {
+    this.svg.selectAll(".conf")
+      .transition()
+      .duration(dt)
+      .style("opacity", 0)
+    .remove();
+  } else {
+    this.svg.selectAll(".conf").remove();
+  }
+  // this.props.confidence = false;
+};
+
+export const drawConfidence = function drawConfidence(dt) {
+  // this.removeConfidence(); // just in case
+  // console.log("drawing:", this.svg.selectAll(".conf"))
+  if (dt) {
+    this.confidence = this.svg.append("g").selectAll(".conf")
+      .data(this.nodes)
+      .enter()
+        .call((sel) => this.drawSingleCI(sel, 0));
+    this.svg.selectAll(".conf")
+        .transition()
+          .duration(dt)
+          .style("opacity", 0.5);
+  } else {
+    this.confidence = this.svg.append("g").selectAll(".conf")
+      .data(this.nodes)
+      .enter()
+        .call((sel) => this.drawSingleCI(sel, 0.5));
+  }
+  // this.props.confidence = true;
+};
+
+const confidenceWidth = (el) =>
+  el["stroke-width"] === 1 ? 0 :
+    el["stroke-width"] > 6 ? el["stroke-width"] + 6 : el["stroke-width"] * 2;
+
+export const drawSingleCI = function drawSingleCI(selection, opacity) {
+  selection.append("path")
+    .attr("class", "conf")
+    .attr("id", (d) => "conf_" + d.n.clade)
+    .attr("d", (d) => d.confLine)
+    .style("stroke", (d) => d.stroke || "#888")
+    .style("opacity", opacity)
+    .style("fill", "none")
+    .style("stroke-width", confidenceWidth);
+};
+
+
+export const updateConfidence = function updateConfidence(dt) {
+  if (dt) {
+    this.svg.selectAll(".conf")
+      .transition()
+        .duration(dt)
+      .style("stroke", (el) => el.stroke)
+      .style("stroke-width", confidenceWidth);
+  } else {
+    this.svg.selectAll(".conf")
+      .style("stroke", (el) => el.stroke)
+      .style("stroke-width", confidenceWidth);
+  }
+};
diff --git a/src/components/tree/phyloTree/grid.js b/src/components/tree/phyloTree/grid.js
new file mode 100644
index 000000000..9d4f8121f
--- /dev/null
+++ b/src/components/tree/phyloTree/grid.js
@@ -0,0 +1,175 @@
+/* eslint-disable */
+
+
+
+export const removeGrid = function removeGrid() {
+  this.svg.selectAll(".majorGrid").remove();
+  this.svg.selectAll(".minorGrid").remove();
+  this.svg.selectAll(".gridTick").remove();
+  this.grid = false;
+};
+
+export const hideGrid = function hideGrid() {
+  this.svg.selectAll(".majorGrid").style('visibility', 'hidden');
+  this.svg.selectAll(".minorGrid").style('visibility', 'hidden');
+  this.svg.selectAll(".gridTick").style('visibility', 'hidden');
+};
+
+/**
+ * add a grid to the svg
+ * @param {layout}
+ */
+export const addGrid = function addGrid(layout, yMinView, yMaxView) {
+  if (typeof layout==="undefined"){ layout=this.layout;}
+
+  const xmin = (this.xScale.domain()[0]>0)?this.xScale.domain()[0]:0.0;
+  const ymin = this.yScale.domain()[1];
+  const ymax = this.yScale.domain()[0];
+  const xmax = layout=="radial"
+                ? max([this.xScale.domain()[1], this.yScale.domain()[1],
+                          -this.xScale.domain()[0], -this.yScale.domain()[0]])
+                : this.xScale.domain()[1];
+
+  const offset = layout==="radial"?this.nodes[0].depth:0.0;
+  const viewTop = yMaxView ?    yMaxView+this.params.margins.top : this.yScale.range()[0];
+  const viewBottom = yMinView ? yMinView-this.params.margins.bottom : this.yScale.range()[1];
+
+  /* should we re-draw the grid? */
+  if (!this.gridParams) {
+    this.gridParams = [xmin, xmax, ymin, ymax, viewTop, viewBottom, layout];
+  } else if (xmin === this.gridParams[0] && xmax === this.gridParams[1] &&
+        ymin === this.gridParams[2] && ymax === this.gridParams[3] &&
+        viewTop === this.gridParams[4] && viewBottom === this.gridParams[5] &&
+        layout === this.gridParams[6]) {
+    // console.log("bailing - no difference");
+    return;
+  }
+
+  /* yes - redraw and update gridParams */
+  this.gridParams = [xmin, xmax, ymin, ymax, viewTop, viewBottom, layout];
+
+
+  const gridline = function(xScale, yScale, layout){
+      return function(x){
+          const xPos = xScale(x[0]-offset);
+          let tmp_d="";
+          if (layout==="rect" || layout==="clock"){
+            tmp_d = 'M'+xPos.toString() +
+              " " +
+              viewBottom.toString() +
+              " L " +
+              xPos.toString() +
+              " " +
+              viewTop.toString();
+          }else if (layout==="radial"){
+            tmp_d = 'M '+xPos.toString() +
+              "  " +
+              yScale(0).toString() +
+              " A " +
+              (xPos - xScale(0)).toString() +
+              " " +
+              (yScale(x[0]) - yScale(offset)).toString() +
+              " 0 1 0 " +
+              xPos.toString() +
+              " " +
+              (yScale(0)+0.001).toString();
+          }
+          return tmp_d;
+      };
+  };
+
+  const logRange = Math.floor(Math.log10(xmax - xmin));
+  const roundingLevel = Math.pow(10, logRange);
+  const gridMin = Math.floor((xmin+offset)/roundingLevel)*roundingLevel;
+  const gridPoints = [];
+  for (let ii = 0; ii <= (xmax + offset - gridMin)/roundingLevel+10; ii++) {
+    const pos = gridMin + roundingLevel*ii;
+    if (pos>offset){
+        gridPoints.push([pos, pos-offset>xmax?"hidden":"visible", "x"]);
+    }
+  }
+
+  const majorGrid = this.svg.selectAll('.majorGrid').data(gridPoints);
+
+  majorGrid.exit().remove(); // EXIT
+  majorGrid.enter().append("path") // ENTER
+    .merge(majorGrid) // ENTER + UPDATE
+    .attr("d", gridline(this.xScale, this.yScale, layout))
+    .attr("class", "majorGrid")
+    .style("fill", "none")
+    .style("visibility", function (d){return d[1];})
+    .style("stroke",this.params.majorGridStroke)
+    .style("stroke-width",this.params.majorGridWidth);
+
+  const xTextPos = function(xScale, layout){
+      return function(x){
+          if (x[2]==="x"){
+              return layout==="radial" ? xScale(0) :  xScale(x[0]);
+          }else{
+              return xScale.range()[1];
+          }
+      }
+  };
+  const yTextPos = function(yScale, layout){
+      return function(x){
+          if (x[2]==="x"){
+              return layout==="radial" ? yScale(x[0]-offset) : viewBottom +  18;
+          }else{
+              return yScale(x[0]);
+          }
+      }
+  };
+
+  let logRangeY = 0;
+  if (this.layout==="clock"){
+      const roundingLevelY = Math.pow(10, logRangeY);
+      logRangeY = Math.floor(Math.log10(ymax - ymin));
+      const offsetY=0;
+      const gridMinY = Math.floor((ymin+offsetY)/roundingLevelY)*roundingLevelY;
+      for (let ii = 0; ii <= (ymax + offsetY - gridMinY)/roundingLevelY+10; ii++) {
+        const pos = gridMinY + roundingLevelY*ii;
+        if (pos>offsetY){
+            gridPoints.push([pos, pos-offsetY>ymax?"hidden":"visible","y"]);
+        }
+      }
+  }
+
+  const minorRoundingLevel = roundingLevel / (this.distanceMeasure === "num_date"
+                                              ? this.params.minorTicksTimeTree
+                                              : this.params.minorTicks);
+  const minorGridPoints = [];
+  for (let ii = 0; ii <= (xmax + offset - gridMin)/minorRoundingLevel+50; ii++) {
+    const pos = gridMin + minorRoundingLevel*ii;
+    if (pos>offset){
+        minorGridPoints.push([pos, pos-offset>xmax+minorRoundingLevel?"hidden":"visible"]);
+    }
+  }
+  const minorGrid = this.svg.selectAll('.minorGrid').data(minorGridPoints);
+  minorGrid.exit().remove(); // EXIT
+  minorGrid.enter().append("path") // ENTER
+    .merge(minorGrid) // ENTER + UPDATE
+    .attr("d", gridline(this.xScale, this.yScale, layout))
+    .attr("class", "minorGrid")
+    .style("fill", "none")
+    .style("visibility", function (d){return d[1];})
+    .style("stroke",this.params.minorGridStroke)
+    .style("stroke-width",this.params.minorGridWidth);
+
+  const gridLabels = this.svg.selectAll('.gridTick').data(gridPoints);
+  const precision = Math.max(0, 1-logRange);
+  const precisionY = Math.max(0, 1-logRangeY);
+  gridLabels.exit().remove(); // EXIT
+  gridLabels.enter().append("text") // ENTER
+    .merge(gridLabels) // ENTER + UPDATE
+    .text(function(d){return d[0].toFixed(d[2]==='y'?precisionY:precision);})
+    .attr("class", "gridTick")
+    .style("font-size",this.params.tickLabelSize)
+    .style("font-family",this.params.fontFamily)
+    .style("fill",this.params.tickLabelFill)
+    .style("text-anchor", this.layout==="radial" ? "end" : "middle")
+    .style("visibility", function (d){return d[1];})
+    .attr("x", xTextPos(this.xScale, layout))
+    .attr("y", yTextPos(this.yScale, layout));
+
+  this.grid=true;
+};
diff --git a/src/components/tree/phyloTree/helpers.js b/src/components/tree/phyloTree/helpers.js
index f00d4bc76..a596fe584 100644
--- a/src/components/tree/phyloTree/helpers.js
+++ b/src/components/tree/phyloTree/helpers.js
@@ -16,3 +16,21 @@ export const addLeafCount = (node) => {
     }
   }
 };
+
+
+/*
+ * this function takes a call back and applies it recursively
+ * to all child nodes, including internal nodes
+ * @params:
+ *   node -- node to whose children the function is to be applied
+ *   func -- call back function to apply
+ */
+export const applyToChildren = (node, func) => {
+  func(node);
+  if (node.terminal) {
+    return;
+  }
+  for (let i = 0; i < node.children.length; i++) {
+    applyToChildren(node.children[i], func);
+  }
+};
diff --git a/src/components/tree/phyloTree/layouts.js b/src/components/tree/phyloTree/layouts.js
index b84487c91..83b8608c8 100644
--- a/src/components/tree/phyloTree/layouts.js
+++ b/src/components/tree/phyloTree/layouts.js
@@ -158,3 +158,28 @@ export const radialLayout = function radialLayout() {
     d.smallBigArc = Math.abs(angleCBar2 - angleCBar1) > Math.PI * 1.0;
   });
 };
+
+/*
+ * set the property that is used as distance along branches
+ * this is set to "depth" of each node. depth is later used to
+ * calculate coordinates. Parent depth is assigned as well.
+ */
+export const setDistance = function setDistance(distanceAttribute) {
+  this.nodes.forEach((d) => {d.update = true;});
+  if (typeof distanceAttribute === "undefined") {
+    this.distance = "div"; // default is "div" for divergence
+  } else {
+    this.distance = distanceAttribute;
+  }
+  // assign node and parent depth
+  const tmp_dist = this.distance;
+  this.nodes.forEach((d) => {
+    d.depth = d.n.attr[tmp_dist];
+    d.pDepth = d.n.parent.attr[tmp_dist];
+    if (d.n.attr[tmp_dist + "_confidence"]) {
+      d.conf = d.n.attr[tmp_dist + "_confidence"];
+    } else {
+      d.conf = [d.depth, d.depth];
+    }
+  });
+};
diff --git a/src/components/tree/phyloTree/phyloTree.js b/src/components/tree/phyloTree/phyloTree.js
index 34f8b3d95..2330cd28c 100644
--- a/src/components/tree/phyloTree/phyloTree.js
+++ b/src/components/tree/phyloTree/phyloTree.js
@@ -11,29 +11,14 @@ import { addLeafCount } from "./helpers";
 /* PROTOTYPES */
 import { render } from "./renderers";
 import * as layouts from "./layouts";
-
+import * as zoom from "./zoom";
+import * as grid from "./grid";
+import * as confidence from "./confidence";
 
 const contains = function(array, elem){
   return array.some(function (d){return d===elem;});
 }
 
-/*
- * this function takes a call back and applies it recursively
- * to all child nodes, including internal nodes
- * @params:
- *   node -- node to whose children the function is to be applied
- *   func -- call back function to apply
- */
-const applyToChildren = function(node,func){
-  func(node);
-  if (node.terminal){ return;}
-  else{
-    for (let i=0; i<node.children.length; i++){
-      applyToChildren(node.children[i], func);
-    }
-  }
-};
-
 /*
  * phylogenetic tree drawing class
  * it is instantiated with a nested tree json.
@@ -42,7 +27,6 @@ const applyToChildren = function(node,func){
  *   treeJson -- tree object as exported by nextstrain.
  */
 var PhyloTree = function(treeJson) {
-  this.grid = false;
   this.grid = false;
   this.attributes = ['r', 'cx', 'cy', 'id', 'class', 'd'];
   this.params = defaultParams;
@@ -95,55 +79,14 @@ var PhyloTree = function(treeJson) {
 /* DEFINE THE PROTOTYPES */
 PhyloTree.prototype.render = render;
 
-
-
-
-/*
- * update branchThicknesses without modifying the SVG
- */
-// PhyloTree.prototype.changeBranchThicknessAttr = function (thicknesses) {
-//   this.nodes.forEach(function(d, i) {
-//     if (thicknesses[i] !== d["stroke-width"]) {
-//       d["stroke-width"] = thicknesses[i];
-//     }
-//   });
-// };
-
-/*
- * set the property that is used as distance along branches
- * this is set to "depth" of each node. depth is later used to
- * calculate coordinates. Parent depth is assigned as well.
- */
-PhyloTree.prototype.setDistance = function(distanceAttribute) {
-  this.nodes.forEach(function(d) {
-    d.update = true
-  });
-  if (typeof distanceAttribute === "undefined") {
-    this.distance = "div"; // default is "div" for divergence
-  } else {
-    this.distance = distanceAttribute;
-  }
-  // assign node and parent depth
-  var tmp_dist = this.distance;
-  this.nodes.forEach(function(d) {
-    d.depth = d.n.attr[tmp_dist];
-    d.pDepth = d.n.parent.attr[tmp_dist];
-    if (d.n.attr[tmp_dist+"_confidence"]){
-      d.conf = d.n.attr[tmp_dist+"_confidence"];
-    }else{
-      d.conf = [d.depth, d.depth];
-    }
-  });
-};
-
 /* LAYOUT PROTOTYPES */
+PhyloTree.prototype.setDistance = layouts.setDistance;
 PhyloTree.prototype.setLayout = layouts.setLayout;
 PhyloTree.prototype.rectangularLayout = layouts.rectangularLayout;
 PhyloTree.prototype.timeVsRootToTip = layouts.timeVsRootToTip;
 PhyloTree.prototype.unrootedLayout = layouts.unrootedLayout;
 PhyloTree.prototype.radialLayout = layouts.radialLayout;
 
-
 /**
  * draws the regression line in the svg and adds a text with the rate estimate
  * @return {null}
@@ -179,131 +122,15 @@ PhyloTree.prototype.drawRegression = function(){
 
 // MAPPING TO SCREEN
 
-/**
- * zoom such that a particular clade fills the svg
- * @param  clade -- branch/node at the root of the clade to zoom into
- * @param  dt -- time of the transition in milliseconds
- * @return {null}
- */
-PhyloTree.prototype.zoomIntoClade = function(clade, dt) {
-  // assign all nodes to inView false and force update
-  this.zoomNode = clade;
-  this.nodes.forEach(function(d){d.inView=false; d.update=true;});
-  // assign all child nodes of the chosen clade to inView=true
-  // if clade is terminal, apply to parent
-  if (clade.terminal){
-    applyToChildren(clade.parent, function(d){d.inView=true;});
-  }else{
-    applyToChildren(clade, function(d){d.inView=true;});
-  }
-  // redraw
-  this.mapToScreen();
-  this.updateGeometry(dt);
-  if (this.grid) this.addGrid(this.layout);
-  this.svg.selectAll(".regression").remove();
-  if (this.layout==="clock" && this.distance === "num_date") this.drawRegression();
-  if (this.params.branchLabels){
-    this.updateBranchLabels(dt);
-  }
-  this.updateTipLabels(dt);
-};
+PhyloTree.prototype.zoomIntoClade = zoom.zoomIntoClade;
+PhyloTree.prototype.zoomToParent = zoom.zoomToParent;
+PhyloTree.prototype.mapToScreen = zoom.mapToScreen;
+
 
 
-/**
- * zoom out a little by using the parent of the current clade
- * as a zoom focus.
- * @param  {int} dt [transition time]
- */
-PhyloTree.prototype.zoomToParent = function(dt) {
-  if (this.zoomNode){
-    this.zoomIntoClade(this.zoomNode.parent, dt);
-  }
-}
 
-/**
- * this function sets the xScale, yScale domains and maps precalculated x,y
- * coordinates to their places on the screen
- * @return {null}
- */
-PhyloTree.prototype.mapToScreen = function(){
-    this.setScales(this.params.margins);
-    // determine x,y values of visibile nodes
-    const tmp_xValues = this.nodes.filter(function(d){return d.inView;}).map(function(d){return d.x});
-    const tmp_yValues = this.nodes.filter(function(d){return d.inView;}).map(function(d){return d.y});
-    this.nNodesInView = this.nodes.filter(function(d){return d.inView&&d.terminal;}).length;
-
-    if (this.layout==="radial" || this.layout==="unrooted") {
-        // handle "radial and unrooted differently since they need to be square
-        // since branch length move in x and y direction
-        // TODO: should be tied to svg dimensions
-        const minX = min(tmp_xValues);
-        const minY = min(tmp_yValues);
-        const spanX = max(tmp_xValues)-minX;
-        const spanY = max(tmp_yValues)-minY;
-        const maxSpan = max([spanY, spanX]);
-        const ySlack = (spanX>spanY) ? (spanX-spanY)*0.5 : 0.0;
-        const xSlack = (spanX<spanY) ? (spanY-spanX)*0.5 : 0.0;
-        this.xScale.domain([minX-xSlack, minX+maxSpan-xSlack]);
-        this.yScale.domain([minY-ySlack, minY+maxSpan-ySlack]);
-    }else if (this.layout==="clock"){
-        // same as rectangular, but flipped yscale
-        this.xScale.domain([min(tmp_xValues), max(tmp_xValues)]);
-        this.yScale.domain([max(tmp_yValues), min(tmp_yValues)]);
-    }else{ //rectangular
-        this.xScale.domain([min(tmp_xValues), max(tmp_xValues)]);
-        this.yScale.domain([min(tmp_yValues), max(tmp_yValues)]);
-    }
 
-    // pass all x,y through scales and assign to xTip, xBase
-    const tmp_xScale=this.xScale;
-    const tmp_yScale=this.yScale;
-    this.nodes.forEach(function(d){d.xTip = tmp_xScale(d.x)});
-    this.nodes.forEach(function(d){d.yTip = tmp_yScale(d.y)});
-    this.nodes.forEach(function(d){d.xBase = tmp_xScale(d.px)});
-    this.nodes.forEach(function(d){d.yBase = tmp_yScale(d.py)});
-    if (this.params.confidence && this.layout==="rect"){
-      this.nodes.forEach(function(d){d.xConf = [tmp_xScale(d.conf[0]), tmp_xScale(d.conf[1])];});
-    }
 
-    // assign the branches as path to each node for the different layouts
-    if (this.layout==="clock" || this.layout==="unrooted"){
-        this.nodes.forEach(function(d){d.branch =[" M "+d.xBase.toString()+","+d.yBase.toString()+
-                                                 " L "+d.xTip.toString()+","+d.yTip.toString(),""];});
-    } else if (this.layout==="rect"){
-        const tmpStrokeWidth = this.params.branchStrokeWidth;
-        this.nodes.forEach(function(d){d.cBarStart = tmp_yScale(d.yRange[0])})
-        this.nodes.forEach(function(d){d.cBarEnd = tmp_yScale(d.yRange[1])  });
-        //this.nodes.forEach(function(d){d.branch =[" M "+d.xBase.toString()+","+d.yBase.toString()+
-        const stem_offset = this.nodes.map(function(d){return (0.5*(d.parent["stroke-width"] - d["stroke-width"]) || 0.0);});
-        this.nodes.forEach(function(d,i){
-          d.branch =[" M "+(d.xBase - stem_offset[i]).toString()
-                       +","+d.yBase.toString()+
-                       " L "+d.xTip.toString()+","+d.yTip.toString(),
-                       " M "+d.xTip.toString()+","+d.cBarStart.toString()+
-                       " L "+d.xTip.toString()+","+d.cBarEnd.toString()];});
-        if (this.params.confidence){
-          this.nodes.forEach(function(d){d.confLine =" M "+d.xConf[0].toString()+","+d.yBase.toString()+
-                                                   " L "+d.xConf[1].toString()+","+d.yTip.toString();});
-        }
-    } else if (this.layout==="radial"){
-        const offset = this.nodes[0].depth;
-        const stem_offset_radial = this.nodes.map(function(d){return (0.5*(d.parent["stroke-width"] - d["stroke-width"]) || 0.0);});
-        this.nodes.forEach(function(d){d.cBarStart = tmp_yScale(d.yRange[0])});
-        this.nodes.forEach(function(d){d.cBarEnd = tmp_yScale(d.yRange[1])});
-        this.nodes.forEach(function(d,i){
-            d.branch =[" M "+(d.xBase-stem_offset_radial[i]*Math.sin(d.angle)).toString()
-                        +" "+(d.yBase-stem_offset_radial[i]*Math.cos(d.angle)).toString()+
-                       " L "+d.xTip.toString()+" "+d.yTip.toString(),""];
-            if (!d.terminal){
-                d.branch[1] =[" M "+tmp_xScale(d.xCBarStart).toString()+" "+tmp_yScale(d.yCBarStart).toString()+
-                           " A "+(tmp_xScale(d.depth)-tmp_xScale(offset)).toString()+" "
-                             +(tmp_yScale(d.depth)-tmp_yScale(offset)).toString()
-                             +" 0 "+(d.smallBigArc?"1 ":"0 ") +" 1 "+
-                           " "+tmp_xScale(d.xCBarEnd).toString()+","+tmp_yScale(d.yCBarEnd).toString()];
-            }
-        });
-    }
-};
 
 
 /**
@@ -338,24 +165,9 @@ PhyloTree.prototype.setScales = function(margins) {
   }
 };
 
-/**
- * remove the grid
- */
-PhyloTree.prototype.removeGrid = function() {
-  this.svg.selectAll(".majorGrid").remove();
-  this.svg.selectAll(".minorGrid").remove();
-  this.svg.selectAll(".gridTick").remove();
-  this.grid = false;
-};
-
-/**
- * hide the grid
- */
-PhyloTree.prototype.hideGrid = function() {
-  this.svg.selectAll(".majorGrid").style('visibility', 'hidden');
-  this.svg.selectAll(".minorGrid").style('visibility', 'hidden');
-  this.svg.selectAll(".gridTick").style('visibility', 'hidden');
-};
+PhyloTree.prototype.hideGrid = grid.hideGrid;
+PhyloTree.prototype.removeGrid = grid.removeGrid;
+PhyloTree.prototype.addGrid = grid.addGrid;
 
 /**
  * hide branchLabels
@@ -391,166 +203,6 @@ PhyloTree.prototype.showBranchLabels = function() {
 // };
 
 
-/**
- * add a grid to the svg
- * @param {layout}
- */
-PhyloTree.prototype.addGrid = function(layout, yMinView, yMaxView) {
-  if (typeof layout==="undefined"){ layout=this.layout;}
-
-  const xmin = (this.xScale.domain()[0]>0)?this.xScale.domain()[0]:0.0;
-  const ymin = this.yScale.domain()[1];
-  const ymax = this.yScale.domain()[0];
-  const xmax = layout=="radial"
-                ? max([this.xScale.domain()[1], this.yScale.domain()[1],
-                          -this.xScale.domain()[0], -this.yScale.domain()[0]])
-                : this.xScale.domain()[1];
-
-  const offset = layout==="radial"?this.nodes[0].depth:0.0;
-  const viewTop = yMaxView ?    yMaxView+this.params.margins.top : this.yScale.range()[0];
-  const viewBottom = yMinView ? yMinView-this.params.margins.bottom : this.yScale.range()[1];
-
-  /* should we re-draw the grid? */
-  if (!this.gridParams) {
-    this.gridParams = [xmin, xmax, ymin, ymax, viewTop, viewBottom, layout];
-  } else if (xmin === this.gridParams[0] && xmax === this.gridParams[1] &&
-        ymin === this.gridParams[2] && ymax === this.gridParams[3] &&
-        viewTop === this.gridParams[4] && viewBottom === this.gridParams[5] &&
-        layout === this.gridParams[6]) {
-    // console.log("bailing - no difference");
-    return;
-  }
-
-  /* yes - redraw and update gridParams */
-  this.gridParams = [xmin, xmax, ymin, ymax, viewTop, viewBottom, layout];
-
-
-  const gridline = function(xScale, yScale, layout){
-      return function(x){
-          const xPos = xScale(x[0]-offset);
-          let tmp_d="";
-          if (layout==="rect" || layout==="clock"){
-            tmp_d = 'M'+xPos.toString() +
-              " " +
-              viewBottom.toString() +
-              " L " +
-              xPos.toString() +
-              " " +
-              viewTop.toString();
-          }else if (layout==="radial"){
-            tmp_d = 'M '+xPos.toString() +
-              "  " +
-              yScale(0).toString() +
-              " A " +
-              (xPos - xScale(0)).toString() +
-              " " +
-              (yScale(x[0]) - yScale(offset)).toString() +
-              " 0 1 0 " +
-              xPos.toString() +
-              " " +
-              (yScale(0)+0.001).toString();
-          }
-          return tmp_d;
-      };
-  };
-
-  const logRange = Math.floor(Math.log10(xmax - xmin));
-  const roundingLevel = Math.pow(10, logRange);
-  const gridMin = Math.floor((xmin+offset)/roundingLevel)*roundingLevel;
-  const gridPoints = [];
-  for (let ii = 0; ii <= (xmax + offset - gridMin)/roundingLevel+10; ii++) {
-    const pos = gridMin + roundingLevel*ii;
-    if (pos>offset){
-        gridPoints.push([pos, pos-offset>xmax?"hidden":"visible", "x"]);
-    }
-  }
-
-  const majorGrid = this.svg.selectAll('.majorGrid').data(gridPoints);
-
-  majorGrid.exit().remove(); // EXIT
-  majorGrid.enter().append("path") // ENTER
-    .merge(majorGrid) // ENTER + UPDATE
-    .attr("d", gridline(this.xScale, this.yScale, layout))
-    .attr("class", "majorGrid")
-    .style("fill", "none")
-    .style("visibility", function (d){return d[1];})
-    .style("stroke",this.params.majorGridStroke)
-    .style("stroke-width",this.params.majorGridWidth);
-
-  const xTextPos = function(xScale, layout){
-      return function(x){
-          if (x[2]==="x"){
-              return layout==="radial" ? xScale(0) :  xScale(x[0]);
-          }else{
-              return xScale.range()[1];
-          }
-      }
-  };
-  const yTextPos = function(yScale, layout){
-      return function(x){
-          if (x[2]==="x"){
-              return layout==="radial" ? yScale(x[0]-offset) : viewBottom +  18;
-          }else{
-              return yScale(x[0]);
-          }
-      }
-  };
-
-  let logRangeY = 0;
-  if (this.layout==="clock"){
-      const roundingLevelY = Math.pow(10, logRangeY);
-      logRangeY = Math.floor(Math.log10(ymax - ymin));
-      const offsetY=0;
-      const gridMinY = Math.floor((ymin+offsetY)/roundingLevelY)*roundingLevelY;
-      for (let ii = 0; ii <= (ymax + offsetY - gridMinY)/roundingLevelY+10; ii++) {
-        const pos = gridMinY + roundingLevelY*ii;
-        if (pos>offsetY){
-            gridPoints.push([pos, pos-offsetY>ymax?"hidden":"visible","y"]);
-        }
-      }
-  }
-
-  const minorRoundingLevel = roundingLevel / (this.distanceMeasure === "num_date"
-                                              ? this.params.minorTicksTimeTree
-                                              : this.params.minorTicks);
-  const minorGridPoints = [];
-  for (let ii = 0; ii <= (xmax + offset - gridMin)/minorRoundingLevel+50; ii++) {
-    const pos = gridMin + minorRoundingLevel*ii;
-    if (pos>offset){
-        minorGridPoints.push([pos, pos-offset>xmax+minorRoundingLevel?"hidden":"visible"]);
-    }
-  }
-  const minorGrid = this.svg.selectAll('.minorGrid').data(minorGridPoints);
-  minorGrid.exit().remove(); // EXIT
-  minorGrid.enter().append("path") // ENTER
-    .merge(minorGrid) // ENTER + UPDATE
-    .attr("d", gridline(this.xScale, this.yScale, layout))
-    .attr("class", "minorGrid")
-    .style("fill", "none")
-    .style("visibility", function (d){return d[1];})
-    .style("stroke",this.params.minorGridStroke)
-    .style("stroke-width",this.params.minorGridWidth);
-
-  const gridLabels = this.svg.selectAll('.gridTick').data(gridPoints);
-  const precision = Math.max(0, 1-logRange);
-  const precisionY = Math.max(0, 1-logRangeY);
-  gridLabels.exit().remove(); // EXIT
-  gridLabels.enter().append("text") // ENTER
-    .merge(gridLabels) // ENTER + UPDATE
-    .text(function(d){return d[0].toFixed(d[2]==='y'?precisionY:precision);})
-    .attr("class", "gridTick")
-    .style("font-size",this.params.tickLabelSize)
-    .style("font-family",this.params.fontFamily)
-    .style("fill",this.params.tickLabelFill)
-    .style("text-anchor", this.layout==="radial" ? "end" : "middle")
-    .style("visibility", function (d){return d[1];})
-    .attr("x", xTextPos(this.xScale, layout))
-    .attr("y", yTextPos(this.yScale, layout));
-
-  this.grid=true;
-};
-
-
 /*
  * add and remove elements from tree, initial render
  */
@@ -741,69 +393,10 @@ PhyloTree.prototype.drawCladeLabels = function() {
 
 /* C O N F I D E N C E    I N T E R V A L S */
 
-PhyloTree.prototype.removeConfidence = function (dt) {
-  if (dt) {
-    this.svg.selectAll(".conf")
-      .transition()
-      .duration(dt)
-      .style("opacity", 0)
-    .remove();
-  } else {
-    this.svg.selectAll(".conf").remove();
-  }
-  // this.props.confidence = false;
-};
-
-PhyloTree.prototype.drawConfidence = function (dt) {
-  // this.removeConfidence(); // just in case
-  // console.log("drawing:", this.svg.selectAll(".conf"))
-  if (dt) {
-    this.confidence = this.svg.append("g").selectAll(".conf")
-      .data(this.nodes)
-      .enter()
-        .call((sel) => this.drawSingleCI(sel, 0));
-    this.svg.selectAll(".conf")
-        .transition()
-          .duration(dt)
-          .style("opacity", 0.5);
-  } else {
-    this.confidence = this.svg.append("g").selectAll(".conf")
-      .data(this.nodes)
-      .enter()
-        .call((sel) => this.drawSingleCI(sel, 0.5));
-  }
-  // this.props.confidence = true;
-};
-
-const confidenceWidth = (el) =>
-  el["stroke-width"] === 1 ? 0 :
-    el["stroke-width"] > 6 ? el["stroke-width"] + 6 : el["stroke-width"] * 2;
-
-PhyloTree.prototype.drawSingleCI = function (selection, opacity) {
-  selection.append("path")
-    .attr("class", "conf")
-    .attr("id", (d) => "conf_" + d.n.clade)
-    .attr("d", (d) => d.confLine)
-    .style("stroke", (d) => d.stroke || "#888")
-    .style("opacity", opacity)
-    .style("fill", "none")
-    .style("stroke-width", confidenceWidth);
-};
-
-
-PhyloTree.prototype.updateConfidence = function (dt) {
-  if (dt) {
-    this.svg.selectAll(".conf")
-      .transition()
-        .duration(dt)
-      .style("stroke", (el) => el.stroke)
-      .style("stroke-width", confidenceWidth);
-  } else {
-    this.svg.selectAll(".conf")
-      .style("stroke", (el) => el.stroke)
-      .style("stroke-width", confidenceWidth);
-  }
-};
+PhyloTree.prototype.removeConfidence = confidence.removeConfidence;
+PhyloTree.prototype.drawConfidence = confidence.drawConfidence;
+PhyloTree.prototype.drawSingleCI = confidence.drawSingleCI;
+PhyloTree.prototype.updateConfidence = confidence.updateConfidence;
 
 /************************************************/
 
diff --git a/src/components/tree/phyloTree/zoom.js b/src/components/tree/phyloTree/zoom.js
new file mode 100644
index 000000000..591c54e19
--- /dev/null
+++ b/src/components/tree/phyloTree/zoom.js
@@ -0,0 +1,135 @@
+/* eslint-disable space-infix-ops */
+import { min, max } from "d3-array";
+import { applyToChildren } from "./helpers";
+
+/**
+ * zoom such that a particular clade fills the svg
+ * @param  clade -- branch/node at the root of the clade to zoom into
+ * @param  dt -- time of the transition in milliseconds
+ * @return {null}
+ */
+export const zoomIntoClade = function zoomIntoClade(clade, dt) {
+  // assign all nodes to inView false and force update
+  this.zoomNode = clade;
+  this.nodes.forEach((d) => {
+    d.inView = false;
+    d.update = true;
+  });
+  // assign all child nodes of the chosen clade to inView=true
+  // if clade is terminal, apply to parent
+  if (clade.terminal) {
+    applyToChildren(clade.parent, (d) => {d.inView = true;});
+  } else {
+    applyToChildren(clade, (d) => {d.inView = true;});
+  }
+  // redraw
+  this.mapToScreen();
+  this.updateGeometry(dt);
+  if (this.grid) this.addGrid(this.layout);
+  this.svg.selectAll(".regression").remove();
+  if (this.layout === "clock" && this.distance === "num_date") this.drawRegression();
+  if (this.params.branchLabels) {
+    this.updateBranchLabels(dt);
+  }
+  this.updateTipLabels(dt);
+};
+
+/**
+ * zoom out a little by using the parent of the current clade
+ * as a zoom focus.
+ * @param  {int} dt [transition time]
+ */
+export const zoomToParent = function zoomToParent(dt) {
+  if (this.zoomNode) {
+    this.zoomIntoClade(this.zoomNode.parent, dt);
+  }
+};
+
+
+/**
+* this function sets the xScale, yScale domains and maps precalculated x,y
+* coordinates to their places on the screen
+* @return {null}
+*/
+export const mapToScreen = function mapToScreen() {
+  this.setScales(this.params.margins);
+  // determine x,y values of visibile nodes
+  const tmp_xValues = this.nodes.filter((d) => {return d.inView;}).map((d) => d.x);
+  const tmp_yValues = this.nodes.filter((d) => {return d.inView;}).map((d) => d.y);
+  this.nNodesInView = this.nodes.filter((d) => {return d.inView && d.terminal;}).length;
+
+  if (this.layout === "radial" || this.layout === "unrooted") {
+    // handle "radial and unrooted differently since they need to be square
+    // since branch length move in x and y direction
+    // TODO: should be tied to svg dimensions
+    const minX = min(tmp_xValues);
+    const minY = min(tmp_yValues);
+    const spanX = max(tmp_xValues)-minX;
+    const spanY = max(tmp_yValues)-minY;
+    const maxSpan = max([spanY, spanX]);
+    const ySlack = (spanX>spanY) ? (spanX-spanY)*0.5 : 0.0;
+    const xSlack = (spanX<spanY) ? (spanY-spanX)*0.5 : 0.0;
+    this.xScale.domain([minX-xSlack, minX+maxSpan-xSlack]);
+    this.yScale.domain([minY-ySlack, minY+maxSpan-ySlack]);
+  } else if (this.layout==="clock") {
+    // same as rectangular, but flipped yscale
+    this.xScale.domain([min(tmp_xValues), max(tmp_xValues)]);
+    this.yScale.domain([max(tmp_yValues), min(tmp_yValues)]);
+  } else { // rectangular
+    this.xScale.domain([min(tmp_xValues), max(tmp_xValues)]);
+    this.yScale.domain([min(tmp_yValues), max(tmp_yValues)]);
+  }
+
+  // pass all x,y through scales and assign to xTip, xBase
+  const tmp_xScale=this.xScale;
+  const tmp_yScale=this.yScale;
+  this.nodes.forEach((d) => {d.xTip = tmp_xScale(d.x);});
+  this.nodes.forEach((d) => {d.yTip = tmp_yScale(d.y);});
+  this.nodes.forEach((d) => {d.xBase = tmp_xScale(d.px);});
+  this.nodes.forEach((d) => {d.yBase = tmp_yScale(d.py);});
+  if (this.params.confidence && this.layout==="rect") {
+    this.nodes.forEach((d) => {d.xConf = [tmp_xScale(d.conf[0]), tmp_xScale(d.conf[1])];});
+  }
+
+  // assign the branches as path to each node for the different layouts
+  if (this.layout==="clock" || this.layout==="unrooted") {
+    this.nodes.forEach((d) => {
+      d.branch = [" M "+d.xBase.toString()+","+d.yBase.toString()+" L "+d.xTip.toString()+","+d.yTip.toString(), ""];
+    });
+  } else if (this.layout==="rect") {
+    this.nodes.forEach((d) => {d.cBarStart = tmp_yScale(d.yRange[0]);});
+    this.nodes.forEach((d) => {d.cBarEnd = tmp_yScale(d.yRange[1]);});
+    const stem_offset = this.nodes.map((d) => {return (0.5*(d.parent["stroke-width"] - d["stroke-width"]) || 0.0);});
+    this.nodes.forEach((d, i) => {
+      d.branch =[" M "+(d.xBase - stem_offset[i]).toString()
+      +","+d.yBase.toString()+
+      " L "+d.xTip.toString()+","+d.yTip.toString(),
+      " M "+d.xTip.toString()+","+d.cBarStart.toString()+
+      " L "+d.xTip.toString()+","+d.cBarEnd.toString()];
+    });
+    if (this.params.confidence) {
+      this.nodes.forEach((d) => {
+        d.confLine =" M "+d.xConf[0].toString()+","+d.yBase.toString()+" L "+d.xConf[1].toString()+","+d.yTip.toString();
+      });
+    }
+  } else if (this.layout==="radial") {
+    const offset = this.nodes[0].depth;
+    const stem_offset_radial = this.nodes.map((d) => {return (0.5*(d.parent["stroke-width"] - d["stroke-width"]) || 0.0);});
+    this.nodes.forEach((d) => {d.cBarStart = tmp_yScale(d.yRange[0]);});
+    this.nodes.forEach((d) => {d.cBarEnd = tmp_yScale(d.yRange[1]);});
+    this.nodes.forEach((d, i) => {
+      d.branch =[
+        " M "+(d.xBase-stem_offset_radial[i]*Math.sin(d.angle)).toString()
+        + " "+(d.yBase-stem_offset_radial[i]*Math.cos(d.angle)).toString()
+        + " L "+d.xTip.toString()+" "+d.yTip.toString(), ""
+      ];
+      if (!d.terminal) {
+        d.branch[1] =[" M "+tmp_xScale(d.xCBarStart).toString()+" "+tmp_yScale(d.yCBarStart).toString()+
+        " A "+(tmp_xScale(d.depth)-tmp_xScale(offset)).toString()+" "
+        +(tmp_yScale(d.depth)-tmp_yScale(offset)).toString()
+        +" 0 "+(d.smallBigArc?"1 ":"0 ") +" 1 "+
+        " "+tmp_xScale(d.xCBarEnd).toString()+","+tmp_yScale(d.yCBarEnd).toString()];
+      }
+    });
+  }
+};

From c97640df628a789703c8ca2c350e08f1d407f8c6 Mon Sep 17 00:00:00 2001
From: James Hadfield <jh22@sanger.ac.uk>
Date: Mon, 5 Feb 2018 09:43:56 -0800
Subject: [PATCH 07/10] break phyloTree into files III

---
 src/components/tree/phyloTree/confidence.js   |  18 +-
 .../tree/phyloTree/generalUpdates.js          | 249 ++++++++++
 src/components/tree/phyloTree/grid.js         | 156 +++---
 src/components/tree/phyloTree/labels.js       | 111 +++++
 src/components/tree/phyloTree/phyloTree.js    | 456 +-----------------
 5 files changed, 463 insertions(+), 527 deletions(-)
 create mode 100644 src/components/tree/phyloTree/generalUpdates.js
 create mode 100644 src/components/tree/phyloTree/labels.js

diff --git a/src/components/tree/phyloTree/confidence.js b/src/components/tree/phyloTree/confidence.js
index 1b1f40357..668555ca2 100644
--- a/src/components/tree/phyloTree/confidence.js
+++ b/src/components/tree/phyloTree/confidence.js
@@ -1,12 +1,10 @@
-/* eslint-disable */
 
 export const removeConfidence = function removeConfidence(dt) {
   if (dt) {
     this.svg.selectAll(".conf")
-      .transition()
-      .duration(dt)
+      .transition().duration(dt)
       .style("opacity", 0)
-    .remove();
+      .remove();
   } else {
     this.svg.selectAll(".conf").remove();
   }
@@ -20,16 +18,15 @@ export const drawConfidence = function drawConfidence(dt) {
     this.confidence = this.svg.append("g").selectAll(".conf")
       .data(this.nodes)
       .enter()
-        .call((sel) => this.drawSingleCI(sel, 0));
+      .call((sel) => this.drawSingleCI(sel, 0));
     this.svg.selectAll(".conf")
-        .transition()
-          .duration(dt)
-          .style("opacity", 0.5);
+      .transition().duration(dt)
+      .style("opacity", 0.5);
   } else {
     this.confidence = this.svg.append("g").selectAll(".conf")
       .data(this.nodes)
       .enter()
-        .call((sel) => this.drawSingleCI(sel, 0.5));
+      .call((sel) => this.drawSingleCI(sel, 0.5));
   }
   // this.props.confidence = true;
 };
@@ -53,8 +50,7 @@ export const drawSingleCI = function drawSingleCI(selection, opacity) {
 export const updateConfidence = function updateConfidence(dt) {
   if (dt) {
     this.svg.selectAll(".conf")
-      .transition()
-        .duration(dt)
+      .transition().duration(dt)
       .style("stroke", (el) => el.stroke)
       .style("stroke-width", confidenceWidth);
   } else {
diff --git a/src/components/tree/phyloTree/generalUpdates.js b/src/components/tree/phyloTree/generalUpdates.js
new file mode 100644
index 000000000..9bed9298c
--- /dev/null
+++ b/src/components/tree/phyloTree/generalUpdates.js
@@ -0,0 +1,249 @@
+
+const contains = (array, elem) => array.some((d) => d === elem);
+
+/**
+ * as updateAttributeArray, but accepts a callback function rather than an array
+ * with the values. will create array and call updateAttributeArray
+ * @param  treeElem  --- the part of the tree to update (.tip, .branch)
+ * @param  attr  --- the attribute to update (e.g. r for tipRadius)
+ * @param  callback -- function that assigns the attribute
+ * @param  dt  --- time of transition in milliseconds
+ * @return {[type]}
+ */
+export const updateStyleOrAttribute = function updateStyleOrAttribute(treeElem, attr, callback, dt, styleOrAttribute) {
+  const attr_array = this.nodes.map((d) => callback(d));
+  this.updateStyleOrAttributeArray(treeElem, attr, attr_array, dt, styleOrAttribute);
+};
+
+/**
+ * update an attribute of the tree for all nodes
+ * @param  treeElem  --- the part of the tree to update (.tip, .branch)
+ * @param  attr  --- the attribute to update (e.g. r for tipRadius)
+ * @param  attr_array  --- an array with values for every node in the tree
+ * @param  dt  --- time of transition in milliseconds
+ * @return {[type]}
+ */
+export const updateStyleOrAttributeArray = function updateStyleOrAttributeArray(treeElem, attr, attr_array, dt, styleOrAttribute) {
+  this.nodes.forEach((d, i) => {
+    const newAttr = attr_array[i];
+    if (newAttr === d[attr]) {
+      d.update = false;
+    } else {
+      d[attr] = newAttr;
+      d.update = true;
+    }
+  });
+  if (typeof styleOrAttribute === "undefined") {
+    styleOrAttribute = contains(this.attributes, attr) ? "attr" : "style"; // eslint-disable-line no-param-reassign
+  }
+  if (styleOrAttribute === "style") {
+    this.redrawStyle(treeElem, attr, dt);
+  } else {
+    this.redrawAttribute(treeElem, attr, dt);
+  }
+};
+
+
+/**
+ * call to change the distance measure
+ * @param  attr -- attribute to be used as a distance measure, e.g. div or num_date
+ * @param  dt -- time of transition in milliseconds
+ * @return null
+ */
+export const updateDistance = function updateDistance(attr, dt) {
+  this.setDistance(attr);
+  this.setLayout(this.layout);
+  this.mapToScreen();
+  this.updateGeometry(dt);
+  if (this.grid && this.layout !== "unrooted") this.addGrid(this.layout);
+  else this.hideGrid();
+  this.svg.selectAll(".regression").remove();
+  if (this.layout === "clock" && this.distance === "num_date") this.drawRegression();
+};
+
+
+/**
+ * call to change the layout
+ * @param  layout -- needs to be one of "rect", "radial", "unrooted", "clock"
+ * @param  dt -- time of transition in milliseconds
+ * @return null
+ */
+export const updateLayout = function updateLayout(layout, dt) {
+  this.setLayout(layout);
+  this.mapToScreen();
+  this.updateGeometryFade(dt);
+  if (this.grid && this.layout !== "unrooted") this.addGrid(layout);
+  else this.hideGrid();
+  this.svg.selectAll(".regression").remove();
+  if (this.layout === "clock" && this.distance === "num_date") this.drawRegression();
+};
+
+
+/**
+ * transition of branches and tips at the same time. only useful within a layout
+ * @param  dt -- time of transition in milliseconds
+ * @return {[type]}
+ */
+export const updateGeometry = function updateGeometry(dt) {
+  this.svg.selectAll(".tip")
+    .filter((d) => d.update)
+    .transition()
+    .duration(dt)
+    .attr("cx", (d) => d.xTip)
+    .attr("cy", (d) => d.yTip);
+
+  this.svg.selectAll(".vaccine")
+    .filter((d) => d.update)
+    .transition()
+    .duration(dt)
+    .attr("x", (d) => d.xTip)
+    .attr("y", (d) => d.yTip);
+
+  const branchEls = [".S", ".T"];
+  for (let i = 0; i < 2; i++) {
+    this.svg.selectAll(".branch")
+      .filter(branchEls[i])
+      .filter((d) => d.update)
+      .transition()
+      .duration(dt)
+      .attr("d", (d) => d.branch[i]);
+  }
+
+  this.svg.selectAll(".conf")
+    .filter((d) => d.update)
+    .transition().duration(dt)
+    .attr("d", (dd) => dd.confLine);
+
+  this.updateBranchLabels(dt);
+  this.updateTipLabels(dt);
+};
+
+
+/*
+ * redraw the tree based on the current xTip, yTip, branch attributes
+ * this function will remove branches, move the tips continuously
+ * and add the new branches again after the tips arrived at their destination
+ *  @params dt -- time of transition in milliseconds
+ */
+export const updateGeometryFade = function updateGeometryFade(dt) {
+  this.removeConfidence(dt);
+
+  // fade out branches
+  this.svg.selectAll('.branch')
+    .filter((d) => d.update)
+    .transition().duration(dt * 0.5)
+    .style("opacity", 0.0);
+  this.svg.selectAll('.branchLabels')
+    .filter((d) => d.update)
+    .transition().duration(dt * 0.5)
+    .style("opacity", 0.0);
+  this.svg.selectAll('.tipLabels')
+    .filter((d) => d.update)
+    .transition().duration(dt * 0.5)
+    .style("opacity", 0.0);
+
+  // closure to move the tips, called via the time out below
+  const tipTransHOF = (svgShadow, dtShadow) => () => {
+    svgShadow.selectAll('.tip')
+      .filter((d) => d.update)
+      .transition().duration(dtShadow)
+      .attr("cx", (d) => d.xTip)
+      .attr("cy", (d) => d.yTip);
+    svgShadow.selectAll(".vaccine")
+      .filter((d) => d.update)
+      .transition()
+      .duration(dtShadow)
+      .attr("x", (d) => d.xTip)
+      .attr("y", (d) => d.yTip);
+  };
+  setTimeout(tipTransHOF(this.svg, dt), 0.5 * dt);
+
+  // closure to change the branches, called via time out after the tipTrans is done
+  const flipBranchesHOF = (svgShadow) => () => {
+    svgShadow.selectAll('.branch').filter('.S')
+      .filter((d) => d.update)
+      .attr("d", (d) => d.branch[0]);
+    svgShadow.selectAll('.branch').filter('.T')
+      .filter((d) => d.update)
+      .attr("d", (d) => d.branch[1]);
+  };
+  setTimeout(flipBranchesHOF(this.svg), 0.5 * dt);
+
+  // closure to add the new branches after the tipTrans
+  const fadeBackHOF = (svgShadow, dtShadow) => () => {
+    svgShadow.selectAll('.branch')
+      .filter((dd) => dd.update)
+      .transition().duration(0.5 * dtShadow)
+      .style("opacity", 1.0);
+  };
+  setTimeout(fadeBackHOF(this.svg, 0.2 * dt), 1.5 * dt);
+  this.updateBranchLabels(dt);
+  this.updateTipLabels(dt);
+};
+
+
+/**
+ * Update multiple style or attributes of tree elements at once
+ * @param {string} treeElem one of .tip or .branch
+ * @param {object} attr object containing the attributes to change as keys, array with values as value
+ * @param {object} styles object containing the styles to change
+ * @param {int} dt time in milliseconds
+ */
+export const updateMultipleArray = function updateMultipleArray(treeElem, attrs, styles, dt, quickdraw) {
+  // assign new values and decide whether to update
+  this.nodes.forEach((d, i) => {
+    d.update = false;
+    /* note that this is not node.attr, but element attr such as <g width="100" vs style="" */
+    let newAttr;
+    for (let attr in attrs) { // eslint-disable-line
+      newAttr = attrs[attr][i];
+      if (newAttr !== d[attr]) {
+        d[attr] = newAttr;
+        d.update = true;
+      }
+    }
+    let newStyle;
+    for (let prop in styles) { // eslint-disable-line
+      newStyle = styles[prop][i];
+      if (newStyle !== d[prop]) {
+        d[prop] = newStyle;
+        d.update = true;
+      }
+    }
+  });
+  let updatePath = false;
+  if (styles["stroke-width"]) {
+    if (quickdraw) {
+      this.debouncedMapToScreen();
+    } else {
+      this.mapToScreen();
+    }
+    updatePath = true;
+  }
+
+  // HOF that returns the closure object for updating the svg
+  const updateSVGHOF = (attrToSet, stylesToSet) => (selection) => {
+    for (let i = 0; i < stylesToSet.length; i++) {
+      const prop = stylesToSet[i];
+      selection.style(prop, (d) => d[prop]);
+    }
+    for (let i = 0; i < attrToSet.length; i++) {
+      const prop = attrToSet[i];
+      selection.attr(prop, (d) => d[prop]);
+    }
+    if (updatePath) {
+      selection.filter('.S').attr("d", (d) => d.branch[0]);
+    }
+  };
+  // update the svg via the HOF we just created
+  if (dt) {
+    this.svg.selectAll(treeElem)
+      .filter((d) => d.update)
+      .transition().duration(dt)
+      .call(updateSVGHOF(Object.keys(attrs), Object.keys(styles)));
+  } else {
+    this.svg.selectAll(treeElem)
+      .filter((d) => d.update)
+      .call(updateSVGHOF(Object.keys(attrs), Object.keys(styles)));
+  }
+};
diff --git a/src/components/tree/phyloTree/grid.js b/src/components/tree/phyloTree/grid.js
index 9d4f8121f..0afab14a5 100644
--- a/src/components/tree/phyloTree/grid.js
+++ b/src/components/tree/phyloTree/grid.js
@@ -1,6 +1,5 @@
-/* eslint-disable */
-
-
+/* eslint-disable space-infix-ops */
+import { max } from "d3-array";
 
 export const removeGrid = function removeGrid() {
   this.svg.selectAll(".majorGrid").remove();
@@ -20,18 +19,17 @@ export const hideGrid = function hideGrid() {
  * @param {layout}
  */
 export const addGrid = function addGrid(layout, yMinView, yMaxView) {
-  if (typeof layout==="undefined"){ layout=this.layout;}
+  if (typeof layout==="undefined") {layout=this.layout;} // eslint-disable-line no-param-reassign
 
   const xmin = (this.xScale.domain()[0]>0)?this.xScale.domain()[0]:0.0;
   const ymin = this.yScale.domain()[1];
   const ymax = this.yScale.domain()[0];
-  const xmax = layout=="radial"
-                ? max([this.xScale.domain()[1], this.yScale.domain()[1],
-                          -this.xScale.domain()[0], -this.yScale.domain()[0]])
-                : this.xScale.domain()[1];
+  const xmax = layout === "radial" ?
+    max([this.xScale.domain()[1], this.yScale.domain()[1], -this.xScale.domain()[0], -this.yScale.domain()[0]]) :
+    this.xScale.domain()[1];
 
   const offset = layout==="radial"?this.nodes[0].depth:0.0;
-  const viewTop = yMaxView ?    yMaxView+this.params.margins.top : this.yScale.range()[0];
+  const viewTop = yMaxView ? yMaxView+this.params.margins.top : this.yScale.range()[0];
   const viewBottom = yMinView ? yMinView-this.params.margins.bottom : this.yScale.range()[1];
 
   /* should we re-draw the grid? */
@@ -49,43 +47,43 @@ export const addGrid = function addGrid(layout, yMinView, yMaxView) {
   this.gridParams = [xmin, xmax, ymin, ymax, viewTop, viewBottom, layout];
 
 
-  const gridline = function(xScale, yScale, layout){
-      return function(x){
-          const xPos = xScale(x[0]-offset);
-          let tmp_d="";
-          if (layout==="rect" || layout==="clock"){
-            tmp_d = 'M'+xPos.toString() +
-              " " +
-              viewBottom.toString() +
-              " L " +
-              xPos.toString() +
-              " " +
-              viewTop.toString();
-          }else if (layout==="radial"){
-            tmp_d = 'M '+xPos.toString() +
-              "  " +
-              yScale(0).toString() +
-              " A " +
-              (xPos - xScale(0)).toString() +
-              " " +
-              (yScale(x[0]) - yScale(offset)).toString() +
-              " 0 1 0 " +
-              xPos.toString() +
-              " " +
-              (yScale(0)+0.001).toString();
-          }
-          return tmp_d;
-      };
+  const gridline = function gridline(xScale, yScale, layoutShadow) {
+    return (x) => {
+      const xPos = xScale(x[0]-offset);
+      let tmp_d="";
+      if (layoutShadow==="rect" || layoutShadow==="clock") {
+        tmp_d = 'M'+xPos.toString() +
+          " " +
+          viewBottom.toString() +
+          " L " +
+          xPos.toString() +
+          " " +
+          viewTop.toString();
+      } else if (layoutShadow==="radial") {
+        tmp_d = 'M '+xPos.toString() +
+          "  " +
+          yScale(0).toString() +
+          " A " +
+          (xPos - xScale(0)).toString() +
+          " " +
+          (yScale(x[0]) - yScale(offset)).toString() +
+          " 0 1 0 " +
+          xPos.toString() +
+          " " +
+          (yScale(0)+0.001).toString();
+      }
+      return tmp_d;
+    };
   };
 
   const logRange = Math.floor(Math.log10(xmax - xmin));
-  const roundingLevel = Math.pow(10, logRange);
+  const roundingLevel = Math.pow(10, logRange); // eslint-disable-line no-restricted-properties
   const gridMin = Math.floor((xmin+offset)/roundingLevel)*roundingLevel;
   const gridPoints = [];
   for (let ii = 0; ii <= (xmax + offset - gridMin)/roundingLevel+10; ii++) {
     const pos = gridMin + roundingLevel*ii;
-    if (pos>offset){
-        gridPoints.push([pos, pos-offset>xmax?"hidden":"visible", "x"]);
+    if (pos>offset) {
+      gridPoints.push([pos, pos-offset>xmax?"hidden":"visible", "x"]);
     }
   }
 
@@ -97,51 +95,45 @@ export const addGrid = function addGrid(layout, yMinView, yMaxView) {
     .attr("d", gridline(this.xScale, this.yScale, layout))
     .attr("class", "majorGrid")
     .style("fill", "none")
-    .style("visibility", function (d){return d[1];})
-    .style("stroke",this.params.majorGridStroke)
-    .style("stroke-width",this.params.majorGridWidth);
-
-  const xTextPos = function(xScale, layout){
-      return function(x){
-          if (x[2]==="x"){
-              return layout==="radial" ? xScale(0) :  xScale(x[0]);
-          }else{
-              return xScale.range()[1];
-          }
-      }
+    .style("visibility", (d) => d[1])
+    .style("stroke", this.params.majorGridStroke)
+    .style("stroke-width", this.params.majorGridWidth);
+
+  const xTextPos = (xScale, layoutShadow) => (x) => {
+    if (x[2] === "x") {
+      return layoutShadow === "radial" ? xScale(0) : xScale(x[0]);
+    }
+    return xScale.range()[1];
   };
-  const yTextPos = function(yScale, layout){
-      return function(x){
-          if (x[2]==="x"){
-              return layout==="radial" ? yScale(x[0]-offset) : viewBottom +  18;
-          }else{
-              return yScale(x[0]);
-          }
-      }
+  const yTextPos = (yScale, layoutShadow) => (x) => {
+    if (x[2] === "x") {
+      return layoutShadow === "radial" ? yScale(x[0]-offset) : viewBottom + 18;
+    }
+    return yScale(x[0]);
   };
 
+
   let logRangeY = 0;
-  if (this.layout==="clock"){
-      const roundingLevelY = Math.pow(10, logRangeY);
-      logRangeY = Math.floor(Math.log10(ymax - ymin));
-      const offsetY=0;
-      const gridMinY = Math.floor((ymin+offsetY)/roundingLevelY)*roundingLevelY;
-      for (let ii = 0; ii <= (ymax + offsetY - gridMinY)/roundingLevelY+10; ii++) {
-        const pos = gridMinY + roundingLevelY*ii;
-        if (pos>offsetY){
-            gridPoints.push([pos, pos-offsetY>ymax?"hidden":"visible","y"]);
-        }
+  if (this.layout==="clock") {
+    const roundingLevelY = Math.pow(10, logRangeY); // eslint-disable-line no-restricted-properties
+    logRangeY = Math.floor(Math.log10(ymax - ymin));
+    const offsetY=0;
+    const gridMinY = Math.floor((ymin+offsetY)/roundingLevelY)*roundingLevelY;
+    for (let ii = 0; ii <= (ymax + offsetY - gridMinY)/roundingLevelY+10; ii++) {
+      const pos = gridMinY + roundingLevelY*ii;
+      if (pos>offsetY) {
+        gridPoints.push([pos, pos-offsetY>ymax ? "hidden" : "visible", "y"]);
       }
+    }
   }
 
-  const minorRoundingLevel = roundingLevel / (this.distanceMeasure === "num_date"
-                                              ? this.params.minorTicksTimeTree
-                                              : this.params.minorTicks);
+  const minorRoundingLevel = roundingLevel /
+    (this.distanceMeasure === "num_date"? this.params.minorTicksTimeTree : this.params.minorTicks);
   const minorGridPoints = [];
   for (let ii = 0; ii <= (xmax + offset - gridMin)/minorRoundingLevel+50; ii++) {
     const pos = gridMin + minorRoundingLevel*ii;
-    if (pos>offset){
-        minorGridPoints.push([pos, pos-offset>xmax+minorRoundingLevel?"hidden":"visible"]);
+    if (pos>offset) {
+      minorGridPoints.push([pos, pos-offset>xmax+minorRoundingLevel?"hidden":"visible"]);
     }
   }
   const minorGrid = this.svg.selectAll('.minorGrid').data(minorGridPoints);
@@ -151,9 +143,9 @@ export const addGrid = function addGrid(layout, yMinView, yMaxView) {
     .attr("d", gridline(this.xScale, this.yScale, layout))
     .attr("class", "minorGrid")
     .style("fill", "none")
-    .style("visibility", function (d){return d[1];})
-    .style("stroke",this.params.minorGridStroke)
-    .style("stroke-width",this.params.minorGridWidth);
+    .style("visibility", (d) => d[1])
+    .style("stroke", this.params.minorGridStroke)
+    .style("stroke-width", this.params.minorGridWidth);
 
   const gridLabels = this.svg.selectAll('.gridTick').data(gridPoints);
   const precision = Math.max(0, 1-logRange);
@@ -161,13 +153,13 @@ export const addGrid = function addGrid(layout, yMinView, yMaxView) {
   gridLabels.exit().remove(); // EXIT
   gridLabels.enter().append("text") // ENTER
     .merge(gridLabels) // ENTER + UPDATE
-    .text(function(d){return d[0].toFixed(d[2]==='y'?precisionY:precision);})
+    .text((d) => d[0].toFixed(d[2]==='y' ? precisionY : precision))
     .attr("class", "gridTick")
-    .style("font-size",this.params.tickLabelSize)
-    .style("font-family",this.params.fontFamily)
-    .style("fill",this.params.tickLabelFill)
+    .style("font-size", this.params.tickLabelSize)
+    .style("font-family", this.params.fontFamily)
+    .style("fill", this.params.tickLabelFill)
     .style("text-anchor", this.layout==="radial" ? "end" : "middle")
-    .style("visibility", function (d){return d[1];})
+    .style("visibility", (d) => d[1])
     .attr("x", xTextPos(this.xScale, layout))
     .attr("y", yTextPos(this.yScale, layout));
 
diff --git a/src/components/tree/phyloTree/labels.js b/src/components/tree/phyloTree/labels.js
new file mode 100644
index 000000000..e11f8a1f1
--- /dev/null
+++ b/src/components/tree/phyloTree/labels.js
@@ -0,0 +1,111 @@
+/**
+ * hide branchLabels
+ */
+export const hideBranchLabels = function hideBranchLabels() {
+  this.params.showBranchLabels = false;
+  this.svg.selectAll(".branchLabel").style('visibility', 'hidden');
+};
+
+/**
+ * show branchLabels
+ */
+export const showBranchLabels = function showBranchLabels() {
+  this.params.showBranchLabels = true;
+  this.svg.selectAll(".branchLabel").style('visibility', 'visible');
+};
+
+/**
+ * hide tipLabels - this function is never called!
+ */
+// PhyloTree.prototype.hideTipLabels = function() {
+//   this.params.showTipLabels=false;
+//   this.svg.selectAll(".tipLabel").style('visibility', 'hidden');
+// };
+
+/**
+ * show tipLabels - this function is never called!
+ */
+// PhyloTree.prototype.showTipLabels = function() {
+//   this.params.showTipLabels=true;
+//   this.svg.selectAll(".tipLabel").style('visibility', 'visible');
+// };
+
+export const updateTipLabels = function updateTipLabels(dt) {
+  this.svg.selectAll('.tipLabel').remove();
+  const tLFunc = this.callbacks.tipLabel;
+  const xPad = this.params.tipLabelPadX;
+  const yPad = this.params.tipLabelPadY;
+  const inViewTerminalNodes = this.nodes
+    .filter((d) => d.terminal)
+    .filter((d) => d.inView);
+  // console.log(`there are ${inViewTerminalNodes.length} nodes in view`)
+  if (inViewTerminalNodes.length < 50) {
+    // console.log("DRAWING!", inViewTerminalNodes)
+    window.setTimeout(() => {
+      this.tipLabels = this.svg.append("g").selectAll('.tipLabel')
+        .data(inViewTerminalNodes)
+        .enter()
+        .append("text")
+        .attr("x", (d) => d.xTip + xPad)
+        .attr("y", (d) => d.yTip + yPad)
+        .text((d) => tLFunc(d))
+        .attr("class", "tipLabel")
+        .style('visibility', 'visible');
+    }, dt);
+  }
+};
+
+export const updateBranchLabels = function updateBranchLabels(dt) {
+  const xPad = this.params.branchLabelPadX, yPad = this.params.branchLabelPadY;
+  const nNIV = this.nNodesInView;
+  const bLSFunc = this.callbacks.branchLabelSize;
+  const showBL = (this.layout === "rect") && this.params.showBranchLabels;
+  const visBL = showBL ? "visible" : "hidden";
+  this.svg.selectAll('.branchLabel')
+    .transition().duration(dt)
+    .attr("x", (d) => d.xTip - xPad)
+    .attr("y", (d) => d.yTip - yPad)
+    .attr("visibility", visBL)
+    .style("fill", this.params.branchLabelFill)
+    .style("font-family", this.params.branchLabelFont)
+    .style("font-size", (d) => bLSFunc(d, nNIV).toString() + "px");
+};
+
+export const drawCladeLabels = function drawCladeLabels() {
+  this.branchLabels = this.svg.append("g").selectAll('.branchLabel')
+    .data(this.nodes.filter((d) => typeof d.n.attr.clade_name !== 'undefined'))
+    .enter()
+    .append("text")
+    .style("visibility", "visible")
+    .text((d) => d.n.attr.clade_name)
+    .attr("class", "branchLabel")
+    .style("text-anchor", "end");
+};
+
+// PhyloTree.prototype.drawTipLabels = function() {
+//   var params = this.params;
+//   const tLFunc = this.callbacks.tipLabel;
+//   const inViewTerminalNodes = this.nodes
+//                   .filter(function(d){return d.terminal;})
+//                   .filter(function(d){return d.inView;});
+//   console.log(`there are ${inViewTerminalNodes.length} nodes in view`)
+//   this.tipLabels = this.svg.append("g").selectAll('.tipLabel')
+//     .data(inViewTerminalNodes)
+//     .enter()
+//     .append("text")
+//     .text(function (d){return tLFunc(d);})
+//     .attr("class", "tipLabel");
+// }
+
+
+// PhyloTree.prototype.drawBranchLabels = function() {
+//   var params = this.params;
+//   const bLFunc = this.callbacks.branchLabel;
+//   this.branchLabels = this.svg.append("g").selectAll('.branchLabel')
+//     .data(this.nodes) //.filter(function (d){return bLFunc(d)!=="";}))
+//     .enter()
+//     .append("text")
+//     .text(function (d){return bLFunc(d);})
+//     .attr("class", "branchLabel")
+//     .style("text-anchor","end");
+// }
diff --git a/src/components/tree/phyloTree/phyloTree.js b/src/components/tree/phyloTree/phyloTree.js
index 2330cd28c..e9edc48aa 100644
--- a/src/components/tree/phyloTree/phyloTree.js
+++ b/src/components/tree/phyloTree/phyloTree.js
@@ -14,10 +14,8 @@ import * as layouts from "./layouts";
 import * as zoom from "./zoom";
 import * as grid from "./grid";
 import * as confidence from "./confidence";
-
-const contains = function(array, elem){
-  return array.some(function (d){return d===elem;});
-}
+import * as labels from "./labels";
+import * as generalUpdates from "./generalUpdates";
 
 /*
  * phylogenetic tree drawing class
@@ -118,10 +116,7 @@ PhyloTree.prototype.drawRegression = function(){
 };
 
 
-///****************************************************************
-
-// MAPPING TO SCREEN
-
+/* Z O O M ,    F I T     TO     S C R E E N ,     E T C */
 PhyloTree.prototype.zoomIntoClade = zoom.zoomIntoClade;
 PhyloTree.prototype.zoomToParent = zoom.zoomToParent;
 PhyloTree.prototype.mapToScreen = zoom.mapToScreen;
@@ -165,42 +160,6 @@ PhyloTree.prototype.setScales = function(margins) {
   }
 };
 
-PhyloTree.prototype.hideGrid = grid.hideGrid;
-PhyloTree.prototype.removeGrid = grid.removeGrid;
-PhyloTree.prototype.addGrid = grid.addGrid;
-
-/**
- * hide branchLabels
- */
-PhyloTree.prototype.hideBranchLabels = function() {
-  this.params.showBranchLabels=false;
-  this.svg.selectAll(".branchLabel").style('visibility', 'hidden');
-};
-
-/**
- * show branchLabels
- */
-PhyloTree.prototype.showBranchLabels = function() {
-  this.params.showBranchLabels=true;
-  this.svg.selectAll(".branchLabel").style('visibility', 'visible');
-};
-
-/* these functions are never called! */
-// /**
-//  * hide tipLabels
-//  */
-// PhyloTree.prototype.hideTipLabels = function() {
-//   this.params.showTipLabels=false;
-//   this.svg.selectAll(".tipLabel").style('visibility', 'hidden');
-// };
-//
-// /**
-//  * show tipLabels
-//  */
-// PhyloTree.prototype.showTipLabels = function() {
-//   this.params.showTipLabels=true;
-//   this.svg.selectAll(".tipLabel").style('visibility', 'visible');
-// };
 
 
 /*
@@ -352,44 +311,8 @@ PhyloTree.prototype.drawBranches = function() {
 };
 
 
-// PhyloTree.prototype.drawBranchLabels = function() {
-//   var params = this.params;
-//   const bLFunc = this.callbacks.branchLabel;
-//   this.branchLabels = this.svg.append("g").selectAll('.branchLabel')
-//     .data(this.nodes) //.filter(function (d){return bLFunc(d)!=="";}))
-//     .enter()
-//     .append("text")
-//     .text(function (d){return bLFunc(d);})
-//     .attr("class", "branchLabel")
-//     .style("text-anchor","end");
-// }
 
 
-PhyloTree.prototype.drawCladeLabels = function() {
-  this.branchLabels = this.svg.append("g").selectAll('.branchLabel')
-    .data(this.nodes.filter(function (d) { return typeof d.n.attr.clade_name !== 'undefined'; }))
-    .enter()
-    .append("text")
-    .style("visibility", "visible")
-    .text(function (d) { return d.n.attr.clade_name; })
-    .attr("class", "branchLabel")
-    .style("text-anchor", "end");
-}
-
-// PhyloTree.prototype.drawTipLabels = function() {
-//   var params = this.params;
-//   const tLFunc = this.callbacks.tipLabel;
-//   const inViewTerminalNodes = this.nodes
-//                   .filter(function(d){return d.terminal;})
-//                   .filter(function(d){return d.inView;});
-//   console.log(`there are ${inViewTerminalNodes.length} nodes in view`)
-//   this.tipLabels = this.svg.append("g").selectAll('.tipLabel')
-//     .data(inViewTerminalNodes)
-//     .enter()
-//     .append("text")
-//     .text(function (d){return tLFunc(d);})
-//     .attr("class", "tipLabel");
-// }
 
 /* C O N F I D E N C E    I N T E R V A L S */
 
@@ -398,319 +321,29 @@ PhyloTree.prototype.drawConfidence = confidence.drawConfidence;
 PhyloTree.prototype.drawSingleCI = confidence.drawSingleCI;
 PhyloTree.prototype.updateConfidence = confidence.updateConfidence;
 
-/************************************************/
-
-/**
- * call to change the distance measure
- * @param  attr -- attribute to be used as a distance measure, e.g. div or num_date
- * @param  dt -- time of transition in milliseconds
- * @return null
- */
-PhyloTree.prototype.updateDistance = function(attr,dt){
-  this.setDistance(attr);
-  this.setLayout(this.layout);
-  this.mapToScreen();
-  this.updateGeometry(dt);
-  if (this.grid && this.layout!=="unrooted") {this.addGrid(this.layout);}
-  else this.hideGrid()
-  this.svg.selectAll(".regression").remove();
-  if (this.layout==="clock" && this.distance === "num_date") this.drawRegression();
-};
-
+/* G E N E R A L    U P D A T E S */
+PhyloTree.prototype.updateDistance = generalUpdates.updateDistance;
+PhyloTree.prototype.updateLayout = generalUpdates.updateLayout;
+PhyloTree.prototype.updateGeometry = generalUpdates.updateGeometry;
+PhyloTree.prototype.updateGeometryFade = generalUpdates.updateGeometryFade;
+PhyloTree.prototype.updateTimeBar = (d) => {};
+PhyloTree.prototype.updateMultipleArray = generalUpdates.updateMultipleArray;
 
-/**
- * call to change the layout
- * @param  layout -- needs to be one of "rect", "radial", "unrooted", "clock"
- * @param  dt -- time of transition in milliseconds
- * @return null
- */
-PhyloTree.prototype.updateLayout = function(layout,dt){
-    this.setLayout(layout);
-    this.mapToScreen();
-    this.updateGeometryFade(dt);
-    if (this.grid && this.layout!=="unrooted") this.addGrid(layout);
-    else this.hideGrid()
-    this.svg.selectAll(".regression").remove();
-    if (this.layout==="clock" && this.distance === "num_date") this.drawRegression();
-};
 
+/* L A B E L S    ( T I P ,    B R A N C H ,   C O N F I D E N C E ) */
+PhyloTree.prototype.drawCladeLabels = labels.drawCladeLabels;
+PhyloTree.prototype.updateBranchLabels = labels.updateBranchLabels;
+PhyloTree.prototype.updateTipLabels = labels.updateTipLabels;
+PhyloTree.prototype.hideBranchLabels = labels.hideBranchLabels;
+PhyloTree.prototype.showBranchLabels = labels.showBranchLabels;
 
-/*
- * redraw the tree based on the current xTip, yTip, branch attributes
- * this function will remove branches, move the tips continuously
- * and add the new branches again after the tips arrived at their destination
- *  @params dt -- time of transition in milliseconds
- */
-PhyloTree.prototype.updateGeometryFade = function(dt) {
-  this.removeConfidence(dt)
-  // fade out branches
-  this.svg.selectAll('.branch').filter(function(d) {
-      return d.update;
-    })
-    .transition().duration(dt * 0.5)
-    .style("opacity", 0.0);
-  this.svg.selectAll('.branchLabels').filter(function(d) {
-      return d.update;
-    })
-    .transition().duration(dt * 0.5)
-    .style("opacity", 0.0);
-  this.svg.selectAll('.tipLabels').filter(function(d) {
-      return d.update;
-    })
-    .transition().duration(dt * 0.5)
-    .style("opacity", 0.0);
-
-  // closure to move the tips, called via the time out below
-  const tipTrans = function(tmp_svg, tmp_dt) {
-    const svg = tmp_svg;
-    return function() {
-      svg.selectAll('.tip').filter(function(d) {
-          return d.update;
-        })
-        .transition().duration(tmp_dt)
-        .attr("cx", function(d) {
-          return d.xTip;
-        })
-        .attr("cy", function(d) {
-          return d.yTip;
-        });
-      svg.selectAll(".vaccine")
-        .filter((d) => d.update)
-        .transition()
-          .duration(dt)
-          .attr("x", (d) => d.xTip)
-          .attr("y", (d) => d.yTip);
-    };
-  };
-  setTimeout(tipTrans(this.svg, dt), 0.5 * dt);
-
-  // closure to change the branches, called via time out after the tipTrans is done
-  const flipBranches = function(tmp_svg) {
-    const svg = tmp_svg;
-    return function() {
-      svg.selectAll('.branch').filter('.S').filter(function(d) {
-          return d.update;
-        })
-        .attr("d", function(d) {
-          return d.branch[0];
-        });
-      svg.selectAll('.branch').filter('.T').filter(function(d) {
-          return d.update;
-        })
-        .attr("d", function(d) {
-          return d.branch[1];
-        });
-    };
-  };
-  setTimeout(flipBranches(this.svg), 0.5 * dt);
-
-  // closure to add the new branches after the tipTrans
-  const fadeBack = function(tmp_svg, tmp_dt) {
-    const svg = tmp_svg;
-    return function(d) {
-      svg.selectAll('.branch').filter(function(d) {
-          return d.update;
-        })
-        .transition().duration(0.5 * tmp_dt)
-        .style("opacity", 1.0)
-    };
-  };
-  setTimeout(fadeBack(this.svg, 0.2 * dt), 1.5 * dt);
-  this.updateBranchLabels(dt);
-  this.updateTipLabels(dt);
-};
-
-/**
- * transition of branches and tips at the same time. only useful within a layout
- * @param  dt -- time of transition in milliseconds
- * @return {[type]}
- */
-PhyloTree.prototype.updateGeometry = function (dt) {
-  this.svg.selectAll(".tip")
-    .filter((d) => d.update)
-    .transition()
-      .duration(dt)
-      .attr("cx", (d) => d.xTip)
-      .attr("cy", (d) => d.yTip);
-
-  this.svg.selectAll(".vaccine")
-    .filter((d) => d.update)
-    .transition()
-      .duration(dt)
-      .attr("x", (d) => d.xTip)
-      .attr("y", (d) => d.yTip);
-
-  const branchEls = [".S", ".T"];
-  for (let i = 0; i < 2; i++) {
-    this.svg.selectAll(".branch")
-      .filter(branchEls[i])
-      .filter((d) => d.update)
-      .transition()
-        .duration(dt)
-        .attr("d", (d) => d.branch[i]);
-  }
 
-  this.svg.selectAll(".conf")
-    .filter((d) => d.update)
-    .transition()
-      .duration(dt)
-      .attr("d", (dd) => dd.confLine);
-
-  this.updateBranchLabels(dt);
-  this.updateTipLabels(dt);
-};
-
-
-PhyloTree.prototype.updateBranchLabels = function(dt){
-  const xPad = this.params.branchLabelPadX, yPad = this.params.branchLabelPadY;
-  const nNIV = this.nNodesInView;
-  const bLSFunc = this.callbacks.branchLabelSize;
-  const showBL = (this.layout==="rect") && this.params.showBranchLabels;
-  const visBL = showBL ? "visible" : "hidden";
-  this.svg.selectAll('.branchLabel')
-    .transition().duration(dt)
-    .attr("x", function(d) {
-      return d.xTip - xPad;
-    })
-    .attr("y", function(d) {
-      return d.yTip - yPad;
-    })
-    .attr("visibility",visBL)
-    .style("fill", this.params.branchLabelFill)
-    .style("font-family", this.params.branchLabelFont)
-    .style("font-size", function(d) {return bLSFunc(d, nNIV).toString()+"px";});
-}
-
-
-/* this was the *old* updateTipLabels */
-// PhyloTree.prototype.updateTipLabels = function(dt){
-//   const xPad = this.params.tipLabelPadX, yPad = this.params.tipLabelPadY;
-//   const nNIV = this.nNodesInView;
-//   const tLSFunc = this.callbacks.tipLabelSize;
-//   const showTL = (this.layout==="rect") && this.params.showTipLabels;
-//   const visTL = showTL ? "visible" : "hidden";
-//   this.svg.selectAll('.tipLabel')
-//     .transition().duration(dt)
-//     .attr("x", function(d) {
-//       return d.xTip + xPad;
-//     })
-//     .attr("y", function(d) {
-//       return d.yTip + yPad;
-//     })
-//     .attr("visibility",visTL)
-//     .style("fill", this.params.tipLabelFill)
-//     .style("font-family", this.params.tipLabelFont)
-//     .style("font-size", function(d) {return tLSFunc(d, nNIV).toString()+"px";});
-// }
-/* the new updateTipLabels is here: */
-PhyloTree.prototype.updateTipLabels = function(dt) {
-  this.svg.selectAll('.tipLabel').remove()
-  var params = this.params;
-  const tLFunc = this.callbacks.tipLabel;
-  const xPad = this.params.tipLabelPadX;
-  const yPad = this.params.tipLabelPadY;
-  const inViewTerminalNodes = this.nodes
-                  .filter(function(d){return d.terminal;})
-                  .filter(function(d){return d.inView;});
-  // console.log(`there are ${inViewTerminalNodes.length} nodes in view`)
-  if (inViewTerminalNodes.length < 50) {
-    // console.log("DRAWING!", inViewTerminalNodes)
-    window.setTimeout( () =>
-      this.tipLabels = this.svg.append("g").selectAll('.tipLabel')
-        .data(inViewTerminalNodes)
-        .enter()
-        .append("text")
-        .attr("x", function(d) {
-          return d.xTip + xPad;
-        })
-        .attr("y", function(d) {
-          return d.yTip + yPad;
-        })
-        .text(function (d){return tLFunc(d);})
-        .attr("class", "tipLabel")
-        .style('visibility', 'visible')
-      , dt
-    )
-  }
-}
+/* G R I D */
+PhyloTree.prototype.hideGrid = grid.hideGrid;
+PhyloTree.prototype.removeGrid = grid.removeGrid;
+PhyloTree.prototype.addGrid = grid.addGrid;
 
-PhyloTree.prototype.updateTimeBar = function(d){
-  return;
-}
 
-/**
- * Update multiple style or attributes of  tree elements at once
- * @param {string} treeElem one of .tip or .branch
- * @param {object} attr object containing the attributes to change as keys, array with values as value
- * @param {object} styles object containing the styles to change
- * @param {int} dt time in milliseconds
- */
-PhyloTree.prototype.updateMultipleArray = function(treeElem, attrs, styles, dt, quickdraw) {
-  // assign new values and decide whether to update
-  this.nodes.forEach(function(d, i) {
-    d.update = false;
-    /* note that this is not node.attr, but element attr such as <g width="100" vs style="" */
-    let newAttr;
-    for (var attr in attrs) {
-      newAttr = attrs[attr][i];
-      if (newAttr !== d[attr]) {
-        d[attr] = newAttr;
-        d.update = true;
-      }
-    }
-    let newStyle;
-    for (var prop in styles) {
-      newStyle = styles[prop][i];
-      if (newStyle !== d[prop]) {
-        d[prop] = newStyle;
-        d.update = true;
-      }
-    }
-  });
-  let updatePath = false;
-  if (styles["stroke-width"]) {
-    if (quickdraw) {
-      this.debouncedMapToScreen();
-    } else {
-      this.mapToScreen();
-    }
-    updatePath = true;
-  }
-
-  // function that return the closure object for updating the svg
-  function update(attrToSet, stylesToSet) {
-    return function(selection) {
-      for (var i=0; i<stylesToSet.length; i++) {
-        var prop = stylesToSet[i];
-        selection.style(prop, function(d) {
-          return d[prop];
-        });
-      }
-      for (var i = 0; i < attrToSet.length; i++) {
-        var prop = attrToSet[i];
-        selection.attr(prop, function(d) {
-          return d[prop];
-        });
-      }
-      if (updatePath){
-	selection.filter('.S').attr("d", function(d){return d.branch[0];})
-      }
-    };
-  };
-  // update the svg
-  if (dt) {
-    this.svg.selectAll(treeElem).filter(function(d) {
-        return d.update;
-      })
-      .transition().duration(dt)
-      .call(update(Object.keys(attrs), Object.keys(styles)));
-  } else {
-    this.svg.selectAll(treeElem).filter(function(d) {
-        return d.update;
-      })
-      .call(update(Object.keys(attrs), Object.keys(styles)));
-  }
-};
 
 /* this need a bit more work as the quickdraw functionality improves */
 PhyloTree.prototype.rerenderAllElements = function () {
@@ -726,54 +359,9 @@ PhyloTree.prototype.rerenderAllElements = function () {
 };
 
 
-/**
- * as updateAttributeArray, but accepts a callback function rather than an array
- * with the values. will create array and call updateAttributeArray
- * @param  treeElem  --- the part of the tree to update (.tip, .branch)
- * @param  attr  --- the attribute to update (e.g. r for tipRadius)
- * @param  callback -- function that assigns the attribute
- * @param  dt  --- time of transition in milliseconds
- * @return {[type]}
- */
-PhyloTree.prototype.updateStyleOrAttribute = function(treeElem, attr, callback, dt, styleOrAttribute) {
-  this.updateStyleOrAttributeArray(treeElem, attr,
-    this.nodes.map(function(d) {
-      return callback(d);
-    }), dt, styleOrAttribute);
-};
+PhyloTree.prototype.updateStyleOrAttribute = generalUpdates.updateStyleOrAttribute;
+PhyloTree.prototype.updateStyleOrAttributeArray = generalUpdates.updateStyleOrAttributeArray;
 
-/**
- * update an attribute of the tree for all nodes
- * @param  treeElem  --- the part of the tree to update (.tip, .branch)
- * @param  attr  --- the attribute to update (e.g. r for tipRadius)
- * @param  attr_array  --- an array with values for every node in the tree
- * @param  dt  --- time of transition in milliseconds
- * @return {[type]}
- */
-PhyloTree.prototype.updateStyleOrAttributeArray = function(treeElem, attr, attr_array, dt, styleOrAttribute) {
-  this.nodes.forEach(function(d, i) {
-    const newAttr = attr_array[i];
-    if (newAttr === d[attr]) {
-      d.update = false;
-    } else {
-      d[attr] = newAttr;
-      d.update = true;
-    }
-  });
-  if (typeof styleOrAttribute==="undefined"){
-    var all_attr = this.attributes;
-    if (contains(all_attr, attr)){
-      styleOrAttribute="attr";
-    }else{
-      styleOrAttribute="style";
-    }
-  }
-  if (styleOrAttribute==="style"){
-    this.redrawStyle(treeElem, attr, dt);
-  }else{
-    this.redrawAttribute(treeElem, attr, dt);
-  }
-};
 
 /**
  * update the svg after all new values have been assigned

From ad2cbfb85c427f12c476449bb0ebd0e150347a84 Mon Sep 17 00:00:00 2001
From: James Hadfield <jh22@sanger.ac.uk>
Date: Mon, 5 Feb 2018 11:30:55 -0800
Subject: [PATCH 08/10] break phylotree into files IV

---
 .../tree/phyloTree/generalUpdates.js          |  29 ++
 src/components/tree/phyloTree/helpers.js      |  27 ++
 src/components/tree/phyloTree/layouts.js      |  35 +-
 src/components/tree/phyloTree/phyloTree.js    | 331 ++----------------
 src/components/tree/phyloTree/renderers.js    | 141 ++++++++
 5 files changed, 255 insertions(+), 308 deletions(-)

diff --git a/src/components/tree/phyloTree/generalUpdates.js b/src/components/tree/phyloTree/generalUpdates.js
index 9bed9298c..77b2f4e26 100644
--- a/src/components/tree/phyloTree/generalUpdates.js
+++ b/src/components/tree/phyloTree/generalUpdates.js
@@ -247,3 +247,32 @@ export const updateMultipleArray = function updateMultipleArray(treeElem, attrs,
       .call(updateSVGHOF(Object.keys(attrs), Object.keys(styles)));
   }
 };
+
+
+/**
+ * update the svg after all new values have been assigned
+ * @param  treeElem -- one of .tip, .branch
+ * @param  attr  -- attribute of the tree element to update
+ * @param  dt -- transition time
+ */
+export const redrawAttribute = function redrawAttribute(treeElem, attr, dt) {
+  this.svg.selectAll(treeElem)
+    .filter((d) => d.update)
+    .transition()
+    .duration(dt)
+    .attr(attr, (d) => d[attr]);
+};
+
+
+/**
+ * update the svg after all new values have been assigned
+ * @param  treeElem -- one of .tip, .branch
+ * @param  styleElem  -- style element of the tree element to update
+ * @param  dt -- transition time
+ */
+export const redrawStyle = function redrawStyle(treeElem, styleElem, dt) {
+  this.svg.selectAll(treeElem)
+    .filter((d) => d.update)
+    .transition().duration(dt)
+    .style(styleElem, (d) => d[styleElem]);
+};
diff --git a/src/components/tree/phyloTree/helpers.js b/src/components/tree/phyloTree/helpers.js
index a596fe584..b607569de 100644
--- a/src/components/tree/phyloTree/helpers.js
+++ b/src/components/tree/phyloTree/helpers.js
@@ -34,3 +34,30 @@ export const applyToChildren = (node, func) => {
     applyToChildren(node.children[i], func);
   }
 };
+
+
+/*
+* given nodes, create the shell property, which links the redux properties
+* (theoretically immutable) with the phylotree properties (changeable)
+*/
+
+
+/*
+* given nodes, create the children and parent properties.
+* modifies the nodes argument in place
+*/
+export const createChildrenAndParents = (nodes) => {
+  nodes.forEach((d) => {
+    d.parent = d.n.parent.shell;
+    if (d.terminal) {
+      d.yRange = [d.n.yvalue, d.n.yvalue];
+      d.children = null;
+    } else {
+      d.yRange = [d.n.children[0].yvalue, d.n.children[d.n.children.length - 1].yvalue];
+      d.children = [];
+      for (let i = 0; i < d.n.children.length; i++) {
+        d.children.push(d.n.children[i].shell);
+      }
+    }
+  });
+};
diff --git a/src/components/tree/phyloTree/layouts.js b/src/components/tree/phyloTree/layouts.js
index 83b8608c8..fd3cfa053 100644
--- a/src/components/tree/phyloTree/layouts.js
+++ b/src/components/tree/phyloTree/layouts.js
@@ -1,5 +1,5 @@
 /* eslint-disable no-multi-spaces */
-import { sum } from "d3-array";
+import { min, sum } from "d3-array";
 import { addLeafCount } from "./helpers";
 
 /**
@@ -183,3 +183,36 @@ export const setDistance = function setDistance(distanceAttribute) {
     }
   });
 };
+
+
+/**
+ * sets the range of the scales used to map the x,y coordinates to the screen
+ * @param {margins} -- object with "right, left, top, bottom" margins
+ */
+export const setScales = function setScales(margins) {
+  const width = parseInt(this.svg.attr("width"), 10);
+  const height = parseInt(this.svg.attr("height"), 10);
+  if (this.layout === "radial" || this.layout === "unrooted") {
+    // Force Square: TODO, harmonize with the map to screen
+    const xExtend = width - (margins["left"] || 0) - (margins["right"] || 0);
+    const yExtend = height - (margins["top"] || 0) - (margins["top"] || 0);
+    const minExtend = min([xExtend, yExtend]);
+    const xSlack = xExtend - minExtend;
+    const ySlack = yExtend - minExtend;
+    this.xScale.range([0.5 * xSlack + margins["left"] || 0, width - 0.5 * xSlack - (margins["right"] || 0)]);
+    this.yScale.range([0.5 * ySlack + margins["top"] || 0, height - 0.5 * ySlack - (margins["bottom"] || 0)]);
+
+  } else {
+    // for rectancular layout, allow flipping orientation of left right and top/botton
+    if (this.params.orientation[0] > 0) {
+      this.xScale.range([margins["left"] || 0, width - (margins["right"] || 0)]);
+    } else {
+      this.xScale.range([width - (margins["right"] || 0), margins["left"] || 0]);
+    }
+    if (this.params.orientation[1] > 0) {
+      this.yScale.range([margins["top"] || 0, height - (margins["bottom"] || 0)]);
+    } else {
+      this.yScale.range([height - (margins["bottom"] || 0), margins["top"] || 0]);
+    }
+  }
+};
diff --git a/src/components/tree/phyloTree/phyloTree.js b/src/components/tree/phyloTree/phyloTree.js
index e9edc48aa..772d2113e 100644
--- a/src/components/tree/phyloTree/phyloTree.js
+++ b/src/components/tree/phyloTree/phyloTree.js
@@ -6,10 +6,10 @@ import { scaleLinear } from "d3-scale";
 import { flattenTree, appendParentsToTree } from "../treeHelpers";
 import { dataFont, darkGrey } from "../../../globalStyles";
 import { defaultParams } from "./defaultParams";
-import { addLeafCount } from "./helpers";
+import { addLeafCount, createChildrenAndParents } from "./helpers";
 
 /* PROTOTYPES */
-import { render } from "./renderers";
+import * as renderers from "./renderers";
 import * as layouts from "./layouts";
 import * as zoom from "./zoom";
 import * as grid from "./grid";
@@ -38,31 +38,14 @@ var PhyloTree = function(treeJson) {
     r: this.params.tipRadius // set defaults
   }));
   // pull out the total number of tips -- the is the maximal yvalue
-  this.numberOfTips = max(this.nodes.map(function(d) {
-    return d.n.yvalue;
-  }));
+  this.numberOfTips = max(this.nodes.map((d) => d.n.yvalue));
   this.nodes.forEach(function(d) {
     d.inView=true; // each node is visible
     d.n.shell = d; // a back link from the original node object to the wrapper
     d.terminal = (typeof d.n.children === "undefined");
   });
 
-  // remember the range of children subtending a node (i.e. the range of yvalues)
-  // this is useful for drawing
-  // and create children structure for the shell.
-  this.nodes.forEach(function(d) {
-    d.parent = d.n.parent.shell;
-    if (d.terminal) {
-      d.yRange = [d.n.yvalue, d.n.yvalue];
-      d.children=null;
-    } else {
-      d.yRange = [d.n.children[0].yvalue, d.n.children[d.n.children.length - 1].yvalue];
-      d.children = [];
-      for (var i=0; i < d.n.children.length; i++){
-        d.children.push(d.n.children[i].shell);
-      }
-    }
-  });
+  createChildrenAndParents(this.nodes);
 
   this.xScale = scaleLinear();
   this.yScale = scaleLinear();
@@ -74,248 +57,27 @@ var PhyloTree = function(treeJson) {
     {leading: false, trailing: true, maxWait: this.params.mapToScreenDebounceTime});
 };
 
-/* DEFINE THE PROTOTYPES */
-PhyloTree.prototype.render = render;
+/* I N I T I A L        R E N D E R       E T C */
+PhyloTree.prototype.render = renderers.render;
+PhyloTree.prototype.rerenderAllElements = renderers.rerenderAllElements;
+PhyloTree.prototype.clearSVG = renderers.clearSVG;
 
-/* LAYOUT PROTOTYPES */
+/* D R A W I N G    F U N C T I O N S */
+PhyloTree.prototype.drawTips = renderers.drawTips;
+PhyloTree.prototype.drawBranches = renderers.drawBranches;
+PhyloTree.prototype.drawVaccines = renderers.drawVaccines;
+PhyloTree.prototype.drawRegression = renderers.drawRegression;
+
+/* C A L C U L A T E    G E O M E T R I E S  E T C   ( M O D I F I E S    N O D E S ,    N O T    S V G ) */
 PhyloTree.prototype.setDistance = layouts.setDistance;
 PhyloTree.prototype.setLayout = layouts.setLayout;
 PhyloTree.prototype.rectangularLayout = layouts.rectangularLayout;
 PhyloTree.prototype.timeVsRootToTip = layouts.timeVsRootToTip;
 PhyloTree.prototype.unrootedLayout = layouts.unrootedLayout;
 PhyloTree.prototype.radialLayout = layouts.radialLayout;
-
-/**
- * draws the regression line in the svg and adds a text with the rate estimate
- * @return {null}
- */
-PhyloTree.prototype.drawRegression = function(){
-    const leftY = this.yScale(this.regression.intercept+this.xScale.domain()[0]*this.regression.slope);
-    const rightY = this.yScale(this.regression.intercept+this.xScale.domain()[1]*this.regression.slope);
-
-    const path = "M "+this.xScale.range()[0].toString()+" "+leftY.toString()
-                +" L " + this.xScale.range()[1].toString()+" "+rightY.toString();
-    this.svg
-        .append("path")
-        .attr("d", path)
-        .attr("class", "regression")
-        .style("fill", "none")
-        .style("visibility", "visible")
-        .style("stroke",this.params.regressionStroke)
-        .style("stroke-width",this.params.regressionWidth);
-    this.svg
-        .append("text")
-        .text("rate estimate: "+this.regression.slope.toFixed(4)+' / year')
-        .attr("class", "regression")
-        .attr("x", this.xScale.range()[1] / 2 - 75)
-        .attr("y", this.yScale.range()[0] + 50)
-        .style("fill", this.params.regressionStroke)
-        .style("font-size", this.params.tickLabelSize + 8)
-        .style("font-weight", 400)
-        .style("font-family",this.params.fontFamily);
-};
-
-
-/* Z O O M ,    F I T     TO     S C R E E N ,     E T C */
-PhyloTree.prototype.zoomIntoClade = zoom.zoomIntoClade;
-PhyloTree.prototype.zoomToParent = zoom.zoomToParent;
-PhyloTree.prototype.mapToScreen = zoom.mapToScreen;
-
-
-
-
-
-
-
-
-/**
- * sets the range of the scales used to map the x,y coordinates to the screen
- * @param {margins} -- object with "right, left, top, bottom" margins
- */
-PhyloTree.prototype.setScales = function(margins) {
-  const width = parseInt(this.svg.attr("width"), 10);
-  const height = parseInt(this.svg.attr("height"), 10);
-  if (this.layout === "radial" || this.layout === "unrooted") {
-    //Force Square: TODO, harmonize with the map to screen
-    const xExtend = width - (margins["left"] || 0) - (margins["right"] || 0);
-    const yExtend = height - (margins["top"] || 0) - (margins["top"] || 0);
-    const minExtend = min([xExtend, yExtend]);
-    const xSlack = xExtend - minExtend;
-    const ySlack = yExtend - minExtend;
-    this.xScale.range([0.5 * xSlack + margins["left"] || 0, width - 0.5 * xSlack - (margins["right"] || 0)]);
-    this.yScale.range([0.5 * ySlack + margins["top"] || 0, height - 0.5 * ySlack - (margins["bottom"] || 0)]);
-
-  } else {
-    // for rectancular layout, allow flipping orientation of left right and top/botton
-    if (this.params.orientation[0]>0){
-      this.xScale.range([margins["left"] || 0, width - (margins["right"] || 0)]);
-    }else{
-      this.xScale.range([width - (margins["right"] || 0), margins["left"] || 0]);
-    }
-    if (this.params.orientation[1]>0){
-      this.yScale.range([margins["top"] || 0, height - (margins["bottom"] || 0)]);
-    } else {
-      this.yScale.range([height - (margins["bottom"] || 0), margins["top"] || 0]);
-    }
-  }
-};
-
-
-
-/*
- * add and remove elements from tree, initial render
- */
-PhyloTree.prototype.clearSVG = function() {
-  this.svg.selectAll('.tip').remove();
-  this.svg.selectAll('.branch').remove();
-  this.svg.selectAll('.branchLabel').remove();
-  this.svg.selectAll(".vaccine").remove();
-};
-
-/**
- * adds crosses to the vaccines
- * @return {null}
- */
-PhyloTree.prototype.drawVaccines = function() {
-  this.tipElements = this.svg.append("g").selectAll(".vaccine")
-    .data(this.vaccines)
-    .enter()
-    .append("text")
-      .attr("class", "vaccine")
-      .attr("x", (d) => d.xTip)
-      .attr("y", (d) => d.yTip)
-      .attr('text-anchor', 'middle')
-      .attr('dominant-baseline', 'central')
-      .style("font-family", this.params.fontFamily)
-      .style("font-size", "20px")
-      .style("stroke", "#fff")
-      .style("fill", darkGrey)
-      .text('\u2716')
-      // .style("cursor", "pointer")
-      .on("mouseover", (d) => console.log("vaccine", d))
-};
-
-/**
- * adds all the tip circles to the svg, they have class tip
- * @return {null}
- */
-PhyloTree.prototype.drawTips = function() {
-  var params=this.params;
-  this.tipElements = this.svg.append("g").selectAll(".tip")
-    .data(this.nodes.filter(function(d) {
-      return d.terminal;
-    }))
-    .enter()
-    .append("circle")
-    .attr("class", "tip")
-    .attr("id", function(d) {
-      return "tip_" + d.n.clade;
-    })
-    .attr("cx", function(d) {
-      return d.xTip;
-    })
-    .attr("cy", function(d) {
-      return d.yTip;
-    })
-    .attr("r", function(d) {
-      return d.r;
-    })
-    .on("mouseover", (d) => {
-      this.callbacks.onTipHover(d, event.pageX, event.pageY)
-    })
-    .on("mouseout", (d) => {
-      this.callbacks.onTipLeave(d)
-    })
-    .on("click", (d) => {
-      this.callbacks.onTipClick(d)
-    })
-    .style("pointer-events", "auto")
-    .style("fill", function(d) {
-      return d.fill || params.tipFill;
-    })
-    .style("stroke", function(d) {
-      return d.stroke || params.tipStroke;
-    })
-    .style("stroke-width", function(d) {
-      // return d['stroke-width'] || params.tipStrokeWidth;
-      return params.tipStrokeWidth; /* don't want branch thicknesses applied */
-    })
-    .style("cursor", "pointer");
-};
-
-/**
- * adds all branches to the svg, these are paths with class branch
- * @return {null}
- */
-PhyloTree.prototype.drawBranches = function() {
-  var params = this.params;
-  this.Tbranches = this.svg.append("g").selectAll('.branch')
-    .data(this.nodes.filter(function(d){return !d.terminal;}))
-    .enter()
-    .append("path")
-    .attr("class", "branch T")
-    .attr("id", function(d) {
-      return "branch_T_" + d.n.clade;
-    })
-    .attr("d", function(d) {
-      return d.branch[1];
-    })
-    .style("stroke", function(d) {
-      return d.stroke || params.branchStroke;
-    })
-    .style("stroke-width", function(d) {
-      return d['stroke-width'] || params.branchStrokeWidth;
-    })
-    .style("fill", "none")
-    // .style("cursor", "pointer")
-    .style("pointer-events", "auto")
-    // .on("mouseover", (d) => {
-    //   this.callbacks.onBranchHover(d, d3.event.pageX, d3.event.pageY)
-    // })
-    // .on("mouseout", (d) => {
-    //   this.callbacks.onBranchLeave(d)
-    // })
-    // .on("click", (d) => {
-    //   this.callbacks.onBranchClick(d)
-    // });
-  this.branches = this.svg.append("g").selectAll('.branch')
-    .data(this.nodes)
-    .enter()
-    .append("path")
-    .attr("class", "branch S")
-    .attr("id", function(d) {
-      return "branch_S_" + d.n.clade;
-    })
-    .attr("d", function(d) {
-      return d.branch[0];
-    })
-    .style("stroke", function(d) {
-      return d.stroke || params.branchStroke;
-    })
-		.style("stroke-linecap", "round")
-    .style("stroke-width", function(d) {
-      return d['stroke-width'] || params.branchStrokeWidth;
-    })
-    .style("fill", "none")
-    .style("cursor", "pointer")
-    .style("pointer-events", "auto")
-    .on("mouseover", (d) => {
-      this.callbacks.onBranchHover(d, event.pageX, event.pageY)
-    })
-    .on("mouseout", (d) => {
-      this.callbacks.onBranchLeave(d)
-    })
-    .on("click", (d) => {
-      this.callbacks.onBranchClick(d)
-    });
-};
-
-
-
-
+PhyloTree.prototype.setScales = layouts.setScales;
 
 /* C O N F I D E N C E    I N T E R V A L S */
-
 PhyloTree.prototype.removeConfidence = confidence.removeConfidence;
 PhyloTree.prototype.drawConfidence = confidence.drawConfidence;
 PhyloTree.prototype.drawSingleCI = confidence.drawSingleCI;
@@ -328,7 +90,10 @@ PhyloTree.prototype.updateGeometry = generalUpdates.updateGeometry;
 PhyloTree.prototype.updateGeometryFade = generalUpdates.updateGeometryFade;
 PhyloTree.prototype.updateTimeBar = (d) => {};
 PhyloTree.prototype.updateMultipleArray = generalUpdates.updateMultipleArray;
-
+PhyloTree.prototype.updateStyleOrAttribute = generalUpdates.updateStyleOrAttribute;
+PhyloTree.prototype.updateStyleOrAttributeArray = generalUpdates.updateStyleOrAttributeArray;
+PhyloTree.prototype.redrawAttribute = generalUpdates.redrawAttribute;
+PhyloTree.prototype.redrawStyle = generalUpdates.redrawStyle;
 
 /* L A B E L S    ( T I P ,    B R A N C H ,   C O N F I D E N C E ) */
 PhyloTree.prototype.drawCladeLabels = labels.drawCladeLabels;
@@ -337,62 +102,14 @@ PhyloTree.prototype.updateTipLabels = labels.updateTipLabels;
 PhyloTree.prototype.hideBranchLabels = labels.hideBranchLabels;
 PhyloTree.prototype.showBranchLabels = labels.showBranchLabels;
 
-
 /* G R I D */
 PhyloTree.prototype.hideGrid = grid.hideGrid;
 PhyloTree.prototype.removeGrid = grid.removeGrid;
 PhyloTree.prototype.addGrid = grid.addGrid;
 
-
-
-/* this need a bit more work as the quickdraw functionality improves */
-PhyloTree.prototype.rerenderAllElements = function () {
-  // console.log("rerenderAllElements")
-  this.mapToScreen();
-  this.svg.selectAll(".branch")
-    .transition().duration(0)
-    .style("stroke-width", (d) => d["stroke-width"]);
-  this.svg.selectAll(".branch")
-    .transition().duration(0)
-    .filter(".S")
-    .attr("d", (d) => d.branch[0]);
-};
-
-
-PhyloTree.prototype.updateStyleOrAttribute = generalUpdates.updateStyleOrAttribute;
-PhyloTree.prototype.updateStyleOrAttributeArray = generalUpdates.updateStyleOrAttributeArray;
-
-
-/**
- * update the svg after all new values have been assigned
- * @param  treeElem -- one of .tip, .branch
- * @param  attr  -- attribute of the tree element to update
- * @param  dt -- transition time
- */
-PhyloTree.prototype.redrawAttribute = function(treeElem, attr, dt) {
-  this.svg.selectAll(treeElem).filter(function(d) {
-      return d.update;
-    })
-    .transition().duration(dt)
-    .attr(attr, function(d) {
-      return d[attr];
-    });
-};
-
-/**
- * update the svg after all new values have been assigned
- * @param  treeElem -- one of .tip, .branch
- * @param  styleElem  -- style element of the tree element to update
- * @param  dt -- transition time
- */
-PhyloTree.prototype.redrawStyle = function(treeElem, styleElem, dt) {
-  this.svg.selectAll(treeElem).filter(function(d) {
-      return d.update;
-    })
-    .transition().duration(dt)
-    .style(styleElem, function(d) {
-      return d[styleElem];
-    });
-};
+/* Z O O M ,    F I T     TO     S C R E E N ,     E T C */
+PhyloTree.prototype.zoomIntoClade = zoom.zoomIntoClade;
+PhyloTree.prototype.zoomToParent = zoom.zoomToParent;
+PhyloTree.prototype.mapToScreen = zoom.mapToScreen;
 
 export default PhyloTree;
diff --git a/src/components/tree/phyloTree/renderers.js b/src/components/tree/phyloTree/renderers.js
index e0401ac26..1a448037f 100644
--- a/src/components/tree/phyloTree/renderers.js
+++ b/src/components/tree/phyloTree/renderers.js
@@ -1,3 +1,6 @@
+import { darkGrey } from "../../../globalStyles";
+
+
 /**
  * @param  svg    -- the svg into which the tree is drawn
  * @param  layout -- the layout to be used, e.g. "rect"
@@ -56,3 +59,141 @@ export const render = function render(svg, layout, distance, options, callbacks,
     this.drawConfidence();
   }
 };
+
+/**
+ * adds crosses to the vaccines
+ * @return {null}
+ */
+export const drawVaccines = function drawVaccines() {
+  this.tipElements = this.svg.append("g").selectAll(".vaccine")
+    .data(this.vaccines)
+    .enter()
+    .append("text")
+    .attr("class", "vaccine")
+    .attr("x", (d) => d.xTip)
+    .attr("y", (d) => d.yTip)
+    .attr('text-anchor', 'middle')
+    .attr('dominant-baseline', 'central')
+    .style("font-family", this.params.fontFamily)
+    .style("font-size", "20px")
+    .style("stroke", "#fff")
+    .style("fill", darkGrey)
+    .text('\u2716');
+  // .style("cursor", "pointer")
+  // .on("mouseover", (d) => console.warn("vaccine mouseover", d));
+};
+
+
+/**
+ * adds all the tip circles to the svg, they have class tip
+ * @return {null}
+ */
+export const drawTips = function drawTips() {
+  const params = this.params;
+  this.tipElements = this.svg.append("g").selectAll(".tip")
+    .data(this.nodes.filter((d) => d.terminal))
+    .enter()
+    .append("circle")
+    .attr("class", "tip")
+    .attr("id", (d) => "tip_" + d.n.clade)
+    .attr("cx", (d) => d.xTip)
+    .attr("cy", (d) => d.yTip)
+    .attr("r", (d) => d.r)
+    .on("mouseover", (d) => this.callbacks.onTipHover(d, event.pageX, event.pageY))
+    .on("mouseout", (d) => this.callbacks.onTipLeave(d))
+    .on("click", (d) => this.callbacks.onTipClick(d))
+    .style("pointer-events", "auto")
+    .style("fill", (d) => d.fill || params.tipFill)
+    .style("stroke", (d) => d.stroke || params.tipStroke)
+    .style("stroke-width", () => params.tipStrokeWidth) /* don't want branch thicknesses applied */
+    .style("cursor", "pointer");
+};
+
+
+/**
+ * adds all branches to the svg, these are paths with class branch
+ * @return {null}
+ */
+export const drawBranches = function drawBranches() {
+  const params = this.params;
+  this.Tbranches = this.svg.append("g").selectAll('.branch')
+    .data(this.nodes.filter((d) => !d.terminal))
+    .enter()
+    .append("path")
+    .attr("class", "branch T")
+    .attr("id", (d) => "branch_T_" + d.n.clade)
+    .attr("d", (d) => d.branch[1])
+    .style("stroke", (d) => d.stroke || params.branchStroke)
+    .style("stroke-width", (d) => d['stroke-width'] || params.branchStrokeWidth)
+    .style("fill", "none")
+    .style("pointer-events", "auto");
+
+  this.branches = this.svg.append("g").selectAll('.branch')
+    .data(this.nodes)
+    .enter()
+    .append("path")
+    .attr("class", "branch S")
+    .attr("id", (d) => "branch_S_" + d.n.clade)
+    .attr("d", (d) => d.branch[0])
+    .style("stroke", (d) => d.stroke || params.branchStroke)
+    .style("stroke-linecap", "round")
+    .style("stroke-width", (d) => d['stroke-width'] || params.branchStrokeWidth)
+    .style("fill", "none")
+    .style("cursor", "pointer")
+    .style("pointer-events", "auto")
+    .on("mouseover", (d) => this.callbacks.onBranchHover(d, event.pageX, event.pageY))
+    .on("mouseout", (d) => this.callbacks.onBranchLeave(d))
+    .on("click", (d) => this.callbacks.onBranchClick(d));
+};
+
+
+/* this need a bit more work as the quickdraw functionality improves */
+export const rerenderAllElements = function rerenderAllElements() {
+  // console.log("rerenderAllElements")
+  this.mapToScreen();
+  this.svg.selectAll(".branch")
+    .transition().duration(0)
+    .style("stroke-width", (d) => d["stroke-width"]);
+  this.svg.selectAll(".branch")
+    .transition().duration(0)
+    .filter(".S")
+    .attr("d", (d) => d.branch[0]);
+};
+
+/**
+ * draws the regression line in the svg and adds a text with the rate estimate
+ * @return {null}
+ */
+export const drawRegression = function drawRegression() {
+  const leftY = this.yScale(this.regression.intercept + this.xScale.domain()[0] * this.regression.slope);
+  const rightY = this.yScale(this.regression.intercept + this.xScale.domain()[1] * this.regression.slope);
+
+  const path = "M " + this.xScale.range()[0].toString() + " " + leftY.toString() +
+    " L " + this.xScale.range()[1].toString() + " " + rightY.toString();
+  this.svg.append("path")
+    .attr("d", path)
+    .attr("class", "regression")
+    .style("fill", "none")
+    .style("visibility", "visible")
+    .style("stroke", this.params.regressionStroke)
+    .style("stroke-width", this.params.regressionWidth);
+  this.svg.append("text")
+    .text("rate estimate: " + this.regression.slope.toFixed(4) + ' / year')
+    .attr("class", "regression")
+    .attr("x", this.xScale.range()[1] / 2 - 75)
+    .attr("y", this.yScale.range()[0] + 50)
+    .style("fill", this.params.regressionStroke)
+    .style("font-size", this.params.tickLabelSize + 8)
+    .style("font-weight", 400)
+    .style("font-family", this.params.fontFamily);
+};
+
+/*
+ * add and remove elements from tree, initial render
+ */
+export const clearSVG = function clearSVG() {
+  this.svg.selectAll('.tip').remove();
+  this.svg.selectAll('.branch').remove();
+  this.svg.selectAll('.branchLabel').remove();
+  this.svg.selectAll(".vaccine").remove();
+};

From f8be36e7c4cf11f96eb0c08d57e1145436bed893 Mon Sep 17 00:00:00 2001
From: James Hadfield <jh22@sanger.ac.uk>
Date: Mon, 5 Feb 2018 13:42:45 -0800
Subject: [PATCH 09/10] optimise phyloTree constructor (c. 10ms -> 6ms)

---
 src/components/tree/index.js               |  2 +-
 src/components/tree/phyloTree/phyloTree.js | 51 ++++++++++------------
 2 files changed, 23 insertions(+), 30 deletions(-)

diff --git a/src/components/tree/index.js b/src/components/tree/index.js
index 3f68d67fc..1f5065a2e 100644
--- a/src/components/tree/index.js
+++ b/src/components/tree/index.js
@@ -122,7 +122,7 @@ class Tree extends React.Component {
   makeTree(nextProps) {
     const nodes = nextProps.tree.nodes;
     if (nodes && this.refs.d3TreeElement) {
-      const myTree = new PhyloTree(nodes[0]);
+      const myTree = new PhyloTree(nodes);
       // https://facebook.github.io/react/docs/refs-and-the-dom.html
       myTree.render(
         select(this.refs.d3TreeElement),
diff --git a/src/components/tree/phyloTree/phyloTree.js b/src/components/tree/phyloTree/phyloTree.js
index 772d2113e..4306a7dc1 100644
--- a/src/components/tree/phyloTree/phyloTree.js
+++ b/src/components/tree/phyloTree/phyloTree.js
@@ -1,10 +1,7 @@
-/* eslint-disable */
 import _debounce from "lodash/debounce";
-import { event } from "d3-selection";
-import { min, max, sum } from "d3-array";
+import { max } from "d3-array";
 import { scaleLinear } from "d3-scale";
 import { flattenTree, appendParentsToTree } from "../treeHelpers";
-import { dataFont, darkGrey } from "../../../globalStyles";
 import { defaultParams } from "./defaultParams";
 import { addLeafCount, createChildrenAndParents } from "./helpers";
 
@@ -17,36 +14,32 @@ import * as confidence from "./confidence";
 import * as labels from "./labels";
 import * as generalUpdates from "./generalUpdates";
 
-/*
- * phylogenetic tree drawing class
- * it is instantiated with a nested tree json.
- * the actual tree is rendered by the render method
- * @params:
- *   treeJson -- tree object as exported by nextstrain.
- */
-var PhyloTree = function(treeJson) {
+
+/* phylogenetic tree drawing function - the actual tree is rendered by the render prototype */
+const PhyloTree = function PhyloTree(reduxNodes) {
   this.grid = false;
   this.attributes = ['r', 'cx', 'cy', 'id', 'class', 'd'];
   this.params = defaultParams;
-  appendParentsToTree(treeJson); // add reference to .parent to each node in tree
-  const nodesArray = flattenTree(treeJson); // convert the tree json into a flat list of nodes
-  // wrap each node in a shell structure to avoid mutating the input data
-  this.nodes = nodesArray.map((d) => ({
-    n: d, // .n is the original node
-    x: 0, // x,y coordinates
-    y: 0,
-    r: this.params.tipRadius // set defaults
-  }));
-  // pull out the total number of tips -- the is the maximal yvalue
-  this.numberOfTips = max(this.nodes.map((d) => d.n.yvalue));
-  this.nodes.forEach(function(d) {
-    d.inView=true; // each node is visible
-    d.n.shell = d; // a back link from the original node object to the wrapper
-    d.terminal = (typeof d.n.children === "undefined");
-  });
 
+  /* create this.nodes, which is an array of nodes with properties used by phylotree for drawing.
+   this.nodes is the same length as reduxNodes such that this.nodes[i] is related to reduxNodes[i]
+   Furthermore, these objects are linked:
+   -- this.nodes[i].n = reduxNodes[i]
+   -- reduxNodes[i].shell = this.nodes[i] */
+  this.nodes = reduxNodes.map((d) => {
+    const phyloNode = {
+      n: d, /* a back link to the redux node */
+      x: 0,
+      y: 0,
+      terminal: (typeof d.children === "undefined"),
+      inView: true, /* each node is visible */
+      r: this.params.tipRadius /* default */
+    };
+    d.shell = phyloNode; /* set the link from the redux node to the phylotree node */
+    return phyloNode;
+  });
+  this.numberOfTips = max(this.nodes.map((d) => d.n.yvalue)); // total number of tips (we kinda cheat by finding the maximal yvalue, made by augur)
   createChildrenAndParents(this.nodes);
-
   this.xScale = scaleLinear();
   this.yScale = scaleLinear();
   this.zoomNode = this.nodes[0];

From eeb5d662a84f67c1b748c456b0f1fea2bfe19530 Mon Sep 17 00:00:00 2001
From: James Hadfield <jh22@sanger.ac.uk>
Date: Mon, 5 Feb 2018 14:00:32 -0800
Subject: [PATCH 10/10] tree linting / syntax

---
 src/components/tree/index.js                  | 69 +++++++++----------
 src/components/tree/infoPanels/click.js       |  6 +-
 src/components/tree/infoPanels/hover.js       | 19 +++--
 src/components/tree/phyloTree/phyloTree.js    |  3 +-
 .../tree/reactD3Interface/callbacks.js        |  2 +-
 5 files changed, 47 insertions(+), 52 deletions(-)

diff --git a/src/components/tree/index.js b/src/components/tree/index.js
index 1f5065a2e..23e6a3289 100644
--- a/src/components/tree/index.js
+++ b/src/components/tree/index.js
@@ -50,6 +50,11 @@ class Tree extends React.Component {
       selectedTip: null,
       tree: null
     };
+    /* bind callbacks */
+    this.clearSelectedTip = callbacks.clearSelectedTip.bind(this);
+    this.resetView = callbacks.resetView.bind(this);
+    this.onViewerChange = callbacks.onViewerChange.bind(this);
+    this.handleIconClickHOF = callbacks.handleIconClickHOF.bind(this);
   }
   static propTypes = {
     mutType: PropTypes.string.isRequired
@@ -72,7 +77,7 @@ class Tree extends React.Component {
     } else if (changes.newData) {
       tree = this.makeTree(nextProps);
       /* extra (initial, once only) call to update the tree colouring */
-      for (const k in changes) {
+      for (const k in changes) { // eslint-disable-line
         changes[k] = false;
       }
       changes.colorBy = true;
@@ -121,21 +126,21 @@ class Tree extends React.Component {
 
   makeTree(nextProps) {
     const nodes = nextProps.tree.nodes;
-    if (nodes && this.refs.d3TreeElement) {
+    if (nodes && this.d3ref) {
       const myTree = new PhyloTree(nodes);
       // https://facebook.github.io/react/docs/refs-and-the-dom.html
       myTree.render(
-        select(this.refs.d3TreeElement),
+        select(this.d3ref),
         this.props.layout,
         this.props.distanceMeasure,
         { /* options */
           grid: true,
           confidence: nextProps.temporalConfidence.display,
           showVaccines: !!nextProps.tree.vaccines,
-          branchLabels: true,      //generate DOM object
-          showBranchLabels: false,  //hide them initially -> couple to redux state
-          tipLabels: true,      //generate DOM object
-          showTipLabels: true   //show
+          branchLabels: true,
+          showBranchLabels: false,
+          tipLabels: true,
+          showTipLabels: true
         },
         { /* callbacks */
           onTipHover: callbacks.onTipHover.bind(this),
@@ -155,9 +160,8 @@ class Tree extends React.Component {
         nextProps.tree.vaccines
       );
       return myTree;
-    } else {
-      return null;
     }
+    return null;
   }
 
   render() {
@@ -185,7 +189,7 @@ class Tree extends React.Component {
           colorScale={this.props.colorScale}
         />
         <TipClickedPanel
-          goAwayCallback={(d) => callbacks.clearSelectedTip.bind(this)(d)}
+          goAwayCallback={this.clearSelectedTip}
           tip={this.state.selectedTip}
           metadata={this.props.metadata}
         />
@@ -203,10 +207,8 @@ class Tree extends React.Component {
           detectAutoPan={false}
           background={"#FFF"}
           miniaturePosition={"none"}
-          // onMouseDown={this.startPan.bind(this)}
-          onDoubleClick={callbacks.resetView.bind(this)}
-          //onMouseUp={this.endPan.bind(this)}
-          onChangeValue={callbacks.onViewerChange.bind(this)}
+          onDoubleClick={this.resetView}
+          onChangeValue={this.onViewerChange}
         >
           <svg style={{pointerEvents: "auto"}}
             width={responsive.width}
@@ -217,35 +219,32 @@ class Tree extends React.Component {
               height={responsive.height}
               id={"d3TreeElement"}
               style={{cursor: "default"}}
-              ref="d3TreeElement"
-            >
-            </g>
+              ref={(c) => {this.d3ref = c;}}
+            />
           </svg>
         </ReactSVGPanZoom>
-        <svg width={50} height={130}
-          style={{position: "absolute", right: 20, bottom: 20}}
-        >
-            <defs>
-              <filter id="dropshadow" height="130%">
-                <feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
-                <feOffset dx="2" dy="2" result="offsetblur"/>
-                <feComponentTransfer>
-                  <feFuncA type="linear" slope="0.2"/>
-                </feComponentTransfer>
-                <feMerge>
-                  <feMergeNode/>
-                  <feMergeNode in="SourceGraphic"/>
-                </feMerge>
-              </filter>
-            </defs>
+        <svg width={50} height={130} style={{position: "absolute", right: 20, bottom: 20}}>
+          <defs>
+            <filter id="dropshadow" height="130%">
+              <feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
+              <feOffset dx="2" dy="2" result="offsetblur"/>
+              <feComponentTransfer>
+                <feFuncA type="linear" slope="0.2"/>
+              </feComponentTransfer>
+              <feMerge>
+                <feMergeNode/>
+                <feMergeNode in="SourceGraphic"/>
+              </feMerge>
+            </filter>
+          </defs>
           <ZoomInIcon
-            handleClick={callbacks.handleIconClick.bind(this)("zoom-in")}
+            handleClick={this.handleIconClickHOF("zoom-in")}
             active
             x={10}
             y={50}
           />
           <ZoomOutIcon
-            handleClick={callbacks.handleIconClick.bind(this)("zoom-out")}
+            handleClick={this.handleIconClickHOF("zoom-out")}
             active
             x={10}
             y={90}
diff --git a/src/components/tree/infoPanels/click.js b/src/components/tree/infoPanels/click.js
index fd67d986a..634562354 100644
--- a/src/components/tree/infoPanels/click.js
+++ b/src/components/tree/infoPanels/click.js
@@ -24,7 +24,7 @@ const styles = {
 };
 
 export const stopProp = (e) => {
-  if (!e) {e = window.event;}
+  if (!e) {e = window.event;} // eslint-disable-line no-param-reassign
   e.cancelBubble = true;
   if (e.stopPropagation) {e.stopPropagation();}
 };
@@ -39,9 +39,9 @@ const item = (key, value) => (
 
 const formatURL = (url) => {
   if (url !== undefined && url.startsWith("https_")) {
-    url = url.replace("https_", "https:");
+    return url.replace("https_", "https:");
   } else if (url !== undefined && url.startsWith("http_")) {
-    url = url.replace("http_", "http:");
+    return url.replace("http_", "http:");
   }
   return url;
 };
diff --git a/src/components/tree/infoPanels/hover.js b/src/components/tree/infoPanels/hover.js
index 51e6e1bb7..ac123a4b8 100644
--- a/src/components/tree/infoPanels/hover.js
+++ b/src/components/tree/infoPanels/hover.js
@@ -20,8 +20,8 @@ const infoBlockJSX = (item, values) => (
     <p style={{marginBottom: "-0.7em", fontWeight: "500"}}>
       {item}
     </p>
-    {values.map((k, i) => (
-      <p key={i} style={{fontWeight: "300", marginBottom: "-0.9em", marginLeft: "0em"}}>
+    {values.map((k) => (
+      <p key={k} style={{fontWeight: "300", marginBottom: "-0.9em", marginLeft: "0em"}}>
         {k}
       </p>
     ))}
@@ -69,7 +69,7 @@ const displayColorBy = (d, distanceMeasure, temporalConfidence, colorByConfidenc
   if (colorByConfidence === true) {
     const lkey = colorBy + "_confidence";
     if (Object.keys(d.attr).indexOf(lkey) === -1) {
-      console.log("Error - couldn't find confidence vals for ", lkey);
+      console.error("Error - couldn't find confidence vals for ", lkey);
       return null;
     }
     const vals = Object.keys(d.attr[lkey])
@@ -144,7 +144,7 @@ const getMutationsJSX = (d, mutType) => {
     }
     return infoLineJSX("No amino acid mutations", "");
   }
-  console.log("Error parsing mutations for branch", d.strain);
+  console.warn("Error parsing mutations for branch", d.strain);
   return null;
 };
 
@@ -194,7 +194,7 @@ const getPanelStyling = (d, viewer) => {
   };
   if (pos.x < viewerState.viewerWidth * 0.6) {
     styles.container.left = pos.x + xOffset;
-  }else{
+  } else {
     styles.container.right = viewerState.viewerWidth - pos.x + xOffset;
   }
   if (pos.y < viewerState.viewerHeight * 0.55) {
@@ -207,11 +207,8 @@ const getPanelStyling = (d, viewer) => {
 
 const tipDisplayColorByInfo = (d, colorBy, distanceMeasure, temporalConfidence, mutType, colorScale) => {
   if (colorBy === "num_date") {
-    if (distanceMeasure === "num_date") {
-      return null;
-    } else {
-      return getBranchTimeJSX(d.n, temporalConfidence);
-    }
+    if (distanceMeasure === "num_date") return null;
+    return getBranchTimeJSX(d.n, temporalConfidence);
   }
   if (colorBy.slice(0, 2) === "gt") {
     const key = mutType === "nuc" ?
@@ -257,7 +254,7 @@ const HoverInfoPanel = ({tree, mutType, temporalConfidence, distanceMeasure,
     inner = (
       <g>
         {getBranchDescendents(d.n)}
-        {/*getFrequenciesJSX(d.n, mutType)*/}
+        {/* getFrequenciesJSX(d.n, mutType) */}
         {getMutationsJSX(d.n, mutType)}
         {distanceMeasure === "div" ? getBranchDivJSX(d.n) : getBranchTimeJSX(d.n, temporalConfidence)}
         {displayColorBy(d.n, distanceMeasure, temporalConfidence, colorByConfidence, colorBy)}
diff --git a/src/components/tree/phyloTree/phyloTree.js b/src/components/tree/phyloTree/phyloTree.js
index 4306a7dc1..2eddd8af6 100644
--- a/src/components/tree/phyloTree/phyloTree.js
+++ b/src/components/tree/phyloTree/phyloTree.js
@@ -1,7 +1,6 @@
 import _debounce from "lodash/debounce";
 import { max } from "d3-array";
 import { scaleLinear } from "d3-scale";
-import { flattenTree, appendParentsToTree } from "../treeHelpers";
 import { defaultParams } from "./defaultParams";
 import { addLeafCount, createChildrenAndParents } from "./helpers";
 
@@ -81,7 +80,7 @@ PhyloTree.prototype.updateDistance = generalUpdates.updateDistance;
 PhyloTree.prototype.updateLayout = generalUpdates.updateLayout;
 PhyloTree.prototype.updateGeometry = generalUpdates.updateGeometry;
 PhyloTree.prototype.updateGeometryFade = generalUpdates.updateGeometryFade;
-PhyloTree.prototype.updateTimeBar = (d) => {};
+PhyloTree.prototype.updateTimeBar = () => {};
 PhyloTree.prototype.updateMultipleArray = generalUpdates.updateMultipleArray;
 PhyloTree.prototype.updateStyleOrAttribute = generalUpdates.updateStyleOrAttribute;
 PhyloTree.prototype.updateStyleOrAttributeArray = generalUpdates.updateStyleOrAttributeArray;
diff --git a/src/components/tree/reactD3Interface/callbacks.js b/src/components/tree/reactD3Interface/callbacks.js
index 3a5146134..0fd076706 100644
--- a/src/components/tree/reactD3Interface/callbacks.js
+++ b/src/components/tree/reactD3Interface/callbacks.js
@@ -155,7 +155,7 @@ export const viewEntireTree = function viewEntireTree() {
   this.setState({selectedBranch: null, selectedTip: null});
 };
 
-export const handleIconClick = function handleIconClick(tool) {
+export const handleIconClickHOF = function handleIconClickHOF(tool) {
   return () => {
     const V = this.Viewer.getValue();
     if (tool === "zoom-in") {