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
;