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") {