From f9e654f8518d471d79431465919374f311f5fb1c Mon Sep 17 00:00:00 2001 From: james hadfield Date: Fri, 28 Jun 2024 12:45:58 +1200 Subject: [PATCH] [color-by confidence] confidence for tip colors The previous code conveyed uncertainty in node attrs for _branches_ by making them appear grey-er, but we never implemented this for _tips_; most likely because we never had a dataset with such data when this was built. Here we use the same approach for tips as for branches, but with a slightly different parameterisation of the interpolation. The mapping of the entropy value into `[0,1]` (`tipOpacityFunction`) was chosen so that tips with no (or very little) uncertainty look unchanged from previous Auspice versions, and uncertainty makes them appear more similar to the branch colour (for an equivalent uncertainty). --- .../tree/reactD3Interface/change.js | 8 ++-- .../tree/reactD3Interface/initialRender.js | 9 ++-- src/util/colorHelpers.js | 47 +++++++++++++------ 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/components/tree/reactD3Interface/change.js b/src/components/tree/reactD3Interface/change.js index 9e759e5af..07bff8b1b 100644 --- a/src/components/tree/reactD3Interface/change.js +++ b/src/components/tree/reactD3Interface/change.js @@ -1,4 +1,4 @@ -import { calcBranchStrokeCols, getBrighterColor } from "../../../util/colorHelpers"; +import { calculateStrokeColors, getBrighterColor } from "../../../util/colorHelpers"; export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps, newProps) => { const args = {}; @@ -16,9 +16,9 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps, (oldTreeRedux.nodeColorsVersion !== newTreeRedux.nodeColorsVersion || newProps.colorByConfidence !== oldProps.colorByConfidence)) { args.changeColorBy = true; - args.branchStroke = calcBranchStrokeCols(newTreeRedux, newProps.colorByConfidence, newProps.colorBy); - args.tipStroke = newTreeRedux.nodeColors; - args.fill = newTreeRedux.nodeColors.map(getBrighterColor); + args.branchStroke = calculateStrokeColors(newTreeRedux, true, newProps.colorByConfidence, newProps.colorBy); + args.tipStroke = calculateStrokeColors(newTreeRedux, false, newProps.colorByConfidence, newProps.colorBy); + args.fill = args.tipStroke.map(getBrighterColor); } /* visibility */ diff --git a/src/components/tree/reactD3Interface/initialRender.js b/src/components/tree/reactD3Interface/initialRender.js index 880fd87c9..b85dd9917 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 'd3-transition'; -import { calcBranchStrokeCols, getBrighterColor } from "../../../util/colorHelpers"; +import { calculateStrokeColors, getBrighterColor } from "../../../util/colorHelpers"; import * as callbacks from "./callbacks"; import { makeTipLabelFunc } from "../phyloTree/labels"; @@ -16,6 +16,7 @@ export const renderTree = (that, main, phylotree, props) => { if (Object.prototype.hasOwnProperty.call(props.scatterVariables, "showBranches") && props.scatterVariables.showBranches===false) { renderBranchLabels=false; } + const tipStrokeColors = calculateStrokeColors(treeState, false, props.colorByConfidence, props.colorBy); /* simply the call to phylotree.render */ phylotree.render( select(ref), @@ -43,9 +44,9 @@ export const renderTree = (that, main, phylotree, props) => { treeState.visibility, props.temporalConfidence.on, /* drawConfidence? */ treeState.vaccines, - calcBranchStrokeCols(treeState, props.colorByConfidence, props.colorBy), - treeState.nodeColors, - treeState.nodeColors.map(getBrighterColor), + calculateStrokeColors(treeState, true, props.colorByConfidence, props.colorBy), + tipStrokeColors, + tipStrokeColors.map(getBrighterColor), // tip fill colors treeState.tipRadii, /* might be null */ [props.dateMinNumeric, props.dateMaxNumeric], props.scatterVariables diff --git a/src/util/colorHelpers.js b/src/util/colorHelpers.js index 389e143cf..d202c2bdd 100644 --- a/src/util/colorHelpers.js +++ b/src/util/colorHelpers.js @@ -68,11 +68,14 @@ export const calcNodeColor = (tree, colorScale) => { // scale entropy such that higher entropy maps to a grayer less-certain branch const branchInterpolateColour = "#BBB"; const branchOpacityConstant = 0.6; -export const branchOpacityFunction = scalePow() +const branchOpacityFunction = scalePow() .exponent([0.6]) - .domain([0, 2.0]) - .range([0.4, 1]) + .domain([0, 2.0]) // entropy values close to 0 -> ~100% confidence, close to 2 -> very little confidence + .range([0.4, 1]) // 0 -> return original node colour, 1 -> return branchInterpolateColour .clamp(true); +const tipOpacityFunction = branchOpacityFunction + .copy() + .range([0, 0.9]); // if entropy close to 0 return the original node color // entropy calculation precomputed in augur @@ -80,23 +83,39 @@ export const branchOpacityFunction = scalePow() // 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 + * Calculate an array of stroke colors to render for a branch or tip node. These are "grey-er" versions + * of the underlying `tree.nodeColours`. The degree of grey-ness is obtained via interpolation + * between the node color and `branchOpacityConstant`. The interpolation parameter varies + * depending on the confidence we have in the trait (the entropy), with more confidence resulting + * in more saturated colours. For branches we always make them slightly greyer (even in the absence + * of uncertainty) for purely aesthetic reasons. * @param {obj} tree phyloTree object + * @param {bool} branch will this color be used for the branch or the tip? * @param {bool} confidence enabled? * @return {array} array of hex's. 1-1 with nodes. */ -export const calcBranchStrokeCols = (tree, confidence, colorBy) => { +export const calculateStrokeColors = (tree, branch, confidence, colorBy) => { if (confidence === true) { - return tree.nodeColors.map((col, idx) => { - const entropy = getTraitFromNode(tree.nodes[idx], colorBy, {entropy: true}); - const opacity = entropy ? branchOpacityFunction(entropy) : branchOpacityConstant; - return rgb(interpolateRgb(col, branchInterpolateColour)(opacity)).toString(); - }); + return tree.nodeColors.map(branch ? _confidenceBranchColor : _confidenceTipColor) + } + return branch ? tree.nodeColors.map(_defaultBranchColor) : tree.nodeColors; + + function _confidenceBranchColor(col, idx) { + const entropy = getTraitFromNode(tree.nodes[idx], colorBy, {entropy: true}); + if (!entropy) return _defaultBranchColor(col); + return rgb(interpolateRgb(col, branchInterpolateColour)(branchOpacityFunction(entropy))).toString(); + } + + function _confidenceTipColor(col, idx) { + if (tree.nodes[idx].hasChildren) return undefined; // skip computation for internal nodes + const entropy = getTraitFromNode(tree.nodes[idx], colorBy, {entropy: true}); + if (!entropy) return col; + return rgb(interpolateRgb(col, branchInterpolateColour)(tipOpacityFunction(entropy))).toString(); + } + + function _defaultBranchColor(col) { + return rgb(interpolateRgb(col, branchInterpolateColour)(branchOpacityConstant)).toString() } - return tree.nodeColors.map((col) => { - return rgb(interpolateRgb(col, branchInterpolateColour)(branchOpacityConstant)).toString(); - }); };