diff --git a/.all-contributorsrc b/.all-contributorsrc index 7a3ba708..ef7e1780 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -77,7 +77,6 @@ "infra" ] } - ], "contributorsPerLine": 7, "projectName": "taxonium", diff --git a/taxonium_web_client/src/Taxonium.jsx b/taxonium_web_client/src/Taxonium.jsx index b0d86f62..f5e2f5c3 100644 --- a/taxonium_web_client/src/Taxonium.jsx +++ b/taxonium_web_client/src/Taxonium.jsx @@ -71,9 +71,9 @@ function Taxonium({ // This can happen with e.g. nextstrain json if (data.base_data && data.base_data.nodes) { const n = data.base_data.nodes[0]; - if (!n.x_dist) { + if (!n.hasOwnProperty("x_dist")) { setxType("x_time"); - } else if (!n.x_time) { + } else if (!n.hasOwnProperty("x_time")) { setxType("x_dist"); } } diff --git a/taxonium_web_client/src/hooks/useInputHelper.js b/taxonium_web_client/src/hooks/useInputHelper.js index 43dadfa1..9c1d8ea2 100644 --- a/taxonium_web_client/src/hooks/useInputHelper.js +++ b/taxonium_web_client/src/hooks/useInputHelper.js @@ -156,7 +156,7 @@ export const useInputHelper = ({ ); const newQuery = { treeUrl: tree_file.name, - ladderizeTree: tree_file.ladderize === "true", + ladderizeTree: tree_file.ladderize === true, treeType: tree_file.filetype, }; if (meta_file) { diff --git a/taxonium_web_client/src/utils/processNewick.js b/taxonium_web_client/src/utils/processNewick.js index b1598ece..227291b9 100644 --- a/taxonium_web_client/src/utils/processNewick.js +++ b/taxonium_web_client/src/utils/processNewick.js @@ -98,7 +98,6 @@ async function cleanup(tree) { const scale_x = 450 / ref_x; - console.log(scale_y, "scale_y"); tree.node.forEach((node) => { node.x_dist = node.x_dist * scale_x; node.y = node.y * scale_y; @@ -106,7 +105,6 @@ async function cleanup(tree) { } export async function processNewick(data, sendStatusMessage) { - console.log("got data", data); let the_data; the_data = await fetch_or_extract(data, sendStatusMessage, "tree"); @@ -139,11 +137,8 @@ export async function processNewick(data, sendStatusMessage) { } assignNumTips(tree.root); const total_tips = tree.root.num_tips; - console.log("tree.root.num_tips", tree.root.num_tips); if (data.ladderize) { - console.log("ladderizing"); - sortWithNumTips(tree.root); tree.node = kn_expand_node(tree.root); } @@ -186,7 +181,7 @@ export async function processNewick(data, sendStatusMessage) { rootId: 0, overwrite_config: { num_tips: total_tips }, }; - console.log(JSON.stringify(output)); + return output; } @@ -198,8 +193,6 @@ export async function processMetadataFile(data, sendStatusMessage) { the_data = await fetch_or_extract(data, logStatusToConsole, "metadata"); - console.log("Got metadata file"); - const lines = the_data.split("\n"); const output = {}; let separator; @@ -275,6 +268,6 @@ export async function processNewickAndMetadata(data, sendStatusMessage) { Object.assign(node, blanks); } }); - console.log(tree); + return tree; } diff --git a/taxonium_web_client/src/utils/processNextstrain.js b/taxonium_web_client/src/utils/processNextstrain.js index 265677c8..6434cd3c 100644 --- a/taxonium_web_client/src/utils/processNextstrain.js +++ b/taxonium_web_client/src/utils/processNextstrain.js @@ -5,6 +5,85 @@ import { kn_expand_node, kn_calxy } from "./jstree"; const emptyList = []; +const nodeMutationsFromNextStrainToTaxonium = ( + mutations, + unique_mutations, + mutation_lookup +) => { + //console.log("mutations", mutations); + const keys = Object.keys(mutations); + const nuc_muts = mutations["nuc"] ? mutations["nuc"] : []; + + const genes = keys.filter((key) => key !== "nuc"); + const taxonium_muts = []; + nuc_muts.forEach((nuc_mut) => { + // input format is like "C123T", we want to break this into old_residue, position and new_residue + // use regex to match the position + const position = nuc_mut.match(/\d+/g); + const index_of_position = nuc_mut.indexOf(position[0]); + const previous_residue = nuc_mut.substring(0, index_of_position); + const new_residue = nuc_mut.substring( + index_of_position + position[0].length + ); + const tax_format = { + type: "nt", + gene: "nt", + previous_residue, + new_residue, + residue_pos: parseInt(position[0]), + }; + const jsonned = JSON.stringify(tax_format); + //console.log("jsonned", jsonned); + if (mutation_lookup[jsonned]) { + taxonium_muts.push(mutation_lookup[jsonned]); + } else { + unique_mutations.push({ + ...tax_format, + mutation_id: unique_mutations.length, + }); + const this_index = unique_mutations.length - 1; + mutation_lookup[jsonned] = this_index; + taxonium_muts.push(this_index); + } + }); + + genes.forEach((gene) => { + const gene_muts = mutations[gene]; + gene_muts.forEach((gene_mut) => { + // input format is like "Q123F", we want to break this into old_residue, position and new_residue + // use regex to match the position + const position = gene_mut.match(/\d+/g); + const index_of_position = gene_mut.indexOf(position[0]); + const previous_residue = gene_mut.substring(0, index_of_position); + const new_residue = gene_mut.substring( + index_of_position + position[0].length + ); + const tax_format = { + type: "aa", + gene, + previous_residue, + new_residue, + residue_pos: parseInt(position[0]), + }; + const jsonned = JSON.stringify(tax_format); + //console.log("jsonned", jsonned); + if (mutation_lookup[jsonned]) { + taxonium_muts.push(mutation_lookup[jsonned]); + } else { + unique_mutations.push({ + ...tax_format, + mutation_id: unique_mutations.length, + }); + const this_index = unique_mutations.length - 1; + mutation_lookup[jsonned] = this_index; + taxonium_muts.push(this_index); + } + }); + }); + + return taxonium_muts; +}; + async function do_fetch(url, sendStatusMessage, whatIsBeingDownloaded) { if (!sendStatusMessage) { sendStatusMessage = () => {}; @@ -81,7 +160,7 @@ async function cleanup(tree) { const cleaned = { name: node.name.replace(/'/g, ""), parent_id: node.parent ? node.parent.node_id : node.node_id, - mutations: emptyList, + mutations: node.mutations ? node.mutations : emptyList, y: node.y, num_tips: node.num_tips, is_tip: node.child.length === 0, @@ -129,7 +208,7 @@ async function cleanup(tree) { }); } -async function processJsTree(tree, data, sendStatusMessage) { +async function processJsTree(tree, data, config, sendStatusMessage) { function assignNumTips(node) { if (node.child.length === 0) { node.num_tips = 1; @@ -169,6 +248,7 @@ async function processJsTree(tree, data, sendStatusMessage) { tree.node.forEach((node) => { node.d = node.pre_x_dist; }); + kn_calxy(tree, true); // kn_calxy sets x -> move x to x_dist tree.node.forEach((node) => { @@ -218,16 +298,22 @@ async function processJsTree(tree, data, sendStatusMessage) { node_to_mut: {}, rootMutations: [], rootId: 0, - overwrite_config: { num_tips: total_tips }, + overwrite_config: { ...config, num_tips: total_tips }, }; + return output; } function json_preorder(root) { + let n_tips = 0; const parents = {}; parents[root.name] = null; const path = []; const stack = [root]; + + const unique_mutations = []; + + const mutation_lookup = {}; while (stack.length > 0) { const nodeJson = stack.pop(); let div = null; @@ -238,7 +324,7 @@ function json_preorder(root) { if (nodeJson.node_attrs.num_date) { time = nodeJson.node_attrs.num_date.value; } - + //console.log(nodeJson); // this is the node format for downstream processing const parsedNode = { name: nodeJson.name, @@ -246,6 +332,14 @@ function json_preorder(root) { meta: "", hl: false, hidden: false, + mutations: + nodeJson.branch_attrs && nodeJson.branch_attrs.mutations + ? nodeMutationsFromNextStrainToTaxonium( + nodeJson.branch_attrs.mutations, + unique_mutations, + mutation_lookup + ) + : [], }; // assign distance @@ -273,15 +367,22 @@ function json_preorder(root) { parents[childJson.name] = parsedNode; stack.push(childJson); } + } else { + n_tips += 1; } } - return [path, parents]; + return { path, parents, n_tips, unique_mutations }; } async function json_to_tree(json) { const rootJson = json.tree; - const [preorder, parents] = json_preorder(rootJson); - let n_tips = 0; + const { + path: preorder, + parents, + n_tips, + unique_mutations, + } = json_preorder(rootJson); + const nodes = []; let root; for (const node of preorder) { @@ -303,32 +404,46 @@ async function json_to_tree(json) { nodes.push(node); } - return { + const jsTree = { // tree in jstree.js format node: nodes, error: 0, n_tips: n_tips, root: root, }; -} -export async function processNextstrain(data, sendStatusMessage) { - console.log("got data", data); - let the_data; + const config = {}; + + console.log("META", json.meta); + config.title = json.meta.title; + console.log("META PROV", json.meta.data_provenance); + config.source = + json.meta.data_provenance.map((source) => source.name).join(" & ") + + " on " + + json.meta.updated + + " in a build maintained by " + + json.meta.maintainers.map((source) => source.name).join(" & "); + config.overlay = `
This is a tree generated from a NextStrain JSON file, being visualised in Taxonium.
.`; + if (json.meta.build_url) { + config.overlay += `The NextStrain build is available here.
`; + } - the_data = await fetch_or_extract(data, sendStatusMessage, "tree"); + return { jsTree, config, unique_mutations }; +} - console.log("the_data", the_data); +export async function processNextstrain(data, sendStatusMessage) { + const the_data = await fetch_or_extract(data, sendStatusMessage, "tree"); sendStatusMessage({ message: "Parsing NS file", }); - const input_string = the_data; - - const jsTree = await json_to_tree(JSON.parse(input_string)); + const { jsTree, config, unique_mutations } = await json_to_tree( + JSON.parse(the_data) + ); - const output = await processJsTree(jsTree, data, sendStatusMessage); + const output = await processJsTree(jsTree, data, config, sendStatusMessage); + const node_to_mut = output.nodes.map((x) => x.mutations); - return output; + return { ...output, mutations: unique_mutations, node_to_mut: node_to_mut }; } diff --git a/taxonium_web_client/src/webworkers/localBackendWorker.js b/taxonium_web_client/src/webworkers/localBackendWorker.js index 9a179d18..0c488594 100644 --- a/taxonium_web_client/src/webworkers/localBackendWorker.js +++ b/taxonium_web_client/src/webworkers/localBackendWorker.js @@ -333,7 +333,6 @@ onmessage = async (event) => { //Process uploaded data: console.log("Worker onmessage"); const { data } = event; - console.log(data, "data"); if ( data.type === "upload" && data.data && @@ -360,8 +359,6 @@ onmessage = async (event) => { data.data.filename && data.data.filetype === "nextstrain" ) { - console.log("got nextstrain file", data.data); - processedUploadedData = await processNextstrain( data.data, sendStatusMessage