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

Update filters to hide ancestral nodes before last common ancestor #1248

Closed
wants to merge 2 commits into from

Conversation

joverlee521
Copy link
Contributor

This PR closes #1240.

Before this commit, filters determined visibility by:

  1. Finding all terminal nodes in view window that match the filters and
    mark them as visible.
  2. Recursively marking all parent nodes of these terminal nodes visible,
    all the way up to the root of the tree.

This commit adds an additional step to hide ancestral nodes before
the last common ancestor of visible terminal nodes. Starting from
the root of the tree, recursively hide each node if it does not have
more than one child node that is visible.

@jameshadfield jameshadfield temporarily deployed to auspice-filter-branch-t-nxoihy December 19, 2020 00:50 Inactive
Before this commit, filters determined visibility by:
1. Finding all terminal nodes in view window that match the filters and
mark them as visible.
2. Recursively marking all parent nodes of these terminal nodes visible,
all the way up to the root of the tree.

This commit adds an additional step to hide ancestral nodes before
the last common ancestor of visible terminal nodes. Starting from
the root of the tree, recursively hide each node if it does not have
more than one child node that is visible.
@joverlee521 joverlee521 force-pushed the filter-branch-thickness branch from 4c94b30 to 2332920 Compare December 19, 2020 00:55
@jameshadfield jameshadfield temporarily deployed to auspice-filter-branch-t-nxoihy December 19, 2020 00:55 Inactive
@joverlee521
Copy link
Contributor Author

@jameshadfield What's the expected behavior if the filter selects a single sample?

The view of a single sample with this change doesn't look great to me:

Screen Shot 2020-12-18 at 4 58 22 PM

@emmahodcroft
Copy link
Member

Good catch Jover! You make a good point. Perhaps if just one node is selected we want to retain the old behaviour, just so it doesn't look so weird. Can we have it both ways, or would that be quite difficult? Also curious to hear if James has even a better idea here!

Copy link
Member

@jameshadfield jameshadfield left a comment

Choose a reason for hiding this comment

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

Thanks @joverlee521 -- this looks great.

The view of a single sample with this change doesn't look great to me:

I agree, especially as we have no way of showing a tip (circle) without its corresponding branch. However I don't think the solution is to make ancestral nodes thicker (as we did previously) as I think that's harder to interpret looking at the tree and results in transmission lines on the map which are really confusing. My preferred solution involves some form of zooming and is best implemented via #1132 or similar.


The only confusing aspect of the current implementation is that we hide nodes above the CA of the selected tips in the context of the current viewport, rather than the true CA. I tried to quickly modify the PR to show this but it got a bit complicated (patch attached below, but would need tidying up). Zooming into the branch shown in (A), the current PR (B) recomputes a CA relative to the two inView tips, whereas I argue it should be the "true" CA, which is offscreen and thus the branches should be as in (C). @trvrb @rneher what are your views here?

image

Patch:

diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js
index 4cf121d9..b78ae380 100644
--- a/src/actions/recomputeReduxState.js
+++ b/src/actions/recomputeReduxState.js
@@ -550,7 +550,8 @@ const modifyTreeStateVisAndBranchThickness = (oldState, zoomSelected, controlsSt
   const visAndThicknessData = calculateVisiblityAndBranchThickness(
     oldState,
     controlsState,
-    {dateMinNumeric: controlsState.dateMinNumeric, dateMaxNumeric: controlsState.dateMaxNumeric}
+    {dateMinNumeric: controlsState.dateMinNumeric, dateMaxNumeric: controlsState.dateMaxNumeric},
+    newIdxRoot
   );
 
   const newState = Object.assign({}, oldState, visAndThicknessData);
diff --git a/src/actions/tree.js b/src/actions/tree.js
index 22accbc0..e3cd1e82 100644
--- a/src/actions/tree.js
+++ b/src/actions/tree.js
@@ -65,7 +65,8 @@ export const updateVisibleTipsAndBranchThicknesses = (
     const data = calculateVisiblityAndBranchThickness(
       tree,
       controls,
-      {dateMinNumeric: controls.dateMinNumeric, dateMaxNumeric: controls.dateMaxNumeric}
+      {dateMinNumeric: controls.dateMinNumeric, dateMaxNumeric: controls.dateMaxNumeric},
+      rootIdxTree1
     );
     const dispatchObj = {
       type: types.UPDATE_VISIBILITY_AND_BRANCH_THICKNESS,
@@ -85,6 +86,7 @@ export const updateVisibleTipsAndBranchThicknesses = (
         treeToo,
         controls,
         {dateMinNumeric: controls.dateMinNumeric, dateMaxNumeric: controls.dateMaxNumeric},
+        rootIdxTree2
         // {tipSelectedIdx: tipIdx2}
       );
       dispatchObj.tangleTipLookup = constructVisibleTipLookupBetweenTrees(tree.nodes, treeToo.nodes, data.visibility, dataToo.visibility);
@@ -121,7 +123,7 @@ export const changeDateFilter = ({newMin = false, newMax = false, quickdraw = fa
       dateMinNumeric: newMin ? calendarToNumeric(newMin) : controls.dateMinNumeric,
       dateMaxNumeric: newMax ? calendarToNumeric(newMax) : controls.dateMaxNumeric
     };
-    const data = calculateVisiblityAndBranchThickness(tree, controls, dates);
+    const data = calculateVisiblityAndBranchThickness(tree, controls, dates, tree.idxOfInViewRootNode);
     const dispatchObj = {
       type: types.CHANGE_DATES_VISIBILITY_THICKNESS,
       quickdraw,
@@ -137,7 +139,7 @@ export const changeDateFilter = ({newMin = false, newMax = false, quickdraw = fa
       stateCountAttrs: Object.keys(controls.filters)
     };
     if (controls.showTreeToo) {
-      const dataToo = calculateVisiblityAndBranchThickness(treeToo, controls, dates);
+      const dataToo = calculateVisiblityAndBranchThickness(treeToo, controls, dates, treeToo.idxOfInViewRootNode);
       dispatchObj.tangleTipLookup = constructVisibleTipLookupBetweenTrees(tree.nodes, treeToo.nodes, data.visibility, dataToo.visibility);
       dispatchObj.visibilityToo = dataToo.visibility;
       dispatchObj.visibilityVersionToo = dataToo.visibilityVersion;
diff --git a/src/util/treeVisibilityHelpers.js b/src/util/treeVisibilityHelpers.js
index 6340982b..431b3319 100644
--- a/src/util/treeVisibilityHelpers.js
+++ b/src/util/treeVisibilityHelpers.js
@@ -133,7 +133,7 @@ FILTERS:
  - filters (in this code) is a list of filters to apply
    e.g. [{trait: "country", values: [...]}, ...]
 */
-export const calcVisibility = (tree, controls, dates) => {
+export const calcVisibility = (tree, controls, dates, idxOfInViewRootNode) => {
   if (tree.nodes) {
     /* inView represents nodes that are within the current view window (i.e. not off the screen) */
     let inView;
@@ -170,8 +170,21 @@ export const calcVisibility = (tree, controls, dates) => {
         makeParentVisible(filtered, tree.nodes[idxsOfFilteredTips[i]]);
       }
       /* Recursivley hide ancestor nodes that are not the last common
-       * ancestor of selected nodes, starting from the root of the tree */
-      hideNodeIfOnlyOneChildVisible(filtered, tree.nodes[0]);
+        * ancestor of selected nodes, starting from the root of the tree.
+        * This is done iff there are no tips which match the filters outside of the current viewport.
+        */
+      const treeIsZoomed = idxOfInViewRootNode !== 0;
+      let visibleNodeOutsideOfViewport = true;
+      for (const [idx, d] of tree.nodes.entries()) {
+        if (!d.hasChildren && !inView[idx] && filters.every((f) => f.values.includes(getTraitFromNode(d, f.trait)))) {
+          console.log("Exists a tip outside the current viewport which is part of the filtering selection");
+          visibleNodeOutsideOfViewport = true;
+          break;
+        }
+      }
+      if (!(treeIsZoomed && visibleNodeOutsideOfViewport)) {
+        hideNodeIfOnlyOneChildVisible(filtered, tree.nodes[0]);
+      }
     }
     /* intersect the various arrays contributing to visibility */
     const visibility = tree.nodes.map((node, idx) => {
@@ -202,8 +215,8 @@ export const calcVisibility = (tree, controls, dates) => {
   return NODE_VISIBLE;
 };
 
-export const calculateVisiblityAndBranchThickness = (tree, controls, dates) => {
-  const visibility = calcVisibility(tree, controls, dates);
+export const calculateVisiblityAndBranchThickness = (tree, controls, dates, idxOfInViewRootNode) => {
+  const visibility = calcVisibility(tree, controls, dates, idxOfInViewRootNode);
   /* recalculate tipCounts over the tree - modifies redux tree nodes in place (yeah, I know) */
   calcTipCounts(tree.nodes[0], visibility);
   /* re-calculate branchThickness (inline) */

/* Recursively hide nodes that do not have more than one child node in
* the param visArray.
* Relies on visArray having been updated by `makeParentVisible` */
const hideNodeIfOnlyOneChildVisible = (visArray, node) => {
Copy link
Member

Choose a reason for hiding this comment

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

Can we rename this hideNodesAboveCommonVisibleAncestor or similar? This function allows nodes with only one visible child if they are below (closer to the tips than) the CA node.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed the function name in the latest commit

return; // Terminal node without children
}
const visibleChildren = node.children.filter((child) => visArray[child.arrayIdx]);
if (visibleChildren.length > 1) {
Copy link
Member

Choose a reason for hiding this comment

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

(Beyond the scope of this PR, but this may be a good place to set some property / state defining the CA of the filtered tips, which will be needed for #1132, although dates will also have to be taken into account for that feature.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I can try to tackle #1132 next in a separate PR.

Copy link
Member

Choose a reason for hiding this comment

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

That would be awesome. #1132 I think would add a whole bunch to usability.

@trvrb
Copy link
Member

trvrb commented Dec 21, 2020

Awesome work with this @joverlee521! I think the resulting UI is little finicky when we have situations like: https://auspice-filter-branch-t-nxoihy.herokuapp.com/zika?f_country=Angola, where it becomes more difficult to zoom into the clade of interest. With the live behavior you effectively can click multiple times on the parental lineage to get this zoom accomplished. That said, I think the right UI to aim for is to combine this PR with #1132 (zoom to selected button). This should make it easy to filter down and see what you're getting and then zoom-to-selected to fill the viewport.

@jameshadfield: Thanks for thinking this through. This is a tough one, but I think I actually prefer B above. I'd like to keep a firm semantic concept on what we considered as "filtered". Currently, if we zoom to a clade we consider to have filtered to this clade and remove other tips from the map etc... If we've filtered to say just USVI (https://auspice-filter-branch-t-nxoihy.herokuapp.com/zika?f_country=Usvi) and then zoom to the upper clade, I'd consider only these 19 tips to be selected. I would apply consistent logic and then just highlight the TMRCA of these tips. If you leave the thick ancestral branch, I wouldn't be sure if the other tips were selected, but just "off screen" and I wouldn't know what to expect if I click a "zoom to selected" button. Or if I downloaded the tree I'd expect to get the off-screen branches as well.

Also, I know it would be quite a can of worms, but fixing the t-bar issue would generally make these filtered trees much more attractive. But this is a very separate issue.

@jameshadfield
Copy link
Member

Thanks @trvrb -- Agree that #1132 becomes even higher priority with the improvements from this PR. (And I've focused on those T-bars for nearly 3 years now! #508)

@joverlee521 -- given trevor's comments above the algorithm you've implemented is 💯 and all that's needed is a slight function rename.

@rneher
Copy link
Member

rneher commented Dec 21, 2020

looks good to me. In your solution B, would it make sense to include the branch leading to the MRCA of the selection?

Renamed to `hideNodesAboveVisibleCommonAncestor` to be clearer purpose
of the function.
@jameshadfield jameshadfield temporarily deployed to auspice-filter-branch-t-nxoihy December 21, 2020 19:11 Inactive
@joverlee521 joverlee521 mentioned this pull request Dec 29, 2020
@joverlee521
Copy link
Contributor Author

Closing in favor of #1257

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ancestral branch thicknesses (below CA) should be minimal
5 participants