Files
enviPy-bayer/static/js/d3-pathway.js
2025-06-23 20:13:54 +02:00

1783 lines
54 KiB
JavaScript

var pwURI = "";
// START PATHWAY GRAPH CONFIGURATION ----------------------------------------------
// colors
var lightGrey = "#E8E8E8";
var grey = "#B2B2B2";
var adOut = "#ff9999";
var adIn = "#007000";
// compounds
var compoundSize = 75; // = diameter of nodes of small compounds
var maxCompoundSize = function(){ return 1.5 * compoundSize }; // limit increased size of larger compounds
var maxCompoundPopupSize = function(){ return 5 * compoundSize };
var compoundImageMargin = 0.15; // compounds images are rectangles, reduce size to fit into nodes
var compoundFill = lightGrey;
var compoundFillSelected = grey;
var showCompoundOutlines = false;
var compoundStroke = "black";
var compoundStrokeRoot = "red";
var compoundStrokeOpacity = function(d){
// set opacity according to depth:
// * depth 0 -> opacity 1
// * depth == max-depth -> opacity 0
return (maxNodeDepth - d.depth) / maxNodeDepth;
};
var compoundStrokeWidth = function(){ return 0.04 * compoundSize };
var compoundTextSize = function(){ return 6 + compoundSize * 0.04 };
var compoundTextYOffset = function(){ return compoundSize * 0.5 };
// links
var linkStroke = grey;
var linkStrokeUp = "red";
var linkStrokeDown = "green";
var linkStrokeOpacity = 1.0;
var linkStrokeOpacitySelected = 1.0;
var linkStrokeWidth = function(){ return 0.02 * compoundSize };
var linkTextSize = function(){ return compoundTextSize() };
// layout settings ----------------------------
// default = pathway !
var treeLayoutEnabled = true;
var mergedView = false;
var ADView = false;
// ROOT in CENTER layout
var centerLayoutLinkDistance = function(){ return 2.5 * compoundSize };
var centerLayoutCharge = -1500; // compounds push each other apart
var centerLayoutRootCharge = centerLayoutCharge * 2;
var centerLayoutGravity = 0.05; // pulls compounds back to graph center
// TREE layout
var treeLayoutLinkDistance = function(){ return 2.5 * compoundSize };
var treeLayoutCharge = -1000; // compounds push each other apart
var treeLayoutPseudoCharge = treeLayoutCharge * 0.1; // relaxes forces on pseudo nodes
var treeLayoutLevelGravity = 0.5; // pulls compounds back to their depth level
var treeLayoutXCenterGravity = 0.03; // pulls compounds to x center
var treeLayoutLevels = function(){ return treeLayoutLinkDistance() }; // distance between depth levels
var treeLayoutXInitMargin = function(){ return treeLayoutLinkDistance() }; // initial x-distance between nodes of same depth
// popup
var popupEnabled = true;
var popupWaitBeforeShow = 1000;
var popushowAtLeast = 1000;
// other
var showCompoundNames = false;
var showReactionNames = false;
var showReactionProb = false;
var selectionEnabled = true;
// this is to not draw the arrow at the end of the line (which is a the compound center)
// but at the beginning of the circle
// MG: dont understand whats going on, depends on stroke-Width!, estimate manually for
// each setting
var arrowOffset = function(target_size){ return target_size * 0.33 };
// END PATHWAY GRAPH CONFIGURATION ----------------------------------------------
var maxNodeDepth = 0;
var educts =[];
var products =[];
//indicates click modes
var mode = 'undefined';
var width;
var height;
var border = "thin solid lightgrey";
var setFix = false;
var markedCompUri;
var markedEdgeUri;
var links;
var nodes;
//backup variable to keep original graph;
var __graph;
var nodeSelected = false;
var edgeSelected = false;
var clickedNode = 'undefined';
var clickedNode_d = 'undefined';
var clickedEdge = 'undefined';
var clickedEdge_d = 'undefined';
var compoundNodeUri = 'undefined';
//Array for links of all paths
var paths = [];
//Array for one path found by dfs
var path = [];
//debug variable
var pathNumeric = [];
//variable for dfs which indicates visited nodes
var greenLinks = [];
var visited;
var svg;
var force;
var modelUri;
var modelName;
function restartPathwayViz(onGraphLoad){
if (!width) {
width = document.getElementById("viz").offsetWidth;
height = width*0.75;
}
if (force)
force.stop();
d3.select("svg").remove();
if (treeLayoutEnabled) {
force = d3.layout.force()
.charge(function(d) {
if (d.pseudo)
return treeLayoutPseudoCharge;
else
return treeLayoutCharge;
})
.gravity(0)
.linkDistance(function(d) {
dist = Math.abs(d.target.treeLevel - d.source.treeLevel) * treeLayoutLinkDistance();
if (dist == 0) {
// nodes with depth in brackets: A (0) -> B (1) + C (2)
// then there is a pseudo node that is on the same level as B
dist = 0.5 * treeLayoutLinkDistance();
}
return dist;
})
.linkStrength(function(d) {
return strength = 1 / d.siblings;
})
.size([width, height]);
}
else {
force = d3.layout.force()
.charge(function(d) {
return d.depth==0 ? centerLayoutRootCharge : centerLayoutCharge;
})
.gravity(centerLayoutGravity)
.linkDistance(centerLayoutLinkDistance())
.size([width, height]);
}
svg = d3.select("#viz")
.append("svg")
.attr("width", "100%")
.attr("height", height)
.style("border", border)
.attr("inline", "block")
.attr("margin", "auto")
.attr("pointer-events", "all")
.append('svg:g')
.call(d3.behavior.zoom().on("zoom", redraw))
.attr("preserveAspectRatio", "xMidYMid meet")
.append('svg:g');
pwUrl = pwURI;
if(mergedView) {
pwUrl += "?merged=true";
}
d3.json(pwUrl, function(error, graph) {
if (graph) {
if (graph.pathwaySetting && graph.pathwaySetting.truncationstrategy) {
modelUri = graph.pathwaySetting.truncationstrategy.modelUri;
modelName = graph.pathwaySetting.truncationstrategy.modelName;
}
if(!graph.isPredicted) {
$("#toggleADView").parent().remove();
$("#toggleADView").remove();
}
if (onGraphLoad)
onGraphLoad(graph);
__graph = graph;
if (graph.nodes.length==0)
alert("pathway is empty");
else
start(graph);
}
});
}
function goFullscreen(id) {
var element = document.getElementById(id);
if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen();
}
}
// now pathway fun
function reformatLink(string) {
return string.replace(/\[([^|]+)\|([^\]]+)]/g, "<a target='parent' href=\"$1\">$2</a>");
}
/*function delComp() {
if (markedCompUri == 'undefined') {
// console.log("markedComp was undefined");
return;
}
var conf = confirm("Do you really want to delete this Node?");
if (conf) {
jQuery.ajax({
type : "delete",
url : markedCompUri,
success : function(response) {
hide(clickedNode_d);
click(clickedNode_d);
},
error : function(jqXHR, textStatus, errorThrown) {
alert("Error while deleting Node");
}
});
} else {
return;
}
}*/
/*function delEdge() {
// console.log(markedEdgeUri);
if (markedEdgeUri == 'undefined') {
return;
}
var conf = confirm("Do you really want to delete this Edge?");
if (conf) {
jQuery.ajax({
type : "delete",
url : markedEdgeUri,
success : function(response) {
linkArray = new Array();
for (var i = 0; i < links[0].length; i++) {
if (links[0][i].__data__.id == clickedEdge_d.id) {
linkArray.push(links[0][i]);
}
}
hideLinks(linkArray);
edgeClick(clickedEdge_d);
},
error : function(jqXHR, textStatus, errorThrown) {
alert("Error while deleting Edge");
}
});
} else {
return;
}
}*/
function start(graph) {
setChildrenAndParents(graph)
setPseudoLinks(graph)
maxNodeDepth = 0
graph.nodes.forEach(function(node, i) {
if (node.depth > maxNodeDepth)
maxNodeDepth = node.depth;
});
if (treeLayoutEnabled) {
setTreeLevel(graph)
var treeLayout = buildTreeLayout(graph)
}
force.nodes(graph.nodes).links(graph.links).start();
computeNodeSizes(graph.nodes);
$('#name').val(graph.pathwayName);
$('#description').val(graph.description);
// Background rectangle to enable zoom function
// even cursor is not on a node or path
svg.append('svg:rect').attr('width', 9 * width).attr('height', 9 * height)
.attr('fill', 'white').attr("x", -4 * width).attr("y", -4 * height)
.attr("opacity", 0.0);
function defineArrow(name, color, opacity, target) {
svg.append("svg:defs").selectAll("marker").data([ name ]).enter()
.append("svg:marker").attr("id", String).attr("viewBox",
"0 -5 10 10")
.attr("refX", 10 + (target ? arrowOffset(target.size) : 0 ))
.attr("refY", 0).attr(
"markerWidth", 10).attr("markerHeight", 10).attr("orient",
"auto").attr("stroke", color).attr("fill", "none").attr(
"stroke-opacity", opacity).append("svg:path").attr("d",
"M0,-5L10,0L0,5");
}
// hack: define arrow statically for each target and each color (dont know how to change dynamically)
for (var i = 0; i < graph.links.length; i++) {
if (graph.links[i].target.size) {
defineArrow("arrow_"+i,linkStroke,linkStrokeOpacity,graph.links[i].target);
defineArrow("arrow-green_"+i,linkStrokeDown,linkStrokeOpacitySelected,graph.links[i].target);
defineArrow("arrow-red_"+i,linkStrokeUp,linkStrokeOpacitySelected,graph.links[i].target);
}
}
defineArrow("arrow",linkStroke,linkStrokeOpacity);
defineArrow("arrow-green",linkStrokeDown,linkStrokeOpacitySelected);
defineArrow("arrow-red",linkStrokeUp,linkStrokeOpacitySelected);
// coordinates are set in tick
links = svg.selectAll("line").data(graph.links).enter().append("g").append("svg:line")
.style("stroke", function(d) {
if(ADView) {
if(d.ad) {
if(d.ad[0]['passesAD']) {
return adIn;
} else {
return adOut;
}
} else {
return linkStroke
}
} else {
return linkStroke
}
})
.style("stroke-opacity", function(d) {
if(ADView) {
if(d.ad) {
return 1;
} else {
return linkStrokeOpacity
}
} else {
return linkStrokeOpacity
}
})
.style("stroke-width", function() {
return linkStrokeWidth();
})
.attr("class", "link arrow")
.on("click", edgeClick)
.attr("marker-end", function(d,i) {
if (!d.target.pseudo) {
// if (d.multistep === 'true') {
// return "";
// }
return "url(#arrow_"+i+")";
} else {
if (d.multistep === 'true') {
return "url(#arrow)";
}
return "";
}
})
.attr("idx", function(d,i) {return i; })
.attr("target-pseudo", function(d) {
return d.target.pseudo;
});
// only for mouse over and clicking (other lines are hard to hit)
// coordinates are set in tick
links_invisible = svg.selectAll("g").append("svg:line")
.style("stroke", "black").style("stroke-opacity", "0").style(
"stroke-width", function() {
return linkStrokeWidth() * 10;
}).attr("class", "link arrow").on("click", edgeClick);
// coordinates are set in tick
link_text = svg.selectAll("g")
.append("text")
.attr("text-anchor", "middle")
.style("fill", "#000")
.style("font-size", function() { return linkTextSize() + "pt"; })
.style("font-family", "sans-serif");
updateReactionText();
// the popup for lines is oriented on the max-right position of the line
// adjust to center of the line / to the pseudo node
function popup_offset(d) {
x = 15;
y = 0;
if (d.target.pseudo) {
y += Math.abs(d.source.y - d.target.y) * 0.5;
if (d.target.x < d.source.x)
x -= Math.abs(d.source.x - d.target.x);
}
else if (d.source.pseudo) {
y -= Math.abs(d.source.y - d.target.y) * 0.5;
if (d.target.x > d.source.x)
x -= Math.abs(d.source.x - d.target.x);
}
else {
x -= Math.abs(d.source.x - d.target.x) * 0.5;
}
return {x: x, y: y};
}
if (popupEnabled) {
function reaction_content(d) {
content = "<p>Reaction: <a href='"+d.idreaction+"'>"+d.name+"</a></p>";
var adcontent = "";
if(d.ad){
for(i in d.ad) {
data = d.ad[i];
adcontent = "<p>";
if(data["timesTriggered"]) {
adcontent += "This rule triggered " + data["timesTriggered"] + " in the training set<br>";
}
adcontent += "Reliability " + round(data["reliability"]) + " (" + (data["reliability"] > data["reliabilityThreshold"] ? "&gt" : "&lt") + " Reliability Threshold of " + data["reliabilityThreshold"] + ")<br>";
adcontent += "Local Compatibility " + round(data["localCompatibility"]) + " (" + (data["localCompatibility"] > data["localCompatibilityThreshold"] ? "&gt" : "&lt") + " Reliability Threshold of " + data["localCompatibilityThreshold"] + ")<br>";
adcontent += "</p>";
}
}
content += adcontent;
if (d.scenarios && d.scenarios.length>0) {
content += "<p>Scenario/s:"
for (var sc in d.scenarios) {
scen = d.scenarios[sc]
content += "<br><a href='"+scen.id+"'>"+scen.name+"</a>";
if (scen.modelpredictionprob) {
content += "<p>Relative Reasoning: <a href='"+modelUri+"'>"+modelName+"</a>";
content += "<br>Probability: "+scen.modelpredictionprob+"</p>";
}
if (scen.modelbayespredictionprob) {
content += "<p>Relative Reasoning: <a href='"+modelUri+"'>"+modelName+"</a>";
content += "<br>Bayes Probability: "+scen.modelbayespredictionprob+"</p>";
}
}
content += "</p>";
}
if (d.ecNumbers && d.ecNumbers.length>0) {
content += "<p>Enzyme/s:";
for (var ec in d.ecNumbers) {
content += "<br><a href=\"http://www.brenda-enzymes.org/enzyme.php?ecno="
+ d.ecNumbers[ec].ecNumber + "\" target=\"_blank\">"
+ d.ecNumbers[ec].ecName + " (" + d.ecNumbers[ec].ecNumber + ")</a>";
}
content += "</p>";
}
return content;
}
pop_add(links_invisible,"Reaction",function(d){ return reaction_content(d);},popup_offset);
pop_add(link_text,"Reaction",function(d){ return reaction_content(d);});
}
var node_drag = d3.behavior.drag().on("dragstart", dragstart).on("drag",
dragmove).on("dragend", dragend);
function dragstart(d, i) {
pop_hideall();
d3.event.sourceEvent.stopPropagation();
force.stop();
}
function dragmove(d, i) {
d3.event.sourceEvent.stopPropagation();
d.px += d3.event.dx;
d.py += d3.event.dy;
d.x += d3.event.dx;
d.y += d3.event.dy;
tick();
}
function dragend(d, i) {
pop_show();
d3.event.sourceEvent.stopPropagation();
d.fixed = true;
tick();
force.resume();
}
nodes = svg.selectAll(".node").data(graph.nodes).enter().append("g").attr(
"class", "node").call(node_drag);
rootIndices = []
graph.nodes.forEach(function(node,i) {
if (node.depth==0) {
rootIndices.push(i);
}
});
// init node positions
if (!treeLayoutEnabled) {
maxTreeSize = -1
node = null
graph.nodes.forEach(function(d,i) {
if (d.depth == 0 && d.treeSize>maxTreeSize) {
maxTreeSize = d.treeSize;
node = d;
}
});
x = width/2;
y = height/2;
node.x = x;
node.y = y;
node.px = x;
node.py = y;
node.fixed = true;
}
else {
// positions for tree layout
// y: set node at treeLevel
// x: equally distriute according to number of nodes per level (for non-pseudo nodes)
for (var level in treeLayout) {
xStart = (width * 0.5) - (treeLayoutXInitMargin() * (treeLayout[level].length-1) * 0.5)
treeLayout[level].forEach(function (node,j) {
if (!node.pseudo) {
x = xStart + j * treeLayoutXInitMargin();
node.x = x;
node.px = x;
}
y = treeLayoutLevels()*node.treeLevel + compoundSize;
node.y = y;
node.py = y;
});
}
// update x for pseudo nodes: set in between parents and children
for (var level in treeLayout) {
treeLayout[level].forEach(function (node,j) {
if (node.pseudo) {
sumX = 0;
countX = 0;
(node.parents.concat(node.children)).forEach(function (n,j) {
if (!n.pseudo) {
sumX += n.x;
countX += 1;
}
});
x = sumX/countX;
node.x = x;
node.px = x;
}
});
}
}
circles = nodes.append("circle").attr("class", "circle").attr("cx", 0).attr(
"cy", 0)
.attr("depth", function(d) {
return d.depth;
}).attr("r", function(d) {
if (!d.pseudo) {
return d.size * 0.5;
} else {
return 0;
}
})
.style("stroke", function(d) {
if(ADView) {
if(d.ad){
if(d.ad['inAD']) {
return adIn;
} else {
return adOut;
}
}
return "black";
} else if (d.depth == 0) {
return compoundStrokeRoot;
} else {
return compoundStroke;
}
}).style("stroke-opacity",function(d) {
if(ADView) {
return 1;
} else if (showCompoundOutlines) {
return compoundStrokeOpacity(d);
} else {
return 0.0;
}
})
.style("stroke-width", function(d) {
if (!d.pseudo) {
return compoundStrokeWidth() + "px";
} else {
return 0;
}
})
.style("fill", function(d) {
if(d.engineeredIntermediate) {
return "#dcfcf9"
}else {
return compoundFill
}
})
.style("fill-opacity", function(d) {
if (!d.pseudo) {
return "1";
} else {
return "0.0";
}
});
nodes.append("inAD").attr("inAD", function(d){
if (!d.pseudo) {
if(d.ad) {
return d.ad['inAD'];
} else {
return false;
}
} else {
return;
}
});
nodes.append("image").attr("xlink:href", function(d) {
if (!d.pseudo) {
if(ADView) {
return d.image += "&highlight=true";
}
return d.image;
} else {
return;
}
}).attr("x", function(d) {
if (!d.pseudo) {
return -0.5*(1.0-compoundImageMargin)*d.size
}
else {
return 0;
}
}).attr("y", function(d) {
if (!d.pseudo) {
return -0.5*(1.0-compoundImageMargin)*d.size
}
else {
return 0;
}
}).on("click",
function(d) {
if (!d.pseudo) {
return click(d);
} else {
return;
}
}).attr("width", function(d) {
if (!d.pseudo) {
return d.size * (1.0-compoundImageMargin);
} else {
return 0;
}
}).attr("height", function(d) {
if (!d.pseudo) {
return d.size * (1.0-compoundImageMargin);
} else {
return 0;
}
});
function removeObjDel(id) {
var button = document.getElementById(id);
button.parentNode.removeChild(button);
}
node_text = nodes.append("text").attr("text-anchor", "middle").style("fill", "#000")
.style("font-size", function() {
return compoundTextSize() + "pt";
}).style("font-family", "sans-serif").style("stroke-width", "0")
.attr("transform", function() {
return "translate(0, -" + compoundTextYOffset() + ")";
}).text(function(d) {
if (!d.pseudo && showCompoundNames) {
return d.name;
} else {
return "";
}
});
if (popupEnabled)
pop_add(nodes, "Compound", function(d) {
var prop = d.proposed;
var proposedInfo = '';
if(d.ad) {
proposedInfo += "This compound is " + (d.ad['inAD'] ? "inside" : "outside") + " the Applicability Domain derived from the chemical (PCA) space constructed using the training data." + "<br>";
if(d.ad['uncoveredFG']) {
proposedInfo += "Compound contains functional groups not covered by the training set <br>";
}
}
if(graph.isIncremental && isLeaf(d)) {
console.log(d);
proposedInfo += '<a id="aNodeProp'+ d.index + '" class="btn btn-primary" onclick="predictFromNode(\'' + d.index + '\')" href="#">Predict from here</a><br>'
}
if(d.properties.length > 0) {
proposedInfo += "<br>";
for(x in d.properties) {
property = d.properties[x];
proposedInfo += property["name"] + ": " + property["value"] + '<br>';
}
proposedInfo += "<br>";
}
sortByKey(prop, 'scenarioName');
proposedInfo += "<a href='"+d.id+"'>"+d.name+"</a><br>"
+"Depth: "+d.depth+"<br><br>"
+"<img src="+d.image+" width='"+d.popupSize+"'>" + "<br>"
+"<br><br><p><b>Half-lives and related scenarios:</b></p>";
for(var i in prop){
// Get scenario name and uri
var scenName = prop[i].scenarioName;
var scenUri = prop[i].scenarioId;
proposedInfo += "<a href='" + scenUri + "'>" + scenName+"</a><br>";
// Iterate over all other properties
for(var key in prop[i]){
if (key === 'scenarioName' || key === 'scenarioId') {
continue;
}
proposedInfo += key+": "+prop[i][key]+"<br>";
}
proposedInfo += '<br>';
}
return proposedInfo;
});
// on each iteration: update links and translate compounds
function tick() {
links_invisible.attr("x1", function(d) {
return d.source.x;
}).attr("y1", function(d) {
return d.source.y;
}).attr("x2", function(d) {
return d.target.x;
}).attr("y2", function(d) {
return d.target.y;
});
links.attr("x1", function(d) {
return d.source.x;
}).attr("y1", function(d) {
return d.source.y;
}).attr("x2", function(d) {
return d.target.x;
}).attr("y2", function(d) {
return d.target.y;
});
link_text.attr("dx", function(d) {
if (d.target.pseudo) {
return d.target.x;
}
else if (d.source.pseudo) {
return d.source.x;
}
else {
return (d.source.x + d.target.x)/2;
}
}).attr("dy", function(d) {
if (d.target.pseudo) {
return d.target.y;
}
else if (d.source.pseudo) {
return d.source.y;
}
else {
return (d.source.y + d.target.y)/2;
}
});
nodes.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
// on each iteration: add forces and manual stopping of forces
force.on("tick", function(e) {
if (treeLayoutEnabled) {
// implement treeLayoutGravity
var ky = treeLayoutLevelGravity * e.alpha;
var kx = treeLayoutXCenterGravity * e.alpha;
graph.nodes.forEach(function(d) {
// pull back to y-level
yLevel = treeLayoutLevels()*d.treeLevel + compoundSize;
d.y -= (d.y - yLevel) * ky;
// pull back to center
xCenter = width/2;
d.x -= (d.x - xCenter) * kx;
});
}
// TODO find a better value =0.03...
if (force.alpha() <= 0.005004) {
for (var i = 0; i < nodes[0].length; i++) {
if (!nodes[0][i].__data__.pseudo) {
nodes[0][i].__data__.fixed = true;
}
}
setFix = true;
force.stop();
} else {
tick();
}
});
}
function isLeaf(node) {
return node.children.length == 0;
}
// adds a parents[] and a children[] array for each node
// set treeSize
function setChildrenAndParents(graph)
{
graph.nodes.forEach(function(node, i) {
node.parents = []
node.children = []
});
graph.links.forEach(function(link,j) {
source = graph.nodes[link.source];
target = graph.nodes[link.target];
source.children.push(target);
target.parents.push(source);
});
function count(node) {
node.touch = true;
c = 1;
node.children.forEach(function(child, i) {
if (!child.touch) {
c += count(child);
}
});
node.count = false;
return c;
}
graph.nodes.forEach(function(node, i) {
graph.nodes.forEach(function(n, i) {
n.touch = false;
});
node.treeSize = count(node);
});
graph.links.forEach(function(link,j) {
p = graph.nodes[link.source].children.length;
c = graph.nodes[link.target].parents.length;
link.siblings = Math.max(p, c);
});
}
// computes tree level for each node
// similar to depth, but not the shortest path to root
// i.e. if a node has parents of depth 1 and 2, its level is set to 3
// sets level of root compounds to level -1 of their children
// pseudo nodes will have intermedate level of .33 and .66
function setTreeLevel(graph) {
// init levels
graph.nodes.forEach(function(node, i) {
node.treeLevel = 0;
});
// recursively set levels
graph.nodes.forEach(function(node, i) {
graph.nodes.forEach(function(n, i) {
n.touch = false;
});
if (node.depth==0)
recursion(node);
});
function recursion(node) {
node.touch = true;
node.children.forEach(function(child, i) {
if (!child.touch) {
d = node.treeLevel;
if (node.pseudo || child.pseudo)
d += 0.33;
else
d += 1;
if (!child.pseudo)
d = Math.round(d);
if (d > child.treeLevel) {
child.treeLevel = d;
recursion(child);
}
}
});
node.touch = false;
}
// increase levels of root compounds that have no level=1 child, but children with higher levels
graph.nodes.forEach(function(node, i) {
if (node.treeLevel==0 && node.children.length>0) {
minLevel = 10000;
node.children.forEach(function(child,j) {
d = child.treeLevel;
if (node.pseudo || child.pseudo)
d -= 0.33;
else
d -= 1;
if (!node.pseudo)
d = Math.round(d);
if (d < minLevel)
minLevel = d;
});
if (minLevel > 0) {
node.treeLevel = minLevel;
}
}
});
// pseudo nodes with non-pseudo parent always have 0.33
// set to 0.5 if they do not have pseudo children
graph.nodes.forEach(function(node, i) {
if (node.pseudo && (node.parents.size>1 || !node.parents[0].pseudo)) {
if (node.children.size>1 || !node.children[0].pseudo) {
node.treeLevel = (node.children[0].treeLevel + node.parents[0].treeLevel) * 0.5;
}
}
});
}
// builds a tree layout by iterating through the tree
// * the treeLevel is determined beforehand and remains unchanged
// * puts all nodes into a map with key treeLevel
// * tree layout is the order of the node arrays for each level
// * dfs insertion
// * idea is to insert children first which have children that are already present
function buildTreeLayout(graph) {
treeLayout = {}
// start off with the root nodes
roots = []
graph.nodes.forEach(function(node, i) {
if (node.depth==0)
roots.push(node);
});
insertNodes(roots);
// picks the node with the max-ratio of already inserted children
function insertNodes(nodes) {
if (nodes.length==1) {
insertNode(nodes[0]);
}
else if (nodes.length>1) {
maxIdx = -1;
nodes.forEach(function(node,j) {
graph.nodes.forEach(function(n, i) {
n.touch = false;
});
insertedChildren = findInserted(node, true);
insertedParents = findInserted(node, false);
total = insertedChildren.total + insertedParents.total;
found = insertedChildren.found + insertedParents.found;
if (total==0)
node.foundRatio = 0;
else
node.foundRatio = found/total;
if (maxIdx==-1 || node.foundRatio>nodes[maxIdx].foundRatio)
maxIdx = j;
});
// insert max-ratio-node, and remove it from node array
maxNode = nodes[maxIdx]
nodes = nodes.slice(); // copy array
nodes.splice(maxIdx, 1); // remove node
insertNode(maxNode);
// recalculate the found-ratio scores for the remaining nodes
insertNodes(nodes);
}
}
// inserts a single node (if not already included) and then inserts its children
function insertNode(node) {
if (!treeLayout[node.treeLevel]) {
treeLayout[node.treeLevel] = [];
}
if ($.inArray(node, treeLayout[node.treeLevel]) == -1) {
treeLayout[node.treeLevel].push(node);
insertNodes(node.children);
}
}
// to determine the ratio of already inserted children/parents
function findInserted(node, downwards) {
var found = 0;
var total = 0;
nodes = downwards ? node.children : node.parents;
nodes.forEach(function(n,j) {
if (!n.touch) {
n.touch = true;
if (treeLayout[n.treeLevel] && $.inArray(n, treeLayout[n.treeLevel])!=-1)
found += 1;
total += 1;
newCC = findInserted(n, downwards);
found += newCC.found;
total += newCC.total;
}
});
return {found: found, total: total};
}
return treeLayout;
}
// sets all links to pseudo that are connected to a pseudo node
function setPseudoLinks(graph) {
graph.links.forEach(function(link,j) {
if (graph.nodes[link.source].pseudo || graph.nodes[link.target].pseudo) {
link.pseudo = true;
}
else {
link.pseudo = false;
}
});
}
// compute size of compound-node and store it in <node>.size value
// size of smallest compound will be assigned to 'compoundSize'
// size of larger compounds is limited by 'maxCompoundSize'
function computeNodeSizes(nodes) {
// step1: determine size of smallest compound
min_size = 1000000;
for (var i = 0; i < nodes.length; i++) {
if (!nodes[i].pseudo) {
s = nodes[i].imageSize;
if (s < min_size) {
min_size = s;
}
}
}
// step2: compute scaled size and store as .size
for (var i = 0; i < nodes.length; i++) {
if (!nodes[i].pseudo) {
size = nodes[i].imageSize;
r = size/min_size;
size = r * compoundSize;
nodes[i].size = Math.min(size,maxCompoundSize());
nodes[i].popupSize = Math.min(size,maxCompoundPopupSize());
}
}
}
function toggleCompoundOutlines() {
showCompoundOutlines = !showCompoundOutlines;
circles.style("stroke-opacity",function(d) {
if (showCompoundOutlines) {
return compoundStrokeOpacity(d);
} else {
return 0.0;
}
});
}
function toggleCompoundNames() {
showCompoundNames = !showCompoundNames;
node_text.text(function(d) {
if (!d.pseudo && showCompoundNames) {
return d.name;
} else {
return "";
}
});
}
function toggleReactionNames() {
showReactionNames = !showReactionNames;
updateReactionText();
}
function toggleReactionProb() {
showReactionProb = !showReactionProb;
updateReactionText();
}
function updateReactionText() {
link_text.text(function(d){
s = "";
if(showReactionNames){
s = d.name;
}
if(d.rule && showReactionProb){
s += " Probability: "+d.modelpredictionprob;
}
return s.trim();
});
}
function pop_add(objects, title, contentFunction, offsetFunction) {
objects.attr("id","pop")
.attr("data-container","body")
.attr("data-toggle","popover")
.attr("data-placement","right")
.attr("title",title);
objects.each(function(d,i) {
options = { trigger: "manual", html: true, animation:false };
this_ = this;
var p = $(this).popover(options).on("mouseenter", function () {
pop_show_e(this);
});
p.on("show.bs.popover", function(e){
// this is to dynamically ajdust the content and bounds of the popup
p.attr('data-content',contentFunction(d));
p.data("bs.popover").setContent();
if (offsetFunction) {
offset = offsetFunction(d);
p.data("bs.popover").tip().css({"margin-left": offset["x"]+"px", "margin-top": offset["y"]+"px"});
}
p.data("bs.popover").tip().css({"max-width": "1000px"});
});
});
}
function pop_hideall() {
svg.selectAll("#pop").each(function(d,i){$(this).popover("hide")});
}
function pop_show_e(element) {
var e = element;
setTimeout(function () {
if ($(e).is(':hover')) { // if element is still hovered
$(e).popover("show");
setTimeout(function () {
var close = setInterval(function(){
if (!$(".popover:hover").length // mouse outside popover
&& !$(e).is(':hover')) { // mouse outside element
$(e).popover('hide');
clearInterval(close);
}
}, 100);
},popushowAtLeast);
}
},popupWaitBeforeShow);
}
function pop_show() {
svg.selectAll("#pop").each(function(d,i){
pop_show_e(this);
});
}
function redraw() {
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale("
+ d3.event.scale + ")");
}
function hide(node) {
var nodeArr = new Array();
var linkArr = new Array();
var nodeIndices = new Array();
// The if case handles the if the user wants to hide nodes with
// a specific atomcount
if (!node) {
var limit = document.getElementById("counter").value;
if (limit.length == 0 || limit == 0) {
// nothing to hide...
return;
}
// Searching all relevant nodes...
for (var i = 0; i < nodes[0].length; i++) {
if (nodes[0][i].__data__.atomCount <= limit) {
nodeArr.push(nodes[0][i]);
nodeIndices.push(nodes[0][i].__data__.index);
}
}
}
// This case handles the deletion of one single node
else {
for (var i = 0; i < nodes[0].length; i++) {
if (nodes[0][i].__data__.id == node.id) {
nodeArr.push(nodes[0][i]);
nodeIndices.push(nodes[0][i].__data__.index);
}
}
}
var tempLinks = new Array();
// searching relevant links
for (var i = 0; i < links[0].length; i++) {
var target = links[0][i].__data__.target.index;
var source = links[0][i].__data__.source.index;
if (nodeIndices.indexOf(source) != -1
|| nodeIndices.indexOf(target) != -1) {
// linkArr.push(links[0][i]);
tempLinks.push(links[0][i]);
}
}
for (var i = 0; i < links[0].length; i++) {
for (var j = 0; j < tempLinks.length; j++) {
if (tempLinks[j].__data__.id == links[0][i].__data__.id) {
linkArr.push(links[0][i]);
}
}
}
hideNodes(nodeArr);
hideLinks(linkArr);
force.resume();
}
// TODO method hideNodes and hideLinks are equal merge it
function hideNodes(nodeArray) {
if (detectedBrowser == "Chromium" || detectedBrowser == "Chrome") {
for (var i = 0; i < nodeArray.length; i++) {
nodeArray[i].remove();
}
}
else {
for (var k = 0; k < nodeArray.length; k++) {
var parentNode = nodeArray[k].parentNode;
if (parentNode == null) {
continue;
}
var childrens = parentNode.childNodes;
for (var j = 0; j < childrens.length; j++) {
if (childrens[j] == nodeArray[k]) {
parentNode.removeChild(childrens[j]);
break;
}
}
}
}
}
function hideLinks(linkArray) {
for (var k = 0; k < linkArray.length; k++) {
var parentNode = linkArray[k].parentNode;
if (parentNode == null) {
continue;
}
var childrens = parentNode.childNodes;
for (var j = 0; j < childrens.length; j++) {
if (childrens[j] == linkArray[k]) {
parentNode.removeChild(childrens[j]);
}
}
}
}
function reset() {
// remove all;
svg.selectAll("g.node").remove();
svg.selectAll("rect").remove();
svg.selectAll("image").remove();
svg.selectAll("line").remove();
svg.selectAll("marker").remove();
svg.selectAll("text").remove();
svg.selectAll("g").remove();
svg.selectAll("defs").remove();
start(__graph);
}
function click(d) {
if (!selectionEnabled)
return;
try {
if (d3.event.defaultPrevented) {
return;
}
} catch (Exception) {
console.log("mhm");
}
for (var i = 0; i < nodes[0].length; i++) {
if (nodes[0][i].__data__.id == d.id) {
tempNode = nodes[0][i].childNodes[0];
}
}
if (mode === "educt" || mode === "product") {
var comp = new Array();
comp.push(tempNode);
if (mode === "educt") {
var list = document.getElementById("eductList");
appendCompounds(educts, list, comp);
} else {
var list = document.getElementById("productList");
appendCompounds(products, list, comp);
}
return;
}
if (edgeSelected) {
edgeClick(clickedEdge_d);
}
// variable of child from where dfs should start
var from = tempNode.parentNode.__data__.index;
if (clickedNode == 'undefined') {
setSelectedDropdownValue('#nodeDropdown', d.id);
setSelectedDropdownValue('#nodeToHl', d.id);
nodeSelected = true;
compoundNodeUri = d.idcomp;
markedCompUri = tempNode.parentNode.__data__.id;
clickedNode = tempNode;
clickedNode_d = d;
mark(clickedNode, nodeSelected);
resetArrays();
rootIndices.forEach(function(rootIndex) {
dfs(from, rootIndex, from, path, pathNumeric);
});
for (var i = 0; i < paths.length; i++) {
paintPath(paths[i],nodeSelected, clickedNode_d);
}
findOutgoing(from);
markOutgoingLinks(greenLinks, nodeSelected);
} else if (clickedNode == tempNode) {
setSelectedDropdownValue('#nodeDropdown', "undefined");
setSelectedDropdownValue('#nodeToHl', "undefined");
nodeSelected = false;
old_d = clickedNode_d;
markedCompUri = "undefined";
mark(clickedNode, nodeSelected);
// update sidebar with empty string
clickedNode = 'undefined';
clickedNode_d = 'undefined';
compoundNodeUri = 'undefined';
// in this case the path is already painted and
// the node is clicked again -> unpaint
for (var i = 0; i < paths.length; i++) {
paintPath(paths[i],nodeSelected, old_d);
}
markOutgoingLinks(greenLinks,nodeSelected);
// initial state
resetArrays();
} else {
setSelectedDropdownValue('#nodeDropdown', d.id);
setSelectedDropdownValue('#nodeToHl', d.id);
nodeSelected = true;
markedCompUri = tempNode.parentNode.__data__.id;
mark(clickedNode, false);
mark(tempNode, true);
clickedNode = tempNode;
clickedNode_d = d;
compoundNodeUri = d.idcomp;
for (var i = 0; i < paths.length; i++) {
paintPath(paths[i],false, clickedNode_d);
}
markOutgoingLinks(greenLinks,false);
resetArrays();
rootIndices.forEach(function(rootIndex) {
dfs(from, rootIndex, from, path, pathNumeric);
});
for (var i = 0; i < paths.length; i++) {
paintPath(paths[i],nodeSelected, clickedNode_d);
}
findOutgoing(from);
markOutgoingLinks(greenLinks,nodeSelected);
}
}
function appendCompounds(datastore, list, comp) {
for (var i = 0; i < comp.length; i++) {
var li = document.createElement("li");
li.innerHTML = comp[i].__data__.name;
list.appendChild(li);
datastore.push([ comp[i].__data__.name, comp[i].__data__.id ]);
}
}
function clearList(list) {
list.innerHTML = "";
educts = [];
products = [];
}
function edgeClick(d) {
if (!selectionEnabled)
return;
var connectors = new Array();
var old_connectors = new Array();
if (nodeSelected) {
// objectSelected = false;
click(clickedNode_d);
}
var tempEdge = 'undefined';
// Determine the selected Edge
for (var i = 0; i < links[0].length; i++) {
if (links[0][i].__data__.id == d.id) {
tempEdge = links[0][i];
connectors.push(tempEdge);
old_connectors.push(tempEdge);
break;
}
}
if (tempEdge == 'undefined') {
return;
}
for (var i = 0; i < links[0].length; i++) {
if (links[0][i].__data__.id == tempEdge.__data__.id) {
connectors.push(links[0][i]);
}
if (clickedEdge != 'undefined') {
if (links[0][i].__data__.id == clickedEdge.__data__.id) {
old_connectors.push(links[0][i]);
}
}
}
var eductsTemp = new Array();
var productsTemp = new Array();
for (var i = 0; i < connectors.length; i++) {
for (var j = 0; j < nodes[0].length; j++) {
if (connectors[i].__data__.source_byname == nodes[0][j].__data__.name) {
eductsTemp.push(nodes[0][j]);
} else if (connectors[i].__data__.target_byname == nodes[0][j].__data__.name) {
productsTemp.push(nodes[0][j]);
}
}
}
// console.log(educts);
// console.log(products);
if (clickedEdge == 'undefined') {
setSelectedDropdownValue('#edgeDropdown', d.id);
edgeSelected = true;
clickedEdge = tempEdge;
clickedEdge_d = d;
markedEdgeUri = clickedEdge.__data__.id;
paintPath(clickedEdge,edgeSelected, clickedEdge_d);
for (var i = 0; i < connectors.length; i++) {
paintPath(connectors[i],edgeSelected, clickedEdge_d);
}
// var list = document.getElementById("eductList");
// appendCompounds(educts,list,eductsTemp);
// var list = document.getElementById("productList");
// appendCompounds(products,list,productsTemp);
} else if (clickedEdge == tempEdge) {
setSelectedDropdownValue('#edgeDropdown', "undefined");
old_d = clickedEdge_d;
edgeSelected = false;
clickedEdge = 'undefined';
markedEdgeUri = 'undefined';
clickedEdge_d = 'undefined';
for (var i = 0; i < connectors.length; i++) {
paintPath(connectors[i],edgeSelected, old_d);
}
paintPath(tempEdge,edgeSelected, old_d);
//
// var list = document.getElementById("eductList");
// clearList(list);
// var list = document.getElementById("productList");
// clearList(list);
} else {
setSelectedDropdownValue('#edgeDropdown', d.id);
// console.log(old_connectors);
edgeSelected = true;
// paintPath(clickedEdge);
clickedEdge = tempEdge;
clickedEdge_d = d;
for (var i = 0; i < old_connectors.length; i++) {
paintPath(old_connectors[i],edgeSelected, clickedEdge_d);
}
for (var i = 0; i < connectors.length; i++) {
paintPath(connectors[i],edgeSelected, clickedEdge_d);
}
// paintPath(clickedEdge);
markedEdgeUri = clickedEdge.__data__.id;
// var list = document.getElementById("eductList");
// clearList(list);
// var list = document.getElementById("productList");
// clearList(list);
// var list = document.getElementById("eductList");
// appendCompounds(educts,list,eductsTemp);
// var list = document.getElementById("productList");
// appendCompounds(products,list,productsTemp);
}
}
function mark(nodeToMark, select) {
if (select){
nodeToMark.style.fill = compoundFillSelected;
}
else {
nodeToMark.style.fill = compoundFill;
}
}
/*
* Depth first search
*/
function dfs(from, to, orig, path, pathNumeric) {
if (from == to) {
// add path to all found paths
for (var i = 0; i < path.length; i++) {
if (paths.indexOf(path[i]) == -1) {
paths.push(path[i]);
}
}
return;
} else {
for (var i = 0; i < links[0].length; i++) {
if (links[0][i].__data__.target.index == nodes[0][from].__data__.index
&& visited[links[0][i].__data__.source.index] == -1) {
if (links[0][i].__data__.target.index != links[0][i].__data__.source.index) {
path.push(links[0][i]);
// @DEBUG pathnumeric stores indices of links
// pathNumeric.push(/*links[0][i].__data__.target.index*/
// i);
var curr = links[0][i].__data__.source.index;
visited[from] = links[0][i].__data__.source.index;
dfs(curr, to, orig, path, pathNumeric);
// Backtracking to find all possible paths
path.pop();
// pathNumeric.pop();
visited[from] = -1;
}
}
}
}
}
function paintPath(path, select, d) {
var curr = path.style;
passes = false;
if(ADView) {
if ('ad' in d){
if(d['id'].includes('node')) {
passes = d.ad['inAD'];
} else {
passes = d.ad[0]['passesAD']
}
}
}
if (select) {
curr.stroke = linkStrokeUp;
curr.strokeOpacity = linkStrokeOpacitySelected;
if (path.getAttribute("marker-end"))
if (path.getAttribute("target-pseudo"))
path.setAttribute("marker-end","url(#arrow-red)");
else
path.setAttribute("marker-end","url(#arrow-red_"+path.getAttribute("idx")+")");
}
else {
curr.stroke = ADView ? (passes ? adIn : adOut) : linkStroke
curr.strokeOpacity = ADView ? (passes ? 1 : 0) : linkStrokeOpacity;
if (path.getAttribute("marker-end"))
if (path.getAttribute("target-pseudo"))
path.setAttribute("marker-end","url(#arrow)");
else
path.setAttribute("marker-end","url(#arrow_"+path.getAttribute("idx")+")");
}
}
function markOutgoingLinks(links, select) {
for (var i = 0; i < links.length; i++) {
var curr = links[i].style;
if (select) {
curr.stroke = linkStrokeDown;
curr.strokeOpacity = linkStrokeOpacitySelected;
if (links[i].getAttribute("marker-end"))
if (links[i].getAttribute("target-pseudo"))
links[i].setAttribute("marker-end","url(#arrow-green)");
else
links[i].setAttribute("marker-end","url(#arrow-green_"+links[i].getAttribute("idx")+")");
}
else {
curr.stroke = linkStroke;
curr.strokeOpacity = linkStrokeOpacity;
if (links[i].getAttribute("marker-end"))
if (links[i].getAttribute("target-pseudo"))
links[i].setAttribute("marker-end","url(#arrow)");
else
links[i].setAttribute("marker-end","url(#arrow_"+links[i].getAttribute("idx")+")");
}
}
}
function findOutgoing(from) {
var tempLinks = new Array();
for (var i = 0; i < links[0].length; i++) {
if (links[0][i].__data__.source.index == from) {
// greenLinks.push(links[0][i]);
tempLinks.push(links[0][i]);
}
}
for (var j = 0; j < tempLinks.length; j++) {
for (var i = 0; i < links[0].length; i++) {
if (tempLinks[j].__data__.id == links[0][i].__data__.id) {
greenLinks.push(links[0][i]);
}
}
}
}
function findConnectedEdges(from) {
// var res = new Array();
for (var i = 0; i < links[0].length; i++) {
if (links[0][i].__data__.source.index == from) {
for (var j = 0; j < links[0].length; j++) {
if (links[0][i].__data__.id == links[0][j].__data__.id) {
greenLinks.push(links[0][j]);
}
}
}
}
}
function resetArrays() {
visited = new Array(nodes[0].length);
for (var i = 0; i < visited.length; i++) {
visited[i] = -1;
}
path = [];
paths = [];
pathNumeric = [];
greenLinks = [];
}
function uploadReaction() {
var eductsidlist = [];
for ( var x in educts) {
eductsidlist.push(educts[x][1]);
}
var productsidlist = [];
for ( var x in products) {
productsidlist.push(products[x][1]);
}
if (educts.length == 0 || products.length == 0) {
console.log("Caught empty list");
return;
}
var url = window.location.href + "/edge";
var multi = $("#multistep").is(":checked");
var data = {
products : productsidlist.join(),
educts : eductsidlist.join(),
edgeReason : "",
multistep : multi
};
jQuery.ajax({
type : "post",
data : data,
url : url,
success : function(response) {
console.log("success");
location.reload();
},
error : function(jqXHR, textStatus, errorThrown) {
alert("Error while adding Edge");
console.log(errorThrown);
console.log(jqXHR);
console.log(textStatus);
}
});
}
function dl(url) {
var hiddenIFrameID = 'hiddenDownloader', iframe = document
.getElementById(hiddenIFrameID);
if (iframe === null) {
iframe = document.createElement('iframe');
iframe.id = hiddenIFrameID;
iframe.style.display = 'none';
document.body.appendChild(iframe);
}
iframe.src = url;
}
function dlCSV() {
var thresh = $('#thresh').val();
console.log(thresh);
if(!thresh){
thresh = 0.001;
}
var uri = '?toCSV=true&threshold='+thresh;
dl(uri);
}
function dlPW(){
var uri = "?hidden=download";
var thresh = $('#threshold').val();
if(!thresh){
thresh = 0.001;
}
uri += "&threshold="+thresh;
var format = $('#format').val();
uri += "&format="+format;
if(format === 'custom'){
var delimiter = $('#delimiter').val();
if(delimiter){
uri += "&delimiter="+delimiter;
}
}
dl(uri);
}