Skip to content

Commit

Permalink
Export svg (#218)
Browse files Browse the repository at this point in the history
* adding a function that generates a SVG string from coords and NodeCoords

* adding button to trigger export

* add new vendor js dependency (as used in Emperor)

* add GUI element for export SVG to side-panel

* adding new dependency

* fix codestyle

* export line thickness

* prettier

* prettier

* plot nodes in generates SVG only if "show node circles" is true

* adding first two js-tests

* adding more tests

* using global size variable for node radius

* adressing suggestions from code review

* adding a function to create a SVG version for a legend

* execute legend to SVG code

* generate svg separately for tree and legend due to internal information flow.

* prettier

* adept to new Biom table and tree format

* codestyle

* file is now "prettier"

* Made a few minor changes

* STY: Prettyfy

Co-authored-by: Yoshiki Vázquez Baeza <[email protected]>
  • Loading branch information
sjanssen2 and ElDeveloper authored Jul 28, 2020
1 parent cd268a8 commit 69ad23a
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 4 deletions.
244 changes: 244 additions & 0 deletions empress/support_files/js/empress.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,250 @@ define([
this._drawer.draw();
};

/**
* Creates an SVG string to export the current drawing
*/
Empress.prototype.exportSvg = function () {
// TODO: use the same value as the actual WebGL drawing engine, but
// right now this value is hard coded on line 327 of drawer.js
NODE_RADIUS = 4;

minX = 0;
maxX = 0;
minY = 0;
maxY = 0;
svg = "";

// create a line from x1,y1 to x2,y2 for every two consecutive coordinates
// 5 array elements encode one coordinate:
// i=x, i+1=y, i+2=red, i+3=green, i+4=blue
svg += "<!-- tree branches -->\n";
coords = this.getCoords();
for (
i = 0;
i + 2 * this._drawer.VERTEX_SIZE <= coords.length;
i += 2 * this._drawer.VERTEX_SIZE
) {
// "normal" lines have a default color,
// all other lines have a user defined thickness
// All lines are defined using the information from the child node.
// So, if coords[i+2] == DEFAULT_COLOR then coords[i+2+5] will
// also be equal to DEFAULT_COLOR. Thus, we can save checking three
// array elements here.
linewidth = 1 + this._currentLineWidth;
if (
coords[i + 2] == this.DEFAULT_COLOR[0] &&
coords[i + 3] == this.DEFAULT_COLOR[1] &&
coords[i + 4] == this.DEFAULT_COLOR[2]
) {
linewidth = 1;
}
svg +=
'<line x1="' +
coords[i] +
'" y1="' +
coords[i + 1] +
'" x2="' +
coords[i + this._drawer.VERTEX_SIZE] +
'" y2="' +
coords[i + 1 + this._drawer.VERTEX_SIZE] +
'" stroke="' +
chroma.gl(coords[i + 2], coords[i + 3], coords[i + 4]).css() +
'" style="stroke-width:' +
linewidth +
'" />\n';

// obtain viewport from tree coordinates
minX = Math.min(
minX,
coords[i],
coords[i + this._drawer.VERTEX_SIZE]
);
maxX = Math.max(
maxX,
coords[i],
coords[i + this._drawer.VERTEX_SIZE]
);

minY = Math.min(
minY,
coords[i + 1],
coords[i + 1 + this._drawer.VERTEX_SIZE]
);
maxY = Math.max(
maxY,
coords[i + 1],
coords[i + 1 + this._drawer.VERTEX_SIZE]
);
}

// create a circle for each node
if (this._drawer.showTreeNodes) {
svg += "<!-- tree nodes -->\n";
coords = this.getNodeCoords();
for (
i = 0;
i + this._drawer.VERTEX_SIZE <= coords.length;
i += this._drawer.VERTEX_SIZE
) {
// getNodeCoords array seem to be larger than necessary and
// elements are initialized with 0. Thus, nodes at (0, 0) will
// be skipped (root will always be positioned at 0,0 and drawn
// below) This is a known issue and will be resolved with #142
if (coords[i] == 0 && coords[i + 1] == 0) {
continue;
}
svg +=
'<circle cx="' +
coords[i] +
'" cy="' +
coords[i + 1] +
'" r="' +
NODE_RADIUS +
'" style="fill:' +
chroma
.gl(coords[i + 2], coords[i + 3], coords[i + 4])
.css() +
'"/>\n';
}
}

// add one black circle to indicate the root
// Not sure if this speacial treatment for root is necessary once #142 is merged.
svg += "<!-- root node -->\n";
svg +=
'<circle cx="0" cy="0" r="' +
NODE_RADIUS +
'" fill="rgb(0,0,0)"/>\n';

return [
svg,
'viewBox="' +
(minX - NODE_RADIUS) +
" " +
(minY - NODE_RADIUS) +
" " +
(maxX - minX + 2 * NODE_RADIUS) +
" " +
(maxY - minY + 2 * NODE_RADIUS) +
'"',
];
};

/**
* Creates an SVG string to export legends
*/
Empress.prototype.exportSVG_legend = function (dom) {
// top left position of legends, multiple legends are placed below
// each other.
top_left_x = 0;
top_left_y = 0;
unit = 30; // all distances are based on this variable, thus "zooming" can be realised by just increasing this single value
factor_lineheight = 1.8; // distance between two text lines as a multiplication factor of unit
svg = ""; // the svg string to be generated

// used as a rough estimate about the consumed width by text strings
var myCanvas = document.createElement("canvas");
var context = myCanvas.getContext("2d");
context.font = "bold " + unit + "pt verdana";

// the document can have up to three legends, of which at most one shall be visible at any given timepoint. This might change and thus this method can draw multiple legends
row = 1; // count the number of used rows
for (let legend of dom.getElementsByClassName("legend")) {
max_line_width = 0;
title = legend.getElementsByClassName("legend-title");
svg_legend = "";
if (title.length > 0) {
titlelabel = title.item(0).innerHTML;
max_line_width = Math.max(
max_line_width,
context.measureText(titlelabel).width
);
svg_legend +=
'<text x="' +
(top_left_x + unit) +
'" y="' +
(top_left_y + row * (unit * factor_lineheight)) +
'" style="font-weight:bold;font-size:' +
unit +
'pt;">' +
titlelabel +
"</text>\n";
row++;
for (let item of legend.getElementsByClassName(
"gradient-bar"
)) {
color = item
.getElementsByClassName("category-color")
.item(0)
.getAttribute("style")
.split(":")[1]
.split(";")[0];
itemlabel = item
.getElementsByClassName("gradient-label")
.item(0)
.getAttribute("title");
max_line_width = Math.max(
max_line_width,
context.measureText(itemlabel).width
);

// a rect left of the label to indicate the used color
svg_legend +=
'<rect x="' +
(top_left_x + unit) +
'" y="' +
(top_left_y + row * (unit * factor_lineheight) - unit) +
'" width="' +
unit +
'" height="' +
unit +
'" style="fill:' +
color +
'"/>\n';
// the key label
svg_legend +=
'<text x="' +
(top_left_x + 2.5 * unit) +
'" y="' +
(top_left_y + row * (unit * factor_lineheight)) +
'" style="font-size:' +
unit +
'pt;">' +
itemlabel +
"</text>\n";
row++;
}
// draw a rect behind, i.e. lower z-order, the legend title and colored keys to visually group the legend. Also acutally put these elements into a group for easier manual editing
// rect shall have a certain padding, its height must exceed number of used text rows and width must be larger than longest key text and/or legend title
svg +=
'<g>\n<rect x="' +
top_left_x +
'" y="' +
(top_left_y +
(row -
legend.getElementsByClassName("gradient-bar")
.length -
2) *
(unit * factor_lineheight)) +
'" width="' +
(max_line_width + 2 * unit) +
'" height="' +
((legend.getElementsByClassName("gradient-bar").length +
1) *
unit *
factor_lineheight +
unit) +
'" style="fill:#eeeeee;stroke:#000000;stroke-width:1" ry="30" />\n' +
svg_legend +
"</g>\n";
row += 2; // one blank row between two legends
}
}

return svg;
};

Empress.prototype.getX = function (nodeObj) {
var xname = "x" + this._layoutToCoordSuffix[this._currentLayout];
return nodeObj[xname];
Expand Down
30 changes: 30 additions & 0 deletions empress/support_files/js/side-panel-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) {
// layout GUI components
this.layoutDiv = document.getElementById("layout-div");

// export GUI components
this.eExportSvgBtn = document.getElementById("export-btn-svg");

// uncheck button
this.sHideChk.checked = false;

Expand Down Expand Up @@ -301,6 +304,33 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) {
}
};

/**
* Initializes export components
*/
SidePanel.prototype.addExportTab = function () {
// for use in closures
var scope = this;

this.eExportSvgBtn.onclick = function () {
// create SVG tags to draw the tree and determine viewbox for whole figure
[svg_tree, svg_viewbox] = scope.empress.exportSvg();
// create SVG tags for legend, collected from the HTML document
svg_legend = scope.empress.exportSVG_legend(document);
// add all SVG elements into one string ...
svg =
'<svg xmlns="http://www.w3.org/2000/svg" ' +
svg_viewbox +
" >\n" +
svg_tree +
"\n" +
svg_legend +
"</svg>\n";
// ... and present user as a downloadable file
var blob = new Blob([svg], { type: "image/svg+xml" });
saveAs(blob, "empress-tree.svg");
};
};

/**
* Initializes sample components
*/
Expand Down
10 changes: 8 additions & 2 deletions empress/support_files/templates/empress-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ <h3 class="hidden" id="menu-sm-header">Sample Presence Information</h3>
'glMatrix' : './vendor/gl-matrix.min',
'chroma' : './vendor/chroma.min',
'underscore' : './vendor/underscore-min',
'filesaver': './vendor/FileSaver.min',

'ByteArray' : './js/byte-array',
'BPTree' : './js/bp-tree',
Expand All @@ -104,10 +105,12 @@ <h3 class="hidden" id="menu-sm-header">Sample Presence Information</h3>
empressRequire(['glMatrix', 'chroma', 'underscore', 'ByteArray',
'BPTree', 'Camera', 'Drawer', 'SidePanel', 'AnimationPanel',
'Animator', 'BIOMTable', 'Empress', 'Legend', 'Colorer',
'VectorOps', 'CanvasEvents', 'SelectedNodeMenu', 'util'],
'VectorOps', 'CanvasEvents', 'SelectedNodeMenu', 'util',
'filesaver'],
function(gl, chroma, underscore, ByteArray, BPTree, Camera, Drawer,
SidePanel, AnimationPanel, Animator, BIOMTable, Empress, Legend,
Colorer, VectorOps, CanvasEvents, SelectedNodeMenu, util) {
Colorer, VectorOps, CanvasEvents, SelectedNodeMenu, util,
filesaver) {
// initialze the tree and model
var tree = new BPTree({{ tree }}, {{ names }});

Expand Down Expand Up @@ -160,6 +163,9 @@ <h3 class="hidden" id="menu-sm-header">Sample Presence Information</h3>
var animationPanel = new AnimationPanel(animator);
animationPanel.addAnimationTab();

// add export drawing tab
sPanel.addExportTab();

// make all tabs collapsable
document.querySelectorAll(".collapsible").forEach(function(btn) {
btn.addEventListener("click", function() {
Expand Down
10 changes: 10 additions & 0 deletions empress/support_files/templates/side-panel.html
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,16 @@
</div>
</div>

<!-- options to export plot into e.g. SVG -->
<button class="side-header collapsible">Export</button>
<div class="side-content control hidden" id="export-div">
<p>
<button id="export-btn-svg">Export tree as SVG</button>
</p>
<p class="side-panel-notes indented">
Export the tree as a vector graphics figure.
</p>
</div>

<!-- Global Tree properties -->
<button class="side-header collapsible">Tree Properties</button>
Expand Down
2 changes: 2 additions & 0 deletions empress/support_files/vendor/FileSaver.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions tests/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
'glMatrix' : './support_files/vendor/gl-matrix.min',
'chroma' : './support_files/vendor/chroma.min',
'underscore' : './support_files/vendor/underscore-min',
'filesaver': './support_files/vendor/FileSaver.min',

/* succinct tree paths */
'AnimationPanel': './support_files/js/animation-panel-handler',
Expand Down Expand Up @@ -71,6 +72,7 @@
'testCircularLayoutComputation' : './../tests/test-circular-layout-computation',
'testVectorOps' : './../tests/test-vector-ops',
'testEmpress' : './../tests/test-empress',
'testExport': './../tests/test-export',
'testAnimationHandler': './../tests/test-animation-panel-handler'
}
});
Expand Down Expand Up @@ -100,7 +102,8 @@
'testCircularLayoutComputation',
'testVectorOps',
'testEmpress',
'testAnimationHandler'
'testAnimationHandler',
'testExport'
],

// start tests
Expand Down Expand Up @@ -128,7 +131,8 @@
testCircularLayoutComputation,
testVectorOps,
testEmpress,
testAnimationHandler
testAnimationHandler,
testExport
) {
$(document).ready(function() {
QUnit.start();
Expand Down
Empty file added tests/test-export.js
Empty file.

0 comments on commit 69ad23a

Please sign in to comment.