|
1 | 1 | /* eslint-disable no-param-reassign */
|
2 | 2 | import { max } from "d3-array";
|
3 | 3 | import {getTraitFromNode, getDivFromNode, getBranchMutations} from "../../../util/treeMiscHelpers";
|
| 4 | +import { NODE_VISIBLE } from "../../../util/globals"; |
| 5 | +import { timerStart, timerEnd } from "../../../util/perf"; |
4 | 6 |
|
5 | 7 | /** get a string to be used as the DOM element ID
|
6 | 8 | * Note that this cannot have any "special" characters
|
@@ -33,18 +35,22 @@ export const applyToChildren = (phyloNode, func) => {
|
33 | 35 | * of nodes in a rectangular tree.
|
34 | 36 | * If `yCounter` is undefined then we wish to hide the node and all descendants of it
|
35 | 37 | * @param {PhyloNode} node
|
| 38 | + * @param {function} incrementer |
36 | 39 | * @param {int|undefined} yCounter
|
37 | 40 | * @sideeffect modifies node.displayOrder and node.displayOrderRange
|
38 | 41 | * @returns {int|undefined} current yCounter after assignment to the tree originating from `node`
|
39 | 42 | */
|
40 |
| -export const setDisplayOrderRecursively = (node, yCounter) => { |
| 43 | +export const setDisplayOrderRecursively = (node, incrementer, yCounter) => { |
41 | 44 | const children = node.n.children; // (redux) tree node
|
42 | 45 | if (children && children.length) {
|
43 | 46 | for (let i = children.length - 1; i >= 0; i--) {
|
44 |
| - yCounter = setDisplayOrderRecursively(children[i].shell, yCounter); |
| 47 | + yCounter = setDisplayOrderRecursively(children[i].shell, incrementer, yCounter); |
45 | 48 | }
|
46 | 49 | } else {
|
47 |
| - node.displayOrder = (node.n.fullTipCount===0 || yCounter===undefined) ? yCounter : ++yCounter; |
| 50 | + if (node.n.fullTipCount !== 0 && yCounter !== undefined) { |
| 51 | + yCounter += incrementer(node); |
| 52 | + } |
| 53 | + node.displayOrder = yCounter; |
48 | 54 | node.displayOrderRange = [yCounter, yCounter];
|
49 | 55 | return yCounter;
|
50 | 56 | }
|
@@ -77,26 +83,63 @@ function _getSpaceBetweenSubtrees(numSubtrees, numTips) {
|
77 | 83 | * PhyloTree can subsequently use this information. Accessed by prototypes
|
78 | 84 | * rectangularLayout, radialLayout, createChildrenAndParents
|
79 | 85 | * side effects: <phyloNode>.displayOrder (i.e. in the redux node) and <phyloNode>.displayOrderRange
|
80 |
| - * @param {Array<PhyloNode>} nodes |
| 86 | + * @param {Object} props |
| 87 | + * @param {Array<PhyloNode>} props.nodes |
| 88 | + * @param {boolean} props.focus |
81 | 89 | * @returns {undefined}
|
82 | 90 | */
|
83 |
| -export const setDisplayOrder = (nodes) => { |
| 91 | +export const setDisplayOrder = ({nodes, focus}) => { |
| 92 | + timerStart("setDisplayOrder"); |
| 93 | + |
84 | 94 | const numSubtrees = nodes[0].n.children.filter((n) => n.fullTipCount!==0).length;
|
85 |
| - const numTips = nodes[0].n.fullTipCount; |
| 95 | + const numTips = focus ? nodes[0].n.tipCount : nodes[0].n.fullTipCount; |
86 | 96 | const spaceBetweenSubtrees = _getSpaceBetweenSubtrees(numSubtrees, numTips);
|
| 97 | + |
| 98 | + // No focus: 1 unit per node |
| 99 | + let incrementer = (_node) => 1; |
| 100 | + |
| 101 | + if (focus) { |
| 102 | + const nVisible = nodes[0].n.tipCount; |
| 103 | + const nTotal = nodes[0].n.fullTipCount; |
| 104 | + |
| 105 | + let yProportionFocused = 0.8; |
| 106 | + // Adjust for a small number of visible tips (n<4) |
| 107 | + yProportionFocused = Math.min(yProportionFocused, nVisible / 5); |
| 108 | + // Adjust for a large number of visible tips (>80% of all tips) |
| 109 | + yProportionFocused = Math.max(yProportionFocused, nVisible / nTotal); |
| 110 | + |
| 111 | + const yPerFocused = (yProportionFocused * nTotal) / nVisible; |
| 112 | + const yPerUnfocused = ((1 - yProportionFocused) * nTotal) / (nTotal - nVisible); |
| 113 | + |
| 114 | + incrementer = (() => { |
| 115 | + let previousWasVisible = false; |
| 116 | + return (node) => { |
| 117 | + // Focus if the current node is visible or if the previous node was visible (for symmetric padding) |
| 118 | + const y = (node.visibility === NODE_VISIBLE || previousWasVisible) ? yPerFocused : yPerUnfocused; |
| 119 | + |
| 120 | + // Update for the next node |
| 121 | + previousWasVisible = node.visibility === NODE_VISIBLE; |
| 122 | + |
| 123 | + return y; |
| 124 | + } |
| 125 | + })(); |
| 126 | + } |
| 127 | + |
87 | 128 | let yCounter = 0;
|
88 | 129 | /* iterate through each subtree, and add padding between each */
|
89 | 130 | for (const subtree of nodes[0].n.children) {
|
90 | 131 | if (subtree.fullTipCount===0) { // don't use screen space for this subtree
|
91 |
| - setDisplayOrderRecursively(nodes[subtree.arrayIdx], undefined); |
| 132 | + setDisplayOrderRecursively(nodes[subtree.arrayIdx], incrementer, undefined); |
92 | 133 | } else {
|
93 |
| - yCounter = setDisplayOrderRecursively(nodes[subtree.arrayIdx], yCounter); |
| 134 | + yCounter = setDisplayOrderRecursively(nodes[subtree.arrayIdx], incrementer, yCounter); |
94 | 135 | yCounter+=spaceBetweenSubtrees;
|
95 | 136 | }
|
96 | 137 | }
|
97 | 138 | /* note that nodes[0] is a dummy node holding each subtree */
|
98 | 139 | nodes[0].displayOrder = undefined;
|
99 | 140 | nodes[0].displayOrderRange = [undefined, undefined];
|
| 141 | + |
| 142 | + timerEnd("setDisplayOrder"); |
100 | 143 | };
|
101 | 144 |
|
102 | 145 |
|
|
0 commit comments