diff --git a/src/components/controls/search.js b/src/components/controls/search.js index 112b839e3..a485edc41 100644 --- a/src/components/controls/search.js +++ b/src/components/controls/search.js @@ -3,6 +3,7 @@ import { connect } from "react-redux"; import Awesomplete from 'awesomplete'; /* https://leaverou.github.io/awesomplete/ */ import { updateVisibleTipsAndBranchThicknesses, updateTipRadii } from "../../actions/tree"; import { dataFont, darkGrey } from "../../globalStyles"; +import { NODE_VISIBLE } from "../../util/globals"; import { SelectLabel } from "../framework/select-label"; import "../../css/awesomplete.css"; @@ -81,7 +82,7 @@ class SearchStrains extends React.Component { /* this tells the serch box which strains are visible and therefore are eligible to be searched */ this.state.awesomplete.list = this.props.nodes - .filter((n) => !n.hasChildren && this.props.visibility[n.arrayIdx] === "visible") + .filter((n) => !n.hasChildren && this.props.visibility[n.arrayIdx] === NODE_VISIBLE) .map((n) => n.strain); this.state.awesomplete.evaluate(); } diff --git a/src/components/info/info.js b/src/components/info/info.js index 4eb69b49f..08dda04b6 100644 --- a/src/components/info/info.js +++ b/src/components/info/info.js @@ -4,7 +4,7 @@ import Card from "../framework/card"; import { titleFont, headerFont, medGrey, darkGrey } from "../../globalStyles"; import { applyFilter, changeDateFilter, updateVisibleTipsAndBranchThicknesses } from "../../actions/tree"; import { prettyString } from "../../util/stringHelpers"; -import { months } from "../../util/globals"; +import { months, NODE_VISIBLE } from "../../util/globals"; import { displayFilterValueAsButton } from "../framework/footer"; const plurals = { @@ -32,7 +32,7 @@ const styliseDateRange = (dateStr) => { const getNumSelectedTips = (nodes, visibility) => { let count = 0; nodes.forEach((d, idx) => { - if (!d.hasChildren && visibility[idx] === "visible") count += 1; + if (!d.hasChildren && visibility[idx] === NODE_VISIBLE) count += 1; }); return count; }; diff --git a/src/components/map/mapHelpersLatLong.js b/src/components/map/mapHelpersLatLong.js index f3f755ca0..193a93c34 100644 --- a/src/components/map/mapHelpersLatLong.js +++ b/src/components/map/mapHelpersLatLong.js @@ -4,6 +4,8 @@ import _minBy from "lodash/minBy"; import { interpolateNumber } from "d3-interpolate"; import { averageColors } from "../../util/colorHelpers"; import { computeMidpoint, Bezier } from "./transmissionBezier"; +import { NODE_NOT_VISIBLE } from "../../util/globals"; + /* global L */ // L is global in scope and placed by leaflet() @@ -61,7 +63,7 @@ const setupDemeData = (nodes, visibility, geoResolution, nodeColors, triplicate, // second pass to fill vectors nodes.forEach((n, i) => { /* demes only count terminal nodes */ - if (!n.children && visibility[i] === "visible") { + if (!n.children && visibility[i] !== NODE_NOT_VISIBLE) { // if tip and visible, push if (n.attr[geoResolution]) { // check for undefined demeMap[n.attr[geoResolution]].push(nodeColors[i]); @@ -202,7 +204,7 @@ const maybeConstructTransmissionEvent = ( originNumDate: node.attr["num_date"], destinationNumDate: child.attr["num_date"], color: nodeColors[node.arrayIdx], - visible: visibility[child.arrayIdx] === "visible" ? "visible" : "hidden" // transmission visible if child is visible + visible: visibility[child.arrayIdx] !== NODE_NOT_VISIBLE ? "visible" : "hidden" // transmission visible if child is visible }; } return transmission; @@ -370,7 +372,7 @@ const updateDemeDataColAndVis = (demeData, demeIndices, nodes, visibility, geoRe // second pass to fill vectors nodes.forEach((n, i) => { /* demes only count terminal nodes */ - if (!n.children && visibility[i] === "visible") { + if (!n.children && visibility[i] !== NODE_NOT_VISIBLE) { // if tip and visible, push if (n.attr[geoResolution]) { // check for undefined demeMap[n.attr[geoResolution]].push(nodeColors[i]); @@ -403,7 +405,7 @@ const updateTransmissionDataColAndVis = (transmissionData, transmissionIndices, // this is a transmission event from n to child const id = node.arrayIdx.toString() + "-" + child.arrayIdx.toString(); const col = nodeColors[node.arrayIdx]; - const vis = visibility[child.arrayIdx] === "visible" ? "visible" : "hidden"; // transmission visible if child is visible + const vis = visibility[child.arrayIdx] !== NODE_NOT_VISIBLE ? "visible" : "hidden"; // transmission visible if child is visible // update transmissionData via index lookup try { diff --git a/src/components/tree/phyloTree/change.js b/src/components/tree/phyloTree/change.js index 6d2e46e58..2ba43ac14 100644 --- a/src/components/tree/phyloTree/change.js +++ b/src/components/tree/phyloTree/change.js @@ -2,6 +2,7 @@ import { timerFlush } from "d3-timer"; import { calcConfidenceWidth } from "./confidence"; import { applyToChildren } from "./helpers"; import { timerStart, timerEnd } from "../../../util/perf"; +import { NODE_VISIBLE } from "../../../util/globals"; /* loop through the nodes and update each provided prop with the new value * additionally, set d.update -> whether or not the node props changed @@ -48,7 +49,7 @@ const svgSetters = { ".tip": { fill: (d) => d.fill, stroke: (d) => d.tipStroke, - visibility: (d) => d["visibility"] + visibility: (d) => d.visibility === NODE_VISIBLE ? "visible" : "hidden" }, ".conf": { stroke: (d) => d.branchStroke, @@ -57,7 +58,7 @@ const svgSetters = { ".branch": { stroke: (d) => d.branchStroke, "stroke-width": (d) => d["stroke-width"] + "px", // style - as per drawBranches() - cursor: (d) => d.visibility === "visible" ? "pointer" : "default" + cursor: (d) => d.visibility === NODE_VISIBLE ? "pointer" : "default" } } }; diff --git a/src/components/tree/phyloTree/renderers.js b/src/components/tree/phyloTree/renderers.js index 813189a8c..06b4b758a 100644 --- a/src/components/tree/phyloTree/renderers.js +++ b/src/components/tree/phyloTree/renderers.js @@ -1,4 +1,5 @@ import { timerStart, timerEnd } from "../../../util/perf"; +import { NODE_VISIBLE } from "../../../util/globals"; /** * @param {d3 selection} svg -- the svg into which the tree is drawn @@ -6,8 +7,8 @@ import { timerStart, timerEnd } from "../../../util/perf"; * @param {string} distance -- the property used as branch length, e.g. div or num_date * @param {object} parameters -- an object that contains options that will be added to this.params * @param {object} callbacks -- an object with call back function defining mouse behavior - * @param {array} branchThickness -- array of branch thicknesses (same shape as tree nodes) - * @param {array} visibility -- array of "visible" or "hidden" (same shape as tree nodes) + * @param {array} branchThickness -- array of branch thicknesses (same ordering as tree nodes) + * @param {array} visibility -- array of visibility of nodes(same ordering as tree nodes) * @param {bool} drawConfidence -- should confidence intervals be drawn? * @param {bool} vaccines -- should vaccine crosses (and dotted lines if applicable) be drawn? * @param {array} branchStroke -- branch stroke colour for each node (set onto each node) @@ -110,7 +111,7 @@ export const drawTips = function drawTips() { .on("mouseout", this.callbacks.onTipLeave) .on("click", this.callbacks.onTipClick) .style("pointer-events", "auto") - .style("visibility", (d) => d["visibility"]) + .style("visibility", (d) => d.visibility === NODE_VISIBLE ? "visible" : "hidden") .style("fill", (d) => d.fill || params.tipFill) .style("stroke", (d) => d.tipStroke || params.tipStroke) .style("stroke-width", () => params.tipStrokeWidth) /* don't want branch thicknesses applied */ @@ -166,7 +167,7 @@ export const drawBranches = function drawBranches() { .style("stroke-linecap", "round") .style("stroke-width", (d) => d['stroke-width']+"px" || params.branchStrokeWidth) .style("fill", "none") - .style("cursor", (d) => d.visibility === "visible" ? "pointer" : "default") + .style("cursor", (d) => d.visibility === NODE_VISIBLE ? "pointer" : "default") .style("pointer-events", "auto") .on("mouseover", this.callbacks.onBranchHover) .on("mouseout", this.callbacks.onBranchLeave) diff --git a/src/components/tree/reactD3Interface/callbacks.js b/src/components/tree/reactD3Interface/callbacks.js index 7e5e8c594..59ff89324 100644 --- a/src/components/tree/reactD3Interface/callbacks.js +++ b/src/components/tree/reactD3Interface/callbacks.js @@ -2,11 +2,12 @@ import { rgb } from "d3-color"; import { interpolateRgb } from "d3-interpolate"; import { updateVisibleTipsAndBranchThicknesses} from "../../../actions/tree"; import { branchOpacityFunction } from "../../../util/colorHelpers"; +import { NODE_VISIBLE } from "../../../util/globals"; /* Callbacks used by the tips / branches when hovered / selected */ export const onTipHover = function onTipHover(d) { - if (d.visibility !== "visible") return; + if (d.visibility !== NODE_VISIBLE) return; const phylotree = d.that.params.orientation[0] === 1 ? this.state.tree : this.state.treeToo; @@ -18,7 +19,7 @@ export const onTipHover = function onTipHover(d) { }; export const onTipClick = function onTipClick(d) { - if (d.visibility !== "visible") return; + if (d.visibility !== NODE_VISIBLE) return; if (this.props.narrativeMode) return; // console.log("tip click", d) this.setState({ @@ -34,7 +35,7 @@ export const onTipClick = function onTipClick(d) { export const onBranchHover = function onBranchHover(d) { - if (d.visibility !== "visible") return; + if (d.visibility !== NODE_VISIBLE) return; /* emphasize the color of the branch */ for (const id of ["#branch_S_" + d.n.clade, "#branch_T_" + d.n.clade]) { if (this.props.colorByConfidence) { @@ -67,7 +68,7 @@ export const onBranchHover = function onBranchHover(d) { }; export const onBranchClick = function onBranchClick(d) { - if (d.visibility !== "visible") return; + if (d.visibility !== NODE_VISIBLE) return; if (this.props.narrativeMode) return; const root = [undefined, undefined]; if (d.that.params.orientation[0] === 1) root[0] = d.n.arrayIdx; diff --git a/src/util/entropy.js b/src/util/entropy.js index ed42e9b4d..af251f8b8 100644 --- a/src/util/entropy.js +++ b/src/util/entropy.js @@ -1,4 +1,4 @@ -import { genotypeColors } from "./globals"; +import { genotypeColors, NODE_VISIBLE } from "./globals"; const intersectGenes = function intersectGenes(geneMap, pos) { for (const gene of Object.keys(geneMap)) { @@ -15,7 +15,7 @@ const calcMutationCounts = (nodes, visibility, geneMap, isAA) => { Object.keys(geneMap).forEach((n) => {sparse[n] = {};}); } nodes.forEach((n) => { - if (visibility[n.arrayIdx] !== "visible") {return;} + if (visibility[n.arrayIdx] !== NODE_VISIBLE) {return;} if (isAA) { if (n.aa_muts) { for (const prot in n.aa_muts) { // eslint-disable-line @@ -126,7 +126,7 @@ const calcEntropy = (nodes, visibility, geneMap, isAA) => { }); recurse(child, newState); } - } else if (visibility[node.arrayIdx] === "visible") { + } else if (visibility[node.arrayIdx] === NODE_VISIBLE) { visibleTips++; for (const prot of arrayOfProts) { for (const pos of Object.keys(state[prot])) { @@ -193,7 +193,7 @@ const calcEntropy = (nodes, visibility, geneMap, isAA) => { /** * traverse the tree and compile the entropy data for the visibile branches * @param {Array} nodes - list of nodes -* @param {Array} visibility - 1-1 correspondence with nodes. Value: "visibile" or "" +* @param {Array} visibility - 1-1 correspondence with nodes. * @param {String} mutType - amino acid | nucleotide mutations - "aa" | "nuc" * @param {obj} geneMap used to NT fill colours. This should be imroved. * @param {bool} showCounts show counts or entropy values? diff --git a/src/util/globals.js b/src/util/globals.js index a61821d3a..ddec0aa24 100644 --- a/src/util/globals.js +++ b/src/util/globals.js @@ -174,3 +174,7 @@ export const months = { export const normalNavBarHeight = 50; export const narrativeNavBarHeight = 55; + +export const NODE_NOT_VISIBLE = 0; +export const NODE_VISIBLE_TO_MAP_ONLY = 1; +export const NODE_VISIBLE = 2; diff --git a/src/util/processFrequencies.js b/src/util/processFrequencies.js index aed4b4173..cd7e65866 100644 --- a/src/util/processFrequencies.js +++ b/src/util/processFrequencies.js @@ -1,3 +1,4 @@ +import { NODE_VISIBLE } from "./globals"; export const unassigned_label = "unassigned"; @@ -42,7 +43,7 @@ export const computeMatrixFromRawData = (data, pivots, nodes, visibility, colorS // let debugTipsSeen = 0; const debugPivotTotals = new Array(pivotsLen).fill(0); data.forEach((d) => { - if (visibility[d.idx] === "visible") { + if (visibility[d.idx] === NODE_VISIBLE) { // debugTipsSeen++; // const colour = tree.nodes[d.idx].attr[colorBy]; const category = assignCategory(colorScale, categories, nodes[d.idx], colorBy, isGenotype) || unassigned_label; diff --git a/src/util/treeCountingHelpers.js b/src/util/treeCountingHelpers.js index a47a6f2bd..db83acc48 100644 --- a/src/util/treeCountingHelpers.js +++ b/src/util/treeCountingHelpers.js @@ -1,3 +1,5 @@ +import { NODE_VISIBLE } from "./globals"; + /** * traverse the tree to get state counts for supplied traits * @param {Array} nodes - list of nodes @@ -22,7 +24,7 @@ export const countTraitsAcrossTree = (nodes, traits, visibility, terminalOnly) = return; } - if (visibility && visibility[node.arrayIdx] !== "visible") { + if (visibility && visibility[node.arrayIdx] !== NODE_VISIBLE) { return; } @@ -45,6 +47,6 @@ export const calcTipCounts = (node, visibility) => { node.tipCount += node.children[i].tipCount; } } else { - node.tipCount = visibility[node.arrayIdx] === "visible" ? 1 : 0; + node.tipCount = visibility[node.arrayIdx] === NODE_VISIBLE ? 1 : 0; } }; diff --git a/src/util/treeTangleHelpers.js b/src/util/treeTangleHelpers.js index b45a146c9..89f188acd 100644 --- a/src/util/treeTangleHelpers.js +++ b/src/util/treeTangleHelpers.js @@ -1,3 +1,4 @@ +import { NODE_VISIBLE } from "./globals"; export const constructVisibleTipLookupBetweenTrees = (nodesLeft, nodesRight, visibilityLeft, visibilityRight) => { const tree2StrainToIdxMap = {}; @@ -13,8 +14,8 @@ export const constructVisibleTipLookupBetweenTrees = (nodesLeft, nodesRight, vis if ( !nodesLeft[i].hasChildren && rightIdx && - visibilityLeft[i] === "visible" && - visibilityRight[rightIdx] === "visible" + visibilityLeft[i] === NODE_VISIBLE && + visibilityRight[rightIdx] === NODE_VISIBLE ) { lookup.push([i, tree2StrainToIdxMap[nodesLeft[i].strain]]); } diff --git a/src/util/treeVisibilityHelpers.js b/src/util/treeVisibilityHelpers.js index 357a27b5d..64f914f89 100644 --- a/src/util/treeVisibilityHelpers.js +++ b/src/util/treeVisibilityHelpers.js @@ -1,4 +1,4 @@ -import { freqScale } from "./globals"; +import { freqScale, NODE_NOT_VISIBLE, NODE_VISIBLE_TO_MAP_ONLY, NODE_VISIBLE } from "./globals"; import { calcTipCounts } from "./treeCountingHelpers"; export const strainNameToIdx = (nodes, name) => { @@ -28,7 +28,7 @@ const calcBranchThickness = (nodes, visibility, rootIdx) => { maxTipCount = 1; } return nodes.map((d, idx) => ( - visibility[idx] === "visible" ? freqScale((d.tipCount + 5) / (maxTipCount + 5)) : 0.5 + visibility[idx] === 2 ? freqScale((d.tipCount + 5) / (maxTipCount + 5)) : 0.5 )); }; @@ -46,13 +46,13 @@ const makeParentVisible = (visArray, node) => { * Create a visibility array to show the path through the tree to the selected tip * @param {array} nodes redux tree nodes * @param {int} tipIdx idx of the selected tip - * @return {array} visibility array (values of "visible" | "hidden") + * @return {array} visibility array (values in {0, 1, 2}) */ const identifyPathToTip = (nodes, tipIdx) => { const visibility = new Array(nodes.length).fill(false); visibility[tipIdx] = true; makeParentVisible(visibility, nodes[tipIdx]); /* recursive */ - return visibility.map((cv) => cv ? "visible" : "hidden"); + return visibility.map((cv) => cv ? 2 : 0); }; @@ -64,12 +64,15 @@ controls.filters use dates NOT controls.dateMin & controls.dateMax RETURNS: -visibility: array of "visible" or "hidden" +visibility: array of integers in {0, 1, 2} + - 0: not displayed by map. Potentially displayed by tree as a thin branch. + - 1: available for display by the map. Displayed by tree as a thin branch. + - 2: Displayed by both the map and the tree. ROUGH DESCRIPTION OF HOW FILTERING IS APPLIED: + - inView filtering (reflects tree zooming): Nodes which are not inView always have visibility=0 - time filtering is simple - all nodes (internal + terminal) not within (tmin, tmax) are excluded. - - inView filtering is similar - nodes out of the view cannot possibly be visible - - filters are a bit more tricky - the visibile tips are calculated, and the parent +- filters are a bit more tricky - the visibile tips are calculated, and the parent branches back to the MRCA are considered visibile. This is then intersected with the time & inView visibile stuff @@ -82,34 +85,19 @@ FILTERS: */ const calcVisibility = (tree, controls, dates) => { if (tree.nodes) { - /* reset visibility */ - let visibility = tree.nodes.map(() => true); - - // if we have an analysis slider active, then we must filter on that as well - // note that min date for analyis doesnt apply - // commented out as analysis slider will probably be removed soon! - // if (controls.analysisSlider && controls.analysisSlider.valid) { - // /* extra slider is numerical rounded to 2dp */ - // const valid = tree.nodes.map((d) => - // d.attr[controls.analysisSlider.key] ? Math.round(d.attr[controls.analysisSlider.key] * 100) / 100 <= controls.analysisSlider.value : true - // ); - // visibility = visibility.map((cv, idx) => (cv && valid[idx])); - // } - - // IN VIEW FILTERING (internal + terminal nodes) - /* edge case: this fn may be called before the shell structure of the nodes - has been created (i.e. phyloTree's not run yet). In this case, it's - safe to assume that everything's in view */ + /* inView represents nodes that are within the current view window (i.e. not off the screen) */ let inView; try { inView = tree.nodes.map((d) => d.shell.inView); } catch (e) { + /* edge case: this fn may be called before the shell structure of the nodes + has been created (i.e. phyloTree's not run yet). In this case, it's + safe to assume that everything's in view */ inView = tree.nodes.map(() => true); } - /* intersect visibility and inView */ - visibility = visibility.map((cv, idx) => (cv && inView[idx])); // FILTERS + let filtered; const filterPairs = []; Object.keys(controls.filters).forEach((key) => { if (controls.filters[key].length) { @@ -118,8 +106,8 @@ const calcVisibility = (tree, controls, dates) => { }); if (filterPairs.length) { /* find the terminal nodes that were (a) already visibile and (b) match the filters */ - const filtered = tree.nodes.map((d, idx) => ( - !d.hasChildren && visibility[idx] && filterPairs.every((x) => x[1].indexOf(d.attr[x[0]]) > -1) + filtered = tree.nodes.map((d, idx) => ( + !d.hasChildren && inView[idx] && filterPairs.every((x) => x[1].indexOf(d.attr[x[0]]) > -1) )); const idxsOfFilteredTips = filtered.reduce((a, e, i) => { if (e) {a.push(i);} @@ -129,23 +117,29 @@ const calcVisibility = (tree, controls, dates) => { for (let i = 0; i < idxsOfFilteredTips.length; i++) { makeParentVisible(filtered, tree.nodes[idxsOfFilteredTips[i]]); } - /* intersect visibility and filtered */ - visibility = visibility.map((cv, idx) => (cv && filtered[idx])); } - // TIME FILTERING (internal + terminal nodes) - const timeFiltered = tree.nodes.map((d) => { - return !(d.attr.num_date < dates.dateMinNumeric || d.parent.attr.num_date > dates.dateMaxNumeric); + /* intersect the various arrays contributing to visibility */ + const visibility = tree.nodes.map((node, idx) => { + if (inView[idx] && (filtered ? filtered[idx] : true)) { + const nodeDate = node.attr.num_date; + /* is the actual node date (the "end" of the branch) in the time slice? */ + if (nodeDate >= dates.dateMinNumeric && nodeDate <= dates.dateMaxNumeric) { + return NODE_VISIBLE; + } + /* is any part of the (parent date -> node date) in the time slice? */ + if (!(nodeDate < dates.dateMinNumeric || node.parent.attr.num_date > dates.dateMaxNumeric)) { + return NODE_VISIBLE_TO_MAP_ONLY; + } + } + return NODE_NOT_VISIBLE; }); - visibility = visibility.map((cv, idx) => (cv && timeFiltered[idx])); - - /* return array of "visible" or "hidden" values */ - return visibility.map((cv) => cv ? "visible" : "hidden"); + return visibility; } - return "visible"; + console.error("calcVisibility ran without tree.nodes"); + return NODE_VISIBLE; }; - export const calculateVisiblityAndBranchThickness = (tree, controls, dates, {idxOfInViewRootNode = 0, tipSelectedIdx = 0} = {}) => { const visibility = tipSelectedIdx ? identifyPathToTip(tree.nodes, tipSelectedIdx) : calcVisibility(tree, controls, dates); /* recalculate tipCounts over the tree - modifies redux tree nodes in place (yeah, I know) */