This code snippet helps you to create a family tree. It define a JavaScript object named xist with a property called tree. The tree property is assigned an anonymous function which creates a new tree object with various options and data. The _count variable is used to generate a unique ID for each new tree object. The tree object also sets default values for various properties such as the root node, direction of the tree, size and gap between nodes, and the parent container for the tree view. The view property of the options object is also given default values for its attributes and CSS styles. The tree object then creates a new view element, assigns it the provided or generated ID, and creates an SVG element within it to display the tree nodes and connections.
Finally, the update() method is called to render the tree.
How to Create JavaScript Family Tree
First of all, load the following assets into the head tag of your HTML document.
<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css'> <link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.12.4/css/bootstrap-select.min.css'>
Now, style the family tree using the following CSS styles:
body {
background: white;
}
.node {
display: table;
position: absolute;
left: 0;
top: 0;
width: 200px;
height: 50px;
background: white;
border: 1px solid black;
color: black;
font-size: 12px;
font-weight: bold;
}
.node:hover {
border-color: blue;
}
.node.selected {
border-color: green;
}
.node.dropTarget {
border-color: red;
background-color: antiquewhite;
}
.node * {
padding: 0;
margin: 0;
}
.node .thumb {
display:table-cell;
padding: 5px;
width: 50px;
height: 50px;
}
.node .thumb img {
height: 100%;
width: auto;
}
.node .details {
display: table-cell;
vertical-align: middle;
}
.node .name {
color: black;
}
.node .born, .node .died {
font-size: 10px;
color: grey;
}
.node .lbl {
font-weight: normal;
}
Load the following scripts before closing the body tag:
<script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/1.19.1/TweenMax.min.js'></script> <script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/1.19.1/plugins/TextPlugin.min.js'></script> <script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/1.19.1/plugins/CSSRulePlugin.min.js'></script> <script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.2/utils/Draggable.min.js'></script> <script src='https://unpkg.com/mkrjs@latest'></script> <script src='https://unpkg.com/mkrjs@latest/dist/constructs/svg/ln.min.js'></script> <script src='https://unpkg.com/js-uuid@0.0.6'></script> <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script> <script src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js'></script> <script src='https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.12.4/js/bootstrap-select.min.js'></script> <script src='https://unpkg.com/rbush@2.0.1/rbush.js'></script>
Finally, add the following JavaScript function for its functionality:
/*
Family Tree
Consists of nodes
Tree is the overarching data structure that manages nodes
Nodes are the primary object in the tree, have direct child-parent relationships with other nodes. Nodes may only have one parent node
*/
var xist = {};
xist.tree = (function() {
var _count = 0;
var tree = function(options, data) {
this._id = 'tree-'+_count++;
this._root = mkr.default(options.root, null);
this._dir = mkr.default(options.dir, xist.tree.TOP_DOWN); //topDown, bottomUp, leftRight, rightLeft
this._grid = mkr.default(options.grid, null);
options.view = mkr.default(options.view, {});
this._nodeW = mkr.default(options.nodeW, 250);
this._nodeH = mkr.default(options.nodeH, 65);
this._gap = mkr.default(options.gap, 40);
this._parent = mkr.default(options.parent, document.body);
this._data = mkr.default(data, null);
this._width = this._height = 0;
this._resized = new signals.Signal();
TweenMax.set(mkr.getRule('.node'), {cssRule:{width:this.nodeW, height:this.nodeH}});
TweenMax.set(mkr.getRule('.node .thumb'), {cssRule:{width:this.nodeH, height:this.nodeH}});
mkr.setDefault(options.view, 'attr', {});
mkr.setDefault(options.view.attr, 'class', 'tree');
mkr.setDefault(options.view.attr, 'id', this.id);
mkr.setDefault(options.view, 'css', {});
mkr.setDefault(options.view.css, 'width', '100%');
mkr.setDefault(options.view.css, 'height', '100%');
mkr.setDefault(options.view.css, 'background', '#dddddd');
this._view = mkr.query('#'+options.view.attr.id) || mkr.create('div', options.view, this._parent);
this._svg = mkr.query('#'+options.view.attr.id, this.view) ||mkr.create('svg', {attr:{class:'lines'}, css:{width:'100%', height:'100%', pointerEvents:'none', overflow:'visible'}}, this.view);
Draggable.create(this.view, {type:"x,y", zIndexBoost:false});
this.update();
};
tree.prototype = {
get id() {
return this._id;
},
get dir() {
return this._dir;
},
set dir(value) {
this._dir = value;
this.refresh();
},
get grid() {
return this._grid;
},
get data() {
return this._data;
},
set data(value) {
this._data = value;
this.update();
},
get view() {
return this._view;
},
get svg() {
return this._svg;
},
get nodeW() {
return this._nodeW;
},
get nodeH() {
return this._nodeH;
},
get gap() {
return this._gap;
},
get count() {
return this._count;
},
get root() {
return this._root;
},
get pairs() {
return this._pairs;
},
get gridW() {
return this._gridW;
},
get gridH() {
return this._gridH;
},
get width() {
return this._width;
},
get height() {
return this._height;
},
get depth() {
return xist.node.instances[this.root].depth;
},
get breadth() {
return xist.node.instances[this.root].breadth;
},
get resized() {
return this._resized;
},
get x() {
return xist.node.instances[this.root].x;
},
set x(value) {
var root = xist.node.instances[this.root];
root.x = root._originX = value;
root.refresh();
},
get y() {
return xist.node.instances[this.root].y;
},
set y(value) {
var root = xist.node.instances[this.root];
root.y = root._originY = value;
root.refresh();
},
setXY: function(x, y) {
var root = xist.node.instances[this.root];
root.x = root._originX = x;
root.y = root._originY = y;
root.setOrigin(root.x, root.y);
root.refresh();
},
create: function(options, data) {
options = options || {};
options.tree = this;
var node = new xist.node(options, data);
return node;
},
add: function(node) {
if(node.tree) {
if(node.tree === this) return;
node.tree.remove(node.id);
}
node.tree = this;
if(!this._root) this._root = node.id;
},
remove: function(id) {
if(this.root == id) this._root = null;
},
delete: function(id) {
var node = xist.node.instances[id];
node.destroy();
},
refresh: function() {
if(!this.root)
return;
var root = xist.node.instances[this.root];
//run DFS to find/assign breadths of all subtrees
root.findBreadth(this);
//run DFS to find/assign depths of all subtrees
root.findDepth();
//run BFS to set offsets
root.refresh();
var w, h;
switch(this.dir) {
default:
case xist.tree.TOP_DOWN :
case xist.tree.BOTTOM_UP :
w = root.breadth*this.nodeW + (root.breadth-1)*this.gap;
h = (root.depth+1)*this.nodeH + root.depth*this.gap;
break;
case xist.tree.LEFT_RIGHT :
case xist.tree.RIGHT_LEFT :
w = (root.depth+1)*this.nodeW + root.depth*this.gap;
h = root.breadth*this.nodeH + (root.breadth-1)*this.gap;
break;
}
var resized = false;
if(this._width != w) {
this._width = w;
resized = true;
}
if(this._height != h) {
this._height = h;
resized = true;
}
if(resized) this.resized.dispatch();
},
clear: function() {
if(this.root) {
this.root.destroy();
this.root = null;
}
},
update: function() {
if(this.root)
xist.node.instances[this.root].data = this.data;
else(this.data)
this.create({}, this.data);
}
};
tree.TOP_DOWN = 0;
tree.BOTTOM_UP = 1;
tree.LEFT_RIGHT = 2;
tree.RIGHT_LEFT = 3;
return tree;
})();
//node class
xist.node = (function() {
var node = function(options, data) {
this._id = uuid.v1();
xist.node.instances[this._id] = this;
var tree = options.tree;
this._parent = mkr.default(options.parent, null);
this._children = mkr.default(options.children, []) || [];
this._lines;
this._depth = 0;
this._breadth = 1;
this._target = null;
this._originX = this._originY = 0;
tree.add(this);
//data setup
this.data = mkr.default(data, {});
mkr.setDefault(options, 'x', 0);
mkr.setDefault(options, 'y', 0);
//this.createView(options.x, options.y);
//drag/drop functionality
var self = this;
if(!this.isRoot) {
this._dragger = Draggable.create(this.view, {
type:"x,y", edgeResistance:0.65, bounds:this.tree.view.container,
onPress: function(e) {
e.stopPropagation();
if(mkr.hasClass(this.target, 'selected') < 0) {
TweenLite.set('.node.selected', {className:"-=selected"});
TweenLite.set(this.target, {className:"+=selected"});
}
else {
TweenLite.set(this.target, {className:"-=selected"});
}
},
onDrag: function(e) {
self.refresh();
xist.node.setBox(self.x, self.right, self.y, self.bottom);
var nodes = self.tree.grid.search(xist.node.box);
var i = nodes.length;
while(--i > -1) { //dropTarget all nodes expect self
if(nodes[i].id == self.id) continue;
TweenLite.set(nodes[i], {className:"+=dropTarget"});
}
//remove dropTarget class from old targets
var targets = mkr.queryAll('.node.dropTarget');
i = targets.length;
while(--i > -1) {
var target = targets[i];
if(nodes.indexOf(target) < 0) TweenMax.set(target, {className:'-=dropTarget'});
}
},
onRelease: function(e) {
e.stopPropagation(); //allow nodes to be dragged without affecting the container
xist.node.setBox(self.x, self.right, self.y, self.bottom);
var nodes = self.tree.grid.search(xist.node.box);
var i = nodes.length;
while(--i > -1) {
if(nodes[i].id == self.id) continue;
self._target = nodes[i];
break;
}
TweenLite.set('.node.dropTarget', {className:"-=dropTarget"});
if(self._target) {
/* console.log(self._target.id);
console.log(self._target);
console.log('\n'); */
xist.node.getInstance(self._target.id).addChild(self);
self._target=null;
}
else {
self.snap();
}
}
})[0];
}
//unused selectpicker code, maybe useful for extended tree
/* var relate = function(e, index) {
// create selectpicker for dropdown
// mkr.create('div', {attr:{id:`select-pop-${_count}`, class:'popover node-popover'}, css:{autoAlpha:0},
// text:`<div class="popover-content" id="select-${_count}">
// <button type="button" class="close" aria-hidden="true">×</button>
// <p>How is this node related to the target?</p>
// <select class="selectpicker" title="Choose one">
// <option>Parent</option>
// <option>Child</option>
// </select>
// </div>`
// }, document.body);
// if(self._target.id == self.tree.root) {
// TweenMax.to(mkr.query('.node-popover', self.tree.view.container), .5, {autoAlpha:1, display:'block'});
// $(self.tree.select).on('changed.bs.select', relate);
// mkr.on('.node-popover .close', 'click', cancelOp);
// return;
// }
//console.log(index, $(self.tree.select).selectpicker('val'));
$(self.tree.select).off('changed.bs.select', relate);
mkr.off('.node-popover .close', 'click', cancelOp);
TweenMax.to(mkr.query('.node-popover', self.tree.view.container), .5, {autoAlpha:0});
$(self.tree.select).selectpicker('val', '');
if(index == 1) {
//balls
}
else {
xist.node.instances[self._target.id].addChild(self);
}
self._target = null;
};
var cancelOp = function() {
self._target = null;
$(self.tree.select).off('changed.bs.select', relate);
mkr.off('.node-popover .close', 'click', cancelOp);
TweenMax.to(mkr.query('.node-popover', self.tree.view.container), .5, {autoAlpha:0});
};*/
};
node.prototype = {
get id() {
return this._id;
},
get isRoot() {
return this.id === this.tree.root;
},
get tree() {
return this._tree;
},
set tree(value) {
var n = this.children.length;
this._tree = value;
for(var i = 0; i < n; i++) {
this.childAt(i).tree = value;
}
},
get lines() {
return this._lines;
},
get data() {
return this._data;
},
set data(value) {
var children;
if(value && 'children' in value) {
children = value.children.concat();
delete value.children;
}
this._data = mkr.default(value, {}) || {};
mkr.setDefault(this._data, 'name', '');
mkr.setDefault(this._data, 'dob', ''); //date of birth
mkr.setDefault(this._data, 'pob', ''); //place of birth
mkr.setDefault(this._data, 'dod', ''); //date of death
mkr.setDefault(this._data, 'pod', ''); //place of death
mkr.setDefault(this._data, 'thumb', 'https://www.1.fm/images/blank.jpg'); //thumbnail url
this.view ? this.update() : this.createView();
this.children = children;
},
get children() {
return this._children;
},
set children(value) {
if(this.children.length) {
this.destroyChildren();
}
var len = value ? value.length : 0, node;
for(var i = 0; i < len; i++) {
node = this.tree.create({}, value[i]);
this.addChild(node);
}
},
get parent() {
return this._parent;
},
set parent(value) {
this._parent = value;
},
get dragger() {
return this._dragger;
},
get view() {
return this._view;
},
get x() {
return this.view._gsTransform.x;
},
set x(value) {
TweenMax.set(this.view, {x:value});
//this.refresh();
},
get y() {
return this.view._gsTransform.y;
},
set y(value) {
TweenMax.set(this.view, {y:value});
//this.refresh();
},
get right() {
return this.view._gsTransform.x + this.tree.nodeW;
},
get bottom() {
return this.view._gsTransform.y + this.tree.nodeH;
},
get originX() {
return this._originX;
},
set originX(value) {
this.tree.grid.remove(this.view);
this._originX = this.view.minX = value;
this.view.maxX = this.originR;
this.tree.grid.insert(this.view);
},
get originY() {
return this._originY;
},
set originY(value) {
this.tree.grid.remove(this.view);
this._originY = this.view.minY = value;
this.view.maxY = this.originB;
this.tree.grid.insert(this.view);
},
get originR() {
return this._originX + this.tree.nodeW;
},
get originB() {
return this._originY + this.tree.nodeH;
},
get breadth() {
return this._breadth;
},
get depth() {
return this._depth;
},
setOrigin: function(x, y) {
this.tree.grid.remove(this.view);
this._originX = this.view.minX = x;
this._originY = this.view.minY = y;
this.view.maxX = this.originR;
this.view.maxY = this.originB;
this.tree.grid.insert(this.view);
},
createView: function(x, y) {
x = mkr.default(x, 0);
y = mkr.default(y, 0);
this._view = mkr.create('div', {
css:{x:x, y:y},
attr:{id:this.id, class:'node'},
text:`<div class='thumb'><img src=''/></div>
<div class='details'>
<p class='name'></p>
<p class='born'>
<span class='lbl'>Born: </span>
<span class='date'></span><span class='place'></span>
</p>
<p class='died'>
<span class='lbl'>Died: </span>
<span class='date'></span><span class='place'></span>
</p>
</div>`
}, this.tree.view);
this.view.minX = x;
this.view.minY = y;
this.view.maxX = x + this.tree.nodeW;
this.view.maxY = x + this.tree.nodeH;
this.view._tree = this.tree.id;
this._lines = [
mkr.construct('ln', {attr:{class:'ln-0', x1:0, x2:0, y1:0, y2:0}}, '#'+this.tree.view.id+' .lines'),
mkr.construct('ln', {attr:{class:'ln-1', x1:0, x2:0, y1:0, y2:0}}, '#'+this.tree.view.id+' .lines'),
mkr.construct('ln', {attr:{class:'ln-2', x1:0, x2:0, y1:0, y2:0}}, '#'+this.tree.view.id+' .lines')
];
if(this.isRoot) this.tree.refresh();
this.update();
},
snap:function() {
//console.log(this);
TweenMax.to(this, .25, {x:this._originX, y:this._originY, onUpdate:this.refresh, onUpdateScope:this});
},
addChild: function(node) {
var id = node.id;
if(node.parent) {
xist.node.instances[node.parent].removeChild(id);
}
this.tree.add(node);
this.children.push(id);
node.parent = this.id;
this.tree.refresh();
},
addChildren: function(ids) {
for(var id of ids) {
this.addChild(id);
}
},
removeChild: function(id) {
var n = this.children.indexOf(id);
if(n >= 0) {
this.children.splice(n, 1);
xist.node.instances[id].parent = null;
this.tree.refresh();
}
},
childAt: function(n) {
if(n < 0) n += this.children.length;
return xist.node.instances[this.children[n]];
},
clearChildren: function() {
for(var id of this.children) {
xist.node.instances[id].parent = null;
}
this.children = [];
},
destroyChildren: function() {
for(var id of this.children) {
xist.node.instances[id].destroy();
}
this.children = [];
},
destroy: function() {
if(this.parent) { //remove from parent
xist.node.instances[this.parent].removeChild(this.id);
this.parent = null;
}
this.destroyChildren(); //destroy all children
if(this.dragger) this.dragger.kill();
mkr.remove(this.view);
mkr.remove([this.lines[0].el, this.lines[1].el, this.lines[2].el]);
this.tree.remove(this.id);
delete xist.node.instances[this.id];
},
//DFS algorithm that determines the total breadth of each subtree
findBreadth: function(tree) {
var n = this.children.length;
if(n == 0) {
this._breadth = 1;
return this._breadth;
}
var node, breadth=0;
for(var i = 0; i < n; i++) {
node = this.childAt(i);
breadth += node.findBreadth(tree);
}
this._breadth = breadth;
return breadth;
},
//DFS algorithm that determines the max depth of each subtree
findDepth: function(level) {
level=mkr.default(level,0);
var n=this.children.length, depth, max=0;
if(n > 0) {
depth = 1;
for(var i = 0; i < n; i++) {
max = Math.max(max, this.childAt(i).findDepth(level+1));
}
}
else {
depth = 0;
}
this._depth = depth+max;
return this._depth;
},
//BFS algorithm that handles node placement
refresh: function() {
var n = this.children.length;
var node, total, startX, startY, delta, subTotal, step=0,
gap=this.tree.gap, nodeW=this.tree.nodeW, nodeH=this.tree.nodeH;
var x1
switch(this.tree.dir) {
default:
case xist.tree.TOP_DOWN:
//calculate total width of the subtree based on the breadth prop
total = this.breadth*nodeW + (this.breadth-1)*gap;
startX = this.x + (nodeW - total)/2;
startY = this.y + nodeH + gap;
delta = nodeW + gap;
for(var i = 0; i < n; i++) {
node = xist.node.instances[this.children[i]];
subTotal = node.breadth*nodeW + (node.breadth-1)*gap;
node.x = startX + step + (subTotal-nodeW)/2;
node.y = startY;
node.setOrigin(node.x, node.y);
step += subTotal + gap;
node.refresh();
}
//update lines, line to parent
TweenMax.set(this.lines[0], {
x1:this.x+nodeW/2,
x2:this.x+nodeW/2,
y1:this.y-gap/2,
y2:this.y
});
//perpendicular child line
TweenMax.set(this.lines[1], {
x1:this.x+nodeW/2,
x2:this.x+nodeW/2,
y1:this.y+nodeH,
y2:this.y+nodeH+gap/2
});
//parallel child line
if(this.children.length) {
TweenMax.set(this.lines[2], {
x1:this.childAt(0).x+nodeW/2,
x2:this.childAt(-1).x+nodeW/2,
y1:this.y+nodeH+gap/2,
y2:this.y+nodeH+gap/2
});
}
break;
case xist.tree.BOTTOM_UP:
//calculate total width of the subtree based on the breadth prop
total = this.breadth*nodeW + (this.breadth-1)*gap;
startX = this.x + (nodeW - total)/2;
startY = this.y - nodeH - gap;
delta = nodeW + gap;
for(var i = 0; i < n; i++) {
node = xist.node.instances[this.children[i]];
subTotal = node.breadth*nodeW + (node.breadth-1)*gap;
node.x = startX + step + (subTotal-nodeW)/2;
node.y = startY;
node.setOrigin(node.x, node.y);
step += subTotal + gap;
node.refresh();
}
//update lines, line to parent
TweenMax.set(this.lines[0], {
x1:this.x+nodeW/2,
x2:this.x+nodeW/2,
y1:this.y+nodeH+gap/2,
y2:this.y+nodeH
});
//perpendicular child line
TweenMax.set(this.lines[1], {
x1:this.x+nodeW/2,
x2:this.x+nodeW/2,
y1:this.y,
y2:this.y-gap/2
});
//parallel child line
if(this.children.length) {
TweenMax.set(this.lines[2], {
x1:this.childAt(0).x+nodeW/2,
x2:this.childAt(-1).x+nodeW/2,
y1:this.y-gap/2,
y2:this.y-gap/2
});
}
break;
case xist.tree.LEFT_RIGHT:
//calculate total height of the subtree based on the breadth prop
total = this.breadth*nodeH + (this.breadth-1)*gap;
startX = this.x + nodeW + gap;
startY = this.y + (nodeH - total)/2;
delta = nodeH + gap;
for(var i = 0; i < n; i++) {
node = xist.node.instances[this.children[i]];
subTotal = node.breadth*nodeH + (node.breadth-1)*gap;
node.x = startX;
node.y = startY + step + (subTotal-nodeH)/2;
node.setOrigin(node.x, node.y);
step += subTotal + gap;
node.refresh();
}
//update lines, line to parent
TweenMax.set(this.lines[0], {
x1:this.x-gap/2,
x2:this.x,
y1:this.y+nodeH/2,
y2:this.y+nodeH/2
});
//perpendicular child line
TweenMax.set(this.lines[1], {
x1:this.x+nodeW,
x2:this.x+nodeW+gap/2,
y1:this.y+nodeH/2,
y2:this.y+nodeH/2
});
//parallel child line
if(this.children.length) {
TweenMax.set(this.lines[2], {
x1:this.x+nodeW+gap/2,
x2:this.x+nodeW+gap/2,
y1:this.childAt(0).y+nodeH/2,
y2:this.childAt(-1).y+nodeH/2,
});
}
break;
case xist.tree.RIGHT_LEFT:
//calculate total height of the subtree based on the breadth prop
total = this.breadth*nodeH + (this.breadth-1)*gap;
startX = this.x - (nodeW + gap);
startY = this.y + (nodeH - total)/2;
delta = nodeH + gap;
for(var i = 0; i < n; i++) {
node = xist.node.instances[this.children[i]];
subTotal = node.breadth*nodeH + (node.breadth-1)*gap;
node.x = startX;
node.y = startY + step + (subTotal-nodeH)/2;
node.setOrigin(node.x, node.y);
step += subTotal + gap;
node.refresh();
}
//update lines, line to parent
TweenMax.set(this.lines[0], {
x1:this.x+nodeW+gap/2,
x2:this.x+nodeW,
y1:this.y+nodeH/2,
y2:this.y+nodeH/2
});
//perpendicular child line
TweenMax.set(this.lines[1], {
x1:this.x,
x2:this.x-gap/2,
y1:this.y+nodeH/2,
y2:this.y+nodeH/2
});
//parallel child line
if(this.children.length) {
TweenMax.set(this.lines[2], {
x1:this.x-gap/2,
x2:this.x-gap/2,
y1:this.childAt(0).y+nodeH/2,
y2:this.childAt(-1).y+nodeH/2,
});
}
break;
}
//update line visibility
TweenMax.set(this.lines[0].el, {autoAlpha:this.parent?1:0});
TweenMax.set([this.lines[1].el, this.lines[2].el], {autoAlpha:this.children.length?1:0});
},
//update node based on data
update: function() {
TweenMax.set(mkr.query('.thumb img', this.view), {attr:{src:this.data.thumb}});
TweenMax.set(mkr.query('.name', this.view), {text:this.data.name});
TweenMax.set(mkr.query('.born .lbl', this.view), {autoAlpha:(this.data.dob || this.data.pob)});
TweenMax.set(mkr.query('.born .date', this.view), {text:this.data.dob});
TweenMax.set(mkr.query('.born .place', this.view), {text:(this.data.dob ? ', ' : '')+this.data.pob});
TweenMax.set(mkr.query('.died .lbl', this.view), {autoAlpha:(this.data.dod || this.data.pod)});
TweenMax.set(mkr.query('.died .date', this.view), {text:this.data.dod});
TweenMax.set(mkr.query('.died .place', this.view), {text:(this.data.dod ? ', ' : '')+this.data.pod});
}
};
node.instances = {};
node.getInstance = function(id) {
return node.instances[id];
};
node.box = {};
node.setBox = function(minX, maxX, minY, maxY) {
node.box.minX = minX;
node.box.maxX = maxX;
node.box.minY = minY;
node.box.maxY = maxY;
};
return node;
})();
//immediate family tree
xist.iTree = (function() {
var iTree = function(data, options) {
//default data
data = data || [];
data[0] = mkr.default(data[0], {name:'You'});
data[1] = mkr.default(data[1], {name:'Mom'});
data[2] = mkr.default(data[2], {name:'Dad'});
//default options
options = options || {};
this._parent = mkr.default(options.parent, document.body);
this._zoom = mkr.default(options.zoom, 1);
//create shared dom elements
var m = new mkr({attr:{id:'tree-container'}, css:{width:'100%', height:'100%', background:'transparent', overflow:'hidden'}});
m.create('div', {attr:{id:'trees', class:'tree'}});
m.create('div', {attr:{id:'trees-bg'}, css:{border:'1px solid black', padding:'25px', x:-25, y:-25}}, '#trees');
//create shared spatial grid
this._grid = rbush(10);
//root tree
this.root = new xist.tree({
dir: xist.tree.TOP_DOWN,
view: {attr:{id:'trees'}},
grid: this._grid
}, data[0]);
//mom tree
this.mom = new xist.tree({
dir: xist.tree.RIGHT_LEFT,
view: {attr:{id:'trees'}},
grid: this._grid
}, data[1]);
//dad tree
this.dad = new xist.tree({
dir: xist.tree.LEFT_RIGHT,
view: {attr:{id:'trees'}},
grid: this._grid
}, data[2]);
this.onTreeResize();
this.centerOnRoot();
this._grid.clear();
this._grid.load(Array.prototype.slice.call(mkr.queryAll('#trees .node')));
this.mom.resized.add(this.onTreeResize, this);
this.dad.resized.add(this.onTreeResize, this);
this.root.resized.add(this.onTreeResize, this);
//create ui elements
//create a button/click listener to toggle animation
m.create('div', {attr:{id:'addNode', class:'btn btn-primary'}, css:{bottom:10, right:10}, text:'Add node'}, this._parent);
//add ui events
mkr.on('#tree-container', 'wheel', function(e) {
this.zoom -= e.deltaY/150;
}, this);
mkr.on('#addNode', 'click', function() {
this.root.create();
}, this);
};
iTree.prototype = {
get zoom() {
return this._zoom;
},
set zoom(value) {
this._zoom = Math.min(2, Math.max(.25, value));
TweenMax.to('#trees', .2, {scale:this._zoom});
},
set data(value) {
var data = value || [];
data[0] = mkr.default(data[0], {name:'You'});
data[1] = mkr.default(data[1], {name:'Mom'});
data[2] = mkr.default(data[2], {name:'Dad'});
this.root.data = data[0];
this.mom.data = data[1];
this.dad.data = data[2];
this.centerOnRoot();
},
clear: function() {
this.data = null;
},
onTreeResize: function() {
//position trees based on dimensions
var momX, momY, dadX, dadY, rootX, rootY;
var momX = this.mom.width - this.mom.nodeW;
var dadX = momX + this.root.nodeW + 2*this.root.gap;
var maxH = Math.max(this.dad.height, this.mom.height);
var momY = dadY = maxH/2 - this.root.nodeH/2;
rootX = momX + ((this.mom.nodeW + this.dad.nodeW + 2*this.root.gap) - this.root.nodeW)/2;
rootY = momY + this.root.nodeH + 2*this.root.gap;
this.mom.setXY(momX, momY);
this.dad.setXY(dadX, dadY);
this.root.setXY(rootX, rootY);
//determine total dimensions and apply to the tree container
var w = this._width = this.mom.width + this.dad.width + 2*this.root.gap;
var h = this._height = maxH;
if((momY + maxH/2) < (rootY + this.root.height)) {
h = (rootY + this.root.height);
}
TweenMax.set('#trees', {width:w, height:h});
TweenMax.set('#trees-bg', {width:w+50, height:h+50});
},
centerOnRoot: function() {
TweenMax.set('#trees', {
x:(mkr.query('#tree-container').offsetWidth - this._width)/2,
y:(mkr.query('#tree-container').offsetHeight - this._height)/2
});
}
};
return iTree;
})();
var data = [
{
name: 'Samuel B Hershey', dob:'12.8.1976', pob:'Cleveland, OH',
children: [
{name: 'Samuel A Hershey', dob:'10.19.2014', pob:'Chapel Hill, NC'}
]
},
{
name:'Birgit Winzer', dob:'10.27.1943', pob:'Boston, MA',
children: [
{name: 'Ingrid Martens', dob:'12.20.1923',
children: [
{name: 'Bertha Groth', dob:'6.7.1888', dod:'1.1.1972',
children: [
{name: 'Christine Voigt'},
{name: 'Christian Groth'},
]},
{name: 'Ludwig Martens', dob:'3.26.1892', dod:'4.4.1972',
children: [
{name: 'Emma Luders'},
{name: 'Johan Martens'},
]},
]},
{name: 'Hans Winzer', dob:'4.2.1923', pob:'Boston, MA', dod:'7.26.1982', pod:'Cleveland, OH',
children: [
{name: 'Edith Rempel', dob:'5.7.1882', dod:'4.30.1959',
children: [
{name: 'Anna Siebert'},
{name: 'Gustav Rempel'},
]},
{name: 'John Winzer', dob:'3.23.1887', dod:'7.26.1982',
children: [
{name: 'Sarah Graham'},
{name: 'Julius Winzer'},
]},
]}
]
},
{
name: 'Loren Hershey', dob:'12.8.1976', pob:'Milwaukee, WI',
children: [
{name: 'Josephine Rosenburg', dob:'9.5.1916', pob:'Milwaukee, WI',
children: [
{name: 'Dora Heiser', dob:'1.2.1896', pob:'Milwaukee, WI',
children: [
{name: 'Josephine'},
{name: 'Henry Heiser'},
]},
{name: 'Simon Rosenburg', dob:'9.5.1916', pob:'Milwaukee, WI',
children: [
{name: 'Hannah'},
{name: 'Benard Rosenburg'},
]},
]},
{name: 'Alvin Hershey', dob:'12.20.1915', pob:'Cleveland, OH', dod:'3.4.1974', pod:'Cleveland, OH',
children: [
{name: 'Bess Fried', dob:'12.8.1891', dod:'1978', pod:'Cleveland, OH',
children: [
{name: 'Esther Green'},
{name: 'Irviny Fried'},
]},
{name: 'Ben Hershovitz', dob:'1889', dod:'1935',
children: [
{name: 'Ruth'},
{name: 'Jacob Hershovitz'},
]},
]}
]
}
];
//data = [];
var tree = new xist.iTree(data);
That’s all! hopefully, you have successfully created the JavaScript family tree. If you have any questions or suggestions, feel free to comment below.
