d3-tree 双向树

svg { margin-top: 32px; border: 1px solid #aaa; }
.person rect { fill: #fff; stroke: rgba(0, 0, 0, 0.15); stroke-width: 1px; }
.person { font: 14px sans-serif; }
.btn { cursor: pointer; }
.link { fill: none; stroke: rgba(0, 0, 0, 0.15); stroke-width: 1.5px; }
.btn circle { stroke: rgba(0, 0, 0, 0.15); fill: #fff; } </style>
<body> <script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> <script> var json = { "name": "Isaac Warren", "id": "acd21329-2b99-5bdb-a080-ff15ad2fcaa6", "_parents": [{ "name": "父Virgie Hampton", "id": "56d25f0e-7c01-50f9-8dfe-9ed953ea5b33", "_parents": [{ "name": "父Jared Evans", "id": "efa251bb-13f7-583d-b5ec-c16f8fcd109f", }, { "name": "父Mina Taylor", "id": "6a2c6fee-7e8c-5d14-97a0-2fb79c20c89f", } ] }, { "name": "父Matthew Haynes", "id": "051d725d-cfe4-5694-a330-28b3f3e8b9b1", "_parents": [{ "name": "父Mayme Moss", "id": "5b79bad1-aa95-5d49-9c4b-ccd681efdeed", }, { "name": "父Barry Perry", "id": "214f52e2-51d5-51f8-9b3a-93b41b49ec58", } ] } ], "_children": [{ "name": "子Celia Frazier", "id": "60d7d16b-2ec3-51ac-b016-a1b16cf56d39", "_children": [{ "name": "子Bill Greene", "id": "bda34c70-061b-5678-af24-f9648ecfb985", }, { "name": "子Travis Day", "id": "dc3a79aa-4a4e-52b0-8e10-a88f00d77b93", }, ] }, { "name": "子Jeremiah Webb", "id": "f774bf05-430d-5cfe-9181-3a732233f0d3", "_children": [{ "name": "子Bill Greene", "id": "bda34c70-061b-5678-af24", "_children": [{ "name": "子Bill Greene", "id": "bda34c70-061b-af24", }, { "name": "子Travis Day", "id": "dc3a79aa-4a4e-8e10", }, ] }, { "name": "子Travis Day", "id": "dc3a79aa-4a4e-52b0-8e10", }, ] }, { "name": "子Amelia Curtis", "id": "a66042ef-d968-50a4-87e0-bea4e6793825", "_children": [{ "name": "子Bill Greene", "id": "bda34c70-061b-5678-af24-", }, { "name": "子Travis Day", "id": "dc3a79aa-4a4e-52b0-8e10-", }, ] } ] } </script> <script> var boxWidth = 150, boxHeight = 40, nodeWidth = 150, 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() {
var zoom = d3.behavior.zoom() .scaleExtent([.1, 1]) //用于设置最小和最大的缩放比例 .on('zoom', function () { svg.attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")"); }) // 当 zoom 事件发生时,调用 zoomed 函数 // :zoomed 函数,用于更改需要缩放的元素的属性,d3.event.translate 是平移的坐标值,d3.event.scale 是缩放的值。 .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)");
//父节点 var ancestorTree = new Tree(svg, 'ancestor', -1); ancestorTree.children(function (person) { if (person.collapsed) { return; } else { return person._parents; } }); //子节点 var descendantsTree = new Tree(svg, 'descendant', 1); descendantsTree.children(function (person) { if (person.collapsed) { return; } else { return person._children; } });
// d3.json('data/8gens.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(); // }); ancestorRoot._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, root_parents: false, root_children: 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]) .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 1.5; }); };
/** * 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); this.drawBtn(nodes, source); } else { throw new Error('Missing root'); } return this; };
/** * Draw/redraw the connecting lines */ Tree.prototype.drawLinks = function (links, 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; });
var diagonal = d3.svg.diagonal() .projection(function (d) { return [d.x, (d.y + 20) * self.direction]; }); // Add new links // Transition new links from the source's // old position to the links final position // var diagonal = d3.linkHorizontal().x(d => d.x).y(d => d.y)
link.enter().append("path") .attr("class", "link " + self.selector) .attr("d", d => { const o = { x: source.x0, y: source.y0 }; return diagonal({ source: o, target: o }); }).transition() .duration(duration) // Update the old links positions // link.transition() // .duration(duration) // .attr("d", function (d) { // return elbow(d, self.direction); // }); link.transition() .duration(duration) .attr("d", function (d) { return diagonal(d); });
// 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", d => { const o = { x: source.x, y: source.y }; return diagonal({ 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.x0 + ',' + (self.direction * (source.y0 + boxHeight / 2)) + ')'; })
// 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", 5) .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(" + d.x + "," + (self.direction * d.y) + ")"; });
// 盒子边框 nodeUpdate.select('rect') .attr({ x: -(boxWidth / 2), y: -(boxHeight / 2), width: boxWidth, height: boxHeight, rx: "2", }) // Move text to it's proper position // 盒子内部文本 nodeUpdate.select('text') .attr("dx", -(boxWidth / 2) + 10) .style('fill-opacity', 1) .style('vertical-align', 'middle');
// Remove nodes we aren't showing anymore var nodeExit = node.exit() .transition() .duration(duration) .attr("transform", function (d) { return 'translate(' + source.x + ',' + (self.direction * (source.y + boxHeight / 2)) + ')'; }) .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);
// a(nodeEnter, self, source) // Stash the old positions for transition. nodes.forEach(function (person) { person.x0 = person.x; person.y0 = person.y; });
}; Tree.prototype.drawBtn = function (nodes, source) { var self = this;
// Update nodes var node = self.svg.selectAll("g.btn." + 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", "btn " + 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.x0 + ',' + (self.direction * (source.y0 + boxHeight / 2)) + ')'; }) .on('click', function (person, ...event) { if (this.childNodes[1].innerHTML === '+') { this.childNodes[1].innerHTML = '-' } else { this.childNodes[1].innerHTML = '+' } 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("circle") .attr({ x: 0, y: 0, r: 0, })
nodeEnter.append("text") .attr("x", "0") .attr("dy", function (d) { if (self.direction === -1) { return -12 } else { return 26 } }) .attr('r', 80) .attr("text-anchor", "middle") .style("fill", "rgba(0, 0, 0, 0.15)") .style('font-size', '22px') .text(function (d) { if (!d.depth) { return '' } if (self.direction === -1) { if (d._parents && d._parents.length) { return d.collapsed ? '+' : '-' } else { return ""; } } else { if (d._children && d._children.length) { return d.collapsed ? '+' : '-' } else { return ""; } } }) .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(" + d.x + "," + (self.direction * d.y) + ")"; });
nodeUpdate.select('circle') .attr("cx", 0) .attr("cy", function (d) { if (self.direction === -1) { return -20 } else { return 20 } }) .attr("r", function (d) { if (!d.depth) { return 0 } if (self.direction === -1) { return d._parents && d._parents.length ? 10 : 0; } else { return d._children && d._children.length ? 10 : 0; } }) nodeUpdate.select('text') .style('fill-opacity', 1)
// Remove nodes we aren't showing anymore
var nodeExit = node.exit() .transition() .duration(duration) .attr("transform", function (d) { return 'translate(' + source.x + ',' + (self.direction * (source.y + boxHeight / 2)) + ')'; }) .remove(); // // Shrink boxes as we remove them nodeExit.select('circle') .attr({ x: 0, y: 0, r: 0, });
// Fade out the text as we remove it nodeExit.select('text') .style('fill-opacity', 0)
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, direction) {
// Don't allow the root to be collapsed because that's // silly (it also makes our life easier) if (person === this.root) { return ''; } 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); } }
// function rootCollapse(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) { let sourceX = d.source.x, sourceY = d.source.y + (boxHeight / 2), targetX = d.target.x, targetY = d.target.y - (boxHeight / 2); return "M" + sourceX + "," + (direction * sourceY) + "V" + (direction * ((targetY - sourceY) / 2 + sourceY)) + "H" + targetX + "V" + (direction * targetY);
}
setup(); </script>

更多精彩