From 679828820389d00a5ba6d87236fb30972d33489d Mon Sep 17 00:00:00 2001 From: James Hadfield Date: Wed, 16 Mar 2022 12:45:09 +1300 Subject: [PATCH 1/2] [phylotree] redraw regression on visibility change This commit updates the PhyloTree logic to call the recalculate & redraw functions each time the visibility changes. Note that the regression calculation does not yet take account of the visible nodes so there should be no functionality changes in this commit. --- src/components/tree/phyloTree/change.js | 5 +++++ src/components/tree/phyloTree/layouts.js | 7 +------ src/components/tree/phyloTree/phyloTree.js | 2 ++ src/components/tree/phyloTree/regression.js | 12 ++++++++++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/components/tree/phyloTree/change.js b/src/components/tree/phyloTree/change.js index 393cc9677..88929688a 100644 --- a/src/components/tree/phyloTree/change.js +++ b/src/components/tree/phyloTree/change.js @@ -333,6 +333,11 @@ export const change = function change({ if (changeColorBy) { this.updateColorBy(); } + // recalculate existing regression if needed + if (changeVisibility && this.regression) { + elemsToUpdate.add(".regression"); + this.calculateRegression(); // Note: must come after `updateNodesWithNewData()` + } /* some things need to update d.inView and/or d.update. This should be centralised */ /* TODO: list all functions which modify these */ if (zoomIntoClade) { /* must happen below updateNodesWithNewData */ diff --git a/src/components/tree/phyloTree/layouts.js b/src/components/tree/phyloTree/layouts.js index 9255662ed..87066f455 100644 --- a/src/components/tree/phyloTree/layouts.js +++ b/src/components/tree/phyloTree/layouts.js @@ -3,7 +3,6 @@ import { min, max } from "d3-array"; import scaleLinear from "d3-scale/src/linear"; import {point as scalePoint} from "d3-scale/src/band"; -import { calculateRegressionThroughRoot, calculateRegressionWithFreeIntercept } from "./regression"; import { timerStart, timerEnd } from "../../../util/perf"; import { getTraitFromNode, getDivFromNode } from "../../../util/treeMiscHelpers"; @@ -126,11 +125,7 @@ export const scatterplotLayout = function scatterplotLayout() { } if (this.scatterVariables.showRegression) { - if (this.layout==="clock") { - this.regression = calculateRegressionThroughRoot(this.nodes); - } else { - this.regression = calculateRegressionWithFreeIntercept(this.nodes); - } + this.calculateRegression(); // sets this.regression } }; diff --git a/src/components/tree/phyloTree/phyloTree.js b/src/components/tree/phyloTree/phyloTree.js index fd23860c2..9c1129c02 100644 --- a/src/components/tree/phyloTree/phyloTree.js +++ b/src/components/tree/phyloTree/phyloTree.js @@ -7,6 +7,7 @@ import * as layouts from "./layouts"; import * as grid from "./grid"; import * as confidence from "./confidence"; import * as labels from "./labels"; +import * as regression from "./regression"; /* phylogenetic tree drawing function - the actual tree is rendered by the render prototype */ const PhyloTree = function PhyloTree(reduxNodes, id, idxOfInViewRootNode) { @@ -68,6 +69,7 @@ PhyloTree.prototype.unrootedLayout = layouts.unrootedLayout; PhyloTree.prototype.radialLayout = layouts.radialLayout; PhyloTree.prototype.setScales = layouts.setScales; PhyloTree.prototype.mapToScreen = layouts.mapToScreen; +PhyloTree.prototype.calculateRegression = regression.calculateRegression; /* C O N F I D E N C E I N T E R V A L S */ PhyloTree.prototype.removeConfidence = confidence.removeConfidence; diff --git a/src/components/tree/phyloTree/regression.js b/src/components/tree/phyloTree/regression.js index 91e36d078..31e45e57f 100644 --- a/src/components/tree/phyloTree/regression.js +++ b/src/components/tree/phyloTree/regression.js @@ -8,7 +8,7 @@ import { formatDivergence, guessAreMutationsPerSite} from "./helpers"; * nodes[0]. * It does not consider which tips are inView / visible. */ -export function calculateRegressionThroughRoot(nodes) { +function calculateRegressionThroughRoot(nodes) { const terminalNodes = nodes.filter((d) => !d.n.hasChildren); const nTips = terminalNodes.length; const offset = nodes[0].x; @@ -29,7 +29,7 @@ export function calculateRegressionThroughRoot(nodes) { * set. These values must be numeric. * This function does not consider which tips are inView / visible. */ -export function calculateRegressionWithFreeIntercept(nodes) { +function calculateRegressionWithFreeIntercept(nodes) { const terminalNodesWithXY = nodes.filter((d) => (!d.n.hasChildren) && d.x!==undefined && d.y!==undefined); const nTips = terminalNodesWithXY.length; const meanX = sum(terminalNodesWithXY.map((d) => d.x))/nTips; @@ -42,6 +42,14 @@ export function calculateRegressionWithFreeIntercept(nodes) { return {slope, intercept, r2}; } +/** sets this.regression */ +export function calculateRegression() { + if (this.layout==="clock") { + this.regression = calculateRegressionThroughRoot(this.nodes); + } else { + this.regression = calculateRegressionWithFreeIntercept(this.nodes); + } +} export function makeRegressionText(regression, layout, yScale) { if (layout==="clock") { From 1663bb579200a83576a79e6d60acb89c5274e312 Mon Sep 17 00:00:00 2001 From: James Hadfield Date: Wed, 16 Mar 2022 13:13:54 +1300 Subject: [PATCH 2/2] [phylotree] regression considers visibility Changes the regression calculations to only consider visible nodes. Closes #1483 Note this has the (rare but strange) UI situation where we have zero visible tips, and thus the regression line isn't drawn, but the sidebar toggle still indicates that the regression is "on". --- src/components/tree/phyloTree/regression.js | 21 ++++++++++++++------- src/components/tree/phyloTree/renderers.js | 5 +++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/tree/phyloTree/regression.js b/src/components/tree/phyloTree/regression.js index 31e45e57f..effcc84d1 100644 --- a/src/components/tree/phyloTree/regression.js +++ b/src/components/tree/phyloTree/regression.js @@ -1,16 +1,19 @@ import { sum } from "d3-array"; import { formatDivergence, guessAreMutationsPerSite} from "./helpers"; +import { NODE_VISIBLE } from "../../../util/globals"; /** * this function calculates a regression between - * the x and y values of terminal nodes, passing through - * nodes[0]. - * It does not consider which tips are inView / visible. + * the x and y values of terminal nodes which are also visible. + * The regression is forced to pass through nodes[0]. */ function calculateRegressionThroughRoot(nodes) { - const terminalNodes = nodes.filter((d) => !d.n.hasChildren); + const terminalNodes = nodes.filter((d) => !d.n.hasChildren && d.visibility === NODE_VISIBLE); const nTips = terminalNodes.length; + if (nTips===0) { + return {slope: undefined, intercept: undefined, r2: undefined}; + } const offset = nodes[0].x; const XY = sum( terminalNodes.map((d) => (d.y) * (d.x - offset)) @@ -25,13 +28,17 @@ function calculateRegressionThroughRoot(nodes) { } /** - * Calculate regression through terminal nodes which have both x & y values + * Calculate regression through visible terminal nodes which have both x & y values * set. These values must be numeric. - * This function does not consider which tips are inView / visible. */ function calculateRegressionWithFreeIntercept(nodes) { - const terminalNodesWithXY = nodes.filter((d) => (!d.n.hasChildren) && d.x!==undefined && d.y!==undefined); + const terminalNodesWithXY = nodes.filter( + (d) => (!d.n.hasChildren) && d.x!==undefined && d.y!==undefined && d.visibility === NODE_VISIBLE + ); const nTips = terminalNodesWithXY.length; + if (nTips===0) { + return {slope: undefined, intercept: undefined, r2: undefined}; + } const meanX = sum(terminalNodesWithXY.map((d) => d.x))/nTips; const meanY = sum(terminalNodesWithXY.map((d) => d.y))/nTips; const slope = sum(terminalNodesWithXY.map((d) => (d.x-meanX)*(d.y-meanY))) / diff --git a/src/components/tree/phyloTree/renderers.js b/src/components/tree/phyloTree/renderers.js index 040538408..1e5e6f9de 100644 --- a/src/components/tree/phyloTree/renderers.js +++ b/src/components/tree/phyloTree/renderers.js @@ -241,6 +241,11 @@ export const drawBranches = function drawBranches() { * @return {null} */ export const drawRegression = function drawRegression() { + /* check we have computed a sensible regression before attempting to draw */ + if (this.regression.slope===undefined) { + return; + } + 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);