forked from enviPath/enviPy
Fix App Domain Bug when a Rule can be applied more than once (#49)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#49
This commit is contained in:
160
epdb/models.py
160
epdb/models.py
@ -2,6 +2,8 @@ import abc
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import secrets
|
||||||
|
import hashlib
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Union, List, Optional, Dict, Tuple, Set
|
from typing import Union, List, Optional, Dict, Tuple, Set
|
||||||
@ -58,27 +60,110 @@ class User(AbstractUser):
|
|||||||
return self.default_setting
|
return self.default_setting
|
||||||
|
|
||||||
|
|
||||||
class APIToken(models.Model):
|
class APIToken(TimeStampedModel):
|
||||||
hashed_key = models.CharField(max_length=128, unique=True)
|
"""
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
API authentication token for users.
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
|
||||||
expires_at = models.DateTimeField(null=True, blank=True, default=timezone.now() + timedelta(days=90))
|
|
||||||
name = models.CharField(max_length=100, blank=True, help_text="Optional name for the token")
|
|
||||||
|
|
||||||
def is_valid(self):
|
Provides secure token-based authentication with expiration support.
|
||||||
return not self.expires_at or self.expires_at > timezone.now()
|
"""
|
||||||
|
hashed_key = models.CharField(
|
||||||
|
max_length=128,
|
||||||
|
unique=True,
|
||||||
|
help_text="SHA-256 hash of the token key"
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
user = models.ForeignKey(
|
||||||
def create_token(user, name="", valid_for=90):
|
User,
|
||||||
import secrets
|
on_delete=models.CASCADE,
|
||||||
raw_token = secrets.token_urlsafe(32)
|
related_name='api_tokens',
|
||||||
hashed = make_password(raw_token)
|
help_text="User who owns this token"
|
||||||
token = APIToken.objects.create(user=user, hashed_key=hashed, name=name,
|
)
|
||||||
expires_at=timezone.now() + timedelta(days=valid_for))
|
|
||||||
return token, raw_token
|
|
||||||
|
|
||||||
def check_token(self, raw_token):
|
expires_at = models.DateTimeField(
|
||||||
return check_password(raw_token, self.hashed_key)
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Token expiration time (null for no expiration)"
|
||||||
|
)
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
help_text="Descriptive name for this token"
|
||||||
|
)
|
||||||
|
|
||||||
|
is_active = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Whether this token is active"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'epdb_api_token'
|
||||||
|
verbose_name = 'API Token'
|
||||||
|
verbose_name_plural = 'API Tokens'
|
||||||
|
ordering = ['-created']
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.name} ({self.user.username})"
|
||||||
|
|
||||||
|
def is_valid(self) -> bool:
|
||||||
|
"""Check if token is valid and not expired."""
|
||||||
|
if not self.is_active:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.expires_at and timezone.now() > self.expires_at:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_token(cls, user: User, name: str, expires_days: Optional[int] = None) -> Tuple['APIToken', str]:
|
||||||
|
"""
|
||||||
|
Create a new API token for a user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: User to create token for
|
||||||
|
name: Descriptive name for the token
|
||||||
|
expires_days: Number of days until expiration (None for no expiration)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (token_instance, raw_key)
|
||||||
|
"""
|
||||||
|
raw_key = secrets.token_urlsafe(32)
|
||||||
|
hashed_key = hashlib.sha256(raw_key.encode()).hexdigest()
|
||||||
|
|
||||||
|
expires_at = None
|
||||||
|
if expires_days:
|
||||||
|
expires_at = timezone.now() + timezone.timedelta(days=expires_days)
|
||||||
|
|
||||||
|
token = cls.objects.create(
|
||||||
|
user=user,
|
||||||
|
name=name,
|
||||||
|
hashed_key=hashed_key,
|
||||||
|
expires_at=expires_at
|
||||||
|
)
|
||||||
|
|
||||||
|
return token, raw_key
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def authenticate(cls, raw_key: str) -> Optional[User]:
|
||||||
|
"""
|
||||||
|
Authenticate a user using an API token.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
raw_key: Raw token key
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User if token is valid, None otherwise
|
||||||
|
"""
|
||||||
|
hashed_key = hashlib.sha256(raw_key.encode()).hexdigest()
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = cls.objects.select_related('user').get(hashed_key=hashed_key)
|
||||||
|
if token.is_valid():
|
||||||
|
return token.user
|
||||||
|
except cls.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class Group(TimeStampedModel):
|
class Group(TimeStampedModel):
|
||||||
@ -1090,16 +1175,15 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
data = self.kv.get('app_domain_assessment', None)
|
data = self.kv.get('app_domain_assessment', None)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
rule_ids = dict()
|
rule_ids = defaultdict(list)
|
||||||
for e in Edge.objects.filter(start_nodes__in=[self]):
|
for e in Edge.objects.filter(start_nodes__in=[self]):
|
||||||
for r in e.edge_label.rules.all():
|
for r in e.edge_label.rules.all():
|
||||||
rule_ids[str(r.uuid)] = e
|
rule_ids[str(r.uuid)].append(e.simple_json())
|
||||||
|
|
||||||
|
|
||||||
for t in data['assessment']['transformations']:
|
for t in data['assessment']['transformations']:
|
||||||
if t['rule']['uuid'] in rule_ids:
|
if t['rule']['uuid'] in rule_ids:
|
||||||
t['is_predicted'] = True
|
t['is_predicted'] = True
|
||||||
t['edge'] = rule_ids[t['rule']['uuid']].simple_json()
|
t['edges'] = rule_ids[t['rule']['uuid']]
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -1141,23 +1225,25 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
|
|
||||||
if app_domain_data:
|
if app_domain_data:
|
||||||
for t in app_domain_data['assessment']['transformations']:
|
for t in app_domain_data['assessment']['transformations']:
|
||||||
if 'edge' in t and t['edge']['uuid'] == str(self.uuid):
|
if 'edges' in t:
|
||||||
passes_app_domain = (
|
for e in t['edges']:
|
||||||
t['local_compatibility'] >= app_domain_data['ad_params']['local_compatibility_threshold']
|
if e['uuid'] == str(self.uuid):
|
||||||
) and (
|
passes_app_domain = (
|
||||||
t['reliability'] >= app_domain_data['ad_params']['reliability_threshold']
|
t['local_compatibility'] >= app_domain_data['ad_params']['local_compatibility_threshold']
|
||||||
)
|
) and (
|
||||||
|
t['reliability'] >= app_domain_data['ad_params']['reliability_threshold']
|
||||||
|
)
|
||||||
|
|
||||||
edge_json['app_domain'] = {
|
edge_json['app_domain'] = {
|
||||||
'passes_app_domain': passes_app_domain,
|
'passes_app_domain': passes_app_domain,
|
||||||
'local_compatibility': t['local_compatibility'],
|
'local_compatibility': t['local_compatibility'],
|
||||||
'local_compatibility_threshold': app_domain_data['ad_params']['local_compatibility_threshold'],
|
'local_compatibility_threshold': app_domain_data['ad_params']['local_compatibility_threshold'],
|
||||||
'reliability': t['reliability'],
|
'reliability': t['reliability'],
|
||||||
'reliability_threshold': app_domain_data['ad_params']['reliability_threshold'],
|
'reliability_threshold': app_domain_data['ad_params']['reliability_threshold'],
|
||||||
'times_triggered': t['times_triggered'],
|
'times_triggered': t['times_triggered'],
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
return edge_json
|
return edge_json
|
||||||
|
|
||||||
|
|||||||
@ -739,7 +739,10 @@ function handleAssessmentResponse(depict_url, data) {
|
|||||||
var objLink = null;
|
var objLink = null;
|
||||||
if (transObj['is_predicted']) {
|
if (transObj['is_predicted']) {
|
||||||
panelName = `Predicted Transformation by ${transObj['rule']['name']}`;
|
panelName = `Predicted Transformation by ${transObj['rule']['name']}`;
|
||||||
objLink = `<a class='list-group-item' href="${transObj['edge']['url']}">${transObj['edge']['name']}</a>`
|
for (e in transObj['edges']) {
|
||||||
|
objLink = `<a class='list-group-item' href="${transObj['edges'][e]['url']}">${transObj['edges'][e]['name']}</a>`
|
||||||
|
break;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
panelName = `Potential Transformation by applying ${transObj['rule']['name']}`;
|
panelName = `Potential Transformation by applying ${transObj['rule']['name']}`;
|
||||||
objLink = `<a class='list-group-item' href="${transObj['rule']['url']}">${transObj['rule']['name']}</a>`
|
objLink = `<a class='list-group-item' href="${transObj['rule']['url']}">${transObj['rule']['name']}</a>`
|
||||||
|
|||||||
192
static/js/pw.js
192
static/js/pw.js
@ -1,6 +1,5 @@
|
|||||||
console.log("loaded pw.js")
|
console.log("loaded pw.js")
|
||||||
|
|
||||||
|
|
||||||
function predictFromNode(url) {
|
function predictFromNode(url) {
|
||||||
$.post("", {node: url})
|
$.post("", {node: url})
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
@ -28,62 +27,165 @@ function draw(pathway, elem) {
|
|||||||
const horizontalSpacing = 75; // horizontal space between nodes
|
const horizontalSpacing = 75; // horizontal space between nodes
|
||||||
const depthMap = new Map();
|
const depthMap = new Map();
|
||||||
|
|
||||||
nodes.forEach(node => {
|
// 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)) {
|
if (!depthMap.has(node.depth)) {
|
||||||
depthMap.set(node.depth, 0);
|
depthMap.set(node.depth, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodesInLevel = nodes.filter(n => n.depth === node.depth).length;
|
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;
|
|
||||||
|
|
||||||
|
// 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);
|
depthMap.set(node.depth, depthMap.get(node.depth) + 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Funktion für das Update der Positionen
|
// 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() {
|
function ticked() {
|
||||||
|
// Update pseudo node positions first
|
||||||
|
updatePseudoNodePositions();
|
||||||
|
|
||||||
link.attr("x1", d => d.source.x)
|
link.attr("x1", d => d.source.x)
|
||||||
.attr("y1", d => d.source.y)
|
.attr("y1", d => d.source.y)
|
||||||
.attr("x2", d => d.target.x)
|
.attr("x2", d => d.target.x)
|
||||||
.attr("y2", d => d.target.y);
|
.attr("y2", d => d.target.y);
|
||||||
|
|
||||||
node.attr("transform", d => `translate(${d.x},${d.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) {
|
function dragstarted(event, d) {
|
||||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||||
d.fx = d.x; // Setzt die Fixierung auf die aktuelle Position
|
d.fx = d.x;
|
||||||
d.fy = d.y;
|
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) {
|
function dragged(event, d) {
|
||||||
d.fx = event.x; // Position direkt an Maus anpassen
|
d.fx = event.x;
|
||||||
d.fy = event.y;
|
d.fy = event.y;
|
||||||
|
|
||||||
|
// Update connected pseudo nodes in real-time
|
||||||
|
if (!d.pseudo) {
|
||||||
|
updateConnectedPseudoNodes(d);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragended(event, d) {
|
function dragended(event, d) {
|
||||||
if (!event.active) simulation.alphaTarget(0);
|
if (!event.active) simulation.alphaTarget(0);
|
||||||
// Knoten bleibt an der neuen Position und wird nicht zurückgezogen
|
|
||||||
|
// 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
|
// t -> ref to "this" from d3
|
||||||
function nodeClick(event, node, t) {
|
function nodeClick(event, node, t) {
|
||||||
console.log(node);
|
console.log(node);
|
||||||
@ -105,7 +207,7 @@ function draw(pathway, elem) {
|
|||||||
pop = $(e).attr("aria-describedby")
|
pop = $(e).attr("aria-describedby")
|
||||||
h = $('#' + pop).height();
|
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;`)
|
$('#' + pop).attr("style", `position: fixed; top: ${clientY - (h / 2.0)}px; left: ${clientX + 10}px; margin: 0px; max-width: 1000px; display: block;`)
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
var close = setInterval(function () {
|
var close = setInterval(function () {
|
||||||
if (!$(".popover:hover").length // mouse outside popover
|
if (!$(".popover:hover").length // mouse outside popover
|
||||||
&& !$(e).is(':hover')) { // mouse outside element
|
&& !$(e).is(':hover')) { // mouse outside element
|
||||||
@ -140,13 +242,12 @@ function draw(pathway, elem) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function node_popup(n) {
|
function node_popup(n) {
|
||||||
popupContent = "<a href='" + n.url +"'>" + n.name + "</a><br>";
|
popupContent = "<a href='" + n.url + "'>" + n.name + "</a><br>";
|
||||||
popupContent += "Depth " + n.depth + "<br>"
|
popupContent += "Depth " + n.depth + "<br>"
|
||||||
|
|
||||||
if (appDomainViewEnabled) {
|
if (appDomainViewEnabled) {
|
||||||
if(n.app_domain != null) {
|
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." + "<br>"
|
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." + "<br>"
|
||||||
if (n.app_domain['uncovered_functional_groups']) {
|
if (n.app_domain['uncovered_functional_groups']) {
|
||||||
popupContent += "Compound contains functional groups not covered by the training set <br>"
|
popupContent += "Compound contains functional groups not covered by the training set <br>"
|
||||||
@ -154,7 +255,7 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
popupContent += "<img src='" + n.image + "' width='"+ 20 * nodeRadius +"'><br>"
|
popupContent += "<img src='" + n.image + "' width='" + 20 * nodeRadius + "'><br>"
|
||||||
if (n.scenarios.length > 0) {
|
if (n.scenarios.length > 0) {
|
||||||
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
||||||
for (var s of n.scenarios) {
|
for (var s of n.scenarios) {
|
||||||
@ -163,7 +264,7 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var isLeaf = pathway.links.filter(obj => obj.source.id === n.id).length === 0;
|
var isLeaf = pathway.links.filter(obj => obj.source.id === n.id).length === 0;
|
||||||
if(pathway.isIncremental && isLeaf) {
|
if (pathway.isIncremental && isLeaf) {
|
||||||
popupContent += '<br><a class="btn btn-primary" onclick="predictFromNode(\'' + n.url + '\')" href="#">Predict from here</a><br>';
|
popupContent += '<br><a class="btn btn-primary" onclick="predictFromNode(\'' + n.url + '\')" href="#">Predict from here</a><br>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,25 +272,24 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function edge_popup(e) {
|
function edge_popup(e) {
|
||||||
popupContent = "<a href='" + e.url +"'>" + e.name + "</a><br>";
|
popupContent = "<a href='" + e.url + "'>" + e.name + "</a><br>";
|
||||||
|
|
||||||
if(e.app_domain){
|
if (e.app_domain) {
|
||||||
adcontent = "<p>";
|
adcontent = "<p>";
|
||||||
if(e.app_domain["times_triggered"]) {
|
if (e.app_domain["times_triggered"]) {
|
||||||
adcontent += "This rule triggered " + e.app_domain["times_triggered"] + " times in the training set<br>";
|
adcontent += "This rule triggered " + e.app_domain["times_triggered"] + " times in the training set<br>";
|
||||||
}
|
}
|
||||||
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"] + ")<br>";
|
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"] + ")<br>";
|
||||||
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"] + ")<br>";
|
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"] + ")<br>";
|
||||||
adcontent += "</p>";
|
adcontent += "</p>";
|
||||||
|
popupContent += adcontent;
|
||||||
}
|
}
|
||||||
popupContent += adcontent;
|
|
||||||
|
|
||||||
popupContent += "<img src='" + e.image + "' width='"+ 20 * nodeRadius +"'><br>"
|
popupContent += "<img src='" + e.image + "' width='" + 20 * nodeRadius + "'><br>"
|
||||||
if (e.reaction_probability) {
|
if (e.reaction_probability) {
|
||||||
popupContent += '<b>Probability:</b><br>' + e.reaction_probability.toFixed(3) + '<br>';
|
popupContent += '<b>Probability:</b><br>' + e.reaction_probability.toFixed(3) + '<br>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (e.scenarios.length > 0) {
|
if (e.scenarios.length > 0) {
|
||||||
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
||||||
for (var s of e.scenarios) {
|
for (var s of e.scenarios) {
|
||||||
@ -202,9 +302,9 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
var clientX;
|
var clientX;
|
||||||
var clientY;
|
var clientY;
|
||||||
document.addEventListener('mousemove', function(event) {
|
document.addEventListener('mousemove', function (event) {
|
||||||
clientX = event.clientX;
|
clientX = event.clientX;
|
||||||
clientY =event.clientY;
|
clientY = event.clientY;
|
||||||
});
|
});
|
||||||
|
|
||||||
const zoomable = d3.select("#zoomable");
|
const zoomable = d3.select("#zoomable");
|
||||||
@ -258,8 +358,8 @@ function draw(pathway, elem) {
|
|||||||
.attr("marker-end", d => d.target.pseudo ? '' : 'url(#arrow)')
|
.attr("marker-end", d => d.target.pseudo ? '' : 'url(#arrow)')
|
||||||
|
|
||||||
// add element to links array
|
// add element to links array
|
||||||
link.each(function(d) {
|
link.each(function (d) {
|
||||||
d.el = this; // attach the DOM element to the data object
|
d.el = this; // attach the DOM element to the data object
|
||||||
});
|
});
|
||||||
|
|
||||||
pop_add(link, "Reaction", edge_popup);
|
pop_add(link, "Reaction", edge_popup);
|
||||||
@ -279,7 +379,7 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
// Kreise für die Knoten hinzufügen
|
// Kreise für die Knoten hinzufügen
|
||||||
node.append("circle")
|
node.append("circle")
|
||||||
// make radius "invisible"
|
// make radius "invisible" for pseudo nodes
|
||||||
.attr("r", d => d.pseudo ? 0.01 : nodeRadius)
|
.attr("r", d => d.pseudo ? 0.01 : nodeRadius)
|
||||||
.style("fill", "#e8e8e8");
|
.style("fill", "#e8e8e8");
|
||||||
|
|
||||||
@ -292,8 +392,8 @@ function draw(pathway, elem) {
|
|||||||
.attr("height", nodeRadius * 2);
|
.attr("height", nodeRadius * 2);
|
||||||
|
|
||||||
// add element to nodes array
|
// add element to nodes array
|
||||||
node.each(function(d) {
|
node.each(function (d) {
|
||||||
d.el = this; // attach the DOM element to the data object
|
d.el = this; // attach the DOM element to the data object
|
||||||
});
|
});
|
||||||
|
|
||||||
pop_add(node, "Compound", node_popup);
|
pop_add(node, "Compound", node_popup);
|
||||||
|
|||||||
@ -639,20 +639,23 @@ class IndigoUtils(object):
|
|||||||
environment.add(mappedAtom.index())
|
environment.add(mappedAtom.index())
|
||||||
|
|
||||||
for k, v in functional_groups.items():
|
for k, v in functional_groups.items():
|
||||||
|
try:
|
||||||
|
sanitized = IndigoUtils.sanitize_functional_group(k)
|
||||||
|
|
||||||
sanitized = IndigoUtils.sanitize_functional_group(k)
|
query = indigo.loadSmarts(sanitized)
|
||||||
|
|
||||||
query = indigo.loadSmarts(sanitized)
|
for match in matcher.iterateMatches(query):
|
||||||
|
if match is not None:
|
||||||
|
|
||||||
for match in matcher.iterateMatches(query):
|
for atom in query.iterateAtoms():
|
||||||
if match is not None:
|
mappedAtom = match.mapAtom(atom)
|
||||||
|
if mappedAtom is None or mappedAtom.index() in environment:
|
||||||
|
continue
|
||||||
|
|
||||||
for atom in query.iterateAtoms():
|
counts[mappedAtom.index()] = max(v, counts[mappedAtom.index()])
|
||||||
mappedAtom = match.mapAtom(atom)
|
|
||||||
if mappedAtom is None or mappedAtom.index() in environment:
|
|
||||||
continue
|
|
||||||
|
|
||||||
counts[mappedAtom.index()] = max(v, counts[mappedAtom.index()])
|
except IndigoException as e:
|
||||||
|
logger.debug(f'Colorizing failed due to {e}')
|
||||||
|
|
||||||
for k, v in counts.items():
|
for k, v in counts.items():
|
||||||
if is_reaction:
|
if is_reaction:
|
||||||
|
|||||||
Reference in New Issue
Block a user