Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1008 split tree and group into subtrees by state #1105

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
4ee5552
#1008: adding a toggle for splitting the tree by state (not yet imple…
frogsquire Apr 24, 2020
5c4005e
#1008: sending data through react from UI control
frogsquire Apr 26, 2020
3f38f81
#1008: passing all the way into change; avoid null/undefined conflict
frogsquire Apr 27, 2020
c983388
#1008: data now available to tree; calculating trait offset TBD
frogsquire Apr 27, 2020
5db49f9
#1008: offest calculation - WIP
frogsquire Apr 27, 2020
bb4ff70
#1008: now trying recursive calculation (WIP); redraw tree (also WIP)
frogsquire May 3, 2020
2c2d57a
#1008: now fully toggling back and forth between split-by-trait and r…
frogsquire May 3, 2020
e30fb80
#1008: now correctly grouping by trait; not yet sorting groups or hid…
frogsquire May 3, 2020
8b847ea
#1008: now sorting by num_date between groups of trait-sharing subtrees
frogsquire May 3, 2020
59e7629
#1008: place oldest subtrees at top
frogsquire May 4, 2020
2870aa3
#1008: hiding t-branches when split by state
frogsquire May 5, 2020
01dff42
#1008: don't convert num_date to date (not needed); traverse all chil…
frogsquire May 14, 2020
7362dee
Revert "#1008: hiding t-branches when split by state"
frogsquire May 16, 2020
c220beb
#1008: correctly draw branches of subtrees so they connect
frogsquire May 16, 2020
cb00571
#1008: totally hide tips and branches which have no relevant trait da…
frogsquire May 17, 2020
55576fd
#1008: removing completed todo comment, unused parameter
frogsquire May 17, 2020
741430a
#1008: fix issue where nodes with children, but no qualfiied children…
frogsquire May 17, 2020
0181e5b
#1008: resolving linter issues
frogsquire May 18, 2020
dd46eca
#1008: adding a toggle for splitting the tree by state (not yet imple…
frogsquire Apr 24, 2020
71f8f7f
#1008: sending data through react from UI control
frogsquire Apr 26, 2020
00a5c9b
#1008: passing all the way into change; avoid null/undefined conflict
frogsquire Apr 27, 2020
e6dc367
#1008: data now available to tree; calculating trait offset TBD
frogsquire Apr 27, 2020
f685295
#1008: offest calculation - WIP
frogsquire Apr 27, 2020
7aba909
#1008: now trying recursive calculation (WIP); redraw tree (also WIP)
frogsquire May 3, 2020
2b19894
#1008: now fully toggling back and forth between split-by-trait and r…
frogsquire May 3, 2020
4d099ef
#1008: now correctly grouping by trait; not yet sorting groups or hid…
frogsquire May 3, 2020
ed3fc76
#1008: now sorting by num_date between groups of trait-sharing subtrees
frogsquire May 3, 2020
9cb34cb
#1008: place oldest subtrees at top
frogsquire May 4, 2020
b8219b2
#1008: hiding t-branches when split by state
frogsquire May 5, 2020
c0931f6
#1008: don't convert num_date to date (not needed); traverse all chil…
frogsquire May 14, 2020
dfdaf13
Revert "#1008: hiding t-branches when split by state"
frogsquire May 16, 2020
4ca0ce8
#1008: correctly draw branches of subtrees so they connect
frogsquire May 16, 2020
2fb9849
#1008: totally hide tips and branches which have no relevant trait da…
frogsquire May 17, 2020
fffadd7
#1008: removing completed todo comment, unused parameter
frogsquire May 17, 2020
e04b689
#1008: fix issue where nodes with children, but no qualfiied children…
frogsquire May 17, 2020
d009829
#1008: resolving linter issues
frogsquire May 18, 2020
bbbb57a
Merge branch '1008-split-by-state' of https://github.com/frogsquire/a…
frogsquire May 24, 2020
61065fd
#1008: when the color-by trait is changed, re-split the tree
frogsquire May 24, 2020
b59f161
#1008: when layout changes from rectangular, unsplit the tree and dis…
frogsquire May 25, 2020
5f86e0b
#1008: running the linter
frogsquire May 25, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ export const CHANGE_ZOOM = "CHANGE_ZOOM";
export const SET_AVAILABLE = "SET_AVAILABLE";
export const TOGGLE_SIDEBAR = "TOGGLE_SIDEBAR";
export const TOGGLE_LEGEND = "TOGGLE_LEGEND";
export const TOGGLE_SPLIT_TREE = "TOGGLE_SPLIT_TREE";
36 changes: 36 additions & 0 deletions src/components/controls/choose-tree-split.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from "react";
import Toggle from "./toggle";
import { connect } from "react-redux";
import { withTranslation } from "react-i18next";
import { TOGGLE_SPLIT_TREE } from "../../actions/types";
// todo: connect if needed

