auspice
Version:
Web app for visualizing pathogen evolution
717 lines (657 loc) • 28.9 kB
JavaScript
import queryString from "query-string";
import { numericToCalendar, calendarToNumeric } from "../util/dateHelpers";
import { reallySmallNumber, twoColumnBreakpoint, defaultColorBy, defaultGeoResolution, defaultDateRange, nucleotide_gene } from "../util/globals";
import { calcBrowserDimensionsInitialState } from "../reducers/browserDimensions";
import { strainNameToIdx, getIdxMatchingLabel, calculateVisiblityAndBranchThickness } from "../util/treeVisibilityHelpers";
import { constructVisibleTipLookupBetweenTrees } from "../util/treeTangleHelpers";
import { calcTipRadii } from "../util/tipRadiusHelpers";
import { getDefaultControlsState } from "../reducers/controls";
import { countTraitsAcrossTree, calcTotalTipsInTree } from "../util/treeCountingHelpers";
import { calcEntropyInView } from "../util/entropy";
import { treeJsonToState } from "../util/treeJsonProcessing";
import { entropyCreateState } from "../util/entropyCreateStateFromJsons";
import { determineColorByGenotypeMutType, calcNodeColor } from "../util/colorHelpers";
import { calcColorScale } from "../util/colorScale";
import { computeMatrixFromRawData } from "../util/processFrequencies";
import { applyInViewNodesToTree } from "../actions/tree";
import { isColorByGenotype, decodeColorByGenotype } from "../util/getGenotype";
import { getTraitFromNode, getDivFromNode } from "../util/treeMiscHelpers";
export const doesColorByHaveConfidence = (controlsState, colorBy) =>
controlsState.coloringsPresentOnTreeWithConfidence.has(colorBy);
export const getMinCalDateViaTree = (nodes, state) => {
/* slider should be earlier than actual day */
/* if no date, use some default dates - slider will not be visible */
const minNumDate = getTraitFromNode(nodes[0], "num_date");
return (minNumDate === undefined) ?
state.dateMaxNumeric - defaultDateRange :
numericToCalendar(minNumDate - 0.01);
};
export const getMaxCalDateViaTree = (nodes) => {
let maxNumDate = reallySmallNumber;
nodes.forEach((node) => {
const numDate = getTraitFromNode(node, "num_date");
if (numDate !== undefined && numDate > maxNumDate) {
maxNumDate = numDate;
}
});
maxNumDate += 0.01; /* slider should be later than actual day */
return numericToCalendar(maxNumDate);
};
/* need a (better) way to keep the queryParams all in "sync" */
const modifyStateViaURLQuery = (state, query) => {
// console.log("Query incoming: ", query);
if (query.l) {
state["layout"] = query.l;
}
if (query.gmin) {
state["zoomMin"] = parseInt(query.gmin, 10);
}
if (query.gmax) {
state["zoomMax"] = parseInt(query.gmax, 10);
}
if (query.m && state.branchLengthsToDisplay === "divAndDate") {
state["distanceMeasure"] = query.m;
}
if (query.c) {
state["colorBy"] = query.c;
}
if (query.r) {
state["geoResolution"] = query.r;
}
if (query.p && state.canTogglePanelLayout && (query.p === "full" || query.p === "grid")) {
state["panelLayout"] = query.p;
}
if (query.d) {
const proposed = query.d.split(",");
state.panelsToDisplay = state.panelsAvailable.filter((n) => proposed.indexOf(n) !== -1);
if (state.panelsToDisplay.indexOf("map") === -1 || state.panelsToDisplay.indexOf("tree") === -1) {
state["panelLayout"] = "full";
}
}
if (query.dmin) {
state["dateMin"] = query.dmin;
state["dateMinNumeric"] = calendarToNumeric(query.dmin);
}
if (query.dmax) {
state["dateMax"] = query.dmax;
state["dateMaxNumeric"] = calendarToNumeric(query.dmax);
}
for (const filterKey of Object.keys(query).filter((c) => c.startsWith('f_'))) {
state.filters[filterKey.replace('f_', '')] = query[filterKey].split(',');
}
if (query.animate) {
const params = query.animate.split(',');
// console.log("start animation!", params);
window.NEXTSTRAIN.animationStartPoint = calendarToNumeric(params[0]);
window.NEXTSTRAIN.animationEndPoint = calendarToNumeric(params[1]);
state.dateMin = params[0];
state.dateMax = params[1];
state.dateMinNumeric = calendarToNumeric(params[0]);
state.dateMaxNumeric = calendarToNumeric(params[1]);
state.mapAnimationShouldLoop = params[2] === "1";
state.mapAnimationCumulative = params[3] === "1";
state.mapAnimationDurationInMilliseconds = parseInt(params[4], 10);
state.animationPlayPauseButton = "Pause";
} else {
state.animationPlayPauseButton = "Play";
}
return state;
};
const restoreQueryableStateToDefaults = (state) => {
for (const key of Object.keys(state.defaults)) {
switch (typeof state.defaults[key]) {
case "string": {
state[key] = state.defaults[key];
break;
}
case "object": { /* can't use Object.assign, must deep clone instead */
state[key] = JSON.parse(JSON.stringify(state.defaults[key]));
break;
}
default: {
console.error("unknown typeof for default state of ", key);
}
}
}
/* dateMin & dateMax get set to their bounds */
state["dateMin"] = state["absoluteDateMin"];
state["dateMax"] = state["absoluteDateMax"];
state["dateMinNumeric"] = state["absoluteDateMinNumeric"];
state["dateMaxNumeric"] = state["absoluteDateMaxNumeric"];
state["zoomMax"] = undefined;
state["zoomMin"] = undefined;
state["panelLayout"] = calcBrowserDimensionsInitialState().width > twoColumnBreakpoint ? "grid" : "full";
state.panelsToDisplay = state.panelsAvailable.slice();
// console.log("state now", state);
return state;
};
const modifyStateViaMetadata = (state, metadata) => {
if (metadata.date_range) {
/* this may be useful if, e.g., one were to want to display an outbreak
from 2000-2005 (the default is the present day) */
if (metadata.date_range.date_min) {
state["dateMin"] = metadata.date_range.date_min;
state["dateMinNumeric"] = calendarToNumeric(state["dateMin"]);
state["absoluteDateMin"] = metadata.date_range.date_min;
state["absoluteDateMinNumeric"] = calendarToNumeric(state["absoluteDateMin"]);
state["mapAnimationStartDate"] = metadata.date_range.date_min;
}
if (metadata.date_range.date_max) {
state["dateMax"] = metadata.date_range.date_max;
state["dateMaxNumeric"] = calendarToNumeric(state["dateMax"]);
state["absoluteDateMax"] = metadata.date_range.date_max;
state["absoluteDateMaxNumeric"] = calendarToNumeric(state["absoluteDateMax"]);
}
}
if (metadata.analysisSlider) {
state["analysisSlider"] = {key: metadata.analysisSlider, valid: false};
}
if (metadata.filters) {
metadata.filters.forEach((v) => {
state.filters[v] = [];
state.defaults.filters[v] = [];
});
} else {
console.warn("JSON did not include any filters");
}
if (metadata.displayDefaults) {
const keysToCheckFor = ["geoResolution", "colorBy", "distanceMeasure", "layout", "mapTriplicate"];
const expectedTypes = ["string", "string", "string", "string", "boolean"];
for (let i = 0; i < keysToCheckFor.length; i += 1) {
if (metadata.displayDefaults[keysToCheckFor[i]]) {
if (typeof metadata.displayDefaults[keysToCheckFor[i]] === expectedTypes[i]) { // eslint-disable-line valid-typeof
/* e.g. if key=geoResoltion, set both state.geoResolution and state.defaults.geoResolution */
state[keysToCheckFor[i]] = metadata.displayDefaults[keysToCheckFor[i]];
state.defaults[keysToCheckFor[i]] = metadata.displayDefaults[keysToCheckFor[i]];
} else {
console.error("Skipping (meta.json) default for ", keysToCheckFor[i], "as it is not of type ", expectedTypes[i]);
}
}
}
}
if (metadata.panels) {
state.panelsAvailable = metadata.panels.slice();
state.panelsToDisplay = metadata.panels.slice();
} else {
state.panelsAvailable = ["tree"];
state.panelsToDisplay = ["tree"];
}
/* if we lack geoResolutions, remove map from panels to display */
if (!metadata.geoResolutions || !metadata.geoResolutions.length) {
state.panelsAvailable = state.panelsAvailable.filter((item) => item !== "map");
state.panelsToDisplay = state.panelsToDisplay.filter((item) => item !== "map");
}
/* if we lack genome annotations, remove entropy from panels to display */
if (!metadata.genomeAnnotations || !metadata.genomeAnnotations.nuc) {
state.panelsAvailable = state.panelsAvailable.filter((item) => item !== "entropy");
state.panelsToDisplay = state.panelsToDisplay.filter((item) => item !== "entropy");
}
/* if only map or only tree, then panelLayout must be full */
/* note - this will be overwritten by the URL query */
if (state.panelsAvailable.indexOf("map") === -1 || state.panelsAvailable.indexOf("tree") === -1) {
state.panelLayout = "full";
state.canTogglePanelLayout = false;
}
/* genome annotations in metadata */
if (metadata.genomeAnnotations) {
for (const gene of Object.keys(metadata.genomeAnnotations)) {
state.geneLength[gene] = metadata.genomeAnnotations[gene].end - metadata.genomeAnnotations[gene].start;
if (gene !== nucleotide_gene) {
state.geneLength[gene] /= 3;
}
}
} else {
console.warn("JSONs did not include `genome_annotations`");
}
return state;
};
const modifyControlsStateViaTree = (state, tree, treeToo, colorings) => {
state["dateMin"] = getMinCalDateViaTree(tree.nodes, state);
state["dateMax"] = getMaxCalDateViaTree(tree.nodes);
state.dateMinNumeric = calendarToNumeric(state.dateMin);
state.dateMaxNumeric = calendarToNumeric(state.dateMax);
if (treeToo) {
const min = getMinCalDateViaTree(treeToo.nodes, state);
const max = getMaxCalDateViaTree(treeToo.nodes);
const minNumeric = calendarToNumeric(min);
const maxNumeric = calendarToNumeric(max);
if (minNumeric < state.dateMinNumeric) {
state.dateMinNumeric = minNumeric;
state.dateMin = min;
}
if (maxNumeric > state.dateMaxNumeric) {
state.dateMaxNumeric = maxNumeric;
state.dateMax = max;
}
}
/* set absolutes */
state["absoluteDateMin"] = state["dateMin"];
state["absoluteDateMax"] = state["dateMax"];
state.absoluteDateMinNumeric = calendarToNumeric(state.absoluteDateMin);
state.absoluteDateMaxNumeric = calendarToNumeric(state.absoluteDateMax);
/* For the colorings (defined in the JSON) we need to check whether they
(a) actually exist on the tree and (b) have confidence values.
TODO - this whole file should be reorganised to make things clearer.
perhaps there's a better place to put this... */
state.coloringsPresentOnTree = new Set();
state.coloringsPresentOnTreeWithConfidence = new Set(); // subset of above
let coloringsToCheck = [];
if (colorings) {
coloringsToCheck = Object.keys(colorings);
}
let [aaMuts, nucMuts] = [false, false];
const examineNodes = function examineNodes(nodes) {
nodes.forEach((node) => {
/* check colorBys */
coloringsToCheck.forEach((colorBy) => {
if (!state.coloringsPresentOnTreeWithConfidence.has(colorBy)) {
if (getTraitFromNode(node, colorBy, {confidence: true})) {
state.coloringsPresentOnTreeWithConfidence.add(colorBy);
state.coloringsPresentOnTree.add(colorBy);
} else if (getTraitFromNode(node, colorBy)) {
state.coloringsPresentOnTree.add(colorBy);
}
}
});
/* check mutations */
if (node.branch_attrs && node.branch_attrs.mutations) {
const keys = Object.keys(node.branch_attrs.mutations);
if (keys.length > 1 || (keys.length === 1 && keys[0]!=="nuc")) aaMuts = true;
if (keys.includes("nuc")) nucMuts = true;
}
});
};
examineNodes(tree.nodes);
if (treeToo) examineNodes(treeToo.nodes);
/* 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";
}
if (aaMuts || nucMuts) {
state.coloringsPresentOnTree.add("gt");
}
/* does the tree have date information? if not, disable controls, modify view */
const numDateAtRoot = getTraitFromNode(tree.nodes[0], "num_date") !== undefined;
const divAtRoot = getDivFromNode(tree.nodes[0]) !== undefined;
state.branchLengthsToDisplay = (numDateAtRoot && divAtRoot) ? "divAndDate" :
numDateAtRoot ? "dateOnly" :
"divOnly";
/* 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;
state.selectedBranchLabel = tree.availableBranchLabels.indexOf("clade") !== -1 ? "clade" : "none";
state.temporalConfidence = getTraitFromNode(tree.nodes[0], "num_date", {confidence: true}) ?
{exists: true, display: true, on: false} :
{exists: false, display: false, on: false};
return state;
};
const checkAndCorrectErrorsInState = (state, metadata, query, tree) => {
/* want to check that the (currently set) colorBy (state.colorBy) is valid,
* and fall-back to an available colorBy if not
*/
if (!metadata.colorings) {
metadata.colorings = {};
}
const fallBackToDefaultColorBy = () => {
const availableNonGenotypeColorBys = Object.keys(metadata.colorings)
.filter((colorKey) => colorKey !== "gt");
if (metadata.displayDefaults && metadata.displayDefaults.colorBy && availableNonGenotypeColorBys.indexOf(metadata.displayDefaults.colorBy) !== -1) {
console.warn("colorBy falling back to", metadata.displayDefaults.colorBy);
state.colorBy = metadata.displayDefaults.colorBy;
state.defaults.colorBy = metadata.displayDefaults.colorBy;
} else if (availableNonGenotypeColorBys.length) {
if (availableNonGenotypeColorBys.indexOf(defaultColorBy) !== -1) {
state.colorBy = defaultColorBy;
state.defaults.colorBy = defaultColorBy;
} else {
console.error("Error detected trying to set colorBy to", state.colorBy, "falling back to", availableNonGenotypeColorBys[0]);
state.colorBy = availableNonGenotypeColorBys[0];
state.defaults.colorBy = availableNonGenotypeColorBys[0];
}
} else {
console.error("Error detected trying to set colorBy to", state.colorBy, " as there are no color options defined in the JSONs!");
state.colorBy = "none";
state.defaults.colorBy = "none";
}
delete query.c;
};
if (isColorByGenotype(state.colorBy)) {
/* Check that the genotype is valid with the current data */
if (!decodeColorByGenotype(state.colorBy, state.geneLength)) {
fallBackToDefaultColorBy();
}
} else if (Object.keys(metadata.colorings).indexOf(state.colorBy) === -1) {
/* if it's a _non_ genotype colorBy AND it's not a valid option, fall back to the default */
fallBackToDefaultColorBy();
}
/* zoom */
if (state.zoomMax > state["absoluteZoomMax"]) { state.zoomMax = state["absoluteZoomMax"]; }
if (state.zoomMin < state["absoluteZoomMin"]) { state.zoomMin = state["absoluteZoomMin"]; }
if (state.zoomMin > state.zoomMax) {
const tempMin = state.zoomMin;
state.zoomMin = state.zoomMax;
state.zoomMax = tempMin;
}
/* colorBy confidence */
state["colorByConfidence"] = doesColorByHaveConfidence(state, state["colorBy"]);
/* distanceMeasure */
if (["div", "num_date"].indexOf(state["distanceMeasure"]) === -1) {
state["distanceMeasure"] = "num_date";
console.error("Error detected. Setting distanceMeasure to ", state["distanceMeasure"]);
}
/* geoResolutions */
if (metadata.geoResolutions) {
const availableGeoResultions = metadata.geoResolutions.map((i) => i.key);
if (availableGeoResultions.indexOf(state["geoResolution"]) === -1) {
/* fallbacks: JSON defined default, then hardocded default, then any available */
if (metadata.displayDefaults && metadata.displayDefaults.geoResolution && availableGeoResultions.indexOf(metadata.displayDefaults.geoResolution) !== -1) {
state.geoResolution = metadata.displayDefaults.geoResolution;
} else if (availableGeoResultions.indexOf(defaultGeoResolution) !== -1) {
state.geoResolution = defaultGeoResolution;
} else {
state.geoResolution = availableGeoResultions[0];
}
console.error("Error detected. Setting geoResolution to ", state.geoResolution);
delete query.r; // no-op if query.r doesn't exist
}
} else {
console.warn("JSONs did not include `geoResolutions`");
}
/* temporalConfidence */
if (state.temporalConfidence.exists) {
if (state.layout !== "rect") {
state.temporalConfidence.display = false;
state.temporalConfidence.on = false;
} else if (state.distanceMeasure === "div") {
state.temporalConfidence.display = false;
state.temporalConfidence.on = false;
}
}
/* if colorBy is a genotype then we need to set mutType */
if (state.colorBy) {
const maybeMutType = determineColorByGenotypeMutType(state.colorBy);
if (maybeMutType) {
state.mutType = maybeMutType;
}
}
/* are filters valid? */
const activeFilters = Object.keys(state.filters).filter((f) => f.length);
const stateCounts = countTraitsAcrossTree(tree.nodes, activeFilters, false, true);
for (const filterType of activeFilters) {
const validValues = state.filters[filterType]
.filter((filterValue) => stateCounts[filterType].has(filterValue));
state.filters[filterType] = validValues;
if (!validValues.length) {
delete query[`f_${filterType}`];
} else {
query[`f_${filterType}`] = validValues.join(",");
}
}
/* can we display branch length by div or num_date? */
if (query.m && state.branchLengthsToDisplay !== "divAndDate") {
delete query.m;
}
return state;
};
const modifyTreeStateVisAndBranchThickness = (oldState, tipSelected, cladeSelected, controlsState) => {
/* calculate new branch thicknesses & visibility */
let tipSelectedIdx = 0;
/* check if the query defines a strain to be selected */
let newIdxRoot = oldState.idxOfInViewRootNode;
if (tipSelected) {
tipSelectedIdx = strainNameToIdx(oldState.nodes, tipSelected);
oldState.selectedStrain = tipSelected;
}
if (cladeSelected) {
const cladeSelectedIdx = cladeSelected === 'root' ? 0 : getIdxMatchingLabel(oldState.nodes, "clade", cladeSelected);
oldState.selectedClade = cladeSelected;
newIdxRoot = applyInViewNodesToTree(cladeSelectedIdx, oldState); // tipSelectedIdx, oldState);
}
const visAndThicknessData = calculateVisiblityAndBranchThickness(
oldState,
controlsState,
{dateMinNumeric: controlsState.dateMinNumeric, dateMaxNumeric: controlsState.dateMaxNumeric},
{tipSelectedIdx, validIdxRoot: newIdxRoot}
);
const newState = Object.assign({}, oldState, visAndThicknessData);
newState.stateCountAttrs = Object.keys(controlsState.filters);
newState.idxOfInViewRootNode = newIdxRoot;
newState.visibleStateCounts = countTraitsAcrossTree(newState.nodes, newState.stateCountAttrs, newState.visibility, true);
newState.totalStateCounts = countTraitsAcrossTree(newState.nodes, newState.stateCountAttrs, false, true); // eslint-disable-line
if (tipSelectedIdx) { /* i.e. query.s was set */
newState.tipRadii = calcTipRadii({tipSelectedIdx, colorScale: controlsState.colorScale, tree: newState});
newState.tipRadiiVersion = 1;
}
return newState;
};
const removePanelIfPossible = (panels, name) => {
const idx = panels.indexOf(name);
if (idx !== -1) {
panels.splice(idx, 1);
}
};
const modifyControlsViaTreeToo = (controls, name) => {
controls.showTreeToo = name;
controls.showTangle = true;
controls.layout = "rect"; /* must be rectangular for two trees */
controls.panelsToDisplay = controls.panelsToDisplay.slice();
removePanelIfPossible(controls.panelsToDisplay, "map");
removePanelIfPossible(controls.panelsToDisplay, "entropy");
removePanelIfPossible(controls.panelsToDisplay, "frequencies");
controls.canTogglePanelLayout = false;
controls.panelLayout = "full";
return controls;
};
/**
* The v2 JSON spec defines colorings as a list, so that order is guaranteed.
* Prior to this, we used a dict, where key insertion order is (guaranteed? essentially always?)
* to be respected. By simply converting it back to a dict, all the auspice
* code may continue to work. This should be attended to in the future.
* @param {obj} coloringsList list of objects
* @returns {obj} a dictionary representation, where the "key" property of each element
* in the list has become a property of the object
*/
const convertColoringsListToDict = (coloringsList) => {
const colorings = {};
coloringsList.forEach((coloring) => {
colorings[coloring.key] = coloring;
delete colorings[coloring.key].key;
});
return colorings;
};
/**
*
* A lot of this is simply changing augur's snake_case to auspice's camelCase
*/
const createMetadataStateFromJSON = (json) => {
const metadata = {};
if (json.meta.colorings) {
metadata.colorings = convertColoringsListToDict(json.meta.colorings);
}
metadata.title = json.meta.title;
metadata.updated = json.meta.updated;
if (json.meta.description) {
metadata.description = json.meta.description;
}
if (json.version) {
metadata.version = json.version;
}
if (json.meta.maintainers) {
metadata.maintainers = json.meta.maintainers;
}
if (json.meta.build_url) {
metadata.buildUrl = json.meta.build_url;
}
if (json.meta.genome_annotations) {
metadata.genomeAnnotations = json.meta.genome_annotations;
}
if (json.meta.filters) {
metadata.filters = json.meta.filters;
}
if (json.meta.panels) {
metadata.panels = json.meta.panels;
}
if (json.meta.display_defaults) {
metadata.displayDefaults = {};
const jsonKeyToAuspiceKey = {
color_by: "colorBy",
geo_resolution: "geoResolution",
distance_measure: "distanceMeasure",
map_triplicate: "mapTriplicate",
layout: "layout"
};
for (const [jsonKey, auspiceKey] of Object.entries(jsonKeyToAuspiceKey)) {
if (json.meta.display_defaults[jsonKey]) {
metadata.displayDefaults[auspiceKey] = json.meta.display_defaults[jsonKey];
}
}
}
if (json.meta.geo_resolutions) {
metadata.geoResolutions = json.meta.geo_resolutions;
}
if (Object.prototype.hasOwnProperty.call(metadata, "loaded")) {
console.error("Metadata JSON must not contain the key \"loaded\". Ignoring.");
}
metadata.loaded = true;
return metadata;
};
export const createStateFromQueryOrJSONs = ({
json = false, /* raw json data - completely nuke existing redux state */
secondTreeDataset = false,
oldState = false, /* existing redux state (instead of jsons) */
narrativeBlocks = false,
mainTreeName = false,
secondTreeName = false,
query
}) => {
let tree, treeToo, entropy, controls, metadata, narrative, frequencies;
/* first task is to create metadata, entropy, controls & tree partial state */
if (json) {
/* create metadata state */
metadata = createMetadataStateFromJSON(json);
/* entropy state */
entropy = entropyCreateState(metadata.genomeAnnotations);
/* new tree state(s) */
tree = treeJsonToState(json.tree);
tree.debug = "LEFT";
tree.name = mainTreeName;
metadata.mainTreeNumTips = calcTotalTipsInTree(tree.nodes);
if (secondTreeDataset) {
treeToo = treeJsonToState(secondTreeDataset.tree);
treeToo.debug = "RIGHT";
treeToo.name = secondTreeName;
/* TODO: calc & display num tips in 2nd tree */
// metadata.secondTreeNumTips = calcTotalTipsInTree(treeToo.nodes);
}
/* new controls state - don't apply query yet (or error check!) */
controls = getDefaultControlsState();
controls = modifyControlsStateViaTree(controls, tree, treeToo, metadata.colorings);
controls = modifyStateViaMetadata(controls, metadata);
controls["absoluteZoomMin"] = 0;
controls["absoluteZoomMax"] = entropy.lengthSequence;
} else if (oldState) {
/* revisit this - but it helps prevent bugs */
controls = {...oldState.controls};
entropy = {...oldState.entropy};
tree = {...oldState.tree};
treeToo = {...oldState.treeToo};
metadata = {...oldState.metadata};
frequencies = {...oldState.frequencies};
controls = restoreQueryableStateToDefaults(controls);
}
if (narrativeBlocks) {
narrative = narrativeBlocks;
const n = parseInt(query.n, 10) || 0;
controls = modifyStateViaURLQuery(controls, queryString.parse(narrative[n].query));
query = {n}; // eslint-disable-line
console.log("redux state changed to relfect n of", n);
} else {
controls = modifyStateViaURLQuery(controls, query);
}
controls = checkAndCorrectErrorsInState(controls, metadata, query, tree); /* must run last */
/* calculate colours if loading from JSONs or if the query demands change */
if (json || controls.colorBy !== oldState.colorBy) {
const colorScale = calcColorScale(controls.colorBy, controls, tree, treeToo, metadata);
const nodeColors = calcNodeColor(tree, colorScale);
controls.colorScale = colorScale;
controls.colorByConfidence = doesColorByHaveConfidence(controls, controls.colorBy);
tree.nodeColorsVersion = colorScale.version;
tree.nodeColors = nodeColors;
}
if (query.clade) {
tree = modifyTreeStateVisAndBranchThickness(tree, undefined, query.clade, controls);
} else { /* if not specifically given in URL, zoom to root */
tree = modifyTreeStateVisAndBranchThickness(tree, undefined, undefined, controls);
}
tree = modifyTreeStateVisAndBranchThickness(tree, query.s, undefined, controls);
if (treeToo && treeToo.loaded) {
treeToo.nodeColorsVersion = tree.nodeColorsVersion;
treeToo.nodeColors = calcNodeColor(treeToo, controls.colorScale);
treeToo = modifyTreeStateVisAndBranchThickness(treeToo, query.s, undefined, controls);
controls = modifyControlsViaTreeToo(controls, treeToo.name);
treeToo.tangleTipLookup = constructVisibleTipLookupBetweenTrees(tree.nodes, treeToo.nodes, tree.visibility, treeToo.visibility);
}
/* calculate entropy in view */
if (entropy.loaded) {
const [entropyBars, entropyMaxYVal] = calcEntropyInView(tree.nodes, tree.visibility, controls.mutType, entropy.geneMap, entropy.showCounts);
entropy.bars = entropyBars;
entropy.maxYVal = entropyMaxYVal;
entropy.zoomMax = controls["zoomMax"];
entropy.zoomMin = controls["zoomMin"];
entropy.zoomCoordinates = [controls["zoomMin"], controls["zoomMax"]];
}
/* update frequencies if they exist (not done for new JSONs) */
if (frequencies && frequencies.loaded) {
frequencies.version++;
frequencies.matrix = computeMatrixFromRawData(
frequencies.data,
frequencies.pivots,
tree.nodes,
tree.visibility,
controls.colorScale,
controls.colorBy
);
}
return {tree, treeToo, metadata, entropy, controls, narrative, frequencies, query};
};
export const createTreeTooState = ({
treeTooJSON, /* raw json data */
oldState,
originalTreeUrl,
secondTreeUrl /* treeToo URL */
}) => {
/* TODO: reconsile choices (filters, colorBys etc) with this new tree */
/* TODO: reconcile query with visibility etc */
let controls = oldState.controls;
const tree = Object.assign({}, oldState.tree);
tree.name = originalTreeUrl;
let treeToo = treeJsonToState(treeTooJSON);
treeToo.name = secondTreeUrl;
treeToo.debug = "RIGHT";
controls = modifyControlsStateViaTree(controls, tree, treeToo, oldState.metadata.colorings);
controls = modifyControlsViaTreeToo(controls, secondTreeUrl);
treeToo = modifyTreeStateVisAndBranchThickness(treeToo, tree.selectedStrain, undefined, controls);
/* calculate colours if loading from JSONs or if the query demands change */
const colorScale = calcColorScale(controls.colorBy, controls, tree, treeToo, oldState.metadata);
const nodeColors = calcNodeColor(treeToo, colorScale);
tree.nodeColors = calcNodeColor(tree, colorScale); // also update main tree's colours
tree.nodeColorsVersion++;
controls.colorScale = colorScale;
controls.colorByConfidence = doesColorByHaveConfidence(controls, controls.colorBy);
treeToo.nodeColorsVersion = colorScale.version;
treeToo.nodeColors = nodeColors;
treeToo.tangleTipLookup = constructVisibleTipLookupBetweenTrees(
tree.nodes, treeToo.nodes, tree.visibility, treeToo.visibility
);
// if (tipSelectedIdx) { /* i.e. query.s was set */
// tree.tipRadii = calcTipRadii({tipSelectedIdx, colorScale: controls.colorScale, tree});
// tree.tipRadiiVersion = 1;
// }
return {tree, treeToo, controls};
};