forked from enviPath/enviPy
[Feature] Sync Features from Client Repo (#425)
- New PackageImporter - Fix persisting Molfile on Structure creation - Add NonPersistent Prediction - Pathway View Options - Edit Node Fix Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#425
This commit is contained in:
@ -39,3 +39,7 @@ select.select[multiple] {
|
||||
display: block;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
p a {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
@ -5,6 +5,126 @@
|
||||
*/
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
|
||||
const basePagination = (
|
||||
items,
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalItems,
|
||||
perPage,
|
||||
isReviewed,
|
||||
instanceId
|
||||
) => ({
|
||||
items,
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalItems,
|
||||
perPage,
|
||||
isReviewed,
|
||||
instanceId,
|
||||
|
||||
get paginatedItems() {
|
||||
return this.items;
|
||||
},
|
||||
|
||||
get showingStart() {
|
||||
if (this.totalItems === 0) return 0;
|
||||
return (this.currentPage - 1) * this.perPage + 1;
|
||||
},
|
||||
|
||||
get showingEnd() {
|
||||
if (this.totalItems === 0) return 0;
|
||||
return Math.min((this.currentPage - 1) * this.perPage + this.items.length, this.totalItems);
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
if (this.currentPage < this.totalPages) {
|
||||
this.fetchPage(this.currentPage + 1);
|
||||
}
|
||||
},
|
||||
|
||||
prevPage() {
|
||||
if (this.currentPage > 1) {
|
||||
this.fetchPage(this.currentPage - 1);
|
||||
}
|
||||
},
|
||||
|
||||
goToPage(page) {
|
||||
if (page >= 1 && page <= this.totalPages) {
|
||||
this.fetchPage(page);
|
||||
}
|
||||
},
|
||||
|
||||
get pageNumbers() {
|
||||
const pages = [];
|
||||
const total = this.totalPages;
|
||||
const current = this.currentPage;
|
||||
|
||||
if (total === 0) {
|
||||
return pages;
|
||||
}
|
||||
|
||||
if (total <= 7) {
|
||||
for (let i = 1; i <= total; i++) {
|
||||
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
||||
}
|
||||
} else {
|
||||
pages.push({ page: 1, isEllipsis: false, key: `${this.instanceId}-page-1` });
|
||||
|
||||
let rangeStart;
|
||||
let rangeEnd;
|
||||
|
||||
if (current <= 4) {
|
||||
rangeStart = 2;
|
||||
rangeEnd = 5;
|
||||
} else if (current >= total - 3) {
|
||||
rangeStart = total - 4;
|
||||
rangeEnd = total - 1;
|
||||
} else {
|
||||
rangeStart = current - 1;
|
||||
rangeEnd = current + 1;
|
||||
}
|
||||
|
||||
if (rangeStart > 2) {
|
||||
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-start` });
|
||||
}
|
||||
|
||||
for (let i = rangeStart; i <= rangeEnd; i++) {
|
||||
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
||||
}
|
||||
|
||||
if (rangeEnd < total - 1) {
|
||||
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-end` });
|
||||
}
|
||||
|
||||
pages.push({ page: total, isEllipsis: false, key: `${this.instanceId}-page-${total}` });
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
});
|
||||
|
||||
Alpine.data('paginatedList',
|
||||
(items, options = {}) => ({
|
||||
...basePagination(items,1, 0, 0, options.perPage || 50, options.isReviewed || false, options.instanceId || Math.random().toString(36).substring(2, 9),),
|
||||
|
||||
init() {
|
||||
this.fetchPage(1);
|
||||
},
|
||||
|
||||
async fetchPage(page) {
|
||||
this.totalItems = this.items.length;
|
||||
this.totalPages = Math.ceil(this.items.length / this.perPage);
|
||||
this.currentPage = page
|
||||
|
||||
const start = page * this.perPage - this.perPage;
|
||||
const end = page * this.perPage;
|
||||
return this.items.slice(start, end);
|
||||
}
|
||||
|
||||
})
|
||||
);
|
||||
|
||||
Alpine.data('remotePaginatedList', (options = {}) => ({
|
||||
items: [],
|
||||
currentPage: 1,
|
||||
|
||||
123
static/js/pw.js
123
static/js/pw.js
@ -67,6 +67,9 @@ function draw(pathway, elem) {
|
||||
const horizontalSpacing = 75; // horizontal space between nodes
|
||||
const depthMap = new Map();
|
||||
|
||||
// Avoid leaving unconnected Nodes leaving the viewport
|
||||
nodes.forEach(node => {if (node.depth < 0) node.depth = 0;});
|
||||
|
||||
// Sort nodes by depth first to minimize crossings
|
||||
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
|
||||
|
||||
@ -154,6 +157,11 @@ function draw(pathway, elem) {
|
||||
});
|
||||
|
||||
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||
|
||||
linkText
|
||||
.attr("x", d => (d.source.x + d.target.x) / 2)
|
||||
.attr("y", d => (d.source.y + d.target.y) / 2);
|
||||
|
||||
}
|
||||
|
||||
function dragstarted(event, d) {
|
||||
@ -256,7 +264,7 @@ function draw(pathway, elem) {
|
||||
}
|
||||
|
||||
// Wait before showing popup (ms)
|
||||
var popupWaitBeforeShow = 1000;
|
||||
var popupWaitBeforeShow = 750;
|
||||
|
||||
// Custom popover element
|
||||
let popoverTimeout = null;
|
||||
@ -488,7 +496,7 @@ function draw(pathway, elem) {
|
||||
if (n.stereo_removed) {
|
||||
popupContent += "<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span>";
|
||||
}
|
||||
popupContent += "<a href='" + n.url + "'>" + n.name + "</a><br>";
|
||||
popupContent += "<a href='" + n.url + "'>" + n.plain_name + "</a><br>";
|
||||
popupContent += "Depth " + n.depth + "<br>"
|
||||
|
||||
if (appDomainViewEnabled) {
|
||||
@ -528,6 +536,21 @@ function draw(pathway, elem) {
|
||||
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
||||
for (var s of n.scenarios) {
|
||||
popupContent += "<a href='" + s.url + "'>" + s.name + "</a><br>";
|
||||
for (var prop in n.proposed) {
|
||||
if (n.proposed[prop].scenarioId === s.url) {
|
||||
if("proposed" in n.proposed[prop]){
|
||||
popupContent += "This compound is a proposed intermediate" + "<br>";
|
||||
}
|
||||
if("Confidence" in n.proposed[prop]){
|
||||
popupContent += "Confidence: " + n.proposed[prop]["Confidence"] + "<br>";
|
||||
}
|
||||
if("Transformation product importance" in n.proposed[prop]){
|
||||
popupContent += "Transformation product importance: " + n.proposed[prop]["Transformation product importance"] + "<br>";
|
||||
}
|
||||
}
|
||||
}
|
||||
popupContent += "<br>";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -540,7 +563,7 @@ function draw(pathway, elem) {
|
||||
}
|
||||
|
||||
function edge_popup(e) {
|
||||
popupContent = "<a href='" + e.url + "'>" + e.name + "</a><br><br>";
|
||||
popupContent = "<a href='" + e.url + "'>" + e.plain_name + "</a><br><br>";
|
||||
|
||||
if (e.reaction.rules) {
|
||||
for (var rule of e.reaction.rules) {
|
||||
@ -591,7 +614,7 @@ function draw(pathway, elem) {
|
||||
|
||||
// Add background rectangle FIRST to enable pan/zoom on empty space
|
||||
// This must be inserted before zoomable group so it's behind everything
|
||||
svg.insert("rect", "#zoomable")
|
||||
const rect = svg.insert("rect", "#zoomable")
|
||||
.attr("x", 0)
|
||||
.attr("y", 0)
|
||||
.attr("width", width)
|
||||
@ -600,6 +623,30 @@ function draw(pathway, elem) {
|
||||
.attr("pointer-events", "all")
|
||||
.style("cursor", "grab");
|
||||
|
||||
// Click on empty SVG space (not on a node or link)
|
||||
rect.on("click", function(event) {
|
||||
// Only treat it as a background click if the direct target is the rect itself
|
||||
// (clicks on nodes/links bubble up but originate from different elements)
|
||||
const target = event.target;
|
||||
const isNode = target.closest && (
|
||||
target.closest("circle") !== null ||
|
||||
target.closest("image") !== null ||
|
||||
target.closest(".node") !== null
|
||||
);
|
||||
const isLink = target.closest && (
|
||||
target.closest("path.link") !== null ||
|
||||
target.closest("path.link_no_arrow") !== null
|
||||
);
|
||||
if (!isNode && !isLink) {
|
||||
// console.log("Clicked outside a node or link");
|
||||
// Clear all highlights
|
||||
d3.selectAll("circle").classed("highlighted", false);
|
||||
d3.selectAll("path").classed("highlighted", false);
|
||||
d3.selectAll("path").classed("inedge", false);
|
||||
d3.selectAll("path").classed("outedge", false);
|
||||
}
|
||||
});
|
||||
|
||||
// Zoom Funktion aktivieren
|
||||
const zoom = d3.zoom()
|
||||
.scaleExtent([0.5, 5])
|
||||
@ -646,22 +693,32 @@ function draw(pathway, elem) {
|
||||
.force("center", d3.forceCenter(width / 2, height / 4))
|
||||
.on("tick", ticked);
|
||||
|
||||
// Kanten zeichnen
|
||||
const link = zoomable.append("g")
|
||||
.selectAll("path")
|
||||
.data(links)
|
||||
.enter().append("path")
|
||||
// Check if target is pseudo and draw marker only if not pseudo
|
||||
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||
.attr("marker-end", d => {
|
||||
if (d.target.pseudo) return '';
|
||||
if (d.source.id === d.target.id) return 'url(#curve-arrow)'; // Use curve arrow for self-loops
|
||||
return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
|
||||
})
|
||||
.attr("fill", "none")
|
||||
.on("click", function(event, d) {
|
||||
// Draw Edges
|
||||
const linkGroup = zoomable.append("g")
|
||||
.selectAll("g")
|
||||
.data(links)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", "link-group");
|
||||
|
||||
const link = linkGroup
|
||||
.append("path")
|
||||
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||
.attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow)")
|
||||
.attr("fill", "none")
|
||||
|
||||
// Check if target is pseudo and draw marker only if not pseudo
|
||||
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||
.attr("marker-end", d => {
|
||||
if (d.target.pseudo) return '';
|
||||
if (d.source.id === d.target.id) return 'url(#curve-arrow)'; // Use curve arrow for self-loops
|
||||
return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
|
||||
}
|
||||
)
|
||||
.attr("fill", "none")
|
||||
.on("click", function(event, d) {
|
||||
const wasHighlighted = d3.select(this).classed("highlighted");
|
||||
d3.selectAll("path").classed("highlighted", false);
|
||||
d3.selectAll("path").classed("highlighted", false);
|
||||
if (!wasHighlighted) {
|
||||
const toHighlight = [];
|
||||
toHighlight.push(d.el);
|
||||
@ -688,6 +745,32 @@ function draw(pathway, elem) {
|
||||
}
|
||||
});
|
||||
|
||||
function reaction_name(d) {
|
||||
const to_pseudo = d?.to_pseudo || false;
|
||||
|
||||
if (to_pseudo) {
|
||||
return "";
|
||||
}
|
||||
|
||||
var suffix = "";
|
||||
if ("reaction_probability" in d && d["reaction_probability"] !== null) {
|
||||
suffix = " (p= " + d["reaction_probability"].toFixed(2) + ")"
|
||||
}
|
||||
return d.plain_name + suffix;
|
||||
}
|
||||
|
||||
const linkText = linkGroup
|
||||
.append("text")
|
||||
.attr("class", "link-label")
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("dy", -6)
|
||||
.style("font-size", "4px")
|
||||
.style("pointer-events", "none")
|
||||
.style("display", "none")
|
||||
.text(d => reaction_name(d))
|
||||
.attr("x", d => (d.source.x + d.target.x) / 2)
|
||||
.attr("y", d => (d.source.y + d.target.y) / 2);
|
||||
|
||||
// add element to links array
|
||||
link.each(function (d) {
|
||||
d.el = this; // attach the DOM element to the data object
|
||||
@ -695,7 +778,7 @@ function draw(pathway, elem) {
|
||||
|
||||
pop_add(link, "Reaction", edge_popup);
|
||||
|
||||
// Knoten zeichnen
|
||||
// Draw Nodes
|
||||
const node = zoomable.append("g")
|
||||
.selectAll("g")
|
||||
.data(nodes)
|
||||
|
||||
Reference in New Issue
Block a user