UNPKG

auspice

Version:

Web app for visualizing pathogen evolution

166 lines (148 loc) 6.5 kB
import { rgb, hsl } from "d3-color"; import { interpolateRgb } from "d3-interpolate"; import scalePow from "d3-scale/src/pow"; import { isColorByGenotype, decodeColorByGenotype } from "./getGenotype"; import { getTraitFromNode } from "./treeMiscHelpers"; import { isValueValid } from "./globals"; import { calendarToNumeric } from "./dateHelpers"; /** * Average over the visible colours for a given location * @param {array} nodes list of nodes whose colours we want to average over * @param {array} nodeColours (redux state) -- list of node hexes. Not in 1-1 correspondence with `nodes`. * @returns {str} a color hex string representing the average of the array. */ export const getAverageColorFromNodes = (nodes, nodeColors) => { let r=0, g=0, b=0; nodes.forEach((n) => { const tmpRGB = rgb(nodeColors[n.arrayIdx]); r += tmpRGB.r; g += tmpRGB.g; b += tmpRGB.b; }); const total = nodes.length; const avg = rgb(r/total, g/total, b/total); return avg.toString(); }; /** * what colorBy trait names are present in the tree but _not_ in the provided scale? * @param {Array} nodes - list of nodes * @param {Array|undefined} nodesToo - list of nodes for the second tree * @param {string} colorBy - * @param {Array} providedVals - list of provided trait values * @return {list} */ export const getExtraVals = (nodes, nodesToo, colorBy, providedVals) => { let valsInTree = nodes.map((n) => getTraitFromNode(n, colorBy)); if (nodesToo) { nodesToo.forEach((n) => valsInTree.push(getTraitFromNode(n, colorBy))); } valsInTree = [...new Set(valsInTree)]; return valsInTree .filter((x) => providedVals.indexOf(x) === -1) .filter((x) => isValueValid(x)); }; /* 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 (isColorByGenotype(colorScale.colorBy) && colorScale.genotype) { return node.currentGt; } return getTraitFromNode(node, 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; }; // scale entropy such that higher entropy maps to a grayer less-certain branch const branchInterpolateColour = "#BBB"; const branchOpacityConstant = 0.6; const branchOpacityFunction = scalePow() .exponent([0.6]) .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 // 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 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 calculateStrokeColors = (tree, branch, confidence, colorBy) => { if (confidence === true) { 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 an emphasized color */ export const getEmphasizedColor = (color) => { const hslColor = hsl(color); hslColor.s *= 1.8; // more saturation hslColor.l /= 1.2; // less luminance return rgb(hslColor).toString(); }; export const getBrighterColor = (color) => rgb(color).brighter([0.65]).toString(); /** * Return the display title for the selected colorBy * @param {obj} colorings an object of available colorings * @param {string} colorBy the select colorBy * @returns {string} the display title for the colorBY */ export const getColorByTitle = (colorings, colorBy) => { if (isColorByGenotype(colorBy)) { const genotype = decodeColorByGenotype(colorBy); return genotype.aa ? `Genotype at ${genotype.gene} site ${genotype.positions.join(", ")}` : `Nucleotide at position ${genotype.positions.join(", ")}`; } return colorings[colorBy] === undefined ? "" : colorings[colorBy].title; }; /** * We allow values (on nodes) to be encoded as numeric dates (2021.123) or * YYYY-MM-DD strings. This helper function handles this flexibility and * translates any provided value to either a number or undefined. */ export function numDate(value) { switch (typeof value) { case "number": return value; case "string": return calendarToNumeric(value, true); // allow XX ambiguity default: return undefined; } }