From 31783306e2e2e8df67eac951592fbb0083206a55 Mon Sep 17 00:00:00 2001 From: jebus Date: Wed, 10 Sep 2025 18:44:18 +1200 Subject: [PATCH] [Feature] Pathway SVG Export (#102) Co-authored-by: Tim Lorsbach Reviewed-on: https://git.envipath.com/enviPath/enviPy/pulls/102 --- epdb/models.py | 4 +- epdb/views.py | 16 +++++ static/js/pw.js | 67 +++++++++++++++++-- templates/actions/objects/pathway.html | 8 ++- ...l.html => download_pathway_csv_modal.html} | 14 ++-- .../objects/download_pathway_image_modal.html | 32 +++++++++ templates/objects/pathway.html | 3 +- 7 files changed, 125 insertions(+), 19 deletions(-) rename templates/modals/objects/{download_pathway_modal.html => download_pathway_csv_modal.html} (67%) create mode 100644 templates/modals/objects/download_pathway_image_modal.html diff --git a/epdb/models.py b/epdb/models.py index c5002211..7202474d 100644 --- a/epdb/models.py +++ b/epdb/models.py @@ -1657,8 +1657,8 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin): "depth": self.depth, "url": self.url, "node_label_id": self.default_node_label.url, - "image": self.url + '?image=svg', - "imageSize": 490, # TODO + "image": f"{self.url}?image=svg", + "image_svg": IndigoUtils.mol_to_svg(self.default_node_label.smiles, width=40, height=40), "name": self.default_node_label.name, "smiles": self.default_node_label.smiles, "scenarios": [{'name': s.name, 'url': s.url} for s in self.scenarios.all()], diff --git a/epdb/views.py b/epdb/views.py index d5b24c98..5358e34f 100644 --- a/epdb/views.py +++ b/epdb/views.py @@ -1452,6 +1452,22 @@ def package_pathway(request, package_uuid, pathway_uuid): return response + # Pathway d3_json() relies on a lot of related objects (Nodes, Structures, Edges, Reaction, Rules, ...) + # we will again fetch the current pathway identified by this url, but this time together with nearly all + # related objects + + current_pathway = Pathway.objects.prefetch_related( + 'node_set', + 'node_set__out_edges', + 'node_set__default_node_label', + 'node_set__scenarios', + 'edge_set', + 'edge_set__start_nodes', + 'edge_set__end_nodes', + 'edge_set__edge_label', + 'edge_set__scenarios' + ).get(uuid=pathway_uuid) + context = get_base_context(request) context['title'] = f'enviPath - {current_package.name} - {current_pathway.name}' diff --git a/static/js/pw.js b/static/js/pw.js index 99c44595..5ccff67e 100644 --- a/static/js/pw.js +++ b/static/js/pw.js @@ -384,12 +384,31 @@ function draw(pathway, elem) { .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); + node.filter(d => !d.pseudo).each(function (d) { + const g = d3.select(this).append("g"); + + // Parse the SVG string + const parser = new DOMParser(); + const svgDoc = parser.parseFromString(d.image_svg, "image/svg+xml"); + const svgContent = svgDoc.documentElement; + + // Remove width/height so scaling works + svgContent.removeAttribute("width"); + svgContent.removeAttribute("height"); + + // Move all children of the parsed SVG into our + while (svgContent.firstChild) { + g.node().appendChild(svgContent.firstChild); + } + + // Get viewBox dimensions for scaling + const vb = svgContent.viewBox.baseVal; + const svgWidth = vb.width || 40; + const svgHeight = vb.height || 40; + + // Center and scale + g.attr("transform", `translate(${-svgWidth/2},${-svgHeight/2}) scale(${(nodeRadius*2)/svgWidth})`); + }); // add element to nodes array node.each(function (d) { @@ -397,4 +416,38 @@ function draw(pathway, elem) { }); pop_add(node, "Compound", node_popup); -} \ No newline at end of file +} + +function serializeSVG(svgElement) { + + svgElement.querySelectorAll("line.link").forEach(line => { + const style = getComputedStyle(line); + line.setAttribute("stroke", style.stroke); + line.setAttribute("stroke-width", style.strokeWidth); + line.setAttribute("fill", style.fill); + }); + + const serializer = new XMLSerializer(); + let svgString = serializer.serializeToString(svgElement); + + // Add namespace if missing + if (!svgString.includes('xmlns="http://www.w3.org/2000/svg"')) { + svgString = svgString.replace(' Copy
  • - - Download Pathway + + Download Pathway as CSV +
  • +
  • + + Download Pathway as Image
  • {% if meta.can_edit %} diff --git a/templates/modals/objects/download_pathway_modal.html b/templates/modals/objects/download_pathway_csv_modal.html similarity index 67% rename from templates/modals/objects/download_pathway_modal.html rename to templates/modals/objects/download_pathway_csv_modal.html index e9a89f88..15240dd5 100644 --- a/templates/modals/objects/download_pathway_modal.html +++ b/templates/modals/objects/download_pathway_csv_modal.html @@ -1,24 +1,24 @@ {% load static %} -