fetch stock publications without dbxrefs which can be multiples for one pub_id
[sgn.git] / js / brapi / PedigreeViewer.js
blobb0963916ed74d16b8dc09e98b97842bf0ed3e143
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){
8         var pdgv = {};
9         var brapijs = BrAPI(server,auth);
10         var root = null;
11         var access_token = null;
12         var loaded_nodes = {};
13         var myTree = null;
14         var locationSelector = null;
15         
16         urlFunc = urlFunc!=undefined?urlFunc:function(){return null};
17                 
18         pdgv.newTree = function(stock_id,callback){
19             root = stock_id;
20             loaded_nodes = {};
21             var all_nodes = [];
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 &&
30                                d!==null &&
31                                loaded_nodes[d]===undefined &&
32                                self.indexOf(d) === index;
33                     });
34                     if (parents.length>0){
35                         load_node_and_all_ancestors(parents);
36                     }
37                     else {
38                         createNewTree(all_nodes);
39                         callback.call(pdgv);
40                     }
41                 });
42             }
43         };
44         
45         pdgv.drawViewer = function(loc,draw_width,draw_height){
46             locationSelector = loc;
47             drawTree(undefined,draw_width,draw_height);
48         };
49         
50         function createNewTree(start_nodes) {  
51             myTree = d3.pedigreeTree()
52               .levelWidth(200)
53               .levelMidpoint(50)
54               .nodePadding(220)
55               .nodeWidth(10)
56               .linkPadding(25)
57               .vertical(true)
58               .parentsOrdered(true)
59               .parents(function(node){
60                 return [loaded_nodes[node.mother_id],loaded_nodes[node.father_id]].filter(Boolean);
61               })
62               .id(function(node){
63                 return node.id;
64               })
65               .groupChildless(true)
66               .iterations(50)
67               .data(start_nodes)
68               .excludeFromGrouping([root]);
69         }
70         
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]);
78                     return false;
79                 }
80                 return true;
81             }).map(function(ped_pro_germId){
82                 var mother = null, 
83                     father = null;
84                 if(ped_pro_germId[0].parent1Type=="FEMALE"){
85                     mother = ped_pro_germId[0].parent1DbId;
86                 }
87                 if(ped_pro_germId[0].parent1Type=="MALE"){
88                     father = ped_pro_germId[0].parent1DbId;
89                 }
90                 if(ped_pro_germId[0].parent2Type=="FEMALE"){
91                     mother = ped_pro_germId[0].parent2DbId;
92                 }
93                 if(ped_pro_germId[0].parent2Type=="MALE"){
94                     father = ped_pro_germId[0].parent2DbId;
95                 }
96                 return {
97                     'id':ped_pro_germId[2],
98                     'mother_id':mother,
99                     'father_id':father,
100                     'name':ped_pro_germId[1].defaultDisplayName,
101                     'children':ped_pro_germId[1].progeny.filter(Boolean).map(function(d){
102                         return d.germplasmDbId;
103                     })
104                 };
105             }).each(function(node){
106                 loaded_nodes[node.id] = node;
107             }).all(callback);
108         }
109         
110         function drawTree(trans,draw_width,draw_height){
111             
112             var layout = myTree();
113             
114             //set default change-transtion to no duration
115             trans = trans || d3.transition().duration(0);
116             
117             //make wrapper(pdg)
118             var wrap = d3.select(locationSelector);
119             var canv = wrap.select("svg.pedigreeViewer");
120             if (canv.empty()){
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);
125             }
126             var cbbox = canv.node().getBoundingClientRect();
127             var canvw = cbbox.width, 
128                 canvh = cbbox.height;
129             var pdg = canv.select('.pedigreeTree');
130             if (pdg.empty()){
131               pdg = canv.append('g').classed('pedigreeTree',true);
132             }
133           
134             //make background
135             var bg = pdg.select('.pdg-bg');
136             if (bg.empty()){
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');
146             }
147             
148             //make scaled content/zoom groups
149             var padding = 50;
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;
157             
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]);
162               
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);
167               });
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);
171                 return false;
172               });
173               
174               content.attr('transform',
175                   d3.zoomIdentity
176                     .translate(offsetx,offsety)
177                     .scale(scale)
178                 );
179             }
180             content.datum().zoom.scaleExtent([0.5,d3.max([pdgtree_height,pdgtree_width])/200]);
181             content.transition(trans)
182               .attr('transform',
183                 d3.zoomIdentity
184                   .translate(offsetx,offsety)
185                   .scale(scale)
186               );
187             
188             
189             //set up draw layers
190             var linkLayer = content.select('.link-layer');
191             if(linkLayer.empty()){
192                 linkLayer = content.append('g').classed('link-layer',true);
193             }
194             var nodeLayer = content.select('.node-layer');
195             if(nodeLayer.empty()){
196                 nodeLayer = content.append('g').classed('node-layer',true);
197             }
198             
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);
205             };
206             
207             //draw nodes
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){
213                 var begin = d;
214                 if(d3.event && d3.event.type=="click"){
215                   begin = d3.select(d3.event.target).datum();
216                 }
217                 return 'translate('+begin.x+','+begin.y+')'
218               });
219             var nodeNodes = newNodes.filter(function(d){
220                 return d.type=="node";
221             });
222             var groupNodes = newNodes.filter(function(d){
223                 return d.type=="node-group";
224             });
225             //draw node group expanders
226             groupNodes.append("circle")
227               .style("cursor","pointer")
228               .attr("fill","purple")
229               .attr("stroke","purple")
230               .attr("cy",0)
231               .attr("r",10);
232             groupNodes.append('text')
233               .style("cursor","pointer")
234               .attr('y',6.5)
235               .attr("font-size","14px")
236               .attr("font-weight","bold")
237               .attr('text-anchor',"middle")
238               .attr('class', 'glyphicon')
239               .html("")
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")
245               .attr("fill","none")
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")
253               .attr("cy",45)
254               .attr("r",10);
255             child_expander.append('text')
256               .style("cursor","pointer")
257               .attr('y',52)
258               .attr('x',-0.5)
259               .attr("font-size","14px")
260               .attr("font-weight","bold")
261               .attr('text-anchor',"middle")
262               .attr('class', 'glyphicon')
263               .html("")
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){
270                   end_blink();
271                   layout.pdgtree.add(nodes);
272                   drawTree(d3.transition().duration(700));
273               });
274             });
275             var parent_expander = expanders.append("g").classed("parent-expander",true);
276             parent_expander.append("path")
277               .attr("fill","none")
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")
285               .attr("cy",-45)
286               .attr("r",10);
287             parent_expander.append('text')
288               .style("cursor","pointer")
289               .attr('y',-39)
290               .attr('x',-0.5)
291               .attr("font-size","14px")
292               .attr("font-weight","bold")
293               .attr('text-anchor',"middle")
294               .attr('class', 'glyphicon')
295               .html("")
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){
302                   end_blink();
303                   layout.pdgtree.add(nodes);
304                   drawTree(d3.transition().duration(700));
305               });
306             });
307             nodeNodes.append('rect').classed("node-name-highlight",true)
308               .attr('fill',function(d){
309                   return d.id==root?"pink":"none";
310               })
311               .attr('stroke-width',0)
312               .attr("width",220)
313               .attr("height",40)
314               .attr("y",-10)
315               .attr("rx",20)
316               .attr("ry",20)
317               .attr("x",-110);
318             nodeNodes.append('rect').classed("node-name-wrapper",true)
319               .attr('fill',"white")
320               .attr('stroke',"grey")
321               .attr('stroke-width',2)
322               .attr("width",200)
323               .attr("height",20)
324               .attr("y",0)
325               .attr("rx",10)
326               .attr("ry",10)
327               .attr("x",-100);
328               var nodeUrlLinks = nodeNodes.filter(function(d){
329                   var url = urlFunc(d.id);
330                   if (url!==null){
331                     d.url = url;
332                     return true;
333                   }
334                   return false;
335                 })
336                 .append('a')
337                 .attr('href',function(d){
338                   return urlFunc(d.id);
339                 })
340                 .attr('target','_blank')
341                 .append('text').classed('node-name-text',true)
342                 .attr('y',15)
343                 .attr('text-anchor',"middle")
344                 .text(function(d){
345                   return d.value.name;
346                 })
347                 .attr('fill',"black");
348               nodeNodes.filter(function(d){return d.url===undefined;})
349                 .append('text').classed('node-name-text',true)
350                 .attr('y',15)
351                 .attr('text-anchor',"middle")
352                 .text(function(d){
353                   return d.value.name;
354                 })
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();
360                 var w = ctl+20;
361                 nn.select('.node-name-wrapper')
362                     .attr("width",w)
363                     .attr("x",-w/2);
364                 nn.select('.node-name-highlight')
365                     .attr("width",w+20)
366                     .attr("x",-(w+20)/2);
367             });
368             var allNodes = newNodes.merge(nodes);
369             //remove expander handles for nodes without unloaded relatives.
370             allNodes.each(function(d){
371                 if (d.type=="node"){
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);
375                         });
376                     var children_unloaded = d.value.children
377                         .filter(function(node_id){
378                             return !!node_id && !loaded_nodes.hasOwnProperty(node_id);
379                         });
380                     if (parents_unloaded.length<1){
381                         d3.select(this).selectAll(".parent-expander").remove();
382                     }
383                     if (children_unloaded.length<1){
384                         d3.select(this).selectAll(".child-expander").remove();
385                     }
386                 }
387             });
388             allNodes.transition(trans).attr('transform',function(d){
389               return 'translate('+d.x+','+d.y+')'
390             });
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));
396             });
397             var oldNodes = nodes.exit().remove();
399             
400             //link colors
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 
407                         : d.sinks[0].value;
408                 if (representative.mother_id == d.source.id){
409                     return "red";
410                 } 
411                 else {
412                     return "blue";
413                 }
414               }
415               return 'gray';
416             };
417             
418             //make links
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();
428                 }
429                 return curveline([[begin.x,begin.y],[begin.x,begin.y],[begin.x,begin.y],[begin.x,begin.y]]);
430               })
431               .attr('fill','none')
432               .attr('stroke',link_color)
433               .attr('opacity',function(d){
434                 if (d.type=="parent->mid") return 0.7;
435                 return 0.999;
436               })
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();
441         }
442         
443         return pdgv;
444     }
445     
446     function load_blink(node){
447         var stop = false;
448         var original_fill = d3.select(node).style("fill");
449         var original_sw = d3.select(node).style("stroke-width");
450         function blink(){
451             if (!stop) d3.select(node)
452                 .transition()
453                 .duration(300)
454                 .style("fill", "white")
455                 .style("stroke-width", "5")
456                 .transition()
457                 .duration(300)
458                 .style("fill", original_fill)
459                 .style("stroke-width", original_sw)
460                 .on("end", blink);
461         }
462         blink();
463         return function(){
464             stop = true;
465         }
466     }
467       
468     function get_fit_scale(w1,h1,w2,h2,pad){
469         w1 -= pad*2;
470         h1 -= pad*2;  
471         if (w1/w2<h1/h2){
472             return w1/w2;
473         } else {
474             return h1/h2;
475         }
476     }
478 return PedigreeViewer;
480 })));