@connect((state) => {
return {
colorBy: state.controls.colorBy,
splitTreeByTrait: state.tree.splitTreeByTrait
};
})
/* Implements a button which splits the tree into visual trees per strain. */
class ChooseTreeSplit extends React.Component {
render() {
const { t } = this.props;
return (
<div style={{margin: 5}}>
<Toggle
display={true}
on={this.props.splitTreeByTrait !== null}
callback={() =>
this.props.dispatch({type: TOGGLE_SPLIT_TREE,
// the presence of splitByTrait means it should be toggled off
splitTreeByTrait: this.props.splitTreeByTrait !== null ? null : this.props.colorBy
})}
label={t("sidebar:Split tree by colored-by trait")}
/>
</div>
);
}
}

const WithTranslation = withTranslation()(ChooseTreeSplit);
export default WithTranslation;
2 changes: 2 additions & 0 deletions src/components/controls/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ChooseLayout from "./choose-layout";
import ChooseDataset from "./choose-dataset";
import ChooseSecondTree from "./choose-second-tree";
import ChooseMetric from "./choose-metric";
import ChooseTreeSplit from "./choose-tree-split";
import PanelLayout from "./panel-layout";
import GeoResolution from "./geo-resolution";
import MapAnimationControls from "./map-animation";
Expand Down Expand Up @@ -36,6 +37,7 @@ function Controls({mapOn}) {
<SidebarHeader>{t("sidebar:Tree Options")}</SidebarHeader>
<ChooseLayout/>
<ChooseMetric/>
<ChooseTreeSplit/>
<ChooseBranchLabelling/>
<SearchStrains/>
<ChooseSecondTree/>
Expand Down
37 changes: 30 additions & 7 deletions src/components/tree/phyloTree/change.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { timerFlush } from "d3-timer";
import { calcConfidenceWidth } from "./confidence";
import { applyToChildren } from "./helpers";
import { applyToChildren, setSplitTreeYValues, setYValues } from "./helpers";
import { timerStart, timerEnd } from "../../../util/perf";
import { NODE_VISIBLE } from "../../../util/globals";
import { getBranchVisibility, strokeForBranch } from "./renderers";
Expand Down Expand Up @@ -275,14 +275,18 @@ export const change = function change({
fill = undefined,
visibility = undefined,
tipRadii = undefined,
branchThickness = undefined
branchThickness = undefined,
resetTreeYValues = false,
splitTreeByTrait = null
}) {
// console.log("\n** phylotree.change() (time since last run:", Date.now() - this.timeLastRenderRequested, "ms) **\n\n");
timerStart("phylotree.change()");
const elemsToUpdate = new Set(); /* what needs updating? E.g. ".branch", ".tip" etc */
const nodePropsToModify = {}; /* which properties (keys) on the nodes should be updated (before the SVG) */
const svgPropsToUpdate = new Set(); /* which SVG properties shall be changed. E.g. "fill", "stroke" */
const useModifySVGInStages = newLayout; /* use modifySVGInStages rather than modifySVG. Not used often. */

/* use modifySVGInStages rather than modifySVG. Not used often. */
const useModifySVGInStages = newLayout || splitTreeByTrait !== null || resetTreeYValues;

/* calculate dt */
const idealTransitionTime = 500;
Expand Down Expand Up @@ -318,7 +322,7 @@ export const change = function change({
svgPropsToUpdate.add("stroke-width");
nodePropsToModify["stroke-width"] = branchThickness;
}
if (newDistance || newLayout || updateLayout || zoomIntoClade || svgHasChangedDimensions) {
if (newDistance || newLayout || updateLayout || zoomIntoClade || svgHasChangedDimensions || splitTreeByTrait) {
elemsToUpdate.add(".tip").add(".branch.S").add(".branch.T").add(".branch");
elemsToUpdate.add(".vaccineCross").add(".vaccineDottedLine").add(".conf");
elemsToUpdate.add('.branchLabel').add('.tipLabel');
Expand Down Expand Up @@ -349,14 +353,31 @@ export const change = function change({
this.nodes.forEach((d) => {d.update = true;});
}

/* if the tree is being split by colored-by trait, update y values */
// todo: this is fairly slow: show something to show recalculating? or, get the d3 transition working?
if (splitTreeByTrait) {
setSplitTreeYValues(this.nodes, splitTreeByTrait);
this.params.isTreeSplitByTrait = true;
}
else if (resetTreeYValues)
{
setYValues(this.nodes);
this.params.isTreeSplitByTrait = false;
}

/* run calculations as needed - these update properties on the phylotreeNodes (similar to updateNodesWithNewData) */
/* distance */
if (newDistance) this.setDistance(newDistance);

/* layout (must run after distance) */
if (newDistance || newLayout || updateLayout) this.setLayout(newLayout || this.layout);
/* also used to split by traits */
if (newDistance || newLayout || updateLayout || splitTreeByTrait || resetTreeYValues)
this.setLayout(newLayout || this.layout);

/* show confidences - set this param which actually adds the svg paths for
confidence intervals when mapToScreen() gets called below */
confidence intervals when mapToScreen() gets called below */
if (showConfidences) this.params.confidence = true;

/* mapToScreen */
if (
svgPropsToUpdate.has(["stroke-width"]) ||
Expand All @@ -365,7 +386,9 @@ export const change = function change({
updateLayout ||
zoomIntoClade ||
svgHasChangedDimensions ||
showConfidences
showConfidences ||
splitTreeByTrait ||
resetTreeYValues
) {
this.mapToScreen();
}
Expand Down
67 changes: 67 additions & 0 deletions src/components/tree/phyloTree/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,73 @@ export const setYValuesRecursively = (node, yCounter) => {
*/
export const setYValues = (nodes) => setYValuesRecursively(nodes[0], 0);

// sorts all child nodes from a subtree into their subtrees
const collectSubtrees = (startNode, subtree, trait, currentTraitValue, subtreeStack) => {
if (!startNode.children) return;

for (let i = startNode.children.length-1; i >= 0; i--) {
let currentNode = startNode.children[i];
let thisNodeTraitValue = getTraitFromNode(currentNode.n, trait);
// todo: nodes with no trait value? should they be grouped together?
frogsquire marked this conversation as resolved.
Show resolved Hide resolved
if (thisNodeTraitValue !== currentTraitValue) { // doesn't belong on this subtree
let matchingSubtree = subtreeStack.find(s => s.traitValue === thisNodeTraitValue);
if (!matchingSubtree) {
subtreeStack.push({ subtreeNodes: [currentNode], traitValue: thisNodeTraitValue });
matchingSubtree = subtreeStack[subtreeStack.length-1];
}
else {
matchingSubtree.subtreeNodes.push(currentNode);
}
collectSubtrees(currentNode, matchingSubtree, trait, thisNodeTraitValue, subtreeStack);
}
else
{
// add this node to the subtree list, and continue
subtree.subtreeNodes.push(currentNode);
collectSubtrees(currentNode, subtree, trait, currentTraitValue, subtreeStack);
}
}
}

/**
* setSplitTreeYValues - works similarly to setYValues above,
* but splits the tree by the given trait, grouping nodes with the
* same trait value together
*
* todo: is 0 guaranteed to be root node?
frogsquire marked this conversation as resolved.
Show resolved Hide resolved
*/
export const setSplitTreeYValues = (nodes, trait) => {
const subtreeStack = [{subtreeNodes: [nodes[0]], traitValue: getTraitFromNode(nodes[0].n, trait)}];

// collect all the subtrees for a given trait, and group them together
collectSubtrees(nodes[0], subtreeStack[0], trait, subtreeStack[0].traitValue, subtreeStack);
subtreeStack.sort((a, b) => {
if (a.traitValue < b.traitValue) return -1;
if (a.traitValue > b.traitValue) return 1;
return 0;
});

// todo: sort the subtrees by num_date
subtreeStack.sort((a, b) => {
if (a.traitValue == b.traitValue)
return 0;

let aDate = new Date(getTraitFromNode(a.subtreeNodes[0].n, "num_date"));
frogsquire marked this conversation as resolved.
Show resolved Hide resolved
let bDate = new Date(getTraitFromNode(b.subtreeNodes[0].n, "num_date"));
if (aDate < bDate) return -1;
if (bDate > aDate) return 1;
return 0;
});

// set the y-values in each subtree
// such that oldest subtrees are at the top
let currentMaxY = 0;
while (subtreeStack.length) {
const nextSubtree = subtreeStack.shift();
nextSubtree.subtreeNodes.forEach(node => node.n.yvalue = currentMaxY++)
frogsquire marked this conversation as resolved.
Show resolved Hide resolved
}
}


export const formatDivergence = (divergence) => {
return divergence > 1 ?
Expand Down
3 changes: 1 addition & 2 deletions src/components/tree/phyloTree/layouts.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@ export const setLayout = function setLayout(layout) {
timerEnd("setLayout");
};


/**
* assignes x,y coordinates for a rectancular layout
* assigns x,y coordinates for a rectangular layout
* @return {null}
*/
export const rectangularLayout = function rectangularLayout() {
Expand Down
2 changes: 1 addition & 1 deletion src/components/tree/phyloTree/renderers.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export const drawBranches = function drawBranches() {
if (!("branchTee" in this.groups)) {
this.groups.branchTee = this.svg.append("g").attr("id", "branchTee");
}
if (this.layout === "clock" || this.layout === "unrooted") {
if (this.layout === "clock" || this.layout === "unrooted" || params.isTreeSplitByTrait) {
frogsquire marked this conversation as resolved.
Show resolved Hide resolved
this.groups.branchTee.selectAll("*").remove();
} else {
this.groups.branchTee
Expand Down
12 changes: 12 additions & 0 deletions src/components/tree/reactD3Interface/change.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps,
args.svgHasChangedDimensions = true;
}

/* split tree by colored-by trait */
/* just copies into tree; will be null/empty string/not defined if no trait */
if (newProps.tree.splitTreeByTrait !== oldProps.tree.splitTreeByTrait)
{
args.splitTreeByTrait = newProps.tree.splitTreeByTrait;
if (args.splitTreeByTrait == null) {// if the split is being unset, reset the layout
// todo: could use updateLayout instead?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's revisit this, and exactly how we're using d3 to update the DOM (e.g. transitions) once the algorithm is working correctly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update on this - the d3 transition is working reasonably well for turning the toggle off. For turning it on, it still looks a bit jerky.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jameshadfield, if you feel we are at this point, do you have any thoughts on why the transition is still not smooth when enabling the toggle (though it is smooth at all other times)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into this shortly, as I do think we're at this point 😄

args.newLayout = newProps.layout;
args.resetTreeYValues = true;
}
}

const change = Object.keys(args).length;
if (change) {
args.animationInProgress = newProps.animationPlayPauseButton === "Pause";
Expand Down
3 changes: 2 additions & 1 deletion src/reducers/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ export const getDefaultControlsState = () => {
sidebarOpen: initialSidebarState.sidebarOpen,
treeLegendOpen: undefined,
mapLegendOpen: undefined,
showOnlyPanels: false
showOnlyPanels: false,
splitTreeByTrait: undefined
};
};

Expand Down
10 changes: 9 additions & 1 deletion src/reducers/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export const getDefaultTreeState = () => {
totalStateCounts: {},
availableBranchLabels: [],
selectedStrain: undefined,
selectedClade: undefined
selectedClade: undefined,
splitTreeByTrait: null
};
};

Expand Down Expand Up @@ -75,6 +76,13 @@ const Tree = (state = getDefaultTreeState(), action) => {
}
});
return state;
case types.TOGGLE_SPLIT_TREE:
// todo: what if nothing is colored-by? can that happen?
// note that there's no reason technically not to split by a state that isn't the colored-by
// state, it just doesn't make as much UI sense
return Object.assign({}, state, {
splitTreeByTrait: action.splitTreeByTrait
});
default:
return state;
}
Expand Down