<!DOCTYPE html> <meta charset="utf-8"> <style> body { text-align: center; } svg { margin-top: 32px; border: 1px solid #aaa; } .person rect { fill: #fff; stroke: steelblue; stroke-width: 1px; } .person { font: 14px sans-serif; cursor: pointer; } .link { fill: none; stroke: #ccc; stroke-width: 1.5px; } </style> <body> <script src="./d3.min.js"></script> <script> var boxWidth = 150, boxHeight = 40, nodeWidth = 100, nodeHeight = 200, // duration of transitions in ms duration = 750, // d3 multiplies the node size by this value // to calculate the distance between nodes separation = .5; /** * For the sake of the examples, I want the setup code to be at the top. * However, since it uses a class (Tree) which is defined later, I wrap * the setup code in a function at call it at the end of the example. * Normally you would extract the entire Tree class defintion into a * separate file and include it before this script tag. */ function setup() { // Setup zoom and pan var zoom = d3.behavior.zoom() .scaleExtent([.1, 1]) .on('zoom', function () { svg.attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")"); }) // Offset so that first pan and zoom does not jump back to the origin .translate([400, 200]); var svg = d3.select("body").append("svg") .attr('width', 1000) .attr('height', 500) .call(zoom) .append('g') // Left padding of tree so that the whole root node is on the screen. // TODO: find a better way .attr("transform", "translate(400,200)"); // One tree to display the ancestors var ancestorTree = new Tree(svg, 'ancestor', 1); ancestorTree.children(function (person) { // If the person is collapsed then tell d3 // that they don't have any ancestors. if (person.collapsed) { return; } else { return person._parents; } }); // Use a separate tree to display the descendants var descendantsTree = new Tree(svg, 'descendant', -1); descendantsTree.children(function (person) { if (person.collapsed) { return; } else { return person._children; } }); d3.json('data/data.json', function (error, json) { if (error) { return console.error(error); } // D3 modifies the objects by setting properties such as // coordinates, parent, and children. Thus the same node // node can't exist in two trees. But we need the root to // be in both so we create proxy nodes for the root only. var ancestorRoot = rootProxy(json); var descendantRoot = rootProxy(json); // Start with only the first few generations of ancestors showing ancestorRoot._parents.forEach(function (parents) { parents._parents.forEach(collapse); }); // Start with only one generation of descendants showing descendantRoot._children.forEach(collapse); // Set the root nodes ancestorTree.data(ancestorRoot); descendantsTree.data(descendantRoot); // Draw the tree ancestorTree.draw(ancestorRoot); descendantsTree.draw(descendantRoot); }); } function rootProxy(root) { return { name: root.name, id: root.id, x0: 0, y0: 0, _children: root._children, _parents: root._parents, collapsed: false }; } /** * Shared code for drawing ancestors or descendants. * `selector` is a class that will be applied to links * and nodes so that they can be queried later when * the tree is redrawn. * `direction` is either 1 (forward) or -1 (backward). */ var Tree = function (svg, selector, direction) { this.svg = svg; this.selector = selector; this.direction = direction; this.tree = d3.layout.tree() // Using nodeSize we are able to control // the separation between nodes. If we used // the size parameter instead then d3 would // calculate the separation dynamically to fill // the available space. .nodeSize([nodeWidth, nodeHeight]) // By default, cousins are drawn further apart than siblings. // By returning the same value in all cases, we draw cousins // the same distance apart as siblings. .separation(function () { return separation; }); }; /** * Set the `children` function for the tree */ Tree.prototype.children = function (fn) { this.tree.children(fn); return this; }; /** * Set the root of the tree */ Tree.prototype.data = function (data) { this.root = data; return this; }; /** * Draw/redraw the tree */ Tree.prototype.draw = function (source) { if (this.root) { var nodes = this.tree.nodes(this.root), links = this.tree.links(nodes); this.drawLinks(links, source); this.drawNodes(nodes, source); } else { throw new Error('Missing root'); } return this; }; /** * Draw/redraw the connecting lines */ Tree.prototype.drawLinks = function (links, source) { console.log("links",links); console.log("source",source); var self = this; // Update links var link = self.svg.selectAll("path.link." + self.selector) // The function we are passing provides d3 with an id // so that it can track when data is being added and removed. // This is not necessary if the tree will only be drawn once // as in the basic example. .data(links, function (d) { return d.target.id; }); // Add new links // Transition new links from the source's // old position to the links final position link.enter().append("path") .attr("class", "link " + self.selector) .attr("d", function () { var o = {x: source.x0, y: self.direction * (source.y0 + boxHeight / 2)}; console.log("enter source", source); console.log("enter o", o); return transitionElbow({source: o, target: o}); }); // Update the old links positions link.transition() .duration(duration) .attr("d", function (d) { console.log("update"); console.log("d", d); return elbow(d, self.direction); }); // Remove any links we don't need anymore // if part of the tree was collapsed // Transition exit links from their current position // to the source's new position link.exit() .transition() .duration(duration) .attr("d", function () { var o = {x: source.x, y: self.direction * (source.y + boxHeight / 2)}; console.log("exit source", source); console.log("exit o", o); return transitionElbow({source: o, target: o}); }) .remove(); }; /** * Draw/redraw the person boxes. */ Tree.prototype.drawNodes = function (nodes, source) { var self = this; // Update nodes var node = self.svg.selectAll("g.person." + self.selector) // The function we are passing provides d3 with an id // so that it can track when data is being added and removed. // This is not necessary if the tree will only be drawn once // as in the basic example. .data(nodes, function (person) { return person.id; }); // Add any new nodes var nodeEnter = node.enter().append("g") .attr("class", "person " + self.selector) // Add new nodes at the right side of their child's box. // They will be transitioned into their proper position. .attr('transform', function (person) { return 'translate(' + (source.y0 + boxHeight / 2) + ',' + self.direction * source.x0 + ')'; }) .on('click', function (person) { self.togglePerson(person); }); // Draw the rectangle person boxes. // Start new boxes with 0 size so that // we can transition them to their proper size. nodeEnter.append("rect") .attr({ x: 0, y: 0, width: 0, height: 0 }); // Draw the person's name and position it inside the box nodeEnter.append("text") .attr("dx", 0) .attr("dy", 0) .attr("text-anchor", "start") .attr('class', 'name') .text(function (d) { return d.name; }) .style('fill-opacity', 0); // Update the position of both old and new nodes var nodeUpdate = node.transition() .duration(duration) .attr("transform", function (d) { return "translate(" + self.direction * d.x * 4 + "," + (self.direction * d.y / 1.5) + ")"; }); // Grow boxes to their proper size nodeUpdate.select('rect') .attr({ x: -(boxWidth / 2), y: -(boxHeight / 2), width: boxWidth, height: boxHeight }); // Move text to it's proper position nodeUpdate.select('text') .attr("dx", -(boxWidth / 2) + 10) .style('fill-opacity', 1); // Remove nodes we aren't showing anymore var nodeExit = node.exit() .transition() .duration(duration) // Transition exit nodes to the source's position .attr("transform", function (d) { return "translate(" + (self.direction * (source.y + boxHeight / 2)) + "," + source.x + ")"; }) .remove(); // Shrink boxes as we remove them nodeExit.select('rect') .attr({ x: 0, y: 0, width: 0, height: 0 }); // Fade out the text as we remove it nodeExit.select('text') .style('fill-opacity', 0) .attr('dx', 0); // Stash the old positions for transition. nodes.forEach(function (person) { person.x0 = person.x; person.y0 = person.y; }); }; /** * Update a person's state when they are clicked. */ Tree.prototype.togglePerson = function (person) { // Don't allow the root to be collapsed because that's // silly (it also makes our life easier) if (person === this.root) { return; } // Non-root nodes else { if (person.collapsed) { person.collapsed = false; } else { collapse(person); } this.draw(person); } }; /** * Collapse person (hide their ancestors). We recursively * collapse the ancestors so that when the person is * expanded it will only reveal one generation. If we don't * recursively collapse the ancestors then when * the person is clicked on again to expand, all ancestors * that were previously showing will be shown again. * If you want that behavior then just remove the recursion * by removing the if block. */ function collapse(person) { person.collapsed = true; if (person._parents) { person._parents.forEach(collapse); } if (person._children) { person._children.forEach(collapse); } } /** * Custom path function that creates straight connecting * lines. Calculate start and end position of links. * Instead of drawing to the center of the node, * draw to the border of the person profile box. * That way drawing order doesn't matter. In other * words, if we draw to the center of the node * then we have to draw the links first and the * draw the boxes on top of them. */ function elbow(d, direction) { // var sourceX = d.source.x, // sourceY = d.source.y + (boxHeight / 2), // targetX = d.target.x / 2, // targetY = 2 * d.target.y; console.log("d",d); console.log("direction",direction); return "M" + d.source.x + "," + d.source.y + (boxHeight / 2) + "V" + (direction * (d.source.y + (3 * boxHeight / 2))) + "H" + direction *(d.target.x + boxWidth / 2) + "V" + (direction * (d.source.y + 2 * boxHeight)); } /** * Use a different elbow function for enter * and exit nodes. This is necessary because * the function above assumes that the nodes * are stationary along the x axis. */ function transitionElbow(d) { return "M" + d.source.y + "," + d.source.x + "V" + d.source.y + "H" + d.source.x + "V" + d.source.y; } setup(); </script> </body>