From 864ed124ed976e98304795cecc907c669cb2e54d Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Tue, 8 Oct 2024 13:27:29 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20Remove=20non-null=20assertions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handle these cases better. --- src/actions/tree.ts | 17 ++++++---- src/components/tree/phyloTree/change.ts | 6 +++- src/components/tree/phyloTree/layouts.ts | 23 +++++++++---- src/components/tree/phyloTree/regression.ts | 15 +++++++-- src/components/tree/phyloTree/renderers.ts | 33 ++++++++++++------- .../tree/reactD3Interface/initialRender.ts | 4 +-- src/components/tree/tree.tsx | 25 ++++++++++---- 7 files changed, 85 insertions(+), 38 deletions(-) diff --git a/src/actions/tree.ts b/src/actions/tree.ts index bec8fce90..7044fe542 100644 --- a/src/actions/tree.ts +++ b/src/actions/tree.ts @@ -25,18 +25,21 @@ import { PhyloNode } from "../components/tree/phyloTree/types"; */ export const applyInViewNodesToTree = (idx: number | undefined, tree: TreeState) => { const validIdxRoot = idx !== undefined ? idx : tree.idxOfInViewRootNode; - if (tree.nodes[0]!.shell) { + + if (!tree.nodes[0] || !tree.nodes[validIdxRoot]) return; + + if (tree.nodes[0].shell) { tree.nodes.forEach((d) => { d.shell.inView = false; d.shell.update = true; }); - if (tree.nodes[validIdxRoot]!.hasChildren) { - applyToChildren(tree.nodes[validIdxRoot]!.shell, (d: PhyloNode) => {d.inView = true;}); - } else if (tree.nodes[validIdxRoot]!.parent.arrayIdx===0) { + if (tree.nodes[validIdxRoot].hasChildren) { + applyToChildren(tree.nodes[validIdxRoot].shell, (d: PhyloNode) => {d.inView = true;}); + } else if (tree.nodes[validIdxRoot].parent.arrayIdx===0) { // subtree with n=1 tips => don't make the parent in-view as this will cover the entire tree! - tree.nodes[validIdxRoot]!.shell.inView = true; + tree.nodes[validIdxRoot].shell.inView = true; } else { - applyToChildren(tree.nodes[validIdxRoot]!.parent.shell, (d: PhyloNode) => {d.inView = true;}); + applyToChildren(tree.nodes[validIdxRoot].parent.shell, (d: PhyloNode) => {d.inView = true;}); } } else { /* FYI applyInViewNodesToTree is now setting inView on the redux nodes */ @@ -50,7 +53,7 @@ export const applyInViewNodesToTree = (idx: number | undefined, tree: TreeState) for (const child of node.children) _markChildrenInView(child); } }; - const startingNode = tree.nodes[validIdxRoot]!.hasChildren ? tree.nodes[validIdxRoot] : tree.nodes[validIdxRoot]!.parent; + const startingNode = tree.nodes[validIdxRoot].hasChildren ? tree.nodes[validIdxRoot] : tree.nodes[validIdxRoot].parent; _markChildrenInView(startingNode); } diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index e3d20cb18..305e161f0 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -331,6 +331,9 @@ export const change = function change(this: PhyloTree, params: ChangeParams) { scatterVariables = undefined }: ChangeParams = params; + // Return if render hasn't happened yet + if (!this.timeLastRenderRequested) return; + // 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 */ @@ -341,7 +344,8 @@ export const change = function change(this: PhyloTree, params: ChangeParams) { /* calculate dt */ const idealTransitionTime = 500; let transitionTime = idealTransitionTime; - if ((Date.now() - this.timeLastRenderRequested!) < idealTransitionTime * 2) { + + if ((Date.now() - this.timeLastRenderRequested) < idealTransitionTime * 2) { transitionTime = 0; } diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index 61767ddd3..b5530fd09 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -143,13 +143,24 @@ export const scatterplotLayout = function scatterplotLayout(this: PhyloTree) { * Utility function for the unrooted tree layout. See `unrootedLayout` for details. */ const unrootedPlaceSubtree = (node: PhyloNode, totalLeafWeight: number) => { - const branchLength = node.depth! - node.pDepth!; - node.x = node.px! + branchLength * Math.cos(node.tau! + node.w! * 0.5); - node.y = node.py! + branchLength * Math.sin(node.tau! + node.w! * 0.5); - let eta = node.tau!; // eta is the cumulative angle for the wedges in the layout + // FIXME: consider an UnrootedPhyloNode that guarantees presence of these? will need some casting... + if (node.depth === undefined || + node.pDepth === undefined || + node.px === undefined || + node.py === undefined || + node.tau === undefined || + node.w === undefined || + node.n.children === undefined + ) { + return; + } + const branchLength = node.depth - node.pDepth; + node.x = node.px + branchLength * Math.cos(node.tau + node.w * 0.5); + node.y = node.py + 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.n.hasChildren) { - for (let i = 0; i < node.n.children!.length; i++) { - const ch = node.n.children![i]!.shell; + for (const child of node.n.children) { + const ch = child.shell; ch.w = 2 * Math.PI * leafWeight(ch.n) / totalLeafWeight; ch.tau = eta; eta += ch.w; diff --git a/src/components/tree/phyloTree/regression.ts b/src/components/tree/phyloTree/regression.ts index 1345209c6..f20e169f3 100644 --- a/src/components/tree/phyloTree/regression.ts +++ b/src/components/tree/phyloTree/regression.ts @@ -16,12 +16,15 @@ export interface Regression { * The regression is forced to pass through nodes[0]. */ function calculateRegressionThroughRoot(nodes: PhyloNode[]): Regression { + if (!nodes[0]) { + throw new Error("`nodes` must contain at least one entry to calculate the regression through the root."); + } 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 offset = nodes[0].x; const XY = sum( terminalNodes.map((d) => (d.y) * (d.x - offset)) ) / nTips; @@ -66,11 +69,17 @@ export function calculateRegression(this: PhyloTree) { } export function makeRegressionText(regression: Regression, layout: string, yScale: any): string { + if (regression.intercept === undefined || + regression.slope === undefined || + regression.r2 === undefined) { + return ""; + } + if (layout==="clock") { if (guessAreMutationsPerSite(yScale)) { - return `rate estimate: ${regression.slope!.toExponential(2)} subs per site per year`; + return `rate estimate: ${regression.slope.toExponential(2)} subs per site per year`; } return `rate estimate: ${formatDivergence(regression.slope)} subs per year`; } - return `intercept = ${regression.intercept!.toPrecision(3)}, slope = ${regression.slope!.toPrecision(3)}, R^2 = ${regression.r2!.toPrecision(3)}`; + return `intercept = ${regression.intercept.toPrecision(3)}, slope = ${regression.slope.toPrecision(3)}, R^2 = ${regression.r2.toPrecision(3)}`; } diff --git a/src/components/tree/phyloTree/renderers.ts b/src/components/tree/phyloTree/renderers.ts index a06b0f612..ceec8e7b2 100644 --- a/src/components/tree/phyloTree/renderers.ts +++ b/src/components/tree/phyloTree/renderers.ts @@ -157,7 +157,9 @@ export const drawTips = function drawTips(this: PhyloTree) { if (!("tips" in this.groups)) { this.groups.tips = this.svg.append("g").attr("id", "tips").attr("clip-path", "url(#treeClip)"); } - this.groups.tips! + if (!this.groups.tips) throw "tips should be set at this point!" + + this.groups.tips .selectAll(".tip") .data(this.nodes.filter((d) => !d.n.hasChildren)) .enter() @@ -230,10 +232,12 @@ export const drawBranches = function drawBranches(this: PhyloTree) { if (!("branchTee" in this.groups)) { this.groups.branchTee = this.svg.append("g").attr("id", "branchTee").attr("clip-path", "url(#treeClip)"); } + if (!this.groups.branchTee) throw "branchTee should be set at this point!" + if (this.layout === "clock" || this.layout === "scatter" || this.layout === "unrooted") { - this.groups.branchTee!.selectAll("*").remove(); + this.groups.branchTee.selectAll("*").remove(); } else { - this.groups.branchTee! + this.groups.branchTee .selectAll('.branch') .data(this.nodes.filter((d) => d.n.hasChildren && d.displayOrder !== undefined)) .enter() @@ -265,7 +269,9 @@ export const drawBranches = function drawBranches(this: PhyloTree) { if (!("branchStem" in this.groups)) { this.groups.branchStem = this.svg.append("g").attr("id", "branchStem").attr("clip-path", "url(#treeClip)"); } - this.groups.branchStem! + if (!this.groups.branchStem) throw "branchStem should be set at this point!" + + this.groups.branchStem .selectAll('.branch') .data(this.nodes.filter((d) => d.displayOrder !== undefined)) .enter() @@ -296,12 +302,14 @@ export const drawBranches = function drawBranches(this: PhyloTree) { */ export const drawRegression = function drawRegression(this: PhyloTree) { /* check we have computed a sensible regression before attempting to draw */ - if (this.regression!.slope===undefined) { + if (this.regression === undefined || + this.regression.intercept === undefined || + 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); + 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(); @@ -309,8 +317,9 @@ export const drawRegression = function drawRegression(this: PhyloTree) { if (!("regression" in this.groups)) { this.groups.regression = this.svg.append("g").attr("id", "regression").attr("clip-path", "url(#treeClip)"); } + if (!this.groups.regression) throw "regression should be set at this point!" - this.groups.regression! + this.groups.regression .append("path") .attr("d", path) .attr("class", "regression") @@ -321,9 +330,9 @@ export const drawRegression = function drawRegression(this: PhyloTree) { /* Compute & draw regression text. Note that the text hasn't been created until now, as we need to wait until rendering time when the scales have been calculated */ - this.groups.regression! + this.groups.regression .append("text") - .text(makeRegressionText(this.regression!, this.layout, this.yScale)) + .text(makeRegressionText(this.regression, this.layout, this.yScale)) .attr("class", "regression") .attr("x", this.xScale.range()[1] / 2 - 75) .attr("y", this.yScale.range()[0] + 50) @@ -413,8 +422,8 @@ const handleBranchHoverColor = (d: PhyloNode, c1: string, c2: string) => { }; export const branchStrokeForLeave = function branchStrokeForLeave(d: PhyloNode) { - if (!d) { return; } - handleBranchHoverColor(d, d.n.parent.shell.branchStroke!, d.branchStroke!); + if (!d || !d.n.parent.shell.branchStroke || !d.branchStroke) { return; } + handleBranchHoverColor(d, d.n.parent.shell.branchStroke, d.branchStroke); }; export const branchStrokeForHover = function branchStrokeForHover(d: PhyloNode) { diff --git a/src/components/tree/reactD3Interface/initialRender.ts b/src/components/tree/reactD3Interface/initialRender.ts index 69e20c194..9e866840e 100644 --- a/src/components/tree/reactD3Interface/initialRender.ts +++ b/src/components/tree/reactD3Interface/initialRender.ts @@ -14,7 +14,7 @@ export const renderTree = ( ) => { const ref = main ? that.domRefs.mainTree : that.domRefs.secondTree; const treeState = main ? props.tree : props.treeToo; - if (!treeState.loaded) { + if (!treeState.loaded || ref === undefined) { console.warn("can't run renderTree (not loaded)"); return; } @@ -26,7 +26,7 @@ export const renderTree = ( const tipStrokeColors = calculateStrokeColors(treeState, false, props.colorByConfidence, props.colorBy); phylotree.render({ - svg: select(ref!), + svg: select(ref), layout: props.layout, distance: props.distanceMeasure, focus: props.focus, diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index 268fc35b5..ea9eb96b1 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -91,8 +91,8 @@ export interface TreeComponentPropsFromState { // FIXME: is this Partial? export interface TreeComponentState { hoveredNode: PhyloNode | null - tree: PhyloTree | null - treeToo: PhyloTree | null + tree?: PhyloTree + treeToo?: PhyloTree geneSortFn?: any selectedNode?: {} } @@ -117,8 +117,8 @@ export class TreeComponent extends React.Component = {}; newState.tree = new PhyloTreeConstructor(this.props.tree.nodes, lhsTreeId, this.props.tree.idxOfInViewRootNode); - renderTree(this, true, newState.tree!, this.props); + if (newState.tree === undefined) { + return; + } + renderTree(this, true, newState.tree, this.props); if (this.props.showTreeToo) { this.setUpAndRenderTreeToo(this.props, newState as TreeComponentState); /* modifies newState in place */ } @@ -163,6 +170,10 @@ export class TreeComponent extends React.Component = {}; let rightTreeUpdated = false; @@ -170,13 +181,13 @@ export class TreeComponent extends React.Component remove the 2nd tree */ - newState.treeToo = null; + newState.treeToo = undefined; } else { /* turned on -> render the 2nd tree */ if (this.state.treeToo) { /* tree has been swapped -> remove the old tree */ this.state.treeToo.clearSVG();