console.log("loaded pw.js")
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();
// Sort nodes by depth first to minimize crossings
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
sortedNodes.forEach(node => {
if (!depthMap.has(node.depth)) {
depthMap.set(node.depth, 0);
}
const nodesInLevel = nodes.filter(n => n.depth === node.depth).length;
// For pseudo nodes, try to position them to minimize crossings
if (node.pseudo) {
const parentLinks = links.filter(l => l.target.id === node.id);
const childLinks = links.filter(l => l.source.id === node.id);
if (parentLinks.length > 0 && childLinks.length > 0) {
const parentX = parentLinks[0].source.x || (width / 2);
const childrenX = childLinks.map(l => l.target.x || (width / 2));
const avgChildX = childrenX.reduce((sum, x) => sum + x, 0) / childrenX.length;
// Position pseudo node between parent and average child position
node.fx = (parentX + avgChildX) / 2;
} else {
node.fx = width / 2 + depthMap.get(node.depth) * horizontalSpacing - ((nodesInLevel - 1) * horizontalSpacing) / 2;
}
} else {
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);
});
}
// Function to update pseudo node positions based on connected nodes
function updatePseudoNodePositions() {
nodes.forEach(node => {
if (node.pseudo && !node.isDragging) { // Don't auto-update if being dragged
const parentLinks = links.filter(l => l.target.id === node.id);
const childLinks = links.filter(l => l.source.id === node.id);
if (parentLinks.length > 0 && childLinks.length > 0) {
const parent = parentLinks[0].source;
const children = childLinks.map(l => l.target);
// Calculate optimal position to minimize crossing
const parentX = parent.x;
const parentY = parent.y;
const childrenX = children.map(c => c.x);
const childrenY = children.map(c => c.y);
const avgChildX = d3.mean(childrenX);
const avgChildY = d3.mean(childrenY);
// Position pseudo node between parent and average child position
node.fx = (parentX + avgChildX) / 2;
node.fy = (parentY + avgChildY) / 2; // Allow vertical movement too
}
}
});
}
// Enhanced ticked function
function ticked() {
// Update pseudo node positions first
updatePseudoNodePositions();
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})`);
}
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
// Mark if this node is being dragged
d.isDragging = true;
// If dragging a non-pseudo node, mark connected pseudo nodes for update
if (!d.pseudo) {
markConnectedPseudoNodes(d);
}
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
// Update connected pseudo nodes in real-time
if (!d.pseudo) {
updateConnectedPseudoNodes(d);
}
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
// Mark that dragging has ended
d.isDragging = false;
// Final update of connected pseudo nodes
if (!d.pseudo) {
updateConnectedPseudoNodes(d);
}
}
// Helper function to mark connected pseudo nodes
function markConnectedPseudoNodes(draggedNode) {
// Find pseudo nodes connected to this node
const connectedPseudos = new Set();
// Check as parent of pseudo nodes
links.filter(l => l.source.id === draggedNode.id && l.target.pseudo)
.forEach(l => connectedPseudos.add(l.target));
// Check as child of pseudo nodes
links.filter(l => l.target.id === draggedNode.id && l.source.pseudo)
.forEach(l => connectedPseudos.add(l.source));
return connectedPseudos;
}
// Helper function to update connected pseudo nodes
function updateConnectedPseudoNodes(draggedNode) {
const connectedPseudos = markConnectedPseudoNodes(draggedNode);
connectedPseudos.forEach(pseudoNode => {
if (!pseudoNode.isDragging) { // Don't update if pseudo node is being dragged
const parentLinks = links.filter(l => l.target.id === pseudoNode.id);
const childLinks = links.filter(l => l.source.id === pseudoNode.id);
if (parentLinks.length > 0 && childLinks.length > 0) {
const parent = parentLinks[0].source;
const children = childLinks.map(l => l.target);
const parentX = parent.fx || parent.x;
const parentY = parent.fy || parent.y;
const childrenX = children.map(c => c.fx || c.x);
const childrenY = children.map(c => c.fy || c.y);
const avgChildX = d3.mean(childrenX);
const avgChildY = d3.mean(childrenY);
// Update pseudo node position - allow both X and Y movement
pseudoNode.fx = (parentX + avgChildX) / 2;
pseudoNode.fy = (parentY + avgChildY) / 2;
}
}
});
// Restart simulation with lower alpha to smooth the transition
simulation.alpha(0.1).restart();
}
// 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 = "" + n.name + "
";
popupContent += "Depth " + n.depth + "
"
if (appDomainViewEnabled) {
if (n.app_domain != null) {
popupContent += "This compound is " + (n.app_domain['inside_app_domain'] ? "inside" : "outside") + " the Applicability Domain derived from the chemical (PCA) space constructed using the training data." + "
"
if (n.app_domain['uncovered_functional_groups']) {
popupContent += "Compound contains functional groups not covered by the training set
"
}
}
}
popupContent += "
"
if (n.scenarios.length > 0) {
popupContent += 'Half-lives and related scenarios:
'
for (var s of n.scenarios) {
popupContent += "" + s.name + "
";
}
}
var isLeaf = pathway.links.filter(obj => obj.source.id === n.id).length === 0;
if (pathway.isIncremental && isLeaf) {
popupContent += '
Predict from here
';
}
return popupContent;
}
function edge_popup(e) {
popupContent = "" + e.name + "
";
if (e.app_domain) {
adcontent = "
";
if (e.app_domain["times_triggered"]) {
adcontent += "This rule triggered " + e.app_domain["times_triggered"] + " times in the training set
";
}
adcontent += "Reliability " + e.app_domain["reliability"].toFixed(2) + " (" + (e.app_domain["reliability"] > e.app_domain["reliability_threshold"] ? ">" : "<") + " Reliability Threshold of " + e.app_domain["reliability_threshold"] + ")
";
adcontent += "Local Compatibility " + e.app_domain["local_compatibility"].toFixed(2) + " (" + (e.app_domain["local_compatibility"] > e.app_domain["local_compatibility_threshold"] ? ">" : "<") + " Local Compatibility Threshold of " + e.app_domain["local_compatibility_threshold"] + ")
";
adcontent += "