forked from enviPath/enviPy
[Feature] Pathway SVG Export (#102)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#102
This commit is contained in:
@ -1657,8 +1657,8 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
"depth": self.depth,
|
"depth": self.depth,
|
||||||
"url": self.url,
|
"url": self.url,
|
||||||
"node_label_id": self.default_node_label.url,
|
"node_label_id": self.default_node_label.url,
|
||||||
"image": self.url + '?image=svg',
|
"image": f"{self.url}?image=svg",
|
||||||
"imageSize": 490, # TODO
|
"image_svg": IndigoUtils.mol_to_svg(self.default_node_label.smiles, width=40, height=40),
|
||||||
"name": self.default_node_label.name,
|
"name": self.default_node_label.name,
|
||||||
"smiles": self.default_node_label.smiles,
|
"smiles": self.default_node_label.smiles,
|
||||||
"scenarios": [{'name': s.name, 'url': s.url} for s in self.scenarios.all()],
|
"scenarios": [{'name': s.name, 'url': s.url} for s in self.scenarios.all()],
|
||||||
|
|||||||
@ -1452,6 +1452,22 @@ def package_pathway(request, package_uuid, pathway_uuid):
|
|||||||
|
|
||||||
return response
|
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 = get_base_context(request)
|
||||||
context['title'] = f'enviPath - {current_package.name} - {current_pathway.name}'
|
context['title'] = f'enviPath - {current_package.name} - {current_pathway.name}'
|
||||||
|
|
||||||
|
|||||||
@ -384,12 +384,31 @@ function draw(pathway, elem) {
|
|||||||
.style("fill", "#e8e8e8");
|
.style("fill", "#e8e8e8");
|
||||||
|
|
||||||
// Add image only for non pseudo nodes
|
// Add image only for non pseudo nodes
|
||||||
node.filter(d => !d.pseudo).append("image")
|
node.filter(d => !d.pseudo).each(function (d) {
|
||||||
.attr("xlink:href", d => d.image)
|
const g = d3.select(this).append("g");
|
||||||
.attr("x", -nodeRadius)
|
|
||||||
.attr("y", -nodeRadius)
|
// Parse the SVG string
|
||||||
.attr("width", nodeRadius * 2)
|
const parser = new DOMParser();
|
||||||
.attr("height", nodeRadius * 2);
|
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 <g>
|
||||||
|
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
|
// add element to nodes array
|
||||||
node.each(function (d) {
|
node.each(function (d) {
|
||||||
@ -397,4 +416,38 @@ function draw(pathway, elem) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
pop_add(node, "Compound", node_popup);
|
pop_add(node, "Compound", node_popup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
|
||||||
|
}
|
||||||
|
|
||||||
|
return svgString;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadSVG(svgElement, filename = 'chart.svg') {
|
||||||
|
const svgString = serializeSVG(svgElement);
|
||||||
|
const blob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|||||||
@ -14,8 +14,12 @@
|
|||||||
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
|
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="button" data-toggle="modal" data-target="#download_pathway_modal">
|
<a class="button" data-toggle="modal" data-target="#download_pathway_csv_modal">
|
||||||
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway</a>
|
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway as CSV</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="button" data-toggle="modal" data-target="#download_pathway_image_modal">
|
||||||
|
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway as Image</a>
|
||||||
</li>
|
</li>
|
||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li role="separator" class="divider"></li>
|
<li role="separator" class="divider"></li>
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
<!-- Download Pathway -->
|
<!-- Download Pathway -->
|
||||||
<div id="download_pathway_modal" class="modal" tabindex="-1">
|
<div id="download_pathway_csv_modal" class="modal" tabindex="-1">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 class="modal-title">Download Pathway</h3>
|
<h3 class="modal-title">Download Pathway as CSV</h3>
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
By clicking on Download the Pathway will be converted into a CSV and directly downloaded.
|
By clicking on Download the Pathway will be converted into a CSV and directly downloaded.
|
||||||
<form id="download-pathway-modal-form" accept-charset="UTF-8" action="{{ pathway.url }}"
|
<form id="download-pathway-csv-modal-form" accept-charset="UTF-8" action="{{ pathway.url }}"
|
||||||
data-remote="true" method="GET">
|
data-remote="true" method="GET">
|
||||||
<input type="hidden" name="download" value="true"/>
|
<input type="hidden" name="download" value="true"/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
<button type="button" class="btn btn-primary" id="download-pathway-modal-submit">Download</button>
|
<button type="button" class="btn btn-primary" id="download-pathway-csv-modal-submit">Download</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -26,10 +26,10 @@
|
|||||||
<script>
|
<script>
|
||||||
$(function () {
|
$(function () {
|
||||||
|
|
||||||
$('#download-pathway-modal-submit').click(function (e) {
|
$('#download-pathway-csv-modal-submit').click(function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$('#download-pathway-modal-form').submit();
|
$('#download-pathway-csv-modal-form').submit();
|
||||||
$('#download_pathway_modal').modal('hide');
|
$('#download_pathway_csv_modal').modal('hide');
|
||||||
});
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
32
templates/modals/objects/download_pathway_image_modal.html
Normal file
32
templates/modals/objects/download_pathway_image_modal.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!-- Download Pathway -->
|
||||||
|
<div id="download_pathway_image_modal" class="modal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">Download Pathway as Image</h3>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
By clicking on Download the Pathway will be saved as SVG.
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="download-pathway-image-modal-submit">Download</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
|
||||||
|
$('#download-pathway-image-modal-submit').click(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
downloadSVG($('#pwsvg')[0], '{{ pathway.name.split|join:"_" }}.svg')
|
||||||
|
$('#download_pathway_image_modal').modal('hide');
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -81,7 +81,8 @@
|
|||||||
{% block action_modals %}
|
{% block action_modals %}
|
||||||
{% include "modals/objects/add_pathway_node_modal.html" %}
|
{% include "modals/objects/add_pathway_node_modal.html" %}
|
||||||
{% include "modals/objects/add_pathway_edge_modal.html" %}
|
{% include "modals/objects/add_pathway_edge_modal.html" %}
|
||||||
{% include "modals/objects/download_pathway_modal.html" %}
|
{% include "modals/objects/download_pathway_csv_modal.html" %}
|
||||||
|
{% include "modals/objects/download_pathway_image_modal.html" %}
|
||||||
{% include "modals/objects/generic_copy_object_modal.html" %}
|
{% include "modals/objects/generic_copy_object_modal.html" %}
|
||||||
{% include "modals/objects/edit_pathway_modal.html" %}
|
{% include "modals/objects/edit_pathway_modal.html" %}
|
||||||
{% include "modals/objects/generic_set_scenario_modal.html" %}
|
{% include "modals/objects/generic_set_scenario_modal.html" %}
|
||||||
|
|||||||
Reference in New Issue
Block a user