forked from enviPath/enviPy
285 lines
9.9 KiB
JavaScript
285 lines
9.9 KiB
JavaScript
console.log("loaded")
|
|
|
|
|
|
function predictFromNode(url) {
|
|
$.post("", {node: url})
|
|
.done(function (data) {
|
|
console.log("Success:", data);
|
|
window.location.href = data.success;
|
|
})
|
|
.fail(function (xhr, status, error) {
|
|
console.error("Error:", xhr.status, xhr.responseText);
|
|
// show user-friendly message or log error
|
|
});
|
|
}
|
|
|
|
// data = {{ pathway.d3_json | safe }};
|
|
// elem = 'vizdiv'
|
|
function draw(pathway, elem) {
|
|
|
|
const nodeRadius = 20;
|
|
const linkDistance = 100;
|
|
const chargeStrength = -200;
|
|
const depthSpacing = 150;
|
|
const width = document.getElementById(elem).offsetWidth, height = width * 0.75;
|
|
|
|
function assignPositions(nodes) {
|
|
const levelSpacing = 75; // vertical space between levels
|
|
const horizontalSpacing = 75; // horizontal space between nodes
|
|
const depthMap = new Map();
|
|
|
|
nodes.forEach(node => {
|
|
if (!depthMap.has(node.depth)) {
|
|
depthMap.set(node.depth, 0);
|
|
}
|
|
|
|
const nodesInLevel = nodes.filter(n => n.depth === node.depth).length;
|
|
node.fx = width / 2 + depthMap.get(node.depth) * horizontalSpacing - ((nodesInLevel - 1) * horizontalSpacing) / 2;
|
|
node.fy = node.depth * levelSpacing + 50;
|
|
|
|
depthMap.set(node.depth, depthMap.get(node.depth) + 1);
|
|
});
|
|
}
|
|
|
|
// Funktion für das Update der Positionen
|
|
function ticked() {
|
|
link.attr("x1", d => d.source.x)
|
|
.attr("y1", d => d.source.y)
|
|
.attr("x2", d => d.target.x)
|
|
.attr("y2", d => d.target.y);
|
|
|
|
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
|
|
|
nodes.forEach(n => {
|
|
if (n.pseudo) {
|
|
// Alle Kinder dieses Pseudonodes finden
|
|
const childLinks = links.filter(l => l.source.id === n.id);
|
|
const childNodes = childLinks.map(l => l.target);
|
|
if (childNodes.length > 0) {
|
|
// Durchschnitt der Kinderpositionen berechnen
|
|
const avgX = d3.mean(childNodes, d => d.x);
|
|
const avgY = d3.mean(childNodes, d => d.y);
|
|
n.fx = avgX;
|
|
// keep level as is
|
|
n.fy = n.y;
|
|
}
|
|
}
|
|
});
|
|
//simulation.alpha(0.3).restart();
|
|
}
|
|
|
|
function dragstarted(event, d) {
|
|
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
d.fx = d.x; // Setzt die Fixierung auf die aktuelle Position
|
|
d.fy = d.y;
|
|
}
|
|
|
|
function dragged(event, d) {
|
|
d.fx = event.x; // Position direkt an Maus anpassen
|
|
d.fy = event.y;
|
|
}
|
|
|
|
function dragended(event, d) {
|
|
if (!event.active) simulation.alphaTarget(0);
|
|
// Knoten bleibt an der neuen Position und wird nicht zurückgezogen
|
|
}
|
|
|
|
// t -> ref to "this" from d3
|
|
function nodeClick(event, node, t) {
|
|
console.log(node);
|
|
d3.select(t).select("circle").classed("highlighted", !d3.select(t).select("circle").classed("highlighted"));
|
|
}
|
|
|
|
// Wait one second before showing popup
|
|
var popupWaitBeforeShow = 1000;
|
|
// Keep Popup at least for one second
|
|
var popushowAtLeast = 1000;
|
|
|
|
function pop_show_e(element) {
|
|
var e = element;
|
|
setTimeout(function () {
|
|
if ($(e).is(':hover')) { // if element is still hovered
|
|
$(e).popover("show");
|
|
|
|
// workaround to set fixed positions
|
|
pop = $(e).attr("aria-describedby")
|
|
h = $('#' + pop).height();
|
|
$('#' + pop).attr("style", `position: fixed; top: ${clientY - (h / 2.0)}px; left: ${clientX + 10}px; margin: 0px; max-width: 1000px; display: block;`)
|
|
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_add(objects, title, contentFunction) {
|
|
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();
|
|
p.data("bs.popover").tip().css({"max-width": "1000px"});
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
function node_popup(n) {
|
|
popupContent = "<a href='" + n.url +"'>" + n.name + "</a><br>";
|
|
popupContent += "Depth " + n.depth + "<br>"
|
|
popupContent += "<img src='" + n.image + "' width='"+ 20 * nodeRadius +"'><br>"
|
|
if (n.scenarios.length > 0) {
|
|
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
|
for (var s of n.scenarios) {
|
|
popupContent += "<a href='" + s.url + "'>" + s.name + "</a><br>";
|
|
}
|
|
}
|
|
|
|
var isLeaf = pathway.links.filter(obj => obj.source.id === n.id).length === 0;
|
|
if(pathway.isIncremental && isLeaf) {
|
|
popupContent += '<br><a class="btn btn-primary" onclick="predictFromNode(\'' + n.url + '\')" href="#">Predict from here</a><br>';
|
|
}
|
|
|
|
return popupContent;
|
|
}
|
|
|
|
function edge_popup(e) {
|
|
popupContent = "<a href='" + e.url +"'>" + e.name + "</a><br>";
|
|
popupContent += "<img src='" + e.image + "' width='"+ 20 * nodeRadius +"'><br>"
|
|
if (e.reaction_probability) {
|
|
popupContent += '<b>Probability:</b><br>' + e.reaction_probability.toFixed(3) + '<br>';
|
|
}
|
|
|
|
|
|
if (e.scenarios.length > 0) {
|
|
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
|
for (var s of e.scenarios) {
|
|
popupContent += "<a href='" + s.url + "'>" + s.name + "</a><br>";
|
|
}
|
|
}
|
|
|
|
return popupContent;
|
|
}
|
|
|
|
var clientX;
|
|
var clientY;
|
|
document.addEventListener('mousemove', function(event) {
|
|
clientX = event.clientX;
|
|
clientY =event.clientY;
|
|
});
|
|
|
|
const zoomable = d3.select("#zoomable");
|
|
|
|
// Zoom Funktion aktivieren
|
|
const zoom = d3.zoom()
|
|
.scaleExtent([0.5, 5])
|
|
.on("zoom", (event) => {
|
|
zoomable.attr("transform", event.transform);
|
|
});
|
|
|
|
d3.select("svg").call(zoom);
|
|
|
|
nodes = pathway['nodes'];
|
|
links = pathway['links'];
|
|
|
|
// Use "max" depth for a neater viz
|
|
nodes.forEach(n => {
|
|
const parents = links
|
|
.filter(l => l.target === n.id)
|
|
.map(l => l.source);
|
|
|
|
orig_depth = n.depth
|
|
// console.log(n.id, parents)
|
|
for (idx in parents) {
|
|
p = nodes[parents[idx]]
|
|
// console.log(p.depth)
|
|
if (p.depth >= n.depth) {
|
|
// keep the .5 steps for pseudo nodes
|
|
n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
|
|
// console.log("Adjusting", orig_depth, Math.floor(p.depth + 1));
|
|
}
|
|
}
|
|
});
|
|
|
|
assignPositions(nodes);
|
|
|
|
const simulation = d3.forceSimulation(nodes)
|
|
.force("link", d3.forceLink(links).id(d => d.id).distance(linkDistance))
|
|
.force("charge", d3.forceManyBody().strength(chargeStrength))
|
|
.force("center", d3.forceCenter(width / 2, height / 4))
|
|
.on("tick", ticked);
|
|
|
|
// Kanten zeichnen
|
|
const link = zoomable.append("g")
|
|
.selectAll("line")
|
|
.data(links)
|
|
.enter().append("line")
|
|
// Check if target is pseudo and draw marker only if not pseudo
|
|
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
|
// .on("mouseover", (event, d) => {
|
|
// tooltip.style("visibility", "visible")
|
|
// .text(`Link: ${d.source.id} → ${d.target.id}`)
|
|
// .style("top", `${event.pageY + 5}px`)
|
|
// .style("left", `${event.pageX + 5}px`);
|
|
// })
|
|
// .on("mouseout", () => tooltip.style("visibility", "hidden"));
|
|
|
|
pop_add(link, "Reaction", edge_popup);
|
|
|
|
// Knoten zeichnen
|
|
const node = zoomable.append("g")
|
|
.selectAll("g")
|
|
.data(nodes)
|
|
.enter().append("g")
|
|
.call(d3.drag()
|
|
.on("start", dragstarted)
|
|
.on("drag", dragged)
|
|
.on("end", dragended))
|
|
.on("click", function (event, d) {
|
|
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
|
|
})
|
|
// .on("mouseover", (event, d) => {
|
|
// if (d.pseudo) {
|
|
// return
|
|
// }
|
|
// tooltip.style("visibility", "visible")
|
|
// .text(`Node: ${d.id} Depth: ${d.depth}`)
|
|
// .style("top", `${event.pageY + 5}px`)
|
|
// .style("left", `${event.pageX + 5}px`);
|
|
// })
|
|
// .on("mouseout", () => tooltip.style("visibility", "hidden"));
|
|
|
|
// Kreise für die Knoten hinzufügen
|
|
node.append("circle")
|
|
// make radius "invisible"
|
|
.attr("r", d => d.pseudo ? 0.01 : nodeRadius)
|
|
.style("fill", "#e8e8e8");
|
|
|
|
// Add image only for non pseudo nodes
|
|
node.filter(d => !d.pseudo).append("image")
|
|
.attr("xlink:href", d => d.image)
|
|
.attr("x", -nodeRadius)
|
|
.attr("y", -nodeRadius)
|
|
.attr("width", nodeRadius * 2)
|
|
.attr("height", nodeRadius * 2);
|
|
|
|
pop_add(node, "Compound", node_popup);
|
|
}
|