1 (function (global, factory) {
2 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3 typeof define === 'function' && define.amd ? define(factory) :
4 (global.PedigreeViewer = factory());
5 }(this, (function () { 'use strict';
7 function PedigreeViewer(server,auth,urlFunc){
9 var brapijs = BrAPI(server,auth);
11 var access_token = null;
12 var loaded_nodes = {};
14 var locationSelector = null;
16 urlFunc = urlFunc!=undefined?urlFunc:function(){return null};
18 pdgv.newTree = function(stock_id,callback){
22 load_node_and_all_ancestors([stock_id]);
23 function load_node_and_all_ancestors(ids){
24 load_nodes(ids,function(nodes){
25 [].push.apply(all_nodes,nodes);
26 var mothers = nodes.map(function(d){return d.mother_id});
27 var fathers = nodes.map(function(d){return d.father_id});
28 var parents = mothers.concat(fathers).filter(function(d, index, self){
29 return d!==undefined &&
31 loaded_nodes[d]===undefined &&
32 self.indexOf(d) === index;
34 if (parents.length>0){
35 load_node_and_all_ancestors(parents);
38 createNewTree(all_nodes);
45 pdgv.drawViewer = function(loc,draw_width,draw_height){
46 locationSelector = loc;
47 drawTree(undefined,draw_width,draw_height);
50 function createNewTree(start_nodes) {
51 myTree = d3.pedigreeTree()
59 .parents(function(node){
60 return [loaded_nodes[node.mother_id],loaded_nodes[node.father_id]].filter(Boolean);
68 .excludeFromGrouping([root]);
71 function load_nodes(stock_ids,callback){
72 var germplasm = brapijs.data(stock_ids);
73 var pedigrees = germplasm.germplasm_pedigree(function(d){return {'germplasmDbId':d}});
74 var progenies = germplasm.germplasm_progeny(function(d){return {'germplasmDbId':d}},"map");
75 pedigrees.join(progenies,germplasm).filter(function(ped_pro_germId){
76 if (ped_pro_germId[0]===null || ped_pro_germId[1]===null) {
77 console.log("Failed to load progeny or pedigree for "+ped_pro_germId[2]);
81 }).map(function(ped_pro_germId){
84 if(ped_pro_germId[0].parent1Type=="FEMALE"){
85 mother = ped_pro_germId[0].parent1DbId;
87 if(ped_pro_germId[0].parent1Type=="MALE"){
88 father = ped_pro_germId[0].parent1DbId;
90 if(ped_pro_germId[0].parent2Type=="FEMALE"){
91 mother = ped_pro_germId[0].parent2DbId;
93 if(ped_pro_germId[0].parent2Type=="MALE"){
94 father = ped_pro_germId[0].parent2DbId;
97 'id':ped_pro_germId[2],
100 'name':ped_pro_germId[1].defaultDisplayName,
101 'children':ped_pro_germId[1].progeny.filter(Boolean).map(function(d){
102 return d.germplasmDbId;
105 }).each(function(node){
106 loaded_nodes[node.id] = node;
110 function drawTree(trans,draw_width,draw_height){
112 var layout = myTree();
114 //set default change-transtion to no duration
115 trans = trans || d3.transition().duration(0);
118 var wrap = d3.select(locationSelector);
119 var canv = wrap.select("svg.pedigreeViewer");
121 canv = wrap.append("svg").classed("pedigreeViewer",true)
122 .attr("width",draw_width)
123 .attr("height",draw_height)
124 .attr("viewbox","0 0 "+draw_width+" "+draw_height);
126 var cbbox = canv.node().getBoundingClientRect();
127 var canvw = cbbox.width,
128 canvh = cbbox.height;
129 var pdg = canv.select('.pedigreeTree');
131 pdg = canv.append('g').classed('pedigreeTree',true);
135 var bg = pdg.select('.pdg-bg');
137 bg = pdg.append('rect')
138 .classed('pdg-bg',true)
139 .attr("x",-canvw*500)
140 .attr("y",-canvh*500)
141 .attr('width',canvw*1000)
142 .attr('height',canvh*1000)
143 .attr('fill',"white")
144 .attr('opacity',"0.00001")
145 .attr('stroke','none');
148 //make scaled content/zoom groups
150 var pdgtree_width = d3.max([500,layout.x[1]-layout.x[0]]);
151 var pdgtree_height = d3.max([500,layout.y[1]-layout.y[0]]);
152 var centeringx = d3.max([0,(500 - (layout.x[1]-layout.x[0]))/2]);
153 var centeringy = d3.max([0,(500 - (layout.y[1]-layout.y[0]))/2]);
154 var scale = get_fit_scale(canvw,canvh,pdgtree_width,pdgtree_height,padding);
155 var offsetx = (canvw-(pdgtree_width)*scale)/2 + centeringx*scale;
156 var offsety = (canvh-(pdgtree_height)*scale)/2 + centeringy*scale;
158 var content = pdg.select('.pdg-content');
159 if (content.empty()){
160 var zoom = d3.zoom();
161 var zoom_group = pdg.append('g').classed('pdg-zoom',true).data([zoom]);
163 content = zoom_group.append('g').classed('pdg-content',true);
164 content.datum({'zoom':zoom});
165 zoom.on("zoom",function(){
166 zoom_group.attr('transform',d3.event.transform);
168 bg.style("cursor", "all-scroll").call(zoom).call(zoom.transform, d3.zoomIdentity);
169 bg.on("dblclick.zoom",function(){
170 zoom.transform(bg.transition(),d3.zoomIdentity);
174 content.attr('transform',
176 .translate(offsetx,offsety)
180 content.datum().zoom.scaleExtent([0.5,d3.max([pdgtree_height,pdgtree_width])/200]);
181 content.transition(trans)
184 .translate(offsetx,offsety)
190 var linkLayer = content.select('.link-layer');
191 if(linkLayer.empty()){
192 linkLayer = content.append('g').classed('link-layer',true);
194 var nodeLayer = content.select('.node-layer');
195 if(nodeLayer.empty()){
196 nodeLayer = content.append('g').classed('node-layer',true);
199 //link curve generators
200 var stepline = d3.line().curve(d3.curveStepAfter);
201 var curveline = d3.line().curve(d3.curveBasis);
202 var build_curve = function(d){
203 if (d.type=="parent->mid") return curveline(d.path);
204 if (d.type=="mid->child") return stepline(d.path);
208 var nodes = nodeLayer.selectAll('.node')
209 .data(layout.nodes,function(d){return d.id;});
210 var newNodes = nodes.enter().append('g')
211 .classed('node',true)
212 .attr('transform',function(d){
214 if(d3.event && d3.event.type=="click"){
215 begin = d3.select(d3.event.target).datum();
217 return 'translate('+begin.x+','+begin.y+')'
219 var nodeNodes = newNodes.filter(function(d){
220 return d.type=="node";
222 var groupNodes = newNodes.filter(function(d){
223 return d.type=="node-group";
225 //draw node group expanders
226 groupNodes.append("circle")
227 .style("cursor","pointer")
228 .attr("fill","purple")
229 .attr("stroke","purple")
232 groupNodes.append('text')
233 .style("cursor","pointer")
235 .attr("font-size","14px")
236 .attr("font-weight","bold")
237 .attr('text-anchor',"middle")
238 .attr('class', 'glyphicon')
240 .attr('fill',"white");
241 //create expander handles on nodes
242 var expanders = nodeNodes.append('g').classed("expanders",true);
243 var child_expander = expanders.append("g").classed("child-expander",true);
244 child_expander.append("path")
246 .attr("stroke","purple")
247 .attr("stroke-width",4)
248 .attr("d",curveline([[0,20],[0,40]]));
249 child_expander.append("circle")
250 .style("cursor","pointer")
251 .attr("fill","purple")
252 .attr("stroke","purple")
255 child_expander.append('text')
256 .style("cursor","pointer")
259 .attr("font-size","14px")
260 .attr("font-weight","bold")
261 .attr('text-anchor',"middle")
262 .attr('class', 'glyphicon')
264 .attr('fill',"white");
265 child_expander.on("click",function(d){
266 d3.select(this).on('click',null);
267 var end_blink = load_blink(d3.select(this).select("circle").node());
268 var to_load = d.value.children.filter(Boolean).map(String);
269 load_nodes(to_load,function(nodes){
271 layout.pdgtree.add(nodes);
272 drawTree(d3.transition().duration(700));
275 var parent_expander = expanders.append("g").classed("parent-expander",true);
276 parent_expander.append("path")
278 .attr("stroke","purple")
279 .attr("stroke-width",4)
280 .attr("d",curveline([[0,0],[0,-40]]));
281 parent_expander.append("circle")
282 .style("cursor","pointer")
283 .attr("fill","purple")
284 .attr("stroke","purple")
287 parent_expander.append('text')
288 .style("cursor","pointer")
291 .attr("font-size","14px")
292 .attr("font-weight","bold")
293 .attr('text-anchor',"middle")
294 .attr('class', 'glyphicon')
296 .attr('fill',"white");
297 parent_expander.on("click",function(d){
298 d3.select(this).on('click',null);
299 var end_blink = load_blink(d3.select(this).select("circle").node());
300 var to_load = [d.value.mother_id,d.value.father_id].filter(Boolean).map(String);
301 load_nodes(to_load,function(nodes){
303 layout.pdgtree.add(nodes);
304 drawTree(d3.transition().duration(700));
307 nodeNodes.append('rect').classed("node-name-highlight",true)
308 .attr('fill',function(d){
309 return d.id==root?"pink":"none";
311 .attr('stroke-width',0)
318 nodeNodes.append('rect').classed("node-name-wrapper",true)
319 .attr('fill',"white")
320 .attr('stroke',"grey")
321 .attr('stroke-width',2)
328 var nodeUrlLinks = nodeNodes.filter(function(d){
329 var url = urlFunc(d.id);
337 .attr('href',function(d){
338 return urlFunc(d.id);
340 .attr('target','_blank')
341 .append('text').classed('node-name-text',true)
343 .attr('text-anchor',"middle")
347 .attr('fill',"black");
348 nodeNodes.filter(function(d){return d.url===undefined;})
349 .append('text').classed('node-name-text',true)
351 .attr('text-anchor',"middle")
355 .attr('fill',"black");
356 //set node width to text width
357 nodeNodes.each(function(d){
358 var nn = d3.select(this);
359 var ctl = nn.select('.node-name-text').node().getComputedTextLength();
361 nn.select('.node-name-wrapper')
364 nn.select('.node-name-highlight')
366 .attr("x",-(w+20)/2);
368 var allNodes = newNodes.merge(nodes);
369 //remove expander handles for nodes without unloaded relatives.
370 allNodes.each(function(d){
372 var parents_unloaded = [d.value.mother_id,d.value.father_id]
373 .filter(function(node_id){
374 return !!node_id && !loaded_nodes.hasOwnProperty(node_id);
376 var children_unloaded = d.value.children
377 .filter(function(node_id){
378 return !!node_id && !loaded_nodes.hasOwnProperty(node_id);
380 if (parents_unloaded.length<1){
381 d3.select(this).selectAll(".parent-expander").remove();
383 if (children_unloaded.length<1){
384 d3.select(this).selectAll(".child-expander").remove();
388 allNodes.transition(trans).attr('transform',function(d){
389 return 'translate('+d.x+','+d.y+')'
391 allNodes.filter(function(d){return d.type=="node-group"})
392 .style("cursor", "pointer")
393 .on("click",function(d){
394 layout.pdgtree.excludeFromGrouping(d.value.slice(0,10).map(function(d){return d.id;}));
395 drawTree(d3.transition().duration(700).ease(d3.easeLinear));
397 var oldNodes = nodes.exit().remove();
401 var link_color = function(d){
402 if (d.type=="mid->child") return 'purple';
403 if (d.type=="parent->mid"){
404 //if its the first parent, red. Otherwise, blue.
405 var representative = d.sinks[0].type=="node-group"?
406 d.sinks[0].value[0].value
408 if (representative.mother_id == d.source.id){
419 var links = linkLayer.selectAll('.link')
420 .data(layout.links,function(d){return d.id;});
421 var newLinks = links.enter().append('g')
422 .classed('link',true);
423 newLinks.append('path')
424 .attr('d',function(d){
425 var begin = (d.sink || d.source);
426 if(d3.event && d3.event.type=="click"){
427 begin = d3.select(d3.event.target).datum();
429 return curveline([[begin.x,begin.y],[begin.x,begin.y],[begin.x,begin.y],[begin.x,begin.y]]);
432 .attr('stroke',link_color)
433 .attr('opacity',function(d){
434 if (d.type=="parent->mid") return 0.7;
437 .attr('stroke-width',4);
438 var allLinks = newLinks.merge(links);
439 allLinks.transition(trans).select('path').attr('d',build_curve);
440 var oldNodes = links.exit().remove();
446 function load_blink(node){
448 var original_fill = d3.select(node).style("fill");
449 var original_sw = d3.select(node).style("stroke-width");
451 if (!stop) d3.select(node)
454 .style("fill", "white")
455 .style("stroke-width", "5")
458 .style("fill", original_fill)
459 .style("stroke-width", original_sw)
468 function get_fit_scale(w1,h1,w2,h2,pad){
478 return PedigreeViewer;