Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export svg #218

Merged
merged 32 commits into from
Jul 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ada6364
adding a function that generates a SVG string from coords and NodeCoords
sjanssen2 Jun 29, 2020
7bb4f04
adding button to trigger export
sjanssen2 Jun 29, 2020
4961830
add new vendor js dependency (as used in Emperor)
sjanssen2 Jun 29, 2020
e979320
add GUI element for export SVG to side-panel
sjanssen2 Jun 29, 2020
3388929
adding new dependency
sjanssen2 Jun 29, 2020
aa44fdb
Merge branch 'master' into exportSVG
sjanssen2 Jun 29, 2020
9eac85b
Merge branch 'master' of https://github.com/biocore/empress into expo…
sjanssen2 Jun 29, 2020
02cb6f6
Merge branch 'exportSVG' of https://github.com/sjanssen2/empress into…
sjanssen2 Jun 29, 2020
da083f7
Merge branch 'exportSVG' of git://github.com/sjanssen2/empress into e…
ElDeveloper Jun 29, 2020
da05def
fix codestyle
sjanssen2 Jun 29, 2020
bfd1b7a
export line thickness
sjanssen2 Jun 29, 2020
6f4938e
prettier
sjanssen2 Jun 30, 2020
ff49d5e
prettier
sjanssen2 Jun 30, 2020
2991208
Merge branch 'master' of https://github.com/biocore/empress into expo…
sjanssen2 Jun 30, 2020
e67db76
plot nodes in generates SVG only if "show node circles" is true
sjanssen2 Jun 30, 2020
a8c3acb
adding first two js-tests
sjanssen2 Jun 30, 2020
27ca5e7
adding more tests
sjanssen2 Jun 30, 2020
623ffc6
using global size variable for node radius
sjanssen2 Jun 30, 2020
2c93b56
Merge branch 'master' of https://github.com/biocore/empress into expo…
sjanssen2 Jul 2, 2020
7ddd9f4
adressing suggestions from code review
sjanssen2 Jul 2, 2020
ae80d82
adding a function to create a SVG version for a legend
sjanssen2 Jul 3, 2020
f1a762e
execute legend to SVG code
sjanssen2 Jul 3, 2020
028c457
generate svg separately for tree and legend due to internal informati…
sjanssen2 Jul 5, 2020
173c2bc
prettier
sjanssen2 Jul 6, 2020
1c22337
Merge branch 'master' of https://github.com/biocore/empress into expo…
sjanssen2 Jul 27, 2020
2b8fed0
adept to new Biom table and tree format
sjanssen2 Jul 27, 2020
099b9c4
codestyle
sjanssen2 Jul 27, 2020
c8413d6
file is now "prettier"
sjanssen2 Jul 27, 2020
dc911b4
Merge branch 'exportSVG' of git://github.com/sjanssen2/empress into e…
ElDeveloper Jul 28, 2020
726f133
Made a few minor changes
ElDeveloper Jul 28, 2020
51d1c55
STY: Prettyfy
ElDeveloper Jul 28, 2020
a52b493
Merge pull request #1 from ElDeveloper/exportsvg-changes
sjanssen2 Jul 28, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a heads up, after #190 is merged, this method will no longer return the proper coordinates since they will be calculated in WebGl. Maybe we can keep this method but rename it to getSvgCoords. @ElDeveloper any thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like a good way to go 👍 . And once we do that, it might be best to draw the arcs with single splines (which are supported in SVG) instead of line segments.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, we can simply draw a line for every two successive coordinates. For circular layout, if #190 is merged, we will than track if we have to draw a "normal" line or an arc. How would this method know? But I figure we should leave this issue for future development.

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.