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,
|
||||
"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()],
|
||||
|
||||
@ -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}'
|
||||
|
||||
|
||||
@ -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 <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
|
||||
node.each(function (d) {
|
||||
@ -398,3 +417,37 @@ function draw(pathway, elem) {
|
||||
|
||||
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>
|
||||
</li>
|
||||
<li>
|
||||
<a class="button" data-toggle="modal" data-target="#download_pathway_modal">
|
||||
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway</a>
|
||||
<a class="button" data-toggle="modal" data-target="#download_pathway_csv_modal">
|
||||
<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>
|
||||
{% if meta.can_edit %}
|
||||
<li role="separator" class="divider"></li>
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
{% load static %}
|
||||
<!-- 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-content">
|
||||
<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">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
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">
|
||||
<input type="hidden" name="download" value="true"/>
|
||||
</form>
|
||||
</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-modal-submit">Download</button>
|
||||
<button type="button" class="btn btn-primary" id="download-pathway-csv-modal-submit">Download</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -26,10 +26,10 @@
|
||||
<script>
|
||||
$(function () {
|
||||
|
||||
$('#download-pathway-modal-submit').click(function (e) {
|
||||
$('#download-pathway-csv-modal-submit').click(function (e) {
|
||||
e.preventDefault();
|
||||
$('#download-pathway-modal-form').submit();
|
||||
$('#download_pathway_modal').modal('hide');
|
||||
$('#download-pathway-csv-modal-form').submit();
|
||||
$('#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 %}
|
||||
{% include "modals/objects/add_pathway_node_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/edit_pathway_modal.html" %}
|
||||
{% include "modals/objects/generic_set_scenario_modal.html" %}
|
||||
|
||||
Reference in New Issue
Block a user