diff --git a/README.md b/README.md index 3490acd..680778b 100644 --- a/README.md +++ b/README.md @@ -101,4 +101,4 @@ Any bug reports, comments and suggestions are highly appreciated. Please An Application Note is in the works. Stay tuned. # Contact -For further support or bug reports please contact us via [email](mailto:jeffgower98@gmail.com). +For further support or bug reports please contact us via [@Jeff](mailto:jeffgower98@gmail.com) or [@Adrian](mailto:faflow456456@gmail.com). diff --git a/matt/app.py b/matt/app.py index 73506e8..ba2be87 100644 --- a/matt/app.py +++ b/matt/app.py @@ -153,7 +153,7 @@ def download(tree_id): file_without.write(tree_without + "\n") file_without.close() - tree_with = Tree(tree_lengths_json).to_newick() + tree_with = Tree(tree_lengths_json, enable_lengths=True).to_newick() path_with = os.path.join(session["working-directory"], (session["session-name"] + "_" if session["session-name"] else "") + "with_lengths.tree") file_with = open(path_with, "w") diff --git a/matt/static/css.css b/matt/static/css.css index c465f5e..77d8569 100644 --- a/matt/static/css.css +++ b/matt/static/css.css @@ -103,6 +103,12 @@ html, body { right: calc(15vw + 156px); } +#compact-button { + position: absolute; + top: 10px; + right: calc(15vw + 203px); +} + #labels-button { position: absolute; top: 10px; diff --git a/matt/static/js.js b/matt/static/js.js index 3365e18..644826a 100644 --- a/matt/static/js.js +++ b/matt/static/js.js @@ -41,6 +41,12 @@ $(function () { let interval; let enableLengths; let alignLabels = true; + let collapsedmap = {}; + let compactlist = []; + let compactmode = false; + let currenttree; + let counter_of_nodes = [] + let label_blacklist = []; // Gets the options initially getOptions(); @@ -397,6 +403,9 @@ $(function () { set_testing(xhr.getResponseHeader("Testing")); } + + + // Enables the undo and redo buttons (depending on edge cases) if (counter_of_trees > 1) { $("#undo-button").prop("disabled", false); @@ -408,6 +417,7 @@ $(function () { $("#redo-button").prop("disabled", true); } + // Saves the first snapshot // TODO This should be toggleable in options if (snapshotTrees.length == 0) { @@ -415,12 +425,30 @@ $(function () { description(trees[counter_of_trees - 1][0], "Original"); } + // Calls the draw function with the chosen tree (with or without branch lengths) - if (typeof xhr !== "undefined" && xhr.getResponseHeader("Length")) { - draw(JSON.parse(trees[counter_of_trees - 1][2])); - } else { - draw(JSON.parse(trees[counter_of_trees - 1][1])); + if(compactmode === false) { + if (typeof xhr !== "undefined" && xhr.getResponseHeader("Length")) { + currenttree = (trees[counter_of_trees - 1][2]); + draw(JSON.parse(trees[counter_of_trees - 1][2])); + } else { + currenttree = (trees[counter_of_trees - 1][1]); + draw(JSON.parse(trees[counter_of_trees - 1][1])); + } + }else{ + //const tempcalc = draw(data).calculateCompactTree(); + let t; + if (typeof xhr !== "undefined" && xhr.getResponseHeader("Length")) { + currenttree = (trees[counter_of_trees - 1][2]); + t = draw().calculateCompactTree(JSON.parse(currenttree),collapsedmap); + draw(t); + } else { + currenttree = (trees[counter_of_trees - 1][1]); + t = draw().calculateCompactTree(JSON.parse(currenttree),collapsedmap); + draw(t); + } } + } /** @@ -476,7 +504,7 @@ $(function () { if (snapshotTrees.find(element => element[0] == trees[counter_of_trees - 1][0])) { // Shows the info modal $("#info-modal-label").text("This snapshot already exists!") - $("#info-modal-body").text("This tree has already been snapshot.") + $("#info-modal-body").text("You already froze this tree topology.") $("#info-modal").modal("show"); return; } @@ -662,8 +690,26 @@ $(function () { * @param data tree informations */ function draw(data) { + + //const collapsedmap = {}; + if(Object.keys(collapsedmap).length === 0){ + data.forEach((n, index) => { + collapsedmap[n["id"]] = {left: false, right: false, left_id: null, right_id: null, label: "Collapsed"+index, + "collapsed-line-Y":null, + }; + label_blacklist.push(collapsedmap[n["id"]]["label"]); + }); + + } + //TODO: Rework this. It works but not really smooth. + if(typeof data === "undefined") return { calculateCompactTree: calculateCompactTree }; + + + + $("#outgroup-button").css("display", "none"); + xBefore = getTransform("x"); yBefore = getTransform("y"); scaleBefore = getTransform("scale"); @@ -727,6 +773,10 @@ $(function () { g = svg.g(); lines = g.g(); + + + + // Draws the frame around the tree g.add(svg.path("M0,0H" + maxX + "V" + maxY + "H0Z").attr({ fill: 'none', @@ -896,6 +946,291 @@ $(function () { toggleOutgroupButton(); } + + /** + * Gets subtree of a givin node + * @param start clicked node + * @returns {*[]} Subtreelist with ids of the subnodes + */ + function getTree(start){ + const children = []; + const stack = [start] + + while(stack.length > 0){ + const a = stack.pop(); + const node = data.find(n => n["id"] === a); + if(node == null) continue; + ["l_child","r_child"].forEach(child => { + const childid = node[child]; + if(childid !== undefined && childid !== null){ + children.push(childid); + stack.push(childid); + } + }); + } + return children.filter(elements => elements); + } + + /** + * Gets subtree of a givin node + * @param start clicked node + * @returns {*[]} Subtreelist with ids of the subnodes + */ + function getTreeCompact(start, source = data){ + const children = []; + const stack = [start] + + while(stack.length > 0){ + const a = stack.pop(); + const node = source.find(n => n["id"] === a); + if(node == null) continue; + ["l_child","r_child"].forEach(child => { + const childid = node[child]; + if(childid !== undefined && childid !== null){ + children.push(childid); + stack.push(childid); + } + }); + } + return children.filter(elements => elements); + } + + + + + /** + * Collapse function. Collapses / Expands a node with the subtree + * @param childitem childnode (l_node / r_node) + * @param start startnode + * @param collapsed_check check if tree at given node is collapsed or not + * @param direct direction of the node (right / left) + */ + function collapse(childitem, start, collapsed_check, direct){ + const sub = getTree(childitem); + const parent = data.find(d => d["id"] === start); + let side; + + if(direct === "right") side = "r_child"; + if(direct === "left") side = "l_child"; + const child = parent[side]; + if(child === null) return; + + const temp = getTree(child); + temp.push(child); + temp.forEach(id =>{ + const clean_lines = svg.selectAll(`[data-collapsed-Line^='${id}']`); + if(clean_lines !== null) clean_lines.forEach(i => i.remove()); + }); + const currentcollapsed = svg.select(`[data-collapsed-Line^='${child}']`); + + if(currentcollapsed !== null){ + svg.selectAll(`[data-collapsed-Line^='${child}']`).remove() + const childsub = getTree(child); + childsub.forEach(c => { + //MERGE: Push all c elements to subtree + if(!sub.includes(c)) sub.push(c); + }); + } + //Resets state for concat collapse + const temptree = getTree(child); + temptree.push(parent[side]); + + temptree.forEach(ci => { + if(collapsedmap[ci]["left"]) collapsedmap[ci]["left"] = false; + if(collapsedmap[ci]["right"]) collapsedmap[ci]["right"] = false; + }); + + // COLAPSE + if(collapsed_check){ + svg.selectAll(`circle[data-id='${childitem}']`).attr({display: "none"}); + minimap.selectAll(`path[data-id='${childitem}']`).attr({display: "none"}); + sub.forEach(child => { + svg.selectAll(`[data-id='${child}']`).attr({display: "none"}); + svg.selectAll(`circle[data-id='${child}']`).attr({display: "none"}); + minimap.selectAll(`path[data-id='${child}']`).attr({display: "none"}); + }); + counter_of_nodes.push(...sub); + draw_collapsed_line(childitem, start, collapsed_check, direct); + if((counter_of_nodes.length > 0)) { + $("#compact-button").prop("disabled", false); + }else{ + $("#compact-button").prop("disabled", true); + } + + + //EXPAND + }else{ + svg.selectAll(`circle[data-id='${childitem}']`).attr({display: "inline"}); + minimap.selectAll(`path[data-id='${childitem}']`).attr({display: "inline"}); + sub.forEach(child => { + svg.selectAll(`[data-id='${child}']`).attr({display: "inline"}); + svg.selectAll(`circle[data-id='${child}']`).attr({display: "inline"}); + minimap.selectAll(`path[data-id='${child}']`).attr({display: "inline"}); + counter_of_nodes = counter_of_nodes.filter(x=> x !== child); + }); + + + draw_collapsed_line(childitem, start, collapsed_check, direct); + if((counter_of_nodes.length > 0)) { + $("#compact-button").prop("disabled", false); + }else{ + $("#compact-button").prop("disabled", true); + } + + + } + } + + /** + * Draws collapsed related stuff. + * @param childitem childnode (l_node / r_node) + * @param start startnode + * @param collapsed_check check if tree at given node is collapsed or not + * @param direct direction of the node (right / left) + */ + function draw_collapsed_line(childitem, start, collapsed_check, direct){ + + const circle = svg.select(`circle[data-id='${start}'][data-direct='${direct}']`); + const sub = getTree(childitem); + const item_counter = sub.filter(id => { + const n = data.find(d => d["id"] === id); + return n["name"] !== "None"; + }).length; + + const item_name_container = sub. + map(id=>data.find(d => d["id"] === id)). + filter(n => n["name"] !== "None"). + map(n => n["name"]); + const preview_containing = item_name_container.slice(0,10).join(", "); + + let coll_line = null; + let nameText = null; + + if(collapsed_check){ + nameText = svg.text(maxX - offset, circle.attr("data-v"), "'"+collapsedmap[childitem]["label"]+"'"+ " (" + item_counter+ ")").attr({ + dominantBaseline: 'middle', + fontSize: fontSize, + fill: "black", + display: "inline", + "data-collapsed-Line": `${start}-${direct}`, + textAnchor: 'end', + fontStyle: "italic" + }); + g.add(nameText); + + coll_line = svg.line(circle.attr("data-h"), circle.attr("data-v"), maxX - (1.5 * offset) - Math.ceil(nameText.getBBox().width), circle.attr("data-v")).attr({ + stroke: "black", + display: "inline", + "data-collapsed-Line": `${start}-${direct}`, + strokeWidth: 2, + }); + g.add(coll_line); + + + nameText.hover( + function (){ + this.node.style.cursor = "pointer"; + this.attr({fill:"#1e90ff"}); + const box = this.getBBox(); + + const previewlist = preview_containing.split(",").map(x => x.trim()).slice(0,10); + let lines = []; + lines.push("This collapsed taxa "+"contains "+item_counter+" taxa:"); + lines.push(" ") + previewlist.forEach(taxa => lines.push("- "+taxa)); + lines.push(" "); + if(item_counter > 10){ + lines.push(" ... ( "+ (item_counter-10)+ " More )"); + }else{ + lines.push(" ... ( 0 More )"); + } + let text = svg.text(box.x + box.width+10, box.y + box.height /2, lines).attr({ + fill: "#171515", + "collapse-hover-id": start + }); + + text.selectAll("tspan").forEach((tspan, i) =>{ + let dyc; + if(i === 0){ + dyc = 0; + }else{ + dyc = 16; + } + tspan.attr({ + x : box.x + box.width+10, + dy: dyc + }); + }); + const textbox = text.getBBox(); + const desc = svg.rect(textbox.x - 5, textbox.y-3,textbox.width+80, textbox.height+25,5,5).attr({ + strokeWidth: 0.5, + fill: "#bdbdbd", + stroke: "black", + "collapse-hover-id": start + }); + + g.add(desc); + g.add(text); + + }, + function (){ + this.node.style.cursor = "default"; + this.attr({fill:"#000000"}); + svg.selectAll(`[collapse-hover-id='${start}']`).remove(); + } + ); + nameText.click(function () { + const box = this.getBBox(); + const ctm = this.node.getScreenCTM(); + const point = svg.node.createSVGPoint(); + point.x = box.x; + point.y = box.y; + const spoint = point.matrixTransform(ctm); + const input = document.createElement("input"); + input.classList.add("svg-edit-input"); + input["type"] = "text"; + input["value"] = "New label(Not empty)"; + input["style"]["position"] = "absolute"; + input["style"]["left"] = `${spoint.x}px`; + input["style"]["top"] = `${spoint.y}px`; + document.body.append(input); + input.focus(); + + input.addEventListener("keydown", e=>{ + if(e.key === "Enter"){ + if(input["value"] !== ""){ + const newtext = input["value"]; + if(label_blacklist.includes(newtext)){ + $("#info-modal-label").text("Warning") + $("#info-modal-body").text("Please use a different name for your label that hasn't been already used."); + $("#info-modal").modal("show"); + return; + } + + label_blacklist.push(newtext); + label_blacklist = label_blacklist.filter(x=> x !== collapsedmap[childitem]["label"]); + collapsedmap[childitem]["label"] = newtext; + + this.attr({ + text: "\""+newtext+"\"" + "("+item_counter+")", + fontStyle: "italic" + }) + const newlinelength = Math.ceil(nameText.getBBox().width); + coll_line.attr({ + x2: maxX - (1.5 * offset) - newlinelength + }); + input.remove(); + } + } + }); + + }); + }else{ + svg.selectAll(`[data-collapsed-Line='${start}-${direct}']`).remove() + } + } + + // Draws the branches and texts data.forEach(function (item, index, array) { // Text @@ -903,13 +1238,13 @@ $(function () { // TODO draw all texts at the right // with path stroke dasharray //g.add(svg.text(item["total_length"] * scaleX + (1.5 * offset), (index + 1) * scaleY, item["name"]).attr({dominantBaseline: 'middle', fontSize: fontSize, 'data-id': item["id"]})); - // Draw at the far right with a dashed line if (alignLabels) { nameText = svg.text(maxX - offset, (index + 1) * scaleY, item["name"]).attr({ dominantBaseline: 'middle', fontSize: fontSize, 'data-id': item["id"], + 'data-parent': item["parent"], textAnchor: 'end' }); g.add(nameText); @@ -918,7 +1253,9 @@ $(function () { fill: 'none', stroke: 'black', strokeWidth: 2, - strokeDasharray: 4 + strokeDasharray: 4, + "data-id": item["id"], + 'data-parent': item["parent"] })); } // Draw next to the leaf without a dashed line @@ -926,10 +1263,12 @@ $(function () { nameText = svg.text(item["total_length"] * scaleX + (1.5 * offset), (index + 1) * scaleY, item["name"]).attr({ dominantBaseline: 'middle', fontSize: fontSize, - 'data-id': item["id"] + 'data-id': item["id"], + 'data-parent': item["parent"] }); g.add(nameText); } + // Branch } else { // Draw bootstrap on the line if provided @@ -939,7 +1278,8 @@ $(function () { g.add(svg.text((parseFloat(array[parent]["total_length"]) + (item["length"] / 2)) * scaleX, (index + 1) * scaleY, item["bootstrap"]).attr({ dominantBaseline: 'baseline', fontSize: 0.5 * fontSize, - 'data-id': item["id"] + 'data-id': item["id"], + 'data-parent': item["parent"] })); // TODO data-id added graying out of child bootstraps but not selected bootstrap } @@ -971,8 +1311,97 @@ $(function () { "data-id": array[r_child]["id"], "data-parent": item["id"] }); + g.add(left, right); + + + // NODE FOR COLLAPSE / SIMPLIFICATION + const r_edge = svg.circle(hRight, vRight, 8).attr({ + fill: "black", + stroke: "black", + strokeWidth: 2, + "data-id": item["id"], + "data-h": hRight, + "data-v": vRight, + "data-direct": "right", + }); + + // NODE FOR COLLAPSE / SIMPLIFICATION + const l_edge = svg.circle(hLeft, vLeft, 8).attr({ + fill: "black", + stroke: "black", + strokeWidth: 2, + "data-id": item["id"], + "data-h": hLeft, + "data-v": vLeft, + "data-direct": "left", + }); + + if(compactmode === true){ + if((collapsedmap[item["id"]] && collapsedmap[item["id"]]["right_id"] === item["r_child"])){ + r_edge.attr({display: "none"}); + }else{ + r_edge.attr({display: "inline"}); + } + if((collapsedmap[item["id"]] && collapsedmap[item["id"]]["left_id"] === item["l_child"])){ + l_edge.attr({display: "none"}); + }else{ + l_edge.attr({display: "inline"}); + } + } + + l_edge.hover( + function () { + this.node.style.cursor = "pointer"; + + }, function () { + this.node.style.cursor = "default"; + }); + r_edge.hover( + function () { + this.node.style.cursor = "pointer"; + + + }, function () { + this.node.style.cursor = "default"; + + }); + + // Leave isn't leave => check one stage before and delete nodes + const r_child_check = data.find(d => d["id"] === item["r_child"]); + const l_child_check = data.find(d => d["id"] === item["l_child"]); + + + r_edge.click(function(){ + const check = compactlist.find(n => n === item["r_child"]); + if(compactmode === true && (check !== undefined)) return; + document.querySelectorAll(".svg-edit-input").forEach(e => e.remove()); + collapsedmap[item["id"]]["collapsed-line-Y"] = r_edge.attr("data-v"); + collapse(item["r_child"],item["id"], !collapsedmap[item["id"]]["right"], "right"); + collapsedmap[item["id"]]["right"] = !collapsedmap[item["id"]]["right"]; + collapsedmap[item["id"]]["right_id"] = item["r_child"]; + + }); + l_edge.click(function(){ + const check = compactlist.find(n => n === item["l_child"]); + if(compactmode === true && (check !== undefined)) return; + document.querySelectorAll(".svg-edit-input").forEach(e => e.remove()); + collapsedmap[item["id"]]["collapsed-line-Y"] = l_edge.attr("data-v"); + collapse(item["l_child"],item["id"], !collapsedmap[item["id"]]["left"], "left"); + collapsedmap[item["id"]]["left"] = !collapsedmap[item["id"]]["left"]; + collapsedmap[item["id"]]["left_id"] = item["l_child"]; + + }); + g.add(r_edge, l_edge); + + //Removing false leaves + if((r_child_check["name"] !== "None")) r_edge.remove(); + if((l_child_check["name"] !== "None")) l_edge.remove(); + + + + // Repeat almost everything for the minimap mXMinimap = minimapMinX + mX / ratio; mYLeftMinimap = minimapMinY + mYLeft / ratio; @@ -986,15 +1415,33 @@ $(function () { minimapPaths = [minimapLeft, minimapRight]; + + minimapPaths.forEach(function (itemMinimapPath, indexMinimapPath, arrayMinimapPath) { + let data_child; + if(indexMinimapPath === 0){ + collapsedmap[item["id"]]["minimap-l-m"] = itemMinimapPath.attr("d"); + data_child = item["l_child"]; + } + if(indexMinimapPath === 1){ + collapsedmap[item["id"]]["minimap-r-m"] = itemMinimapPath.attr("d"); + data_child = item["r_child"]; + } + + + itemMinimapPath.attr({ fill: 'none', stroke: 'black', strokeWidth: strokeWidth / ratio, - strokeLinecap: 'square' + strokeLinecap: 'square', + "data-id": item["id"], + "data-child": data_child // r or l child + }); }); + if (array[l_child]["bootstrap"] != "") { left.append(Snap.parse('' + array[l_child]["bootstrap"] + '')); } @@ -1014,7 +1461,6 @@ $(function () { "data-r_child": array[r_child]["r_child"] }); } - // Configure path hovering and clicking paths = [left, right]; @@ -1057,6 +1503,7 @@ $(function () { itemPath.attr({ strokeOpacity: 0.25 }); + childrenIds = [itemPath.attr("data-l_child"), itemPath.attr("data-r_child")]; while ((typeof childrenIds !== "undefined") && (childrenIds.length > 0)) { childId = childrenIds.shift(); @@ -1117,12 +1564,17 @@ $(function () { } else { // Send data to the backend either with the last tree if (counter_of_trees == number_of_trees) { + + load("get", { 'from': clickedPath.attr("data-id"), 'to': itemPath.attr("data-id") }); clicked_id = null; clickedPath = null; + + + // or with the currently shown tree } else { load("get", { @@ -1140,6 +1592,179 @@ $(function () { } }); + if(compactmode === false) { + Object.keys(collapsedmap).forEach(id => { + document.querySelectorAll(".svg-edit-input").forEach(e => e.remove()); + const node = data.find(d => d["id"] === id); + if (node === undefined) return; + + + //SIDE SWAP CHECK IF STATEMENTS AFTER BRANCH SWAP + if (collapsedmap[id]["right"] && collapsedmap[id]["right_id"] !== null) { + if (node["r_child"] !== collapsedmap[id]["right_id"]) { + if (node["l_child"] === collapsedmap[id]["right_id"]) { + collapsedmap[id]["left"] = true; + collapsedmap[id]["right"] = false; + collapsedmap[id]["left_id"] = collapsedmap[id]["right_id"]; + collapsedmap[id]["right_id"] = null; + } else { + collapsedmap[id]["right_id"] = null; + collapsedmap[id]["right"] = false; + } + } + } + if (collapsedmap[id]["left"] && collapsedmap[id]["left_id"] !== null) { + if (node["l_child"] !== collapsedmap[id]["left_id"]) { + if (node["r_child"] === collapsedmap[id]["left_id"]) { + collapsedmap[id]["right"] = true; + collapsedmap[id]["left"] = false; + collapsedmap[id]["right_id"] = collapsedmap[id]["left_id"]; + collapsedmap[id]["left_id"] = null; + collapsedmap[id]["minimap-l-m"] = null; + } else { + collapsedmap[id]["left_id"] = null; + collapsedmap[id]["left"] = false; + collapsedmap[id]["minimap-l-m"] = null; + } + } + } + + if (collapsedmap[id]["right"] && collapsedmap[id]["right_id"]) { + const child = collapsedmap[id]["right_id"]; + const path = svg.select(`path[data-id='${child}']`); + if (path) collapsedmap[id]["r-d"] = path.attr("d"); + const tree = getTree(id); + tree.forEach(children => { + if (collapsedmap[children]["r-d"]) { + const rightpath_id = collapsedmap[children]["right_id"]; + const children_path = svg.select(`path[data-id='${rightpath_id}']`); + collapsedmap[children]["r-d"] = children_path.attr("d"); + } else if (collapsedmap[children]["l-d"]) { + const leftpath_id = collapsedmap[children]["left_id"]; + const children_path = svg.select(`path[data-id='${leftpath_id}']`); + collapsedmap[children]["l-d"] = children_path.attr("d"); + } + }); + } + if (collapsedmap[id]["left"] && collapsedmap[id]["left_id"]) { + const child = collapsedmap[id]["left_id"]; + const path = svg.select(`path[data-id='${child}']`); + if (path) collapsedmap[id]["l-d"] = path.attr("d"); + const tree = getTree(id); + tree.forEach(children => { + if (collapsedmap[children]["r-d"] && collapsedmap[children]["right_id"]) { + const rightpath_id = collapsedmap[children]["right_id"]; + const children_path = svg.select(`path[data-id='${rightpath_id}']`); + collapsedmap[children]["r-d"] = children_path.attr("d"); + } else if (collapsedmap[children]["l-d"] && collapsedmap[children]["left_id"]) { + const leftpath_id = collapsedmap[children]["left_id"]; + const children_path = svg.select(`path[data-id='${leftpath_id}']`); + collapsedmap[children]["l-d"] = children_path.attr("d"); + } + }); + } + if (collapsedmap[id]["right"]) { + collapse(node["r_child"], id, collapsedmap[id]["right"], "right"); + + } + if (collapsedmap[id]["left"]) { + collapse(node["l_child"], id, collapsedmap[id]["left"], "left"); + + } + }); + } + + if(compactmode === true) { + data.forEach(function (item) { + const compactlabel = Object.values(collapsedmap).some(v => + ("'" + v["label"] + "'" === item["name"]) + ); + + if (compactlabel) { + const c = Object.entries(collapsedmap).map(([id, v]) => { + if((v["left_id"]!== null)){ + return v["left_id"]; + } + if((v["right_id"]!== null)) { + return v["right_id"]; + } + return undefined; + }).filter(v => (v !== undefined)); + + + c.forEach(entry => { + const sub = getTreeCompact(entry,JSON.parse(currenttree)); + const item_counter = sub.filter(id => { + const n = JSON.parse(currenttree).find(d => d["id"] === id); + return (n["name"] !== "None" && n["name"] !== undefined); + }).length; + + const item_name_container = sub. + map(id=>JSON.parse(currenttree).find(d => d["id"] === id)). + filter(n => n["name"] !== "None" && n["name"] !== undefined). + map(n => n["name"]); + const preview_containing = item_name_container.slice(0,10).join(", "); + + + const taxa = svg.select(`text[data-id='${entry}']`); + if(taxa === null) return; + taxa.hover(function (){ + this.node.style.cursor = "pointer"; + this.attr({fill:"#1e90ff"}); + + const box = this.getBBox(); + + const previewlist = preview_containing.split(",").map(x => x.trim()).slice(0,10); + let lines = []; + lines.push("This collapsed taxa "+"contains "+item_counter+" taxa:"); + lines.push(" ") + previewlist.forEach(taxa => lines.push("- "+taxa)); + lines.push(" "); + if(item_counter > 10){ + lines.push(" ... ( "+ (item_counter-10)+ " More )"); + }else{ + lines.push(" ... ( 0 More )"); + } + let text = svg.text(box.x + box.width+10, box.y + box.height /2, lines).attr({ + fill: "#171515", + "collapse-hover-id": entry + }); + + text.selectAll("tspan").forEach((tspan, i) =>{ + let dyc; + if(i === 0){ + dyc = 0; + }else{ + dyc = 16; + } + tspan.attr({ + x : box.x + box.width+10, + dy: dyc + }); + }); + const textbox = text.getBBox(); + const desc = svg.rect(textbox.x - 5, textbox.y-3,textbox.width+80, textbox.height+25,5,5).attr({ + strokeWidth: 0.5, + fill: "#bdbdbd", + stroke: "black", + "collapse-hover-id": entry + }); + + g.add(desc); + g.add(text); + + },function (){ + this.node.style.cursor = "default"; + this.attr({fill:"#000000"}); + svg.selectAll(`[collapse-hover-id='${entry}']`).remove(); + }); + + }); + } + }); + } + + // Sets the initial position if (typeof xBefore === "undefined") { xBefore = 20; @@ -1321,6 +1946,8 @@ $(function () { } } + + $(svg.node).mousedown(funcMouseDown); $(svg.node).mouseup(funcMouseUp); $(svg.node).mousemove(funcMouseMove); @@ -1340,6 +1967,7 @@ $(function () { $("#labels-button").prop("disabled", false); $("#snapshots-button").prop("disabled", false); + // Calls the undo function for button and context option $("#undo-button").click(function (event) { undo(); @@ -1364,7 +1992,7 @@ $(function () { if (snapshotTrees.find(element => element[0] == trees[counter_of_trees - 1][0])) { // Shows the info modal $("#info-modal-label").text("This snapshot already exists!") - $("#info-modal-body").text("This tree has already been snapshot.") + $("#info-modal-body").text("You already froze this tree topology.") $("#info-modal").modal("show"); return; } @@ -1381,7 +2009,7 @@ $(function () { if (snapshotTrees.find(element => element[0] == trees[counter_of_trees - 1][0])) { // Shows the info modal $("#info-modal-label").text("This snapshot already exists!") - $("#info-modal-body").text("This tree has already been snapshot.") + $("#info-modal-body").text("You already froze this tree topology.") $("#info-modal").modal("show"); return; } @@ -1434,10 +2062,12 @@ $(function () { // Calls the search function with input from the search field for button and context option $("#search-button").click(function (event) { + document.querySelectorAll(".svg-edit-input").forEach(e => e.remove()); search($("#search-text").val()); }); $('#search-text').keypress(function (event) { + document.querySelectorAll(".svg-edit-input").forEach(e => e.remove()); if (event.which == '13') { search($("#search-text").val()); } @@ -1469,6 +2099,39 @@ $(function () { $("#context-menu").removeClass("visible"); outgroup(); }); + $('#compact-button').off("click").on("click",function (event) { + if(compactmode === false) { + compactmode = true; + const compacttree = JSON.parse(currenttree); + + $("#compact-button-show").hide(); + $("#compact-button-hide").show(); + + $("#info-modal-label").text("Drawing compact tree.") + $("#info-modal-body").text("For larger trees this might take a while.") + $("#info-modal").modal("show"); + draw(calculateCompactTree(compacttree, collapsedmap)); + + //$("#lengths-button").prop("disabled", true); + //$("#labels-button").prop("disabled", true); + + }else{ + + $("#info-modal-label").text("Expanding back to original tree structure") + $("#info-modal-body").text("For larger trees this might take a while") + $("#info-modal").modal("show"); + + compactmode = false; + draw(JSON.parse(currenttree)); + + //$("#lengths-button").prop("disabled", false); + //$("#labels-button").prop("disabled", false); + + $("#compact-button-show").show(); + $("#compact-button-hide").hide(); + + } + }); // Sets the flag for button activation buttons_activated = true; @@ -1481,10 +2144,18 @@ $(function () { */ function search(value) { // Different x coordinate depending on whether branch lengths are shown - if (!(enableLengths)) { - data = JSON.parse(trees[counter_of_trees - 1][1]); - } else { - data = JSON.parse(trees[counter_of_trees - 1][2]); + if(compactmode === false) { + if (!(enableLengths)) { + data = JSON.parse(trees[counter_of_trees - 1][1]); + } else { + data = JSON.parse(trees[counter_of_trees - 1][2]); + } + }else{ + if (!(enableLengths)) { + data = calculateCompactTree(JSON.parse(currenttree), collapsedmap); + } else { + data = calculateCompactTree(JSON.parse(currenttree), collapsedmap); + } } data.forEach(function(item, index, array) { // Searches through the all texts, colors all and jumps to the last match @@ -1508,6 +2179,7 @@ $(function () { function undo() { if (counter_of_trees > 1) { counter_of_trees -= 1; + } if (counter_of_trees == 1) { $("#undo-button").prop("disabled", true); @@ -1532,11 +2204,11 @@ $(function () { * Shows the branch lengths if they are available and hides them if they are shown */ function toggleLength() { - if (enableLengths) { + if (enableLengths && compactmode === false) { draw(JSON.parse(trees[counter_of_trees - 1][1])); $("#lengths-button-hide").hide(); $("#lengths-button-show").show(); - } else { + } else if(!enableLengths && compactmode === false){ if (trees[counter_of_trees - 1][2] != null) { draw(JSON.parse(trees[counter_of_trees - 1][2])); $("#lengths-button-show").hide(); @@ -1544,6 +2216,14 @@ $(function () { } else { $("#compute-modal").modal("show"); } + }else if(enableLengths && compactmode === true){ + draw(calculateCompactTree(JSON.parse(trees[counter_of_trees - 1][1]), collapsedmap)); + $("#lengths-button-hide").hide(); + $("#lengths-button-show").show(); + }else if(trees[counter_of_trees - 1][2] != null && compactmode === true){ + draw(calculateCompactTree(JSON.parse(trees[counter_of_trees - 1][2]), collapsedmap)); + $("#lengths-button-show").hide(); + $("#lengths-button-hide").show(); } } @@ -1560,10 +2240,15 @@ $(function () { $("#labels-button-align").show(); alignLabels = true; } - if (!enableLengths) { + if (!enableLengths && compactmode === false) { draw(JSON.parse(trees[counter_of_trees - 1][1])); - } else if (trees[counter_of_trees - 1][2] != null) { + } else if (trees[counter_of_trees - 1][2] != null && compactmode === false) { draw(JSON.parse(trees[counter_of_trees - 1][2])); + }else if(!enableLengths && compactmode === true){ + draw(calculateCompactTree(JSON.parse(trees[counter_of_trees - 1][1]), collapsedmap)); + }else if( compactmode === true && trees[counter_of_trees - 1][2] != null) { + draw(calculateCompactTree(JSON.parse(trees[counter_of_trees - 1][2]), collapsedmap)); + } } @@ -1600,6 +2285,36 @@ $(function () { return {normalizedX, normalizedY}; } + /** + * calculates a new tree to draw for the compact view + * @param tree current tree + * @param colmap current collapsed info map + */ + function calculateCompactTree(tree, colmap){ + const collapsednodes = Object.keys(colmap).map(id => { + const temparray = []; + if(colmap[id]["left"] === true) temparray.push(colmap[id]["left_id"]); + if(colmap[id]["right"] === true) temparray.push(colmap[id]["right_id"]); + + return temparray + }).flat(); + + const removeablenodes = collapsednodes.map(id => { + return getTreeCompact(id, tree); + }).flat(); + compactlist = collapsednodes; + + let newtree = tree.filter(node => !removeablenodes.includes(node["id"])); + collapsednodes.map(id => { + const neededchange = newtree.find(i => i["id"] === id); + neededchange["name"] = "'"+colmap[neededchange["id"]]["label"]+"'"; + neededchange["l_child"] = ""; + neededchange["r_child"] = ""; + + }); + return newtree; + } + /** * Changes the context menu to the custom one */ @@ -1629,6 +2344,8 @@ $(function () { }); } + + /** * Enables the custom buttons for alignment file handling */ @@ -1703,7 +2420,7 @@ $(function () { instructions_lines = instructions_g.g(); instructions_texts = instructions_g.g(); - buttons_names = ["undo", "redo", "save", "snapshots", "zoom-in", "zoom-out", "search", "lengths", "labels", "options", "help"]; + buttons_names = ["undo", "redo", "save", "snapshots", "zoom-in", "zoom-out", "help", "lengths", "labels", "options", "compact", "search"]; buttons_names.forEach(function (value, index) { current_button = $("#" + value + "-button"); diff --git a/matt/templates/index.html b/matt/templates/index.html index 31d8298..7c0be4d 100644 --- a/matt/templates/index.html +++ b/matt/templates/index.html @@ -194,11 +194,22 @@

Import

+ + @@ -585,6 +596,7 @@
Align/Attach labels
Options
Help
+
Make tree compact