diff --git a/src/actions/colors.js b/src/actions/colors.js index 45b398dd9..a44294ea5 100644 --- a/src/actions/colors.js +++ b/src/actions/colors.js @@ -1,33 +1,9 @@ -import { parseEncodedGenotype } from "../util/getGenotype"; -import getColorScale from "../util/getColorScale"; -import { setGenotype } from "../util/setGenotype"; -import { calcNodeColor } from "../components/tree/treeHelpers"; -import { determineColorByGenotypeType } from "../util/colorHelpers"; +import { determineColorByGenotypeType, calcColorScaleAndNodeColors } from "../util/colorHelpers"; import { timerStart, timerEnd } from "../util/perf"; import { updateEntropyVisibility } from "./entropy"; import { updateFrequencyDataDebounced } from "./frequencies"; import * as types from "./types"; -export const calcColorScaleAndNodeColors = (colorBy, controls, tree, metadata) => { - let genotype; - if (colorBy.slice(0, 3) === "gt-" && controls.geneLength) { - genotype = parseEncodedGenotype(colorBy, controls.geneLength); - if (genotype.length > 1) { - console.warn("Cannot deal with multiple proteins yet - using first only."); - } - setGenotype(tree.nodes, genotype[0].prot || "nuc", genotype[0].positions); /* modifies nodes recursively */ - } - - /* step 1: calculate the required colour scale */ - const version = controls.colorScale === undefined ? 1 : controls.colorScale.version + 1; - const colorScale = getColorScale(colorBy, tree, controls.geneLength, metadata.colorOptions, version, controls.absoluteDateMaxNumeric); - if (genotype) colorScale.genotype = genotype; - - /* step 2: calculate the node colours */ - const nodeColors = calcNodeColor(tree, colorScale); - return {nodeColors, colorScale, version}; -}; - /* providedColorBy: undefined | string */ export const changeColorBy = (providedColorBy = undefined) => { // eslint-disable-line import/prefer-default-export return (dispatch, getState) => { diff --git a/src/actions/entropy.js b/src/actions/entropy.js index ffe01e14f..94cebfb48 100644 --- a/src/actions/entropy.js +++ b/src/actions/entropy.js @@ -1,5 +1,5 @@ import { debounce } from 'lodash'; -import { calcEntropyInView } from "../util/treeTraversals"; +import { calcEntropyInView } from "../util/entropy"; import * as types from "./types"; /* debounce works better than throttle, as it _won't_ update while events are still coming in (e.g. dragging the date slider) */ diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index a93499e21..ef50b3e53 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -1,15 +1,16 @@ import { numericToCalendar, calendarToNumeric } from "../util/dateHelpers"; import { reallySmallNumber, twoColumnBreakpoint } from "../util/globals"; import { calcBrowserDimensionsInitialState } from "../reducers/browserDimensions"; -import { strainNameToIdx, calcTipRadii, constructVisibleTipLookupBetweenTrees } from "../components/tree/treeHelpers"; +import { strainNameToIdx, calculateVisiblityAndBranchThickness } from "../util/treeVisibilityHelpers"; +import { constructVisibleTipLookupBetweenTrees } from "../util/treeTangleHelpers"; +import { calcTipRadii } from "../util/tipRadiusHelpers"; import { getDefaultControlsState } from "../reducers/controls"; -import { calculateVisiblityAndBranchThickness } from "./treeProperties"; -import { calcEntropyInView, getValuesAndCountsOfVisibleTraitsFromTree, - getAllValuesAndCountsOfTraitsFromTree } from "../util/treeTraversals"; -import { calcColorScaleAndNodeColors } from "./colors"; +import { getValuesAndCountsOfVisibleTraitsFromTree, + getAllValuesAndCountsOfTraitsFromTree } from "../util/treeCountingHelpers"; +import { calcEntropyInView } from "../util/entropy"; import { treeJsonToState } from "../util/treeJsonProcessing"; import { entropyCreateStateFromJsons } from "../util/entropyCreateStateFromJsons"; -import { determineColorByGenotypeType } from "../util/colorHelpers"; +import { determineColorByGenotypeType, calcColorScaleAndNodeColors } from "../util/colorHelpers"; export const checkColorByConfidence = (attrs, colorBy) => { return colorBy !== "num_date" && attrs.indexOf(colorBy + "_confidence") > -1; diff --git a/src/actions/treeProperties.js b/src/actions/tree.js similarity index 89% rename from src/actions/treeProperties.js rename to src/actions/tree.js index d9dd82907..1cf0ece97 100644 --- a/src/actions/treeProperties.js +++ b/src/actions/tree.js @@ -1,28 +1,11 @@ -import { calcVisibility, - calcTipRadii, - calcTipCounts, - identifyPathToTip, - strainNameToIdx, - calcBranchThickness } from "../components/tree/treeHelpers"; +import { calcTipRadii } from "../util/tipRadiusHelpers"; +import { strainNameToIdx, calculateVisiblityAndBranchThickness } from "../util/treeVisibilityHelpers"; import * as types from "./types"; import { updateEntropyVisibility } from "./entropy"; import { updateFrequencyDataDebounced } from "./frequencies"; import { calendarToNumeric } from "../util/dateHelpers"; import { applyToChildren } from "../components/tree/phyloTree/helpers"; -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) */ - calcTipCounts(tree.nodes[0], visibility); - /* re-calculate branchThickness (inline) */ - return { - visibility: visibility, - visibilityVersion: tree.visibilityVersion + 1, - branchThickness: calcBranchThickness(tree.nodes, visibility, idxOfInViewRootNode), - branchThicknessVersion: tree.branchThicknessVersion + 1 - }; -}; - /** * define the visible branches and their thicknesses. This could be a path to a single tip or a selected clade. * filtering etc will "turn off" branches, etc etc diff --git a/src/components/controls/analysis-date-slider.js b/src/components/controls/analysis-date-slider.js index 595517ae4..b5776beee 100644 --- a/src/components/controls/analysis-date-slider.js +++ b/src/components/controls/analysis-date-slider.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { connect } from "react-redux"; import Slider from "./slider"; import { controlsWidth } from "../../util/globals"; -import { changeAnalysisSliderValue } from "../../actions/treeProperties"; +import { changeAnalysisSliderValue } from "../../actions/tree"; import { dataFont, darkGrey } from "../../globalStyles"; @connect((state) => { diff --git a/src/components/controls/choose-metric.js b/src/components/controls/choose-metric.js index d1e88944e..cc37560ae 100644 --- a/src/components/controls/choose-metric.js +++ b/src/components/controls/choose-metric.js @@ -5,7 +5,7 @@ import { materialButton, materialButtonSelected } from "../../globalStyles"; import Toggle from "./toggle"; import { CHANGE_DISTANCE_MEASURE } from "../../actions/types"; import { analyticsControlsEvent } from "../../util/googleAnalytics"; -import { toggleTemporalConfidence } from "../../actions/treeProperties"; +import { toggleTemporalConfidence } from "../../actions/tree"; /* * implements a pair of buttons the toggle between timetree and divergence tree diff --git a/src/components/controls/date-range-inputs.js b/src/components/controls/date-range-inputs.js index 083e75ba4..0ebe68238 100644 --- a/src/components/controls/date-range-inputs.js +++ b/src/components/controls/date-range-inputs.js @@ -4,7 +4,7 @@ import { connect } from "react-redux"; import Slider from "./slider"; import { controlsWidth } from "../../util/globals"; import { numericToCalendar } from "../../util/dateHelpers"; -import { changeDateFilter } from "../../actions/treeProperties"; +import { changeDateFilter } from "../../actions/tree"; import { MAP_ANIMATION_PLAY_PAUSE_BUTTON } from "../../actions/types"; import { headerFont, lightGrey, darkGrey } from "../../globalStyles"; diff --git a/src/components/controls/search.js b/src/components/controls/search.js index bda83b4e1..305a52430 100644 --- a/src/components/controls/search.js +++ b/src/components/controls/search.js @@ -1,7 +1,7 @@ import React from "react"; import { connect } from "react-redux"; import Awesomplete from 'awesomplete'; /* https://leaverou.github.io/awesomplete/ */ -import { updateVisibleTipsAndBranchThicknesses, updateTipRadii } from "../../actions/treeProperties"; +import { updateVisibleTipsAndBranchThicknesses, updateTipRadii } from "../../actions/tree"; import { dataFont, darkGrey } from "../../globalStyles"; import "../../css/awesomplete.css"; diff --git a/src/components/framework/animationController.js b/src/components/framework/animationController.js index 0d720825b..bd918ba14 100644 --- a/src/components/framework/animationController.js +++ b/src/components/framework/animationController.js @@ -2,7 +2,7 @@ import React from "react"; import { connect } from "react-redux"; import { animationWindowWidth, animationTick } from "../../util/globals"; import { numericToCalendar } from "../../util/dateHelpers"; -import { changeDateFilter } from "../../actions/treeProperties"; +import { changeDateFilter } from "../../actions/tree"; import { MAP_ANIMATION_PLAY_PAUSE_BUTTON, MIDDLEWARE_ONLY_ANIMATION_STARTED } from "../../actions/types"; import { timerStart, timerEnd } from "../../util/perf"; diff --git a/src/components/framework/footer.js b/src/components/framework/footer.js index 4d4fd83e8..a5d3a500d 100644 --- a/src/components/framework/footer.js +++ b/src/components/framework/footer.js @@ -4,7 +4,7 @@ import { dataFont, medGrey, materialButton } from "../../globalStyles"; import { prettyString } from "../../util/stringHelpers"; import { TRIGGER_DOWNLOAD_MODAL } from "../../actions/types"; import Flex from "./flex"; -import { applyFilter } from "../../actions/treeProperties"; +import { applyFilter } from "../../actions/tree"; const dot = ( diff --git a/src/components/info/info.js b/src/components/info/info.js index da8e99400..fef743183 100644 --- a/src/components/info/info.js +++ b/src/components/info/info.js @@ -2,7 +2,7 @@ import React from "react"; import { connect } from "react-redux"; import Card from "../framework/card"; import { titleFont, headerFont, medGrey, darkGrey } from "../../globalStyles"; -import { applyFilter, changeDateFilter, updateVisibleTipsAndBranchThicknesses } from "../../actions/treeProperties"; +import { applyFilter, changeDateFilter, updateVisibleTipsAndBranchThicknesses } from "../../actions/tree"; import { prettyString } from "../../util/stringHelpers"; import { displayFilterValueAsButton } from "../framework/footer"; diff --git a/src/components/map/map.js b/src/components/map/map.js index 28be68fc4..435e7b4b1 100644 --- a/src/components/map/map.js +++ b/src/components/map/map.js @@ -13,7 +13,7 @@ import { updateDemeAndTransmissionDataColAndVis, updateDemeAndTransmissionDataLatLong } from "./mapHelpersLatLong"; -import { changeDateFilter } from "../../actions/treeProperties"; +import { changeDateFilter } from "../../actions/tree"; import { MAP_ANIMATION_PLAY_PAUSE_BUTTON } from "../../actions/types"; import { incommingMapPNG } from "../download/helperFunctions"; import { timerStart, timerEnd } from "../../util/perf"; diff --git a/src/components/tree/infoPanels/hover.js b/src/components/tree/infoPanels/hover.js index cd8dd692e..97646446c 100644 --- a/src/components/tree/infoPanels/hover.js +++ b/src/components/tree/infoPanels/hover.js @@ -2,7 +2,7 @@ import React from "react"; import { infoPanelStyles } from "../../../globalStyles"; import { prettyString } from "../../../util/stringHelpers"; import { numericToCalendar } from "../../../util/dateHelpers"; -import { getTipColorAttribute } from "../treeHelpers"; +import { getTipColorAttribute } from "../../../util/colorHelpers"; const infoLineJSX = (item, value) => ( diff --git a/src/components/tree/legend/item.js b/src/components/tree/legend/item.js index 15410c3e0..8856ddc9d 100644 --- a/src/components/tree/legend/item.js +++ b/src/components/tree/legend/item.js @@ -1,5 +1,5 @@ import React from "react"; -import { updateTipRadii } from "../../../actions/treeProperties"; +import { updateTipRadii } from "../../../actions/tree"; import { dataFont, darkGrey } from "../../../globalStyles"; import { prettyString } from "../../../util/stringHelpers"; diff --git a/src/components/tree/phyloTree/helpers.js b/src/components/tree/phyloTree/helpers.js index b607569de..9c501e3cd 100644 --- a/src/components/tree/phyloTree/helpers.js +++ b/src/components/tree/phyloTree/helpers.js @@ -36,12 +36,6 @@ export const applyToChildren = (node, func) => { }; -/* -* given nodes, create the shell property, which links the redux properties -* (theoretically immutable) with the phylotree properties (changeable) -*/ - - /* * given nodes, create the children and parent properties. * modifies the nodes argument in place diff --git a/src/components/tree/reactD3Interface/callbacks.js b/src/components/tree/reactD3Interface/callbacks.js index 23044ce92..039fd5393 100644 --- a/src/components/tree/reactD3Interface/callbacks.js +++ b/src/components/tree/reactD3Interface/callbacks.js @@ -1,8 +1,8 @@ import { rgb } from "d3-color"; import { interpolateRgb } from "d3-interpolate"; -import { updateVisibleTipsAndBranchThicknesses} from "../../../actions/treeProperties"; +import { updateVisibleTipsAndBranchThicknesses} from "../../../actions/tree"; import { mediumTransitionDuration } from "../../../util/globals"; -import { branchOpacityFunction } from "../treeHelpers"; +import { branchOpacityFunction } from "../../../util/colorHelpers"; /* Callbacks used by the tips / branches when hovered / selected */ diff --git a/src/components/tree/reactD3Interface/change.js b/src/components/tree/reactD3Interface/change.js index 3229a7d68..5903ad6b6 100644 --- a/src/components/tree/reactD3Interface/change.js +++ b/src/components/tree/reactD3Interface/change.js @@ -1,5 +1,5 @@ import { rgb } from "d3-color"; -import { calcBranchStrokeCols } from "../treeHelpers"; +import { calcBranchStrokeCols } from "../../../util/colorHelpers"; export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, viewer, oldProps, newProps) => { const args = {}; diff --git a/src/components/tree/reactD3Interface/initialRender.js b/src/components/tree/reactD3Interface/initialRender.js index 2de43de0f..8dfe17f82 100644 --- a/src/components/tree/reactD3Interface/initialRender.js +++ b/src/components/tree/reactD3Interface/initialRender.js @@ -1,6 +1,6 @@ import { select } from "d3-selection"; import { rgb } from "d3-color"; -import { calcBranchStrokeCols } from "../treeHelpers"; +import { calcBranchStrokeCols } from "../../../util/colorHelpers"; import * as callbacks from "./callbacks"; export const renderTree = (that, main, phylotree, props) => { diff --git a/src/components/tree/tree.js b/src/components/tree/tree.js index ff89724aa..cc6123522 100644 --- a/src/components/tree/tree.js +++ b/src/components/tree/tree.js @@ -1,6 +1,6 @@ import React from "react"; import { ReactSVGPanZoom } from "react-svg-pan-zoom"; -import { updateVisibleTipsAndBranchThicknesses } from "../../actions/treeProperties"; +import { updateVisibleTipsAndBranchThicknesses } from "../../actions/tree"; import Card from "../framework/card"; import Legend from "./legend/legend"; import PhyloTree from "./phyloTree/phyloTree"; diff --git a/src/components/tree/treeHelpers.js b/src/components/tree/treeHelpers.js deleted file mode 100644 index 28a7dcdf9..000000000 --- a/src/components/tree/treeHelpers.js +++ /dev/null @@ -1,280 +0,0 @@ -import { rgb } from "d3-color"; -import { interpolateRgb } from "d3-interpolate"; -import { scalePow } from "d3-scale"; -import { tipRadius, freqScale, tipRadiusOnLegendMatch } from "../../util/globals"; - - -/** -* for each node, calculate the number of subtending tips which are visible -* side effects: n.tipCount for each node -* @param root - deserialized JSON root to begin traversal -*/ -export const calcTipCounts = (node, visibility) => { - node.tipCount = 0; - if (typeof node.children !== "undefined") { - for (let i = 0; i < node.children.length; i++) { - calcTipCounts(node.children[i], visibility); - node.tipCount += node.children[i].tipCount; - } - } else { - node.tipCount = visibility[node.arrayIdx] === "visible" ? 1 : 0; - } -}; - -/** -* calculates (and returns) an array of node (branch) thicknesses. -* If the node isn't visible, the thickness is 1. -* No side effects. -* @param nodes - JSON nodes -* @param visibility - visibility array (1-1 with nodes) -* @param rootIdx - nodes index of the currently in-view root -* @returns array of thicknesses (numeric) -*/ -export const calcBranchThickness = (nodes, visibility, rootIdx) => { - let maxTipCount = nodes[rootIdx].tipCount; - /* edge case: no tips selected */ - if (!maxTipCount) { - maxTipCount = 1; - } - return nodes.map((d, idx) => ( - visibility[idx] === "visible" ? freqScale((d.tipCount + 5) / (maxTipCount + 5)) : 1 - )); -}; - -/* a getter for the value of the colour attribute of the node provided for the currently set colour -note this is not the colour HEX */ -export const getTipColorAttribute = (node, colorScale) => { - if (colorScale.colorBy.slice(0, 3) === "gt-" && colorScale.genotype) { - return node.currentGt; - } - return node.attr[colorScale.colorBy]; -}; - -/* generates and returns an array of colours (HEXs) for the nodes under the given colorScale */ -/* takes around 2ms on a 2000 tip tree */ -export const calcNodeColor = (tree, colorScale) => { - if (tree && tree.nodes && colorScale && colorScale.colorBy) { - const nodeColorAttr = tree.nodes.map((n) => getTipColorAttribute(n, colorScale)); - // console.log(nodeColorAttr.map((n) => colorScale.scale(n))) - return nodeColorAttr.map((n) => colorScale.scale(n)); - } - return null; -}; - -/** -* equates a single tip and a legend element -* exact match is required for categorical qunantities such as genotypes, regions -* continuous variables need to fall into the interal (lower_bound[leg], leg] -* @param selectedLegendItem - value of the selected tip attribute (numeric or string) -* @param node - node (tip) in question -* @param legendBoundsMap - if falsey, then exact match required. Else contains bounds for match. -* @param colorScale - used to get the value of the attribute being used for colouring -* @returns bool -*/ -const determineLegendMatch = (selectedLegendItem, node, legendBoundsMap, colorScale) => { - const nodeAttr = getTipColorAttribute(node, colorScale); - if (legendBoundsMap) { - return (nodeAttr <= legendBoundsMap.upper_bound[selectedLegendItem]) && - (nodeAttr > legendBoundsMap.lower_bound[selectedLegendItem]); - } - return nodeAttr === selectedLegendItem; -}; - -/** -* produces the array of tip radii - if nothing's selected this is the hardcoded tipRadius -* if there's a selectedLegendItem, then values will be small (like normal) or big (for those tips selected) -* @param selectedLegendItem - value of the selected tip attribute (numeric or string) OPTIONAL -* @param tipSelectedIdx - idx of a single tip to show with increased tipRadius OPTIONAL -* @param colorScale - node (tip) in question -* @param tree -* @returns null (if data not ready) or array of tip radii -*/ -export const calcTipRadii = ({tipSelectedIdx = false, selectedLegendItem = false, colorScale, tree}) => { - if (selectedLegendItem && tree && tree.nodes) { - const legendMap = colorScale.continuous ? colorScale.legendBoundsMap : false; - return tree.nodes.map((d) => determineLegendMatch(selectedLegendItem, d, legendMap, colorScale) ? tipRadiusOnLegendMatch : tipRadius); - } else if (tipSelectedIdx) { - const radii = tree.nodes.map(() => tipRadius); - radii[tipSelectedIdx] = tipRadiusOnLegendMatch + 3; - return radii; - } else if (tree && tree.nodes) { - return tree.nodes.map(() => tipRadius); - } - return null; // fallthrough -}; - -/* recursively mark the parents of a given node active -by setting the node idx to true in the param visArray */ -const makeParentVisible = (visArray, node) => { - if (node.arrayIdx === 0 || visArray[node.parent.arrayIdx]) { - return; // this is the root of the tree or the parent was already visibile - } - visArray[node.parent.arrayIdx] = true; - makeParentVisible(visArray, node.parent); -}; - -/** - * 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") - */ -export 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"); -}; - -/* calcVisibility -USES: -inView: attribute of phyloTree.nodes, but accessible through redux.tree.nodes[idx].shell.inView - Bool. Set by phyloTree, determines if the tip is within the view. -controls.filters -use dates NOT controls.dateMin & controls.dateMax - -RETURNS: -visibility: array of "visible" or "hidden" - -ROUGH DESCRIPTION OF HOW FILTERING IS APPLIED: - - 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 - branches back to the MRCA are considered visibile. This is then intersected with - the time & inView visibile stuff - -FILTERS: - - filters stored in redux - controls.filters - - filters have 2 keys, each with an array of values - keys: "region" and/or "authors" - - filterPairs is a list of lists. Each list defines the filtering to do. - i.e. [ [ region, [...values]], [authors, [...values]]] -*/ -export 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 */ - let inView; - try { - inView = tree.nodes.map((d) => d.shell.inView); - } catch (e) { - inView = tree.nodes.map(() => true); - } - /* intersect visibility and inView */ - visibility = visibility.map((cv, idx) => (cv && inView[idx])); - - // FILTERS - const filterPairs = []; - Object.keys(controls.filters).forEach((key) => { - if (controls.filters[key].length) { - filterPairs.push([key, controls.filters[key]]); - } - }); - 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) - )); - const idxsOfFilteredTips = filtered.reduce((a, e, i) => { - if (e) {a.push(i);} - return a; - }, []); - /* for each visibile tip, make the parent nodes visible (recursively) */ - 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); - }); - visibility = visibility.map((cv, idx) => (cv && timeFiltered[idx])); - - /* return array of "visible" or "hidden" values */ - return visibility.map((cv) => cv ? "visible" : "hidden"); - } - return "visible"; -}; - -const branchInterpolateColour = "#BBB"; -const branchOpacityConstant = 0.6; -const branchOpacityLowerBound = 0.4; -export const branchOpacityFunction = scalePow() - .exponent([0.3]) - .domain([0, 1]) - .range([branchOpacityLowerBound, 1]) - .clamp(true); -// entropy calculation precomputed in augur -// export const calcEntropyOfValues = (vals) => -// vals.map((v) => v * Math.log(v + 1E-10)).reduce((a, b) => a + b, 0) * -1 / Math.log(vals.length); - - -/** - * calculate array of HEXs to actually be displayed. - * (colorBy) confidences manifest as opacity ramps - * @param {obj} tree phyloTree object - * @param {bool} confidence enabled? - * @return {array} array of hex's. 1-1 with nodes. - */ -export const calcBranchStrokeCols = (tree, confidence, colorBy) => { - if (confidence === true) { - return tree.nodeColors.map((col, idx) => { - const entropy = tree.nodes[idx].attr[colorBy + "_entropy"]; - return rgb(interpolateRgb(col, branchInterpolateColour)(branchOpacityFunction(entropy))).toString(); - }); - } - return tree.nodeColors.map((col) => { - return rgb(interpolateRgb(col, branchInterpolateColour)(branchOpacityConstant)).toString(); - }); -}; - -export const strainNameToIdx = (nodes, name) => { - let i; - for (i = 0; i < nodes.length; i++) { - if (nodes[i].strain === name) { - break; - } - } - return i; -}; - -export const constructVisibleTipLookupBetweenTrees = (nodesLeft, nodesRight, visibility) => { - console.log("constructVisibleTipLookupBetweenTrees"); - const rightStrainIndexMap = {}; - for (let i = 0; i < nodesRight.length; i++) { - if (!nodesRight[i].hasChildren) { - // if (nodesRight[i].strain!=="A/Fujian/5/2014") continue; - rightStrainIndexMap[nodesRight[i].strain] = i; - } - } - const lookup = []; // each entry is [idxInNodes, idxInNodesToo] - for (let i = 0; i < nodesLeft.length; i++) { - if ( - !nodesLeft[i].hasChildren && - rightStrainIndexMap[nodesLeft[i].strain] && - visibility[i] === "visible" - ) { - lookup.push([i, rightStrainIndexMap[nodesLeft[i].strain]]); - } - } - return lookup; -}; diff --git a/src/reducers/tree.js b/src/reducers/tree.js index c8952db3b..2fb966a4e 100644 --- a/src/reducers/tree.js +++ b/src/reducers/tree.js @@ -1,4 +1,4 @@ -import { getValuesAndCountsOfVisibleTraitsFromTree } from "../util/treeTraversals"; +import { getValuesAndCountsOfVisibleTraitsFromTree } from "../util/treeCountingHelpers"; import * as types from "../actions/types"; /* A version increase (i.e. props.version !== nextProps.version) necessarily implies diff --git a/src/reducers/treeToo.js b/src/reducers/treeToo.js index 47e4756cb..2613b22a8 100644 --- a/src/reducers/treeToo.js +++ b/src/reducers/treeToo.js @@ -1,4 +1,4 @@ -import { getValuesAndCountsOfVisibleTraitsFromTree } from "../util/treeTraversals"; +import { getValuesAndCountsOfVisibleTraitsFromTree } from "../util/treeCountingHelpers"; import * as types from "../actions/types"; import {getDefaultTreeState, getAttrsOnTerminalNodes} from "./tree"; /* A version increase (i.e. props.version !== nextProps.version) necessarily implies diff --git a/src/util/colorHelpers.js b/src/util/colorHelpers.js index 07e85c76d..c4b1d0bf3 100644 --- a/src/util/colorHelpers.js +++ b/src/util/colorHelpers.js @@ -1,5 +1,10 @@ import { rgb } from "d3-color"; import { mean } from "d3-array"; +import { interpolateRgb } from "d3-interpolate"; +import { scalePow } from "d3-scale"; +import { parseEncodedGenotype } from "./getGenotype"; +import getColorScale from "./colorScale"; +import { setGenotype } from "./setGenotype"; /** * Takes an array of color hex strings. @@ -7,14 +12,13 @@ import { mean } from "d3-array"; * @param {Array} colors - array of hex strings */ export const averageColors = (hexColors) => { - const n = hexColors.length; const colors = hexColors.map((hex) => rgb(hex)); const reds = colors.map((col) => col.r); const greens = colors.map((col) => col.g); const blues = colors.map((col) => col.b); const avg = rgb(mean(reds), mean(greens), mean(blues)); return avg.toString(); -} +}; export const determineColorByGenotypeType = (colorBy) => { /* note that nucleotide genotypes are either gt-nucXXX or gt-XXX */ @@ -26,3 +30,93 @@ export const determineColorByGenotypeType = (colorBy) => { } return false; }; + +/** +* what values (for colorBy) are present in the tree and not in the color_map? +* @param {Array} nodes - list of nodes +* @param {string} colorBy - +* @param {Array} color_map - list of colorBy values with colours +* @return {list} +*/ +export const getExtraVals = (nodes, colorBy, color_map) => { + let valsInTree = []; + nodes.forEach((n) => valsInTree.push(n.attr[colorBy])); + valsInTree = [...new Set(valsInTree)]; + const valsInMeta = color_map.map((d) => { return d[0];}); + // console.log("here", valsInMeta, valsInTree, valsInTree.filter((x) => valsInMeta.indexOf(x) === -1)) + // only care about values in tree NOT in metadata + return valsInTree.filter((x) => valsInMeta.indexOf(x) === -1); +}; + + +/* a getter for the value of the colour attribute of the node provided for the currently set colour +note this is not the colour HEX */ +export const getTipColorAttribute = (node, colorScale) => { + if (colorScale.colorBy.slice(0, 3) === "gt-" && colorScale.genotype) { + return node.currentGt; + } + return node.attr[colorScale.colorBy]; +}; + +/* generates and returns an array of colours (HEXs) for the nodes under the given colorScale */ +/* takes around 2ms on a 2000 tip tree */ +const calcNodeColor = (tree, colorScale) => { + if (tree && tree.nodes && colorScale && colorScale.colorBy) { + const nodeColorAttr = tree.nodes.map((n) => getTipColorAttribute(n, colorScale)); + // console.log(nodeColorAttr.map((n) => colorScale.scale(n))) + return nodeColorAttr.map((n) => colorScale.scale(n)); + } + return null; +}; + + +const branchInterpolateColour = "#BBB"; +const branchOpacityConstant = 0.6; +const branchOpacityLowerBound = 0.4; +export const branchOpacityFunction = scalePow() + .exponent([0.3]) + .domain([0, 1]) + .range([branchOpacityLowerBound, 1]) + .clamp(true); +// entropy calculation precomputed in augur +// export const calcEntropyOfValues = (vals) => +// vals.map((v) => v * Math.log(v + 1E-10)).reduce((a, b) => a + b, 0) * -1 / Math.log(vals.length); + +/** + * calculate array of HEXs to actually be displayed. + * (colorBy) confidences manifest as opacity ramps + * @param {obj} tree phyloTree object + * @param {bool} confidence enabled? + * @return {array} array of hex's. 1-1 with nodes. + */ +export const calcBranchStrokeCols = (tree, confidence, colorBy) => { + if (confidence === true) { + return tree.nodeColors.map((col, idx) => { + const entropy = tree.nodes[idx].attr[colorBy + "_entropy"]; + return rgb(interpolateRgb(col, branchInterpolateColour)(branchOpacityFunction(entropy))).toString(); + }); + } + return tree.nodeColors.map((col) => { + return rgb(interpolateRgb(col, branchInterpolateColour)(branchOpacityConstant)).toString(); + }); +}; + +export const calcColorScaleAndNodeColors = (colorBy, controls, tree, metadata) => { + let genotype; + if (colorBy.slice(0, 3) === "gt-" && controls.geneLength) { + genotype = parseEncodedGenotype(colorBy, controls.geneLength); + if (genotype.length > 1) { + console.warn("Cannot deal with multiple proteins yet - using first only."); + } + setGenotype(tree.nodes, genotype[0].prot || "nuc", genotype[0].positions); /* modifies nodes recursively */ + } + + /* step 1: calculate the required colour scale */ + const version = controls.colorScale === undefined ? 1 : controls.colorScale.version + 1; + const colorScale = getColorScale(colorBy, tree, controls.geneLength, metadata.colorOptions, version, controls.absoluteDateMaxNumeric); + if (genotype) colorScale.genotype = genotype; + + /* step 2: calculate the node colours */ + const nodeColors = calcNodeColor(tree, colorScale); + return {nodeColors, colorScale, version}; +}; diff --git a/src/util/getColorScale.js b/src/util/colorScale.js similarity index 89% rename from src/util/getColorScale.js rename to src/util/colorScale.js index 339b9a3d9..60ef123f0 100644 --- a/src/util/getColorScale.js +++ b/src/util/colorScale.js @@ -3,25 +3,10 @@ import { min, max, range as d3Range } from "d3-array"; import { rgb } from "d3-color"; import { interpolateHcl } from "d3-interpolate"; import { genericDomain, colors, genotypeColors, reallySmallNumber, reallyBigNumber } from "./globals"; -import { getAllValuesAndCountsOfTraitsFromTree } from "./treeTraversals"; +import { getAllValuesAndCountsOfTraitsFromTree } from "./treeCountingHelpers"; import { setLBI } from "./localBranchingIndex"; +import { getExtraVals } from "./colorHelpers"; -/** -* what values (for colorBy) are present in the tree and not in the color_map? -* @param {Array} nodes - list of nodes -* @param {string} colorBy - -* @param {Array} color_map - list of colorBy values with colours -* @return {list} -*/ -const getExtraVals = (nodes, colorBy, color_map) => { - let valsInTree = []; - nodes.forEach((n) => valsInTree.push(n.attr[colorBy])); - valsInTree = [...new Set(valsInTree)]; - const valsInMeta = color_map.map((d) => { return d[0];}); - // console.log("here", valsInMeta, valsInTree, valsInTree.filter((x) => valsInMeta.indexOf(x) === -1)) - // only care about values in tree NOT in metadata - return valsInTree.filter((x) => valsInMeta.indexOf(x) === -1); -}; const createLegendMatchBound = (colorScale) => { const lower_bound = {}; diff --git a/src/util/entropy.js b/src/util/entropy.js index cc6275a3c..adca603ec 100644 --- a/src/util/entropy.js +++ b/src/util/entropy.js @@ -9,7 +9,7 @@ const intersectGenes = function intersectGenes(geneMap, pos) { return false; }; -export const calcMutationCounts = (nodes, visibility, geneMap, isAA) => { +const calcMutationCounts = (nodes, visibility, geneMap, isAA) => { const sparse = isAA ? {} : []; if (isAA) { Object.keys(geneMap).forEach((n) => {sparse[n] = {};}); @@ -65,7 +65,7 @@ export const calcMutationCounts = (nodes, visibility, geneMap, isAA) => { return [counts, m]; }; -export const calcEntropy = (nodes, visibility, geneMap, isAA) => { +const calcEntropy = (nodes, visibility, geneMap, isAA) => { const arrayOfProts = isAA ? Object.keys(geneMap) : ["nuc"]; const initialState = {}; const anc_state = {}; @@ -182,3 +182,19 @@ export const calcEntropy = (nodes, visibility, geneMap, isAA) => { // console.log(entropy) return [entropy, m]; }; + +/** +* 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 {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? +* @return {obj} keys: the entries in attrs. Values: an object mapping values -> counts +* TODO: this algorithm can be much improved, and the data structures returned improved also +*/ +export const calcEntropyInView = (nodes, visibility, mutType, geneMap, showCounts) => { + return showCounts ? + calcMutationCounts(nodes, visibility, geneMap, mutType === "aa") : + calcEntropy(nodes, visibility, geneMap, mutType === "aa"); +}; diff --git a/src/util/tipRadiusHelpers.js b/src/util/tipRadiusHelpers.js new file mode 100644 index 000000000..eea033f33 --- /dev/null +++ b/src/util/tipRadiusHelpers.js @@ -0,0 +1,44 @@ +import { tipRadius, tipRadiusOnLegendMatch } from "./globals"; +import { getTipColorAttribute } from "./colorHelpers"; + +/** +* equates a single tip and a legend element +* exact match is required for categorical qunantities such as genotypes, regions +* continuous variables need to fall into the interal (lower_bound[leg], leg] +* @param selectedLegendItem - value of the selected tip attribute (numeric or string) +* @param node - node (tip) in question +* @param legendBoundsMap - if falsey, then exact match required. Else contains bounds for match. +* @param colorScale - used to get the value of the attribute being used for colouring +* @returns bool +*/ +const determineLegendMatch = (selectedLegendItem, node, legendBoundsMap, colorScale) => { + const nodeAttr = getTipColorAttribute(node, colorScale); + if (legendBoundsMap) { + return (nodeAttr <= legendBoundsMap.upper_bound[selectedLegendItem]) && + (nodeAttr > legendBoundsMap.lower_bound[selectedLegendItem]); + } + return nodeAttr === selectedLegendItem; +}; + +/** +* produces the array of tip radii - if nothing's selected this is the hardcoded tipRadius +* if there's a selectedLegendItem, then values will be small (like normal) or big (for those tips selected) +* @param selectedLegendItem - value of the selected tip attribute (numeric or string) OPTIONAL +* @param tipSelectedIdx - idx of a single tip to show with increased tipRadius OPTIONAL +* @param colorScale - node (tip) in question +* @param tree +* @returns null (if data not ready) or array of tip radii +*/ +export const calcTipRadii = ({tipSelectedIdx = false, selectedLegendItem = false, colorScale, tree}) => { + if (selectedLegendItem && tree && tree.nodes) { + const legendMap = colorScale.continuous ? colorScale.legendBoundsMap : false; + return tree.nodes.map((d) => determineLegendMatch(selectedLegendItem, d, legendMap, colorScale) ? tipRadiusOnLegendMatch : tipRadius); + } else if (tipSelectedIdx) { + const radii = tree.nodes.map(() => tipRadius); + radii[tipSelectedIdx] = tipRadiusOnLegendMatch + 3; + return radii; + } else if (tree && tree.nodes) { + return tree.nodes.map(() => tipRadius); + } + return null; // fallthrough +}; diff --git a/src/util/treeTraversals.js b/src/util/treeCountingHelpers.js similarity index 72% rename from src/util/treeTraversals.js rename to src/util/treeCountingHelpers.js index d3057fd34..b71a7994f 100644 --- a/src/util/treeTraversals.js +++ b/src/util/treeCountingHelpers.js @@ -1,4 +1,3 @@ -import * as entropy from "./entropy"; /** * traverse the tree and get the values -> counts for a single * attr. Visibility of the node is ignored. Terminal nodes only. @@ -58,19 +57,19 @@ export const getValuesAndCountsOfVisibleTraitsFromTree = (nodes, visibility, att return stateCount; }; - /** -* 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 {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? -* @return {obj} keys: the entries in attrs. Values: an object mapping values -> counts -* TODO: this algorithm can be much improved, and the data structures returned improved also +* for each node, calculate the number of subtending tips which are visible +* side effects: n.tipCount for each node +* @param root - deserialized JSON root to begin traversal */ -export const calcEntropyInView = (nodes, visibility, mutType, geneMap, showCounts) => { - return showCounts ? - entropy.calcMutationCounts(nodes, visibility, geneMap, mutType === "aa") : - entropy.calcEntropy(nodes, visibility, geneMap, mutType === "aa"); +export const calcTipCounts = (node, visibility) => { + node.tipCount = 0; + if (typeof node.children !== "undefined") { + for (let i = 0; i < node.children.length; i++) { + calcTipCounts(node.children[i], visibility); + node.tipCount += node.children[i].tipCount; + } + } else { + node.tipCount = visibility[node.arrayIdx] === "visible" ? 1 : 0; + } }; diff --git a/src/util/treeTangleHelpers.js b/src/util/treeTangleHelpers.js new file mode 100644 index 000000000..1582bd454 --- /dev/null +++ b/src/util/treeTangleHelpers.js @@ -0,0 +1,22 @@ + +export const constructVisibleTipLookupBetweenTrees = (nodesLeft, nodesRight, visibility) => { + console.log("constructVisibleTipLookupBetweenTrees"); + const rightStrainIndexMap = {}; + for (let i = 0; i < nodesRight.length; i++) { + if (!nodesRight[i].hasChildren) { + // if (nodesRight[i].strain!=="A/Fujian/5/2014") continue; + rightStrainIndexMap[nodesRight[i].strain] = i; + } + } + const lookup = []; // each entry is [idxInNodes, idxInNodesToo] + for (let i = 0; i < nodesLeft.length; i++) { + if ( + !nodesLeft[i].hasChildren && + rightStrainIndexMap[nodesLeft[i].strain] && + visibility[i] === "visible" + ) { + lookup.push([i, rightStrainIndexMap[nodesLeft[i].strain]]); + } + } + return lookup; +}; diff --git a/src/util/treeVisibilityHelpers.js b/src/util/treeVisibilityHelpers.js new file mode 100644 index 000000000..833cd9bd1 --- /dev/null +++ b/src/util/treeVisibilityHelpers.js @@ -0,0 +1,159 @@ +import { freqScale } from "./globals"; +import { calcTipCounts } from "./treeCountingHelpers"; + +export const strainNameToIdx = (nodes, name) => { + let i; + for (i = 0; i < nodes.length; i++) { + if (nodes[i].strain === name) { + break; + } + } + return i; +}; + +/** calcBranchThickness ** +* returns an array of node (branch) thicknesses based on the tipCount at each node +* If the node isn't visible, the thickness is 1. +* Pure. +* @param nodes - JSON nodes +* @param visibility - visibility array (1-1 with nodes) +* @param rootIdx - nodes index of the currently in-view root +* @returns array of thicknesses (numeric) +*/ +const calcBranchThickness = (nodes, visibility, rootIdx) => { + let maxTipCount = nodes[rootIdx].tipCount; + /* edge case: no tips selected */ + if (!maxTipCount) { + maxTipCount = 1; + } + return nodes.map((d, idx) => ( + visibility[idx] === "visible" ? freqScale((d.tipCount + 5) / (maxTipCount + 5)) : 1 + )); +}; + +/* recursively mark the parents of a given node active +by setting the node idx to true in the param visArray */ +const makeParentVisible = (visArray, node) => { + if (node.arrayIdx === 0 || visArray[node.parent.arrayIdx]) { + return; // this is the root of the tree or the parent was already visibile + } + visArray[node.parent.arrayIdx] = true; + makeParentVisible(visArray, node.parent); +}; + +/** + * 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") + */ +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"); +}; + + +/* calcVisibility +USES: +inView: attribute of phyloTree.nodes, but accessible through redux.tree.nodes[idx].shell.inView + Bool. Set by phyloTree, determines if the tip is within the view. +controls.filters +use dates NOT controls.dateMin & controls.dateMax + +RETURNS: +visibility: array of "visible" or "hidden" + +ROUGH DESCRIPTION OF HOW FILTERING IS APPLIED: + - 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 + branches back to the MRCA are considered visibile. This is then intersected with + the time & inView visibile stuff + +FILTERS: + - filters stored in redux - controls.filters + - filters have 2 keys, each with an array of values + keys: "region" and/or "authors" + - filterPairs is a list of lists. Each list defines the filtering to do. + i.e. [ [ region, [...values]], [authors, [...values]]] +*/ +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 */ + let inView; + try { + inView = tree.nodes.map((d) => d.shell.inView); + } catch (e) { + inView = tree.nodes.map(() => true); + } + /* intersect visibility and inView */ + visibility = visibility.map((cv, idx) => (cv && inView[idx])); + + // FILTERS + const filterPairs = []; + Object.keys(controls.filters).forEach((key) => { + if (controls.filters[key].length) { + filterPairs.push([key, controls.filters[key]]); + } + }); + 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) + )); + const idxsOfFilteredTips = filtered.reduce((a, e, i) => { + if (e) {a.push(i);} + return a; + }, []); + /* for each visibile tip, make the parent nodes visible (recursively) */ + 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); + }); + visibility = visibility.map((cv, idx) => (cv && timeFiltered[idx])); + + /* return array of "visible" or "hidden" values */ + return visibility.map((cv) => cv ? "visible" : "hidden"); + } + return "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) */ + calcTipCounts(tree.nodes[0], visibility); + /* re-calculate branchThickness (inline) */ + return { + visibility: visibility, + visibilityVersion: tree.visibilityVersion + 1, + branchThickness: calcBranchThickness(tree.nodes, visibility, idxOfInViewRootNode), + branchThicknessVersion: tree.branchThicknessVersion + 1 + }; +};