Skip to content
Permalink
Browse files

Merge pull request #709 from nextstrain/muttype

Check mutation type against tree
  • Loading branch information...
trvrb committed Mar 11, 2019
2 parents 5502ce4 + c2bfcb9 commit 2bfe1dcdffb8e68425ad8dc2b7caaa3bae1fb646
@@ -258,20 +258,40 @@ const modifyStateViaTree = (state, tree, treeToo) => {
state.absoluteDateMinNumeric = calendarToNumeric(state.absoluteDateMin);
state.absoluteDateMaxNumeric = calendarToNumeric(state.absoluteDateMax);


/* available tree attrs - based upon the root node */
/* collect all available tree attrs, and available mutations */
const attrs = new Set();
let [aaMuts, nucMuts] = [false, false];
const examineNodes = function examineNodes(nodes) {
nodes.forEach((node) => {
Object.keys(node.attr).forEach((attr) => {
attrs.add(attr);
});
if (node.aa_muts && Object.keys(node.aa_muts).length) aaMuts = true;
if (node.muts && node.muts.length) nucMuts = true;
});
};
examineNodes(tree.nodes);
if (treeToo) {
state.attrs = [...new Set([...Object.keys(tree.nodes[0].attr), ...Object.keys(treeToo.nodes[0].attr)])];
} else {
state.attrs = Object.keys(tree.nodes[0].attr);
examineNodes(treeToo.nodes);
}
state.attrs = [...attrs]; /* convert to list */

/* ensure specified mutType is indeed available */
if (!aaMuts && !nucMuts) {
state.mutType = null;
} else if (state.mutType === "aa" && !aaMuts) {
state.mutType = "nuc";
} else if (state.mutType === "nuc" && !nucMuts) {
state.mutType = "aa";
}

/* does the tree have date information? if not, disable controls, modify view */
state.branchLengthsToDisplay = Object.keys(tree.nodes[0].attr).indexOf("num_date") === -1 ? "divOnly" :
Object.keys(tree.nodes[0].attr).indexOf("div") === -1 ? "dateOnly" : "divAndDate";

/* if branchLengthsToDisplay is divOnly, force to display by divergence
if branchLengthsToDisplay is dateONly, force to display by date */
/* if branchLengthsToDisplay is "divOnly", force to display by divergence
* if branchLengthsToDisplay is "dateOnly", force to display by date
*/
state.distanceMeasure = state.branchLengthsToDisplay === "divOnly" ? "div" :
state.branchLengthsToDisplay === "dateOnly" ? "num_date" : state.distanceMeasure;

@@ -282,9 +302,6 @@ const modifyStateViaTree = (state, tree, treeToo) => {
};

const checkAndCorrectErrorsInState = (state, metadata, query, tree) => {
/* The one (bigish) problem with this being in the reducer is that
we can't have any side effects. So if we detect and error introduced by
a URL QUERY (and correct it in state), we can't correct the URL */
/* colorBy */
if (!metadata.colorOptions) {
metadata.colorOptions = {};
@@ -5,7 +5,7 @@ import { numericToCalendar } from "../../../util/dateHelpers";
import { getTipColorAttribute } from "../../../util/colorHelpers";
import { isColorByGenotype, decodeColorByGenotype } from "../../../util/getGenotype";

const infoLineJSX = (item, value) => (
const renderInfoLine = (item, value) => (
<span>
<span style={{fontWeight: "500"}}>
{item + " "}
@@ -16,7 +16,7 @@ const infoLineJSX = (item, value) => (
</span>
);

const infoBlockJSX = (item, values) => (
const renderInfoBlock = (item, values) => (
<div>
<p style={{marginBottom: "-0.7em", fontWeight: "500"}}>
{item}
@@ -30,10 +30,17 @@ const infoBlockJSX = (item, values) => (
</div>
);

const getBranchDivJSX = (d) =>
<p>{infoLineJSX("Divergence:", prettyString(d.attr.div.toExponential(3)))}</p>;
const VerticalBreak = () => (
<br style={{display: "block", marginTop: "10px", lineHeight: "22px"}} />
);

const renderBranchDivergence = (d) => (
<p>
{renderInfoLine("Divergence:", prettyString(d.attr.div.toExponential(3)))}
</p>
);

const getBranchTimeJSX = (d, temporalConfidence) => {
const renderBranchTime = (d, temporalConfidence) => {
const date = d.attr.date || numericToCalendar(d.attr.num_date);
let dateRange = false;
if (temporalConfidence && d.attr.num_date_confidence) {
@@ -45,13 +52,13 @@ const getBranchTimeJSX = (d, temporalConfidence) => {
if (dateRange && dateRange[0] !== dateRange[1]) {
return (
<p>
{infoLineJSX("Inferred Date:", date)}
<br/>
{infoLineJSX("Date Confidence Interval:", `(${dateRange[0]}, ${dateRange[1]})`)}
{renderInfoLine("Inferred Date:", date)}
<VerticalBreak/>
{renderInfoLine("Date Confidence Interval:", `(${dateRange[0]}, ${dateRange[1]})`)}
</p>
);
}
return (<p>{infoLineJSX("Date:", date)}</p>);
return (<p>{renderInfoLine("Date:", date)}</p>);
};

/**
@@ -67,7 +74,7 @@ const displayColorBy = (d, distanceMeasure, temporalConfidence, colorByConfidenc
}
if (colorBy === "num_date") {
/* if colorBy is date and branch lengths are divergence we should still show node date */
return (colorBy !== distanceMeasure) ? getBranchTimeJSX(d, temporalConfidence) : null;
return (colorBy !== distanceMeasure) ? renderBranchTime(d, temporalConfidence) : null;
}
if (colorByConfidence === true) {
const lkey = colorBy + "_confidence";
@@ -79,9 +86,9 @@ const displayColorBy = (d, distanceMeasure, temporalConfidence, colorByConfidenc
.sort((a, b) => d.attr[lkey][a] > d.attr[lkey][b] ? -1 : 1)
.slice(0, 4)
.map((v) => `${prettyString(v)} (${(100 * d.attr[lkey][v]).toFixed(0)}%)`);
return infoBlockJSX(`${prettyString(colorBy)} (confidence):`, vals);
return renderInfoBlock(`${prettyString(colorBy)} (confidence):`, vals);
}
return infoLineJSX(prettyString(colorBy), prettyString(d.attr[colorBy]));
return renderInfoLine(prettyString(colorBy), prettyString(d.attr[colorBy]));
};

/**
@@ -133,61 +140,66 @@ const getMutationsJSX = (d, mutType) => {
mGap += gapLen > nGapDisp ? " + " + (gapLen - nGapDisp) + " more" : "";

if (gapLen === 0) {
return infoLineJSX("Nucleotide mutations:", m);
return renderInfoLine("Nucleotide mutations:", m);
}
if (nucLen === 0) {
return infoLineJSX("Gap/N mutations:", mGap);
return renderInfoLine("Gap/N mutations:", mGap);
}
return (
<p>
{infoLineJSX("Nucleotide mutations:", m)}
<div height="5"/>
{infoLineJSX("Gap/N mutations:", mGap)}
{renderInfoLine("Nucleotide mutations:", m)}
<VerticalBreak/>
{renderInfoLine("Gap/N mutations:", mGap)}
</p>
);

}
return infoLineJSX("No nucleotide mutations", "");
} else if (typeof d.aa_muts !== "undefined") {
/* calculate counts */
const prots = Object.keys(d.aa_muts);
const counts = {};
for (const prot of prots) {
counts[prot] = d.aa_muts[prot].length;
}
/* are there any AA mutations? */
if (prots.map((k) => counts[k]).reduce((a, b) => a + b, 0)) {
const nDisplay = 3; // number of mutations to display per protein
const nProtsToDisplay = 7; // max number of proteins to display
let protsSeen = 0;
const m = [];
prots.forEach((prot) => {
if (counts[prot] && protsSeen < nProtsToDisplay) {
let x = prot + ":\u00A0\u00A0" + d.aa_muts[prot].slice(0, Math.min(nDisplay, counts[prot])).join(", ");
if (counts[prot] > nDisplay) {
x += " + " + (counts[prot] - nDisplay) + " more";
}
m.push(x);
protsSeen++;
if (protsSeen === nProtsToDisplay) {
m.push(`(protein mutations truncated)`);
return renderInfoLine("No nucleotide mutations", "");
} else if (mutType === "aa") {
if (typeof d.aa_muts !== "undefined") {
/* calculate counts */
const prots = Object.keys(d.aa_muts);
const counts = {};
for (const prot of prots) {
counts[prot] = d.aa_muts[prot].length;
}
/* are there any AA mutations? */
if (prots.map((k) => counts[k]).reduce((a, b) => a + b, 0)) {
const nDisplay = 3; // number of mutations to display per protein
const nProtsToDisplay = 7; // max number of proteins to display
let protsSeen = 0;
const m = [];
prots.forEach((prot) => {
if (counts[prot] && protsSeen < nProtsToDisplay) {
let x = prot + ":\u00A0\u00A0" + d.aa_muts[prot].slice(0, Math.min(nDisplay, counts[prot])).join(", ");
if (counts[prot] > nDisplay) {
x += " + " + (counts[prot] - nDisplay) + " more";
}
m.push(x);
protsSeen++;
if (protsSeen === nProtsToDisplay) {
m.push(`(protein mutations truncated)`);
}
}
}
});
return infoBlockJSX("AA mutations:", m);
});
return renderInfoBlock("AA mutations:", m);
}
return renderInfoLine("No amino acid mutations", "");
}
return infoLineJSX("No amino acid mutations", "");
}
console.warn("Error parsing mutations for branch", d.strain);
/* if mutType is neither "aa" nor "muc" then render nothing */
return null;
};

const getBranchDescendents = (n) => {
if (n.fullTipCount === 1) {
return <span>{infoLineJSX("Branch leading to", n.strain)}<p/></span>;
}
return <span>{infoLineJSX("Number of descendants:", n.fullTipCount)}<p/></span>;
};
const getBranchDescendents = (n) => (
<>
{n.fullTipCount === 1 ?
renderInfoLine("Branch leading to", n.strain) :
renderInfoLine("Number of descendants:", n.fullTipCount)
}
<VerticalBreak/>
</>
);

const getPanelStyling = (d, panelDims) => {
const xOffset = 10;
@@ -233,24 +245,24 @@ const getPanelStyling = (d, panelDims) => {
const tipDisplayColorByInfo = (d, colorBy, distanceMeasure, temporalConfidence, mutType, colorScale) => {
if (colorBy === "num_date") {
if (distanceMeasure === "num_date") return null;
return getBranchTimeJSX(d.n, temporalConfidence);
return renderBranchTime(d.n, temporalConfidence);
}
if (isColorByGenotype(colorBy)) {
const genotype = decodeColorByGenotype(colorBy);
const key = genotype.aa
? `Amino Acid at ${genotype.gene} site ${genotype.positions.join(", ")}`
: `Nucleotide at pos ${genotype.positions.join(", ")}`;
const state = getTipColorAttribute(d.n, colorScale);
return infoLineJSX(key + ":", state);
return renderInfoLine(key + ":", state);
}
return infoLineJSX(prettyString(colorBy) + ":", prettyString(d.n.attr[colorBy]));
return renderInfoLine(prettyString(colorBy) + ":", prettyString(d.n.attr[colorBy]));
};

const displayVaccineInfo = (d) => {
if (d.n.vaccineDate) {
return (
<span>
{infoLineJSX("Vaccine strain:", d.n.vaccineDate)}
{renderInfoLine("Vaccine strain:", d.n.vaccineDate)}
<p/>
</span>
);
@@ -273,7 +285,7 @@ const HoverInfoPanel = ({mutType, temporalConfidence, distanceMeasure,
<span>
{displayVaccineInfo(d)}
{tipDisplayColorByInfo(d, colorBy, distanceMeasure, temporalConfidence, mutType, colorScale)}
{distanceMeasure === "div" ? getBranchDivJSX(d.n) : getBranchTimeJSX(d.n, temporalConfidence)}
{distanceMeasure === "div" ? renderBranchDivergence(d.n) : renderBranchTime(d.n, temporalConfidence)}
</span>
);
} else {
@@ -282,7 +294,7 @@ const HoverInfoPanel = ({mutType, temporalConfidence, distanceMeasure,
{getBranchDescendents(d.n)}
{/* getFrequenciesJSX(d.n, mutType) */}
{getMutationsJSX(d.n, mutType)}
{distanceMeasure === "div" ? getBranchDivJSX(d.n) : getBranchTimeJSX(d.n, temporalConfidence)}
{distanceMeasure === "div" ? renderBranchDivergence(d.n) : renderBranchTime(d.n, temporalConfidence)}
{displayColorBy(d.n, distanceMeasure, temporalConfidence, colorByConfidence, colorBy)}
</span>
);
@@ -4,7 +4,7 @@ import { defaultGeoResolution,
defaultDateRange,
defaultDistanceMeasure,
defaultLayout,
mutType,
defaultMutType,
twoColumnBreakpoint } from "../util/globals";
import * as types from "../actions/types";
import { calcBrowserDimensionsInitialState } from "./browserDimensions";
@@ -36,7 +36,7 @@ export const getDefaultControlsState = () => {
search: null,
strain: null,
geneLength: {},
mutType: mutType,
mutType: defaultMutType,
temporalConfidence: {exists: false, display: false, on: false},
layout: defaults.layout,
distanceMeasure: defaults.distanceMeasure,
@@ -28,7 +28,7 @@ export const encodeColorByGenotype = ({ gene, positions }) => {
export const decodeColorByGenotype = (colorBy, geneLengths) => {
// If we're passed a map of gene name → length, then validate the decoded
// gene name and positions. Otherwise, just decode without validation.
const validate = geneLengths != null;
const validate = typeof geneLengths === "object" && Object.keys(geneLengths).length;

// Split the encoded string into tokens of gene and positions.
const match = colorBy.match(/^gt-(.+)_([0-9,]+)$/);
@@ -38,7 +38,7 @@ export const reallyBigNumber = 10000000;
export const LBItime_window = 0.5;
export const LBItau = 0.0005;
export const attemptUntangle = false;
export const mutType = "aa";
export const defaultMutType = "aa";
export const nucleotide_gene = "nuc";
export const plot_frequencies = false;
export const genericDomain = [0, 0.111, 0.222, 0.333, 0.444, 0.555, 0.666, 0.777, 0.888, 1.0];

0 comments on commit 2bfe1dc

Please sign in to comment.
You can’t perform that action at this time.