├── .gitignore
├── tree_radial.png
├── tree_rect.png
├── js
├── q2_phylogram.js
├── phylogram_d3.js
├── lib
│ └── tooltip.js
└── utils.js
├── css
└── styles.css
├── README.md
└── docs
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tree_radial.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ConstantinoSchillebeeckx/phylogram_d3/HEAD/tree_radial.png
--------------------------------------------------------------------------------
/tree_rect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ConstantinoSchillebeeckx/phylogram_d3/HEAD/tree_rect.png
--------------------------------------------------------------------------------
/js/q2_phylogram.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | TODO
4 |
5 | */
6 |
7 |
8 | /* initialize tree
9 |
10 | This function is meant to be used with q2_phylogram
11 | since QIIME2 does not allow cross-origin loading of
12 | data. This function must be loaded after the initial
13 | init() defined in js/phylogram.js and will thus replace
14 | it.
15 |
16 | It is expected then, that the Newick file contents are
17 | wrapped in the d3.jsonp.readNewick() callback. Furthemore,
18 | the contents of the mapping file will be stored in the var
19 | options.mapping_dat.
20 |
21 | Function called from front-end with all user-defined
22 | opts to format the tree. Will validate input
23 | Newick tree, show a loading spinner, and then
24 | render the tree
25 |
26 | Parameters:
27 | ==========
28 | - dat : string
29 | filepath for input Newick tre
30 | - div : string
31 | div id (with included #) in which to generated tree
32 | - options: obj
33 | options object with potential keys and values
34 |
35 | options obj:
36 | - mapping_file: path to OTU mapping file (if there is one)
37 | - hideRuler: (bool) if true, background distance ruler is not rendered TODO
38 | - skipBranchLengthScaling: (bool) if true, tree will not be scaled by distance TODO
39 | - skipLabels: (bool) if true, leaves will not be labeled by name or distance
40 | - treeType: either rectangular or radial
41 | TODO
42 |
43 | */
44 |
45 |
46 | function init(dat, div, options) {
47 |
48 | // ensure a file was passed
49 | if (!dat) {
50 | var msg = 'Please ensure that, at a minimum, a Newick file is passed to the init() call!';
51 | displayErrMsg(msg, div);
52 | return false;
53 | }
54 |
55 | // ensure options is obj if not passed as such
56 | if (!(options !== null && typeof options === 'object')){
57 | options = {};
58 | }
59 |
60 | renderDiv = div;
61 |
62 | // show loading spinner
63 | showSpinner(renderDiv, true)
64 |
65 | //d3.text(dat, function(error, fileStr) {
66 | d3.jsonp(dat + '?callback=d3.jsonp.readNewick', function(fileStr) {
67 |
68 | if (fileStr == '' || fileStr == null) {
69 | var msg = 'Input file ' + dat + ' could not be parsed, ensure it is a proper Newick tree file!';
70 | displayErrMsg(msg, renderDiv);
71 | }
72 |
73 | // process Newick tree
74 | newick = processNewick(fileStr);
75 |
76 |
77 | // render tree
78 | buildTree(renderDiv, newick, options, function() { resizeSVG(); });
79 | });
80 | }
81 |
82 |
--------------------------------------------------------------------------------
/css/styles.css:
--------------------------------------------------------------------------------
1 | .render {
2 | font-family: -apple-system, BlinkMacSystemFont, "Open Sans", Helvetica, sans-serif;
3 | font-size: 11px;
4 | }
5 |
6 | .legend {
7 | font-size: 11px;
8 | }
9 |
10 | /* branch tip styling */
11 | .leaf circle {
12 | fill: greenYellow;
13 | stroke: yellowGreen;
14 | stroke-width: 2px;
15 | }
16 |
17 | .leaf text {
18 | font-size: 10px;
19 | fill: black;
20 | }
21 |
22 | .leaf rect {
23 | fill: steelblue;
24 | opacity: 1e-6; /* initially hide leaf background */
25 | }
26 | /* branch tip styling */
27 |
28 |
29 | /* background distance rulers */
30 | text.rule {
31 | font-size: 8px;
32 | fill: #ccc;
33 | text-decoration: underline;
34 | }
35 |
36 | .ruleGroup {
37 | stroke: #ddd;
38 | fill: none;
39 | stroke-width: 1px;
40 | }
41 | /* background distance rulers */
42 |
43 |
44 | .root {
45 | fill : steelblue;
46 | stroke-width: 2px;
47 | stroke: #369;
48 | }
49 |
50 |
51 | /* inside of tree */
52 | .inner text {
53 | font-size: 8px;
54 | fill: #ccc;
55 | }
56 |
57 |
58 | /* branches of tree */
59 | path.link {
60 | fill: none;
61 | stroke: #aaa;
62 | stroke-width: 2px;
63 | }
64 |
65 | /* extra small dropdown */
66 | .form-control.input-xs {
67 | height: 20px;
68 | }
69 |
70 | .node.inner {
71 | fill: #aaa;
72 | cursor: pointer;
73 | }
74 |
75 | /* tooltip */
76 | .d3-tip {
77 | line-height: 1;
78 | padding: 12px;
79 | background: rgba(0, 0, 0, 0.8);
80 | color: #fff;
81 | border-radius: 2px;
82 | line-height: 130%;
83 | font-size: 12px;
84 | font-family: "Open Sans", Helvetica, sans-serif;
85 | max-width: 500px;
86 | word-wrap: break-word;
87 | z-index: 10;
88 | }
89 |
90 | .d3-tip .tip-title {
91 | font-size: 16px;
92 | margin-bottom: 20px;
93 | }
94 |
95 | .d3-tip .tip-name {
96 | color: red;
97 | }
98 |
99 | .tip-meta-title {
100 | }
101 |
102 | .tip-meta-name {
103 | }
104 |
105 |
106 | text {
107 | font-family: "Open Sans";
108 | }
109 |
110 | @media print {
111 | #gui { display:none; }
112 | svg {
113 | width: 9in;
114 | height: 6.5in;
115 | margin: 0 0 0 0;
116 | }
117 | }
118 |
119 |
120 | /* manually added because external CSS
121 | doesn't seem to be loading well when
122 | saving SVG
123 | */
124 | .lead {
125 | margin-bottom: 20px;
126 | font-size: 16px;
127 | font-weight: 300;
128 | line-height: 1.4
129 | }
130 |
131 | @media (min-width:768px) {
132 | .lead {
133 | font-size: 21px
134 | }
135 | }
136 |
137 |
138 | /* slider */
139 | .noUi-horizontal {
140 | height: 10px;
141 | margin-top: 3px;
142 | margin-bottom: 3px;
143 | }
144 |
145 | .noUi-horizontal .noUi-handle {
146 | width: 18px;
147 | height: 15px;
148 | top: -4px;
149 | left: -6px;
150 | }
151 |
152 | .noUi-handle:before,
153 | .noUi-handle:after {
154 | content: none;
155 | }
156 |
157 | /* hide scrolls bar since everything is fit to screen */
158 | body {
159 | overflow: hidden;
160 | }
161 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # phylogram_d3
2 |
3 | This project is an adaptation of the [GitHubGist](https://gist.github.com/) created by [kueda](https://gist.github.com/kueda/1036776). Given a Newick formatted file as well as an OTU mapping file containing metadata for the leaves in the tree, the script is used to generate a rooted phylogenetic tree:
4 | > A phylogenetic tree or evolutionary tree is a branching diagram or "tree" showing the inferred evolutionary relationships among various biological species or other entities—their phylogeny—based upon similarities and differences in their physical or genetic characteristics. The taxa joined together in the tree are implied to have descended from a common ancestor.
5 |
6 | [- Wikipedia](https://en.wikipedia.org/wiki/Phylogenetic_tree#Unrooted_tree)
7 |
8 | ---
9 | A [index.html](docs/index.html) file has been provided to render the tree as an example.
10 |
11 | A working demo can be found [here](http://constantinoschillebeeckx.github.io/phylogram_d3/) which was used to generate the following:
12 | 
13 | 
14 |
15 | ## Dependencies
16 | * [Twitter Bootstrap](https://getbootstrap.com/)
17 |
18 | * [jQuery](https://jquery.com/)
19 |
20 | * [Colorbrewer.js](https://bl.ocks.org/mbostock/5577023)
21 |
22 | * [Newick.js](https://github.com/jasondavies/newick.js)
23 |
24 | * [d3.js](https://d3js.org/)
25 |
26 | * [D3-tip](https://github.com/emiguevara/d3-tip)
27 |
28 | * [noUiSlider](http://refreshless.com/nouislider/)
29 |
30 | ## Usage
31 |
32 | **in development**
33 |
34 | Rendering the phylogram begins with the function [`init()`][1] which you can simply call in the `body` tag of your HTML page using the [onload](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onload) event handle: `
`. Currently, the function takes three parameters:
35 |
36 | 1. String of the Newick tree used to build the phylogram.
37 |
38 | 2. String identifier for the div id into which to generate the phylogram, note that it must contain the '#' in it. For example, if you want to render your tree in a div with the id 'phylogram' you'd pass '#phylogram' to the function and you'd need the following in your HTML: ``.
39 |
40 | 3. an **optional** object with tree parameters, this should be passed as a javascript object with the following optional keys
41 | * mapping_dat : `(obj)` list of objects where the object keys are metadata and the object values are the metadata values. This is used to color the phylogram.
42 | * treeType : `(str)` either rectangular or radial [default: rectangular]
43 | * hideRuler : `(bool)` if True, the background ruler will be hidden [default: show ruler]
44 | * skipBranchLengthScaling : `(bool)` if True, a [cladogram](https://en.wikipedia.org/wiki/Cladogram) will be displayed instead of a [phylogram](https://en.wikipedia.org/wiki/Phylogenetic_tree) [default: phylogram]
45 | * skipLabels : `(bool)` if True, all node labels will be hidden [default: show labels]
46 |
47 |
48 | ##### Notes on the `mapping_dat`
49 | The `mapping_dat` is similar to a standard QIIME mapping file in that it associates additional metadata to the objects of interest; however in this case the objects of interest are OTUs instead of samples. This file has the following caveats:
50 |
51 | * each object must have a key of `OTUID` which must exist as a leaf in the Newick tree
52 |
53 | * if providing taxonomy data, format it in the QIIME default manner (e.g. *k__bacteria_2;p__firmicutes;c__negativicutes;o__selenomonadales;f__veillonellaceae;g__megasphaera;s__elsdenii*). If done correctly, this format will be parsed and cleaned up automatically
54 |
55 | * if using categorical data (e.g. GroupA, GroupB) the legend will render a row for every category value
56 |
57 | * if passing ordinal data (e.g. 0.4, 1.90) the legend will render a color bar with min/max values. Therefore, if you have categorical data labeled with numbers (e.g. Site 1, 2, 3), ensure that you format the values with alphanumeric characters (e.g. Site1, Site2, Site3)
58 |
59 | [1]: https://github.com/ConstantinoSchillebeeckx/phylogram_d3/blob/master/js/phylogram_d3.js#L126
60 |
--------------------------------------------------------------------------------
/js/phylogram_d3.js:
--------------------------------------------------------------------------------
1 |
2 | // GLOBALS
3 | // --------------
4 | var options = {};
5 | var mapParse, colorScales, mappingFile;
6 |
7 | // use margin convention
8 | // https://bl.ocks.org/mbostock/3019563
9 | var margin = {top: 0, right: 10, bottom: 10, left: 10};
10 | var startW = 800, startH = 600;
11 | var width = startW - margin.left - margin.right;
12 | var height = startH - margin.top - margin.bottom;
13 | var nodes, links, node, link;
14 | var newick;
15 | var shiftX = 0;
16 | var shiftY = 0;
17 | var zoom = d3.behavior.zoom()
18 | // tree defaults
19 | var treeType = 'rectangular'; // rectangular or circular [currently rendered treeType]
20 | var scale = true; // if true, tree will be scaled by distance metric
21 |
22 | // scale for adjusting legend
23 | // text color based on background
24 | // [background HSL -> L value]
25 | // domain is L (0,1)
26 | // range is RBG
27 | var legendColorScale = d3.scale.linear().domain([0.5,1]).range([255,0])
28 |
29 | // tooltip
30 | var tip = d3.tip()
31 | .attr('class', 'd3-tip')
32 | .offset([0,20])
33 | .html(function(d) {
34 | return formatTooltip(d, options.mapping);
35 | })
36 | var outerRadius = startW / 2,
37 | innerRadius = outerRadius - 170;
38 |
39 | // setup radial tree
40 | var radialTree = d3.layout.cluster()
41 | .size([360, innerRadius])
42 | .children(function(d) { return d.branchset; })
43 |
44 | // setup rectangular tree
45 | var rectTree = d3.layout.cluster()
46 | .children(function(node) {
47 | return node.branchset
48 | })
49 | .size([height, width]);
50 |
51 | var duration = 1000;
52 |
53 |
54 |
55 |
56 | // --------------
57 | // GLOBALS
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | /* initialize tree
66 |
67 | Function called from front-end with all user-defined
68 | opts to format the tree. Will validate input
69 | Newick tree, show a loading spinner, and then
70 | render the tree
71 |
72 | Parameters:
73 | ==========
74 | - dat : string
75 | Newick tree as javascript var
76 | - div : string
77 | div id (with included #) in which to generated tree
78 | - options: obj
79 | options object with potential keys and values
80 |
81 | options obj:
82 | - mapping_file: path to OTU mapping file (if there is one)
83 | - hideRuler: (bool) if true, background distance ruler is not rendered TODO
84 | - skipBranchLengthScaling: (bool) if true, tree will not be scaled by distance TODO
85 | - skipLabels: (bool) if true, leaves will not be labeled by name or distance
86 | - treeType: either rectangular or radial
87 |
88 | */
89 |
90 |
91 | function init(dat, div, options) {
92 |
93 | // show loading spinner
94 | showSpinner(div, true)
95 |
96 | validateInputs(dat, options);
97 |
98 | // process Newick tree
99 | newick = processNewick(dat);
100 |
101 | // render tree
102 | buildTree(div, newick, options, function() { updateTree(); });
103 |
104 |
105 | }
106 |
107 |
108 |
109 | /* Primary tree building function
110 |
111 | Will do an initial render of all SVG elements
112 | including the GUI and the initial layout of
113 | the tree. Subsequent updating in both style
114 | and format of the tree is done through updateTree()
115 |
116 |
117 | Parameters:
118 | ===========
119 | - div : string
120 | div id (with included #) in which to generated tree
121 | - newick : Newick obj
122 | return of function processNewick()
123 | - opts: obj
124 | opts object with potential keys and values
125 |
126 |
127 | Retrurns:
128 | =========
129 | - nothing
130 |
131 | */
132 |
133 | function buildTree(div, newick, opts, callback) {
134 |
135 | if ('mapping_dat' in opts) {
136 | var parsed = parseMapping(opts.mapping_dat);
137 | mapParse = parsed[0];
138 | colorScales = parsed[1];
139 | options.mapping = mapParse;
140 | options.colorScale = colorScales;
141 | }
142 |
143 | // check opts, if not set, set to default
144 | if (!('treeType' in opts)) {
145 | opts['treeType'] = treeType;
146 | } else {
147 | treeType = opts.treeType;
148 | }
149 | if (!('skipBranchLengthScaling' in opts)) {
150 | opts['skipBranchLengthScaling'] = !scale;
151 | } else {
152 | scale = opts.skipBranchLengthScaling;
153 | }
154 |
155 | // add bootstrap container class
156 | d3.select(div)
157 | .attr("class","container-fluid render")
158 |
159 | // build GUI
160 | var gui = buildGUI(div, opts);
161 |
162 | var tmp = d3.select(div).append("div")
163 | .attr("class","row")
164 | .attr("id","canvas")
165 |
166 | // NOTE: size of SVG and SVG g are updated in fitTree()
167 | svg = tmp.append("div")
168 | .attr("class", "col-sm-12")
169 | .attr("id","tree")
170 | .append("svg:svg")
171 | .attr("xmlns","http://www.w3.org/2000/svg")
172 | .attr("id","SVGtree")
173 | .call(zoom.on("zoom", panZoom))
174 | .append("g") // svg g group is translated by fitTree()
175 | .attr("id",'canvasSVG')
176 | .attr("transform","translate(" + margin.left + "," + margin.top + ")")
177 |
178 | svg.append("g")
179 | .attr("id","rulerSVG")
180 | svg.append("g")
181 | .attr("id","treeSVG")
182 |
183 |
184 | // generate intial layout and all tree elements
185 | d3.select("#canvasSVG")
186 | if (opts.treeType == 'rectangular') {
187 | layoutTree(rectTree, newick, opts);
188 | } else if (opts.treeType == 'radial') {
189 | layoutTree(radialTree, newick, opts);
190 | }
191 |
192 | svg.call(tip);
193 | showSpinner(null, false); // hide spinner
194 | callback(); // calls updateTree
195 |
196 | }
197 |
198 | /* will layout tree elements including nodes and links
199 |
200 | Assumes globals (nodes, links) exist
201 |
202 | Parameters:
203 | -----------
204 | - tree : d3.tree layout
205 | - newick : Newick obj
206 | return of function processNewick()
207 | - opts: obj
208 | opts object with potential keys and values
209 |
210 | */
211 | function layoutTree(tree, newick, opts) {
212 | d3.selectAll("g.ruleGroup").remove() // remove ruler
213 |
214 | nodes = tree.nodes(newick);
215 | if (!opts.skipBranchLengthScaling) { var yscale = scaleBranchLengths(nodes); }
216 | if (opts.treeType == 'rectangular') { var xscale = scaleLeafSeparation(tree, nodes); }
217 | links = tree.links(nodes);
218 |
219 | formatTree(nodes, links, yscale, xscale, height, opts);
220 | }
221 |
222 |
223 |
224 | /* Function used to update existing tree
225 |
226 | Function called from front-end everytime GUI
227 | is changed; this will redraw the tree based
228 | on GUI settings.
229 |
230 | */
231 | function updateTree() {
232 |
233 |
234 | getGUIoptions(); // set our globals
235 |
236 |
237 |
238 | // adjust physical positioning
239 | if (options.typeChange || options.skipBranchLengthScaling != scale) {
240 |
241 | layoutTree( options.treeType == 'rectangular' ? rectTree : radialTree, newick, options);
242 |
243 | // reset rotation to 0 (rect) or to previous pos (radial)
244 | d3.select('#treeSVG').attr('transform', function(d) {
245 | if (options.treeType == 'rectangular') {
246 | return 'rotate(0)';
247 | } else {
248 | return 'rotate(' + rotationSlider.noUiSlider.get() + ')';
249 | }
250 | })
251 |
252 | scale = options.skipBranchLengthScaling;
253 | }
254 |
255 | // enable/disable sliders
256 | if (treeType == 'radial') {
257 | rotationSlider.removeAttribute('disabled');
258 | scaleHSlider.setAttribute('disabled',true);
259 | } else {
260 | rotationSlider.setAttribute('disabled', true);
261 | scaleHSlider.removeAttribute('disabled');
262 | }
263 |
264 |
265 | // if leaf radius becomes too large, adjust vertical scale
266 | if (options.sliderLeafR * 2 > options.sliderScaleV && treeType == 'rectangular') { scaleHSlider.noUiSlider.set( 2 * options.sliderLeafR ); }
267 |
268 | // adjust vertical scale
269 | if (options.treeType == 'rectangular') {
270 | var xscale = scaleLeafSeparation(rectTree, nodes, options.sliderScaleV); // this will update x-pos
271 |
272 | // update ruler length
273 | var treeH = getTreeBox().height + 32; // +32 extends rulers outside treeSVG
274 | d3.selectAll(".ruleGroup line")
275 | .attr("y2", treeH + margin.top + margin.bottom) // TODO doesn't work quite right with large scale
276 |
277 |
278 | // scale vertical pos
279 | svg.selectAll("g.node")
280 | .data(nodes)
281 | .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
282 | svg.selectAll("path.link")
283 | .data(links)
284 | .attr("d", elbow);
285 | }
286 |
287 | // scale leaf radius
288 | svg.selectAll("g.leaf circle")
289 | .attr("r", options.sliderLeafR);
290 | orientTreeLabels();
291 |
292 |
293 | // toggle leaf labels
294 | svg.selectAll('g.leaf.node text')
295 | .style('fill-opacity', options.skipLeafLabel? 1e-6 : 1 )
296 |
297 | // toggle distance labels
298 | svg.selectAll('g.inner.node text')
299 | .style('fill-opacity', options.skipDistanceLabel? 1e-6 : 1 )
300 |
301 | svg.selectAll('g.leaf.node text')
302 | .text(function(d) {
303 | if (options.skipDistanceLabel) {
304 | return d.name;
305 | } else {
306 | if (options.leafText == 'distance' || !mapParse) {
307 | return d.name + ' ('+d.length+')';
308 | } else {
309 | return d.name + ' (' + mapParse.get(options.leafText).get(d.name) + ')';
310 | }
311 | }
312 | });
313 |
314 |
315 | if ('mapping' in options) {
316 | updateLegend(); // will reposition legend as well
317 | } else {
318 | d3.select('svg').attr("viewBox", "0 0 " + parseInt(window.innerWidth) + " " + parseInt(window.innerHeight)); // set viewbox
319 | }
320 |
321 | }
322 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Phylogram d3
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
156 |
157 |
158 |
163 |
164 |
165 |
--------------------------------------------------------------------------------
/js/lib/tooltip.js:
--------------------------------------------------------------------------------
1 | // d3.tip
2 | // Copyright (c) 2013 Justin Palmer
3 | //
4 | // Tooltips for d3.js SVG visualizations
5 | // https://github.com/emiguevara/d3-tip/blob/master/index.js
6 |
7 | (function (root, factory) {
8 | if (typeof define === 'function' && define.amd) {
9 | // AMD. Register as an anonymous module with d3 as a dependency.
10 | define(['d3'], factory)
11 | } else if (typeof module === 'object' && module.exports) {
12 | // CommonJS
13 | module.exports = function(d3) {
14 | d3.tip = factory(d3)
15 | return d3.tip
16 | }
17 | } else {
18 | // Browser global.
19 | root.d3.tip = factory(root.d3)
20 | }
21 | }(this, function (d3) {
22 |
23 | // Public - contructs a new tooltip
24 | //
25 | // Returns a tip
26 | return function() {
27 | var direction = d3_tip_direction,
28 | offset = d3_tip_offset,
29 | html = d3_tip_html,
30 | node = initNode(),
31 | svg = null,
32 | point = null,
33 | target = null
34 |
35 | function tip(vis) {
36 | svg = getSVGNode(vis)
37 | point = svg.createSVGPoint()
38 | document.body.appendChild(node)
39 | }
40 |
41 | // Public - show the tooltip on the screen
42 | //
43 | // Returns a tip
44 | tip.show = function(d, idx, absolute) {
45 | var args = Array.prototype.slice.call(arguments)
46 | if(args[args.length - 1] instanceof SVGElement) target = args.pop()
47 |
48 | var content = html.apply(this, args),
49 | poffset = offset.apply(this, args),
50 | dir = direction.apply(this, args),
51 | nodel = getNodeEl(),
52 | i = directions.length,
53 | coords,
54 | scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
55 | scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft
56 |
57 | nodel.html(content)
58 | .style({ opacity: 1, 'pointer-events': 'all' })
59 |
60 | // set direction based on where mouse is w.r.t entire screen
61 | // NOTE: this will not work if absolute is being used
62 | var x = d3.event.pageX;
63 | var y = d3.event.pageY;
64 |
65 | if ( x <= window.innerWidth / 2.0) { // left side of screen
66 | if (y <= window.innerHeight / 2.0) { // top of screen
67 | dir = 'se';
68 | } else { // bottom of screen
69 | dir = 'ne';
70 | }
71 | } else { // right side of screen
72 | if (y <= window.innerHeight / 2.0) { // top of screen
73 | dir = 'sw';
74 | } else { // bottom of screen
75 | dir = 'nw';
76 | }
77 | }
78 | direction(dir);
79 |
80 | while(i--) nodel.classed(directions[i], false)
81 | if (!absolute || absolute === false) {
82 | coords = direction_callbacks.get(dir).apply(this);
83 | nodel.classed(dir, true).style({
84 | top: (coords.top + poffset[0]) + scrollTop + 'px',
85 | left: (coords.left + poffset[1]) + scrollLeft + 'px'
86 | });
87 | }
88 | else {
89 | coords = direction_callbacks.get(dir).apply(this);
90 | nodel.classed(dir, true).style({
91 | top: (absolute[0] + poffset[0]) + 'px',
92 | left: (absolute[1] + poffset[1]) + 'px'
93 | });
94 | }
95 |
96 | return tip
97 | }
98 |
99 | // Public - hide the tooltip
100 | //
101 | // Returns a tip
102 | tip.hide = function() {
103 | var nodel = getNodeEl()
104 | nodel.style({ opacity: 0, 'pointer-events': 'none' })
105 | return tip
106 | }
107 |
108 | // Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value.
109 | //
110 | // n - name of the attribute
111 | // v - value of the attribute
112 | //
113 | // Returns tip or attribute value
114 | tip.attr = function(n, v) {
115 | if (arguments.length < 2 && typeof n === 'string') {
116 | return getNodeEl().attr(n)
117 | } else {
118 | var args = Array.prototype.slice.call(arguments)
119 | d3.selection.prototype.attr.apply(getNodeEl(), args)
120 | }
121 |
122 | return tip
123 | }
124 |
125 | // Public: Proxy style calls to the d3 tip container. Sets or gets a style value.
126 | //
127 | // n - name of the property
128 | // v - value of the property
129 | //
130 | // Returns tip or style property value
131 | tip.style = function(n, v) {
132 | if (arguments.length < 2 && typeof n === 'string') {
133 | return getNodeEl().style(n)
134 | } else {
135 | var args = Array.prototype.slice.call(arguments)
136 | d3.selection.prototype.style.apply(getNodeEl(), args)
137 | }
138 |
139 | return tip
140 | }
141 |
142 | // Public: Set or get the direction of the tooltip
143 | //
144 | // v - One of n(north), s(south), e(east), or w(west), nw(northwest),
145 | // sw(southwest), ne(northeast) or se(southeast)
146 | //
147 | // Returns tip or direction
148 | tip.direction = function(v) {
149 | if (!arguments.length) return direction
150 | direction = v == null ? v : d3.functor(v)
151 |
152 | return tip
153 | }
154 |
155 | // Public: Sets or gets the offset of the tip
156 | //
157 | // v - Array of [x, y] offset
158 | //
159 | // Returns offset or
160 | tip.offset = function(v) {
161 | if (!arguments.length) return offset
162 | offset = v == null ? v : d3.functor(v)
163 |
164 | return tip
165 | }
166 |
167 | // Public: sets or gets the html value of the tooltip
168 | //
169 | // v - String value of the tip
170 | //
171 | // Returns html value or tip
172 | tip.html = function(v) {
173 | if (!arguments.length) return html
174 | html = v == null ? v : d3.functor(v)
175 |
176 | return tip
177 | }
178 |
179 | // Public: destroys the tooltip and removes it from the DOM
180 | //
181 | // Returns a tip
182 | tip.destroy = function() {
183 | if(node) {
184 | getNodeEl().remove();
185 | node = null;
186 | }
187 | return tip;
188 | }
189 |
190 | function d3_tip_direction() { return 'n' }
191 | function d3_tip_offset() { return [0, 0] }
192 | function d3_tip_html() { return ' ' }
193 | function d3_tip_absolute() { return [0, 0] }
194 |
195 | var direction_callbacks = d3.map({
196 | n: direction_n,
197 | s: direction_s,
198 | e: direction_e,
199 | w: direction_w,
200 | nw: direction_nw,
201 | ne: direction_ne,
202 | sw: direction_sw,
203 | se: direction_se
204 | }),
205 |
206 | directions = direction_callbacks.keys()
207 |
208 | function direction_n() {
209 | var bbox = getScreenBBox()
210 | return {
211 | top: bbox.n.y - node.offsetHeight,
212 | left: bbox.n.x - node.offsetWidth / 2
213 | }
214 | }
215 |
216 | function direction_s() {
217 | var bbox = getScreenBBox()
218 | return {
219 | top: bbox.s.y,
220 | left: bbox.s.x - node.offsetWidth / 2
221 | }
222 | }
223 |
224 | function direction_e() {
225 | var bbox = getScreenBBox()
226 | return {
227 | top: bbox.e.y - node.offsetHeight / 2,
228 | left: bbox.e.x
229 | }
230 | }
231 |
232 | function direction_w() {
233 | var bbox = getScreenBBox()
234 | return {
235 | top: bbox.w.y - node.offsetHeight / 2,
236 | left: bbox.w.x - node.offsetWidth
237 | }
238 | }
239 |
240 | function direction_nw() {
241 | var bbox = getScreenBBox()
242 | return {
243 | top: bbox.nw.y - node.offsetHeight,
244 | left: bbox.nw.x - node.offsetWidth
245 | }
246 | }
247 |
248 | function direction_ne() {
249 | var bbox = getScreenBBox()
250 | return {
251 | top: bbox.ne.y - node.offsetHeight,
252 | left: bbox.ne.x
253 | }
254 | }
255 |
256 | function direction_sw() {
257 | var bbox = getScreenBBox()
258 | return {
259 | top: bbox.sw.y,
260 | left: bbox.sw.x - node.offsetWidth
261 | }
262 | }
263 |
264 | function direction_se() {
265 | var bbox = getScreenBBox()
266 | return {
267 | top: bbox.se.y,
268 | left: bbox.e.x
269 | }
270 | }
271 |
272 | function initNode() {
273 | var node = d3.select(document.createElement('div'))
274 | node.style({
275 | position: 'absolute',
276 | top: 0,
277 | opacity: 0,
278 | 'pointer-events': 'none',
279 | 'box-sizing': 'border-box'
280 | })
281 |
282 | return node.node()
283 | }
284 |
285 | function getSVGNode(el) {
286 | el = el.node()
287 | if(el.tagName.toLowerCase() === 'svg')
288 | return el
289 |
290 | return el.ownerSVGElement
291 | }
292 |
293 | function getNodeEl() {
294 | if(node === null) {
295 | node = initNode();
296 | // re-add node to DOM
297 | document.body.appendChild(node);
298 | };
299 | return d3.select(node);
300 | }
301 |
302 | // Private - gets the screen coordinates of a shape
303 | //
304 | // Given a shape on the screen, will return an SVGPoint for the directions
305 | // n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest),
306 | // sw(southwest).
307 | //
308 | // +-+-+
309 | // | |
310 | // + +
311 | // | |
312 | // +-+-+
313 | //
314 | // Returns an Object {n, s, e, w, nw, sw, ne, se}
315 | function getScreenBBox() {
316 | var targetel = target || d3.event.target;
317 |
318 | while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) {
319 | targetel = targetel.parentNode;
320 | }
321 |
322 | var bbox = {},
323 | matrix = targetel.getScreenCTM(),
324 | tbbox = targetel.getBBox(),
325 | width = tbbox.width,
326 | height = tbbox.height,
327 | x = tbbox.x,
328 | y = tbbox.y
329 |
330 | point.x = x
331 | point.y = y
332 | bbox.nw = point.matrixTransform(matrix)
333 | point.x += width
334 | bbox.ne = point.matrixTransform(matrix)
335 | point.y += height
336 | bbox.se = point.matrixTransform(matrix)
337 | point.x -= width
338 | bbox.sw = point.matrixTransform(matrix)
339 | point.y -= height / 2
340 | bbox.w = point.matrixTransform(matrix)
341 | point.x += width
342 | bbox.e = point.matrixTransform(matrix)
343 | point.x -= width / 2
344 | point.y -= height / 2
345 | bbox.n = point.matrixTransform(matrix)
346 | point.y += height
347 | bbox.s = point.matrixTransform(matrix)
348 |
349 | return bbox
350 | }
351 |
352 | return tip
353 | };
354 |
355 | }));
356 |
--------------------------------------------------------------------------------
/js/utils.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Helper utilities; supports tidy code organization
4 |
5 | */
6 |
7 |
8 | /* Ensure leaf nodes are not overlapping
9 |
10 | Finds the smallest vertical distance between leaves
11 | and scales things to a minimum distance so that
12 | branches don't overlap.
13 |
14 | Note that this will update the node.x positions
15 | for all nodes found in passed var 'nodes' as well
16 | as update the global 'links' var.
17 |
18 | Parameters:
19 | ===========
20 | - tree : d3.tree layout (cluster)
21 | - nodes : d3.tree nodes
22 | - minSeparation : int (default: 22)
23 | mininum distance between leaf nodes
24 |
25 | Returns:
26 | ========
27 | - xscale : d3.scale
28 | scale for leaf height separation; given the
29 | svg height, it will scale properly so leaf
30 | nodes have minimum separation
31 | */
32 | function scaleLeafSeparation(tree, nodes, minSeparation=22) {
33 |
34 | var traverseTree = function(root, callback) {
35 | callback(root);
36 | if (root.children) {
37 | for (var i = root.children.length - 1; i >= 0; i--){
38 | traverseTree(root.children[i], callback)
39 | };
40 | }
41 | }
42 |
43 | // get all leaf X positions
44 | leafXpos = [];
45 | traverseTree(nodes[0], function(node) {
46 | if (!node.children) {
47 | leafXpos.push(node.x);
48 | }
49 | });
50 |
51 | // calculate leaf vertical distances
52 | leafXdist = [];
53 | leafXpos = leafXpos.sort(function(a, b) { return a-b });
54 | leafXpos.forEach( function(x,i) {
55 | if (i + 1 != leafXpos.length) {
56 | var dist = leafXpos[i + 1] - x;
57 | if (dist) {
58 | leafXdist.push(dist);
59 | }
60 | }
61 | })
62 |
63 | var xScale = d3.scale.linear()
64 | .range([0, minSeparation])
65 | .domain([0, d3.min(leafXdist)])
66 |
67 | // update node x pos & links
68 | traverseTree(nodes[0], function(node) {
69 | node.x = xScale(node.x)
70 | })
71 |
72 | links = tree.links(nodes);
73 |
74 | return xScale;
75 | }
76 |
77 |
78 | /* Scale tree by distance metric
79 |
80 | Will iterate through tree and set the attribute
81 | rootDist (at each node) and will adjust the
82 | y-pos of the tree properly
83 |
84 | Parameters:
85 | ===========
86 | - nodes : d3.tree nodes
87 | - width : int
88 | svg width
89 |
90 | Returns:
91 | ========
92 | - yscale : d3.scale
93 | horizontal scale for svg
94 | */
95 | function scaleBranchLengths(nodes) {
96 |
97 | // Visit all nodes and adjust y pos width distance metric
98 | var visitPreOrder = function(root, callback) {
99 | callback(root)
100 | if (root.children) {
101 | for (var i = root.children.length - 1; i >= 0; i--){
102 | visitPreOrder(root.children[i], callback)
103 | };
104 | }
105 | }
106 | visitPreOrder(nodes[0], function(node) {
107 | node.rootDist = (node.parent ? node.parent.rootDist : 0) + (node.length || 0)
108 | })
109 | var rootDists = nodes.map(function(n) { return n.rootDist; });
110 |
111 | var yscale = d3.scale.linear()
112 | .domain([0, d3.max(rootDists)])
113 | .range([0, width]);
114 |
115 | visitPreOrder(nodes[0], function(node) {
116 | node.y = yscale(node.rootDist)
117 | })
118 | return yscale
119 | }
120 |
121 |
122 |
123 |
124 | // https://bl.ocks.org/mbostock/c034d66572fd6bd6815a
125 | // Like d3.svg.diagonal.radial, but with square corners.
126 | function step(startAngle, startRadius, endAngle, endRadius) {
127 | var c0 = Math.cos(startAngle = (startAngle - 90) / 180 * Math.PI),
128 | s0 = Math.sin(startAngle),
129 | c1 = Math.cos(endAngle = (endAngle - 90) / 180 * Math.PI),
130 | s1 = Math.sin(endAngle);
131 | return "M" + startRadius * c0 + "," + startRadius * s0
132 | + (endAngle === startAngle ? "" : "A" + startRadius + "," + startRadius + " 0 0 " + (endAngle > startAngle ? 1 : 0) + " " + startRadius * c1 + "," + startRadius * s1)
133 | + "L" + endRadius * c1 + "," + endRadius * s1;
134 | }
135 |
136 |
137 | // http://bl.ocks.org/mbostock/2429963
138 | // draw right angle links to join nodes
139 | function elbow(d, i) {
140 | return "M" + d.source.y + "," + d.source.x
141 | + "V" + d.target.x + "H" + d.target.y;
142 | }
143 |
144 |
145 |
146 |
147 |
148 |
149 | /* Master format tree function
150 |
151 | Parameters:
152 | ===========
153 | - nodes : d3 tree nodes
154 | - links : d3 tree links
155 | - yscale : quantitative scale
156 | horizontal scaling factor for distance
157 | if null, ruler is not drawn
158 | - xscale : quantitative scale
159 | vertical scale
160 | if null, ruler is not drawn
161 | - height : int
162 | height of svg
163 | - opts : obj
164 | tree opts, see documentation for keys
165 |
166 | */
167 | function formatTree(nodes, links, yscale=null, xscale=null, height, opts) {
168 |
169 | /* Format links (branches) of tree
170 | formatLinks
171 |
172 | Will render the lines connecting nodes (links)
173 | with right angle elbows.
174 |
175 | Parameters:
176 | ===========
177 | - svg : svg selctor
178 | svg HTML element into which to render
179 | - links : d3.tree.links
180 | - opts : obj
181 | tree opts, see documentation for keys
182 |
183 |
184 | */
185 |
186 | // set to global!
187 | link = d3.select('#treeSVG').selectAll("path.link")
188 | .data(links)
189 | .enter().append("path")
190 | .attr("class","link")
191 | .style("fill","none") // setting style inline otherwise AI doesn't render properly
192 | .style("stroke","#aaa")
193 | .style("stroke-width","2px")
194 |
195 | d3.selectAll('.link')
196 | .attr("d", function(d) { return opts.tree == 'rectangular' ? elbow(d) : step(d.source.x, d.source.y, d.target.x, d.target.y); })
197 |
198 |
199 | /* Render and format tree nodes
200 | formatNodes
201 |
202 | Will render all tree nodes as well as format them
203 | with color, shape, size; additionally all leaf
204 | nodes and internal nodes will get labels by default.
205 |
206 | A node is a generalized group which can contain shapes
207 | (circle) as well as labels (text).
208 |
209 | Parameters:
210 | ===========
211 | - svg : svg selctor
212 | svg HTML element into which to render
213 | - nodes : d3.tree.nodes
214 | - opts : obj
215 | tree opts, see documentation for keys
216 |
217 |
218 | */
219 |
220 | // set default leaf radius if not present
221 | if (!('sliderLeafR' in opts)) {
222 | opts['sliderLeafR'] = 5;
223 | }
224 |
225 | node = d3.select('#treeSVG').selectAll("g.node")
226 | .data(nodes)
227 | .enter().append("g")
228 | .attr("class", function(n) {
229 | if (n.children) {
230 | if (n.depth == 0) {
231 | return "root node"
232 | } else {
233 | return "inner node"
234 | }
235 | } else {
236 | return "leaf node"
237 | }
238 | })
239 | .attr("id", function(d) {
240 | if (!d.children) {
241 | var name = d.name.replace(new RegExp('\\.', 'g'), '_');
242 | return 'leaf_' + name;
243 | }
244 | })
245 |
246 | d3.selectAll('.node')
247 | .attr("transform", function(d) {
248 | if (opts.treeType == 'rectangular') {
249 | return "translate(" + d.y + "," + d.x + ")";
250 | } else if (opts.treeType == 'radial') {
251 | return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
252 | }
253 | })
254 |
255 | d3.selectAll('.leaf')
256 | .on('mouseover', tip.show)
257 | .on('mouseout', tip.hide)
258 |
259 | // node backgrounds
260 | node.append("rect")
261 | .attr('width', 0 ) // width is set when choosing background color
262 | .attr('height', 10 + opts.sliderLeafR * 2)
263 | .attr('y', -opts.sliderLeafR - 5)
264 | .attr("opacity", function(d) { return d.children ? 1e-6 : 1 });
265 |
266 | // node circles
267 | node.append("circle")
268 | .attr("r", function(d) {
269 | if (!d.children || d.depth == 0) {
270 | return opts.sliderLeafR;
271 | } else {
272 | return 3;
273 | }
274 | });
275 |
276 | d3.selectAll('.inner.node circle')
277 | .on("mouseover", function() {
278 | d3.select(this)
279 | .transition()
280 | .duration(100)
281 | .attr("r",6);
282 | })
283 | .on("mouseout", function() {
284 | d3.select(this)
285 | .transition()
286 | .duration(100)
287 | .attr("r",3);
288 | })
289 |
290 |
291 | // node label
292 | node.append("text")
293 | .attr("class",function(d) { return d.children ? "distanceLabel" : "leafLabel" })
294 | .attr("dy", function(d) { return d.children ? -6 : 3 })
295 | .text(function(d) {
296 | if (d.children) {
297 | if (d.length && d.length.toFixed(2) > 0.01) {
298 | return d.length.toFixed(2);
299 | } else {
300 | return '';
301 | }
302 | } else {
303 | if (opts['leafText']) {
304 | return d.name + ' (' + mapParse.get(opts['leafText']).get(d.name) + ')';
305 | } else {
306 | return d.name + ' (' + d.length + ')';
307 | }
308 | }
309 | })
310 | .attr("opacity", function(d) { return opts.skipLabels ? 1e-6 : 1; });
311 |
312 | orientTreeLabels();
313 |
314 |
315 |
316 | /* Render and format background rules
317 | formatRuler
318 |
319 | Parameters:
320 | ===========
321 | - id : id selector
322 | id (with #) into which to render ruler
323 | - yscale : quantitative scale
324 | horizontal scaling factor for distance
325 | - xscale : quantitative scale
326 | vertical scale
327 | - height : int
328 | height of svg
329 | - opts : obj
330 | tree opts, expects a key hideRuler;
331 | if true, rules won't be drawn. also
332 | expects a key treeType (rectangular/radial)
333 |
334 | */
335 |
336 |
337 | if (!opts.hideRuler && yscale != null) {
338 |
339 | if (opts.treeType == 'rectangular') {
340 |
341 | rulerG = d3.select('#rulerSVG').selectAll("g")
342 | .data(yscale.ticks(10))
343 | .enter().append("g")
344 | .attr("class", "ruleGroup")
345 | .append('svg:line')
346 | .attr("class", "rule")
347 | .attr('y1', 0)
348 | .attr('y2', getTreeBox().height + margin.top + margin.bottom)
349 | .attr('x1', yscale)
350 | .attr('x2', yscale)
351 |
352 |
353 | } else if (opts.treeType == 'radial') {
354 |
355 | rulerG = d3.select('#rulerSVG').selectAll("g")
356 | .data(yscale.ticks(10))
357 | .enter().append("g")
358 | .attr("class", "ruleGroup")
359 | .append('circle')
360 | .attr("class","rule")
361 | .attr('r', yscale);
362 |
363 | }
364 | }
365 | }
366 |
367 |
368 | /* Display error message
369 |
370 | Will display an error messages within
371 | a bootstrap3 alert div with a message
372 |
373 | Parameters:
374 | ==========
375 | - msg : string
376 | message to display within div, formatted as html
377 | - div : div
378 | div into which to render message
379 |
380 | Returns:
381 | =======
382 | - nothing
383 |
384 | */
385 | function displayErrMsg(msg, div) {
386 |
387 | showSpinner(null, false);
388 |
389 | d3.select(div).append('div')
390 | .attr('class','alert alert-danger lead col-sm-8 col-sm-offset-2')
391 | .style('margin-top','20px')
392 | .attr('role','alert')
393 | .html(msg);
394 | }
395 |
396 |
397 |
398 | /* When called, will display a div with a spinner
399 |
400 | Parameters:
401 | ==========
402 | - div : string
403 | div id (with included #) in which to generated tree
404 | - show : bool (default true)
405 | optional boolean to show div, when false the spinner
406 | will be removed
407 | */
408 |
409 | function showSpinner(div, show=true) {
410 |
411 | if (!show) {
412 | d3.select('#spinner').remove();
413 | } else {
414 |
415 | // give user a spinner for feedback
416 | var spinner = d3.select(div).append('div')
417 | .attr('id','spinner')
418 | .attr('class','lead alert alert-info col-sm-3 col-sm-offset-4')
419 | .style('margin-top','20px');
420 |
421 | spinner.append('i')
422 | .attr('class','fa fa-cog fa-spin fa-3x fa-fw')
423 | spinner.append('span')
424 | .text('Reading file...'); // TODO center vertically
425 | }
426 | }
427 |
428 |
429 |
430 |
431 | /* Parse Newick tree
432 |
433 | Will process a Newick tree string into
434 | a format that d3 can parse.
435 |
436 | Parameters:
437 | ==========
438 | - fileStr : str
439 | a Newick tree read in as a string
440 |
441 | Returns:
442 | - returns parsed Newick object
443 | see: https://github.com/jasondavies/newick.js
444 |
445 | */
446 | function processNewick(fileStr) {
447 |
448 | var newick = Newick.parse(fileStr)
449 | var newickNodes = []
450 | function buildNewickNodes(node, callback) {
451 | newickNodes.push(node)
452 | if (node.branchset) {
453 | for (var i=0; i < node.branchset.length; i++) {
454 | buildNewickNodes(node.branchset[i])
455 | }
456 | }
457 | }
458 |
459 | return newick;
460 | }
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 | /* Process mapping file into useable format
470 |
471 | Function responsible for parsing the TSV mapping
472 | file which contains metadata for formatting the
473 | tree as well as generating color scales
474 | for use in the legend and coloring the tree.
475 |
476 | Note that any QIIME formatted taxonomy data
477 | found will automatically be cleaned up removing
478 | the level prefix and splitting the taxonomy on
479 | each level into its own metadata category. This
480 | allows users to color by a specific taxonomic
481 | level.
482 |
483 | It is assumed that the first column in the mapping
484 | file has the same values as the leaf names.
485 |
486 | Parameters:
487 | ===========
488 | - data: d3.tsv() parsed data
489 | input mapping file processed by d3.tsv; will be
490 | an array (where each row in the TSV is an array
491 | value) of objects where objects have col headers
492 | as keys and file values as values
493 |
494 |
495 | Returns:
496 | ========
497 | - array
498 | returns an array of length 2:
499 | 0: d3.map() of parsed TSV data with file column
500 | headers as the keys and the values are a d3.map()
501 | where leaf names are keys (TSV rows) and values
502 | are the row/column values in the file.
503 | 1: d3.map() as colorScales where keys are file
504 | column headers and values are the color scales.
505 | scales take as input the leaf name (file row)
506 | */
507 | function parseMapping(data) {
508 |
509 | // get mapping file column headers
510 | // we assume first column is the leaf ID
511 | var colTSV = d3.map(data[0]).keys();
512 | var id = colTSV[0];
513 | colTSV.shift(); // remove first col (ID)
514 |
515 | var mapParse = d3.map(); // {colHeader: { ID1: val, ID2: val } }
516 |
517 | taxaDat = {};
518 | data.forEach(function(row) {
519 | var leafName = row[id];
520 | colTSV.forEach( function(col, i) {
521 | var colVal = cleanTaxa(row[col]);
522 | if (!mapParse.has(col)) {
523 | var val = d3.map();
524 | } else {
525 | var val = mapParse.get(col);
526 | }
527 |
528 | if (typeof colVal === 'object') { // if data was taxa info, it comes back as an obj
529 | for (var level in colVal) {
530 | var taxa = colVal[level];
531 | if (!mapParse.has(level)) {
532 | var val = d3.map();
533 | } else {
534 | var val = mapParse.get(level);
535 | }
536 | val.set(leafName, taxa);
537 | mapParse.set(level, val);
538 | }
539 | } else {
540 | val.set(leafName, colVal);
541 | mapParse.set(col, val);
542 | }
543 | })
544 | })
545 |
546 | // setup color scales for mapping columns
547 | // keys are mapping column headers and values are scales
548 | // for converting column value to a color
549 | var colorScales = d3.map();
550 | mapParse.forEach(function(k, v) { // v is a d3.set of mapping column values, with leaf ID has key
551 |
552 | // check if values for mapping column are string or numbers
553 | // strings are turned into ordinal scales, numbers into quantitative
554 | // we simply check the first value in the obj
555 | var vals = autoSort(v.values(), true);
556 | var scale;
557 | if (typeof vals[0] === 'string' || vals[0] instanceof String) { // ordinal scale
558 | var tmp = d3.scale.category10();
559 | if (vals.length > 10) {
560 | tmp = d3.scale.category10();;
561 | }
562 | scale = tmp.domain(vals);
563 | } else { // quantitative scale
564 | scale = d3.scale.quantize()
565 | .domain(d3.extent(vals))
566 | .range(colorbrewer.Spectral[11]);
567 | }
568 | colorScales.set(k, scale);
569 | })
570 |
571 | return [mapParse, colorScales];
572 | }
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 | /* Clean-up a QIIME formatted taxa string
583 |
584 | Will clean-up a QIIME formatted taxonomy string
585 | by removing the class prefix and returning the
586 | original taxa string as an object split into taxonomic
587 | levels e.g. {"Kingdom":"bacteria", ... }
588 |
589 | NOTE: any taxa level with an assignment "unassigned"
590 | will be thrown out - this way the tree will not
591 | color by this level (tree can only be colored by
592 | defined taxa)
593 |
594 | Parameters:
595 | ===========
596 | - taxa : string
597 | QIIME formatted string
598 |
599 | Returns:
600 | ========
601 | - cleaned string
602 |
603 | */
604 | function cleanTaxa(taxa) {
605 |
606 | if ((typeof taxa === 'string' || taxa instanceof String) && taxa.slice(0, 2) == 'k_') {
607 |
608 | var str = taxa.replace(/.__/g, "");
609 |
610 | // some taxa strings end in ';' some don't,
611 | // remove it if it exists
612 | if (str.substr(str.length - 1) == ';') {
613 | str = str.substring(0, str.length - 1);
614 | }
615 |
616 | var clean = str.split(";");
617 |
618 | var ret = {};
619 |
620 | // construct object
621 | var taxaLevels = ['Taxa [Kingdom]','Taxa [Phylum]','Taxa [Class]','Taxa [Order]','Taxa [Family]','Taxa [Genus]','Taxa [Species]'];
622 | clean.forEach(function(taxa, i) {
623 | if (taxa != 'unassigned') {
624 | ret[taxaLevels[i]] = taxa;
625 | }
626 | })
627 |
628 | return ret;
629 |
630 | } else {
631 |
632 | return taxa;
633 |
634 | }
635 |
636 | }
637 |
638 | // get the viewBox attribute of the outermost svg in
639 | // format {x0, y0, x1, y1}
640 | function getViewBox() {
641 | var vb = jQuery('svg')[0].getAttribute('viewBox');
642 |
643 | if (vb) {
644 | var arr = vb.split(' ').map(function(d) { return parseInt(d); })
645 | return {'x0':arr[0], 'y0':arr[1], 'x1':arr[2], 'y1':arr[3]};
646 | } else {
647 | return false;
648 | }
649 | }
650 |
651 |
652 |
653 |
654 | /* Fit the SVG viewBox to browser size
655 |
656 | function called by "center view" button in GUI
657 |
658 | Will adjust the X-Y position as well as the zoom
659 | so that the entire tree (width & height) are fit
660 | on the screen. It then aligns the left-most
661 | and top-most elements with the window.
662 |
663 | */
664 | function fitTree() {
665 |
666 | var y1 = window.innerHeight;
667 | var x1 = window.innerWidth;
668 |
669 | d3.select('svg').attr("viewBox", "0 0 " + parseInt(x1) + " " + parseInt(y1)); // fit viewbox
670 |
671 | // reset position
672 | d3.select('#canvasSVG')
673 | .attr('transform','translate(0,0) scale(1)')
674 |
675 | // get bounding box of content to fit
676 | if (treeType == 'rectangular') {
677 | var content = d3.select('#canvasSVG').node().getBoundingClientRect();
678 | } else {
679 | var content = d3.select('#treeSVG').node().getBoundingClientRect();
680 | var root = d3.select('.root').node().getBoundingClientRect();
681 | console.log(content, d3.select('#treeSVG').node().getBoundingClientRect())
682 | }
683 |
684 | var zoomScale = d3.min([
685 | (jQuery('#gui').outerWidth() - margin.left - margin.right) / content.width,
686 | (y1 - jQuery('#gui').outerHeight(true) - margin.bottom - margin.top) / content.height
687 | ]);
688 |
689 | svg.call(zoom.event);
690 |
691 | zoom.scale(zoomScale);
692 | if (treeType == 'rectangular') {
693 | zoom.translate([margin.left,margin.top]);
694 | } else {
695 | zoom.translate([x1 / 2, (root.bottom - content.top) * zoom.scale()]);
696 | }
697 |
698 | svg.transition().duration(750).call(zoom.event);
699 |
700 | }
701 |
702 |
703 |
704 |
705 | // get the transform values of selection
706 | // returns array [X,Y]
707 | function getTransform(sel) {
708 |
709 | var transf = d3.select(sel).attr('transform').replace('translate(','').replace(')','');
710 | var tmp = transf.split(',');
711 |
712 | return [parseInt(tmp[0]), parseInt(tmp[1])];
713 | }
714 |
715 |
716 |
717 |
718 |
719 |
720 |
721 | // get BoundingClientRect of tree
722 | function getTreeBox() {
723 |
724 | if (treeType == 'rectangular') {
725 | var tmp_height = d3.extent(nodes.map(function(d) { return d.x }));
726 | var tmp_width = d3.extent(nodes.map(function(d) { return d.y })); // note width will be off since it doesn't take into account the label text
727 | return {'height':tmp_height[1] - tmp_height[0], 'width':tmp_width[1] - tmp_width[0] };
728 | } else {
729 |
730 | return d3.select('#treeSVG').node().getBoundingClientRect();
731 | }
732 |
733 |
734 | }
735 |
736 |
737 |
738 |
739 |
740 |
741 |
742 |
743 | /* Generated front-end GUI controls
744 |
745 | Build all the HTML elements that serve as GUI controls for
746 | editing the tree format.
747 |
748 | If a mapping file is provided, function will generate
749 | dropdowns for the mapping file columns; one for leaf color
750 | and one for leaf background.
751 |
752 | Parameters:
753 | ==========
754 | - selector : string
755 | div ID (with '#') into which to place GUI controls
756 | - opts : obj
757 | opts obj, same as passed to init()
758 | if present mapping file key present, two
759 | select dropdowns are generated with the columns
760 | of the file. one dropdown is for coloring the
761 | leaf nodes, the other for the leaf backgrounds.
762 | The first index of the output of parseMapping()
763 | should be used here.
764 | */
765 |
766 | function buildGUI(selector, opts) {
767 |
768 | var collapse = d3.select(selector).append('div')
769 | .attr("class","panel panel-default")
770 | .attr("id", "gui")
771 | .style("margin-top","20px");
772 |
773 | collapse.append("div")
774 | .attr("class","panel-heading")
775 | .style("overflow","auto")
776 | .attr("role","tab")
777 | .append("h4")
778 | .attr("class","panel-title")
779 | .append("a")
780 | .attr("role","button")
781 | .attr("data-toggle","collapse")
782 | .attr("data-parent","#accordion")
783 | .attr("href","#collapseGUI")
784 | .attr("aria-expanded","true")
785 | .attr("aria-controls","collapseGUI")
786 | //.html(" Controls")
787 | .html('Controls')
788 | .append("button")
789 | .attr('class', 'btn btn-success pull-right btn')
790 | .style('padding','1px 7px')
791 | .on("click",saveSVG)
792 | .append('i')
793 | .attr('class','fa fa-floppy-o')
794 | .attr('title','Save image')
795 |
796 | var gui = collapse.append("div")
797 | .attr("id","collapseGUI")
798 | .attr("class","panel-collapse collapse in")
799 | .attr("role","tabpanel")
800 | .attr("aria-labelledby","headingOne")
801 | .append("div")
802 | .attr("class","panel-body")
803 |
804 |
805 | var guiRow1 = gui.append("div")
806 | .attr("class","row")
807 |
808 | var col1 = guiRow1.append("div")
809 | .attr("class","col-sm-2")
810 |
811 | var tmp = col1.append("div")
812 | .attr("class","btn-toolbar btn-group")
813 |
814 | tmp.append("button")
815 | .attr("class","btn btn-info")
816 | .attr("id","rectangular")
817 | .attr("title","Generate a rectangular layout")
818 | .attr("onclick","options.treeType = this.id; updateTree();")
819 | .html('')
820 |
821 | tmp.append("button")
822 | .attr("class","btn btn-warning")
823 | .attr("id","radial")
824 | .attr("title","Generate a radial layout")
825 | .attr("onclick","options.treeType = this.id; updateTree();")
826 | .html('')
827 |
828 | tmp.append("button")
829 | .attr("class","btn btn-success")
830 | .attr("id","reset")
831 | .attr("title","Reset view")
832 | .attr("onclick","fitTree();")
833 | .html('')
834 |
835 | var check1 = col1.append("div")
836 | .attr("class","checkbox")
837 | .append("label")
838 |
839 | check1.append("input")
840 | .attr("type","checkbox")
841 | .attr("id","toggle_distance")
842 | .attr("checked","")
843 | .attr("onclick","updateTree()")
844 |
845 | check1.append('text')
846 | .text("Toggle distance labels")
847 |
848 | var check2 = col1.append("div")
849 | .attr("class","checkbox")
850 | .append("label")
851 |
852 | check2.append("input")
853 | .attr("type","checkbox")
854 | .attr("id","toggle_leaf")
855 | .attr("checked","")
856 | .attr("onclick","updateTree()")
857 |
858 | check2.append('text')
859 | .text("Toggle leaf labels")
860 |
861 | var check3 = col1.append("div")
862 | .attr("class","checkbox")
863 | .append("label")
864 |
865 | check3.append("input")
866 | .attr("type","checkbox")
867 | .attr("id","scale_distance")
868 | .attr("checked","")
869 | .attr("onclick","updateTree()")
870 |
871 | check3.append('text')
872 | .text("Scale by distance")
873 |
874 |
875 | // if mapping file was passed
876 | if (mapParse && !mapParse.empty() && typeof mapParse != 'undefined') {
877 |
878 | // select for leaf color
879 | var col2 = guiRow1.append("div")
880 | .attr("class","col-sm-2 form-group")
881 |
882 | col2.append("label")
883 | .text("Leaf node color")
884 |
885 | var select1 = col2.append("select")
886 | .attr('onchange','updateTree();')
887 | .attr('id','leafColor')
888 | .attr("class","form-control")
889 |
890 | select1.selectAll("option")
891 | .data(mapParse.keys()).enter()
892 | .append("option")
893 | .attr('value',function(d) { return d; })
894 | .text(function(d) { return d; })
895 |
896 | select1.append("option")
897 | .attr("selected","")
898 | .attr("value","")
899 | .text('None');
900 | // select for leaf color
901 |
902 |
903 | // select for background color
904 | col2.append("label")
905 | .text("Leaf background color")
906 |
907 | var select2 = col2.append("select")
908 | .attr('onchange','updateTree()')
909 | .attr('id','backgroundColor')
910 | .attr("class","form-control")
911 |
912 | select2.selectAll("option")
913 | .data(mapParse.keys()).enter()
914 | .append("option")
915 | .text(function(d) { return d; })
916 |
917 | select2.append("option")
918 | .attr("selected","")
919 | .attr("value","")
920 | .text('None');
921 | // select for background color
922 |
923 |
924 | // select for leaf text
925 | var col3 = guiRow1.append("div")
926 | .attr("class","col-sm-2 form-group")
927 |
928 | col3.append("label")
929 | .text("Leaf node label")
930 |
931 | var select3 = col3.append("select")
932 | .attr('onchange','updateTree();')
933 | .attr('id','leafText')
934 | .attr("class","form-control")
935 |
936 | select3.selectAll("option")
937 | .data(mapParse.keys()).enter()
938 | .append("option")
939 | .attr('value',function(d) { return d; })
940 | .text(function(d) { return d; })
941 |
942 | select3.append("option")
943 | .attr("selected","")
944 | .attr("value","distance")
945 | .text('Distance');
946 | // select for leaf text
947 | }
948 | var col4 = guiRow1.append("div")
949 | .attr("class","col-sm-2")
950 |
951 | col4.append("label")
952 | .text("Vertical scale")
953 |
954 | col4.append("div")
955 | .attr("id","scaleH")
956 |
957 | scaleHSlider = document.getElementById('scaleH')
958 |
959 | noUiSlider.create(scaleHSlider, {
960 | start: 22,
961 | step: 0.05,
962 | connect: [true, false],
963 | range: {
964 | 'min': 5,
965 | 'max': 100
966 | }
967 | });
968 |
969 | scaleHSlider.noUiSlider.on('slide', function(){
970 | updateTree();
971 | });
972 |
973 | col4.append("label")
974 | .text("Leaf radius")
975 |
976 | col4.append("div")
977 | .attr("id","leafR")
978 |
979 | leafRSlider = document.getElementById('leafR')
980 |
981 | noUiSlider.create(leafRSlider, {
982 | start: 5,
983 | step: 0.05,
984 | connect: [true, false],
985 | range: {
986 | 'min': 1,
987 | 'max': 20
988 | }
989 | });
990 |
991 | leafRSlider.noUiSlider.on('slide', function(){
992 | updateTree();
993 | });
994 |
995 | col4.append("label")
996 | .text("Rotation")
997 |
998 | col4.append("div")
999 | .attr("id","rotation")
1000 |
1001 | rotationSlider = document.getElementById('rotation')
1002 |
1003 | noUiSlider.create(rotationSlider, {
1004 | start: 0,
1005 | connect: [true, false],
1006 | range: {
1007 | 'min': -180,
1008 | 'max': 180
1009 | }
1010 | });
1011 |
1012 | rotationSlider.noUiSlider.on('slide', function(){
1013 | rotateTree();
1014 | });
1015 | rotationSlider.noUiSlider.on('end', function(){
1016 | updateTree();
1017 | orientTreeLabels();
1018 | });
1019 |
1020 | }
1021 |
1022 |
1023 |
1024 |
1025 |
1026 | /* Automatically sort an array
1027 |
1028 | Given an array of strings, were the string
1029 | could be a float (e.g. "1.2") or an int
1030 | (e.g. "5"), this function will convert the
1031 | array if all strings are ints or floats and
1032 | sort it (either alphabetically or numerically
1033 | ascending).
1034 |
1035 | Parameters:
1036 | ===========
1037 | - arr: array of strings
1038 | an array of strings
1039 | - unique: bool
1040 | default: false
1041 | if true, only unique values will be returned
1042 |
1043 | Returns:
1044 | - sorted, converted array, will be either
1045 | all strings or all numbers
1046 |
1047 | */
1048 | function autoSort(arr, unique=false) {
1049 |
1050 | // get unique values of array
1051 | // by converting to d3.set()
1052 | if (unique) { arr = d3.set(arr).values(); }
1053 |
1054 | var vals = arr.map(filterTSVval); // convert to int or float if needed
1055 | var sorted = (typeof vals[0] === 'string' || vals[0] instanceof String) ? vals.sort() : vals.sort(function(a,b) { return a - b; }).reverse();
1056 |
1057 | return sorted;
1058 |
1059 | }
1060 |
1061 |
1062 |
1063 |
1064 |
1065 |
1066 |
1067 |
1068 | // helper function for filtering input TSV values
1069 | // will automatically detect if value is int, float or string and return
1070 | // it as such
1071 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat
1072 | function filterTSVval(value) {
1073 | if (parseFloat(value)) { // if float
1074 | return parseFloat(value);
1075 | } else if (parseInt(value)) { // if int
1076 | return parseInt(value);
1077 | }
1078 |
1079 | // ignore blank values
1080 | if (value != '') {
1081 | return value;
1082 | } else {
1083 | return null;
1084 | }
1085 | }
1086 |
1087 |
1088 |
1089 |
1090 | /* Function for styling tooltip content
1091 |
1092 | Parameters:
1093 | ==========
1094 | - d : node attributes
1095 |
1096 | - mapParse : obj (optional)
1097 | optional parsed mapping file; keys are mapping file
1098 | column headers, values are d3 map obj with key as
1099 | node name and value as file value
1100 |
1101 | Returns:
1102 | ========
1103 | - formatted HTML with all node data
1104 |
1105 | */
1106 | function formatTooltip(d, mapParse) {
1107 | var html = "Leaf " + d.name + "
";
1108 |
1109 | if (mapParse) {
1110 | html += '
';
1111 | mapParse.keys().forEach(function(col) {
1112 | html += '- ' + col + ': ' + mapParse.get(col).get(d.name) + '
';
1113 | })
1114 | }
1115 |
1116 | return html;
1117 | }
1118 |
1119 |
1120 | // when called, will open a new tab with the SVG
1121 | // which can then be right-clicked and 'save as...'
1122 | function saveSVG(){
1123 |
1124 | var viewX = getViewBox().x1;
1125 | var viewY = d3.select("#canvasSVG").node().getBoundingClientRect().height;
1126 |
1127 | d3.select('svg')
1128 | .attr('width', viewX)
1129 | .attr('height', viewY)
1130 | .attr('viewBox',null);
1131 |
1132 | // get styles from all stylesheets
1133 | // http://www.coffeegnome.net/converting-svg-to-png-with-canvg/
1134 | var style = "\n";
1135 | for (var i=0; i 180 && treeType == 'radial' ? "rotate(180)" : "" })
1461 | .attr("text-anchor", function(d) { return addAngles(d.x, deg) > 180 && treeType == 'radial' ? "end" : "start" })
1462 | .attr("dx", function(d) {
1463 | if (d.children) { // if inner node
1464 | return treeType == 'radial' && addAngles(deg, d.x) > 180 ? 20 : -20;
1465 | } else { // if leaf node
1466 | return treeType == 'radial' && addAngles(deg, d.x) > 180 ? -(5 + rad) : (5 + rad);
1467 | }
1468 | })
1469 |
1470 |
1471 | }
1472 |
1473 | // given two angles, will return the sum clamped to [0, 360]
1474 | function addAngles(a,b) {
1475 |
1476 | var sum = parseFloat(a) + parseFloat(b);
1477 |
1478 | if (sum > 360) {
1479 | return sum - 360;
1480 | } else if (sum < 0) {
1481 | return sum + 360;
1482 | } else {
1483 | return sum;
1484 | }
1485 | }
1486 |
1487 |
1488 | /* Set options global
1489 |
1490 | Should be called everytime the tree needs to be updated due to
1491 | changes in the GUI
1492 |
1493 | */
1494 | function getGUIoptions() {
1495 |
1496 | // set tree type if GUI was updated
1497 | // by anything other than tree type
1498 | // buttons
1499 | if (!('treeType' in options)) {
1500 | options.treeType = treeType;
1501 | }
1502 |
1503 | // somewhere in the code, global var 'options' is
1504 | // being emptied ({}) so we are resetting the
1505 | // mapping info here
1506 | if (typeof mappingFile != 'undefined') {
1507 | options.mapping = mapParse;
1508 | options.colorScale = colorScales;
1509 | }
1510 |
1511 | if (options.treeType != treeType) {
1512 | var typeChange = true;
1513 | } else {
1514 | var typeChange = false;
1515 | }
1516 | options.typeChange = typeChange;
1517 | treeType = options.treeType; // update current tree type
1518 |
1519 |
1520 | // get checkbox state
1521 | options.skipDistanceLabel = !jQuery('#toggle_distance').is(':checked');
1522 | options.skipLeafLabel = !jQuery('#toggle_leaf').is(':checked');
1523 | options.skipBranchLengthScaling = !jQuery('#scale_distance').is(':checked');
1524 |
1525 | // get slider vals
1526 | options.sliderScaleV = parseInt(scaleHSlider.noUiSlider.get());
1527 | options.sliderLeafR = parseInt(leafRSlider.noUiSlider.get());
1528 |
1529 | // get dropdown values
1530 | var leafColor, backgroundColor;
1531 | if ('mapping' in options && !options.mapping.empty()) {
1532 | var e = document.getElementById("leafColor");
1533 | options['leafColor'] = e.options[e.selectedIndex].value;
1534 | var e = document.getElementById("leafText");
1535 | options['leafText'] = e.options[e.selectedIndex].value;
1536 | var e = document.getElementById("backgroundColor");
1537 | options['backgroundColor'] = e.options[e.selectedIndex].value;
1538 | }
1539 |
1540 | }
1541 |
1542 |
1543 | /* Generate tree legend if needed
1544 | */
1545 | function updateLegend() {
1546 |
1547 | // remove legend if one exists so we can update
1548 | d3.selectAll("#legendID g").remove()
1549 |
1550 | // update leaf node
1551 | if (options.leafColor != '') {
1552 | var colorScale = options.colorScale.get(options.leafColor); // color scale
1553 | var mapVals = options.mapping.get(options.leafColor); // d3.map() obj with leaf name as key
1554 |
1555 | // fill out legend
1556 | generateLegend(options.leafColor, mapVals, colorScale, 'circle');
1557 |
1558 | // update node styling
1559 | svg.selectAll('g.leaf.node circle')
1560 | .transition()
1561 | .style('fill', function(d) {
1562 | //console.log(d.name, mapVals.get(d.name), colorScale(mapVals.get(d.name)))
1563 | return mapVals.get(d.name) ? dimColor(colorScale(mapVals.get(d.name))) : 'white'
1564 | })
1565 | .style('stroke', function(d) {
1566 | return mapVals.get(d.name) ? colorScale(mapVals.get(d.name)) : 'gray'
1567 | })
1568 |
1569 | } else if (options.leafColor == '') {
1570 | svg.selectAll('g.leaf.node circle')
1571 | .transition()
1572 | .attr("style","");
1573 | }
1574 |
1575 | // update leaf background
1576 | if (options.backgroundColor != '') {
1577 | var colorScale = colorScales.get(options.backgroundColor) // color scale
1578 | var mapVals = mapParse.get(options.backgroundColor) // d3.map() obj with leaf name as key
1579 |
1580 |
1581 | // fill out legend
1582 | var offset = 25;
1583 | generateLegend(options.backgroundColor, mapVals, colorScale, 'rect');
1584 |
1585 | // update node background style
1586 | svg.selectAll('g.leaf.node rect')
1587 | .transition()
1588 | .duration(500)
1589 | .attr("width", function(d) {
1590 | var name = d.name.replace(new RegExp('\\.', 'g'), '_');
1591 | var textWidth = d3.select('#leaf_' + name + ' text').node().getComputedTextLength();
1592 | var radius = d3.select('#leaf_' + name + ' circle').node().getBBox().height / 2.0;
1593 | return textWidth + radius + 10; // add extra so background is wider than label
1594 | })
1595 | .style('fill', function(d) {
1596 | return mapVals.get(d.name) ? colorScale(mapVals.get(d.name)) : 'none'
1597 | })
1598 | .style('opacity',1)
1599 | } else if (options.backgroundColor == '') {
1600 | svg.selectAll('g.leaf.node rect')
1601 | .transition()
1602 | .duration(500)
1603 | .attr('width','0')
1604 | .style('opacity','1e-6')
1605 | }
1606 |
1607 | if (options.backgroundColor != '' || options.leafColor != '') {
1608 | positionLegend();
1609 | }
1610 |
1611 |
1612 | d3.select('svg').attr("viewBox", "0 0 " + parseInt(window.innerWidth) + " " + parseInt(window.innerHeight)); // set viewbox
1613 |
1614 | }
1615 |
--------------------------------------------------------------------------------