JavaScript Family Tree

JavaScript Family Tree
Project: Family Tree
Author: Chris
Edit Online: View on CodePen
License: MIT

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.

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *