UNPKG

d3-dtree

Version:

A library for visualizing data trees built on top of D3.

383 lines (329 loc) 9.56 kB
class TreeBuilder { constructor(root, siblings, opts) { TreeBuilder.DEBUG_LEVEL = opts.debug ? 1 : 0; this.root = root; this.siblings = siblings; this.opts = opts; // flatten nodes this.allNodes = this._flatten(this.root); // calculate node sizes this.nodeSize = opts.callbacks.nodeSize.call(this, // filter hidden and marriage nodes _.filter( this.allNodes, node => !(node.hidden || _.get(node, 'data.isMarriage')) ), opts.nodeWidth, opts.callbacks.textRenderer ) this.marriageSize = opts.callbacks.marriageSize.call(this, // filter hidden and non marriage nodes _.filter( this.allNodes, node => !node.hidden && _.get(node, 'data.isMarriage') ), this.opts.marriageNodeSize ) } create() { let opts = this.opts; let allNodes = this.allNodes; let nodeSize = this.nodeSize; let width = opts.width + opts.margin.left + opts.margin.right; let height = opts.height + opts.margin.top + opts.margin.bottom; // create zoom handler const zoom = this.zoom = d3.zoom() .scaleExtent([0.1, 10]) .on('zoom', function () { g.attr('transform', d3.event.transform) }) // make a svg const svg = this.svg = d3.select(opts.target) .append('svg') .attr('viewBox', [0, 0, width, height]) .call(zoom) // create svg group that holds all nodes const g = this.g = svg.append('g') // set zoom identity svg.call(zoom.transform, d3.zoomIdentity.translate(width / 2, opts.margin.top).scale(1)) // Compute the layout. this.tree = d3.tree() .nodeSize([nodeSize[0] * 2, opts.callbacks.nodeHeightSeperation.call(this, nodeSize[0], nodeSize[1])]); this.tree.separation(function separation(a, b) { if (a.data.hidden || b.data.hidden) { return 0.3; } else { return 0.6; } }); this._update(this.root); } _update(source) { let opts = this.opts; let allNodes = this.allNodes; let nodeSize = this.nodeSize; let marriageSize = this.marriageSize; let treenodes = this.tree(source); let links = treenodes.links(); // Create the link lines. this.g.selectAll('.link') .data(links) .enter() // filter links with no parents to prevent empty nodes .filter(function(l) { return !l.target.data.noParent; }) .append('path') .attr('class', opts.styles.linage) .attr('d', this._elbow); let nodes = this.g.selectAll('.node') .data(treenodes.descendants()) .enter(); this._linkSiblings(); // Draw siblings (marriage) this.g.selectAll('.sibling') .data(this.siblings) .enter() .append('path') .attr('class', opts.styles.marriage) .attr('d', _.bind(this._siblingLine, this)); // Create the node rectangles. nodes.append('foreignObject') .filter(function(d) { return d.data.hidden ? false : true; }) .attr('x', function(d) { return Math.round(d.x - d.cWidth / 2) + 'px'; }) .attr('y', function(d) { return Math.round(d.y - d.cHeight / 2) + 'px'; }) .attr('width', function(d) { return d.cWidth + 'px'; }) .attr('height', function(d) { return d.cHeight + 'px'; }) .attr('id', function(d) { return d.id; }) .html(function(d) { if (d.data.isMarriage) { return opts.callbacks.marriageRenderer.call(this, d.x, d.y, marriageSize[0], marriageSize[1], d.data.extra, d.data.id, d.data.class ) } else { return opts.callbacks.nodeRenderer.call(this, d.data.name, d.x, d.y, nodeSize[0], nodeSize[1], d.data.extra, d.data.id, d.data.class, d.data.textClass, opts.callbacks.textRenderer ) } }) .on('dblclick', function () { // do not propagate a double click on a node // to prevent the zoom from being triggered d3.event.stopPropagation() }) .on('click', function(d)  { // ignore double-clicks and clicks on hidden nodes if (d3.event.detail === 2 || d.data.hidden) { return; } if (d.data.isMarriage) { opts.callbacks.marriageClick.call(this, d.data.extra, d.data.id) } else { opts.callbacks.nodeClick.call(this, d.data.name, d.data.extra, d.data.id) } }) .on('contextmenu', function(d)  { if (d.data.hidden) { return; } d3.event.preventDefault(); if (d.data.isMarriage) { opts.callbacks.marriageRightClick.call(this, d.data.extra, d.data.id) } else { opts.callbacks.nodeRightClick.call(this, d.data.name, d.data.extra, d.data.id) } }); } _flatten(root) { let n = []; let i = 0; function recurse(node) { if (node.children) { node.children.forEach(recurse); } if (!node.id) { node.id = ++i; } n.push(node); } recurse(root); return n; } _elbow(d, i) { if (d.target.data.noParent) { return 'M0,0L0,0'; } let ny = Math.round(d.target.y + (d.source.y - d.target.y) * 0.50); let linedata = [{ x: d.target.x, y: d.target.y }, { x: d.target.x, y: ny }, { x: d.source.x, y: d.source.y }]; let fun = d3.line().curve(d3.curveStepAfter) .x(function(d) { return d.x; }) .y(function(d) { return d.y; }); return fun(linedata); } _linkSiblings() { let allNodes = this.allNodes; _.forEach(this.siblings, function(d) { let start = allNodes.filter(function(v) { return d.source.id == v.data.id; }); let end = allNodes.filter(function(v) { return d.target.id == v.data.id; }); d.source.x = start[0].x; d.source.y = start[0].y; d.target.x = end[0].x; d.target.y = end[0].y; let marriageId = (start[0].data.marriageNode != null ? start[0].data.marriageNode.id : end[0].data.marriageNode.id); let marriageNode = allNodes.find(function(n) { return n.data.id == marriageId; }); d.source.marriageNode = marriageNode; d.target.marriageNode = marriageNode; }); } _siblingLine(d, i) { let ny = Math.round(d.target.y + (d.source.y - d.target.y) * 0.50); let nodeWidth = this.nodeSize[0]; let nodeHeight = this.nodeSize[1]; // Not first marriage if (d.number > 0) { ny -= Math.round(nodeHeight * 8 / 10); } let linedata = [{ x: d.source.x, y: d.source.y }, { x: Math.round(d.source.x + nodeWidth * 6 / 10), y: d.source.y }, { x: Math.round(d.source.x + nodeWidth * 6 / 10), y: ny }, { x: d.target.marriageNode.x, y: ny }, { x: d.target.marriageNode.x, y: d.target.y }, { x: d.target.x, y: d.target.y }]; let fun = d3.line().curve(d3.curveStepAfter) .x(function(d) { return d.x; }) .y(function(d) { return d.y; }); return fun(linedata); } static _nodeHeightSeperation(nodeWidth, nodeMaxHeight) { return nodeMaxHeight + 25; } static _nodeSize(nodes, width, textRenderer) { let maxWidth = 0; let maxHeight = 0; let tmpSvg = document.createElement('svg'); document.body.appendChild(tmpSvg); _.map(nodes, function(n) { let container = document.createElement('div'); container.setAttribute('class', n.data.class); container.style.visibility = 'hidden'; container.style.maxWidth = width + 'px'; let text = textRenderer(n.data.name, n.data.extra, n.data.textClass); container.innerHTML = text; tmpSvg.appendChild(container); let height = container.offsetHeight; tmpSvg.removeChild(container); maxHeight = Math.max(maxHeight, height); n.cHeight = height; if (n.data.hidden) { n.cWidth = 0; } else { n.cWidth = width; } }); document.body.removeChild(tmpSvg); return [width, maxHeight]; } static _marriageSize (nodes, size) { _.map(nodes, function (n) { if (!n.data.hidden) { n.cHeight = size n.cWidth = size } }) return [size, size] } static _nodeRenderer(name, x, y, height, width, extra, id, nodeClass, textClass, textRenderer) { let node = ''; node += '<div '; node += 'style="height:100%;width:100%;" '; node += 'class="' + nodeClass + '" '; node += 'id="node' + id + '">\n'; node += textRenderer(name, extra, textClass); node += '</div>'; return node; } static _textRenderer(name, extra, textClass) { let node = ''; node += '<p '; node += 'align="center" '; node += 'class="' + textClass + '">\n'; node += name; node += '</p>\n'; return node; } static _marriageRenderer (x, y, height, width, extra, id, nodeClass) { return `<div style="height:100%" class="${nodeClass}" id="node${id}"></div>` } static _debug(msg) { if (TreeBuilder.DEBUG_LEVEL > 0)  { console.log(msg); } } } export default TreeBuilder;