Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convey uncertainty via tip colors #1796

Merged
merged 2 commits into from
Jun 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Changelog

* We now use the reported confidence / entropy values to change the saturation of tips (circles) on the tree, which matches the behaviour seen for branches. If there is no (or very little) uncertainty in these nodes then the tips will appear the same as seen in previous versions of Auspice. ([#1796](https://github.com/nextstrain/auspice/pull/1796))
* We no longer show the "second tree" sidebar dropdown when there are no available options. The possible options are defined by [the charon/getAvailable API](https://docs.nextstrain.org/projects/auspice/en/stable/server/api.html) response and as such vary depending on the server in use. ([#1795](https://github.com/nextstrain/auspice/pull/1795))


Expand Down
8 changes: 4 additions & 4 deletions src/components/tree/reactD3Interface/change.js
Original file line number Diff line number Diff line change
@@ -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 = {};
Expand All @@ -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 */
Expand Down
9 changes: 5 additions & 4 deletions src/components/tree/reactD3Interface/initialRender.js
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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),
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/reducers/controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const getDefaultControlsState = () => {
absoluteDateMax: dateMax,
absoluteDateMaxNumeric: dateMaxNumeric,
colorBy: defaults.colorBy,
colorByConfidence: { display: false, on: false },
colorByConfidence: false,
colorScale: undefined,
explodeAttr: undefined,
selectedBranchLabel: "none",
Expand Down
47 changes: 33 additions & 14 deletions src/util/colorHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,35 +68,54 @@ 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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The range (and the domain) of this scale are the magic values which control how entropy values affect the stroke colour (via an interpolation between the original colour & grey). The tip fill colour is a brighter version of the stroke colour. Very happy for adjustments here, although I think it's important that tipOpacityFn(close-to-zero) -> 0 so that we don't change how the majority of datasets appear.



// 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
* 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) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a request for change, just curious

Why combine branch/tip colors into one function with the branch flag when they are essentially completely different paths within the function?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While they are ~separate code paths, they both do the same thing: take a colour and modify it according to the node's uncertainty. Co-locating them feels natural to me and should help them to stay in-sync.

I actually thought about taking this further and refactoring it into a function calculateNodeColors -> {branchColors, tipStrokeColors, tipFillColors} so we calculate everything at once, but I'll leave that for another day (and I need to check that we never have a situation where we only need to recompute one of those sets).

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();
});
};


Expand Down