Skip to content

Instantly share code, notes, and snippets.

@turbaszek
Last active November 8, 2018 22:19
Show Gist options
  • Save turbaszek/182f4c9651aa017047c66726f385427a to your computer and use it in GitHub Desktop.
Save turbaszek/182f4c9651aa017047c66726f385427a to your computer and use it in GitHub Desktop.
Tree diagram with d3.js v4 and flat JSON

A simple tree diagram built with d3.js. It has simple tooltips, node filter and custom styles for nodes and links. In scripts.js you can find how to use icons for node representation.

The input data is a flat json

[
  {"name": "Eve",   "parent": "" , "label": "L1", "size": 10, "color": "steelblue"},
  {"name": "Cain",  "parent": "Eve", "label": "L2", "size": 10, "color": "steelblue"},
 ...
];

where:

  • label is value used instead of name,
  • size and color provide style for a single link.

The work is a mixture of the following solutions:

http://bl.ocks.org/d3noob/8375092

https://bl.ocks.org/d3noob/08ecb6ea9bb68ba0d9a7e89f344acec8

http://bl.ocks.org/shubhgo/80323b7f3881f874c02f

http://bl.ocks.org/Caged/6476579

It uses d3-tip.js:

https://github.com/caged/d3-tip

// d3.tip
// Copyright (c) 2013 Justin Palmer
// ES6 / D3 v4 Adaption Copyright (c) 2016 Constantin Gavrilete
// Removal of ES6 for D3 v4 Adaption Copyright (c) 2016 David Gotz
//
// Tooltips for d3.js SVG visualizations
d3.functor = function functor(v) {
return typeof v === "function" ? v : function() {
return v;
};
};
d3.tip = function() {
var direction = d3_tip_direction,
offset = d3_tip_offset,
html = d3_tip_html,
node = initNode(),
svg = null,
point = null,
target = null
function tip(vis) {
svg = getSVGNode(vis)
point = svg.createSVGPoint()
document.body.appendChild(node)
}
// Public - show the tooltip on the screen
//
// Returns a tip
tip.show = function() {
var args = Array.prototype.slice.call(arguments)
if(args[args.length - 1] instanceof SVGElement) target = args.pop()
var content = html.apply(this, args),
poffset = offset.apply(this, args),
dir = direction.apply(this, args),
nodel = getNodeEl(),
i = directions.length,
coords,
scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft
nodel.html(content)
.style('position', 'absolute')
.style('opacity', 1)
.style('pointer-events', 'all')
while(i--) nodel.classed(directions[i], false)
coords = direction_callbacks[dir].apply(this)
nodel.classed(dir, true)
.style('top', (coords.top + poffset[0]) + scrollTop + 'px')
.style('left', (coords.left + poffset[1]) + scrollLeft + 'px')
return tip
}
// Public - hide the tooltip
//
// Returns a tip
tip.hide = function() {
var nodel = getNodeEl()
nodel
.style('opacity', 0)
.style('pointer-events', 'none')
return tip
}
// Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value.
//
// n - name of the attribute
// v - value of the attribute
//
// Returns tip or attribute value
tip.attr = function(n, v) {
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().attr(n)
} else {
var args = Array.prototype.slice.call(arguments)
d3.selection.prototype.attr.apply(getNodeEl(), args)
}
return tip
}
// Public: Proxy style calls to the d3 tip container. Sets or gets a style value.
//
// n - name of the property
// v - value of the property
//
// Returns tip or style property value
tip.style = function(n, v) {
// debugger;
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().style(n)
} else {
var args = Array.prototype.slice.call(arguments);
if (args.length === 1) {
var styles = args[0];
Object.keys(styles).forEach(function(key) {
return d3.selection.prototype.style.apply(getNodeEl(), [key, styles[key]]);
});
}
}
return tip
}
// Public: Set or get the direction of the tooltip
//
// v - One of n(north), s(south), e(east), or w(west), nw(northwest),
// sw(southwest), ne(northeast) or se(southeast)
//
// Returns tip or direction
tip.direction = function(v) {
if (!arguments.length) return direction
direction = v == null ? v : d3.functor(v)
return tip
}
// Public: Sets or gets the offset of the tip
//
// v - Array of [x, y] offset
//
// Returns offset or
tip.offset = function(v) {
if (!arguments.length) return offset
offset = v == null ? v : d3.functor(v)
return tip
}
// Public: sets or gets the html value of the tooltip
//
// v - String value of the tip
//
// Returns html value or tip
tip.html = function(v) {
if (!arguments.length) return html
html = v == null ? v : d3.functor(v)
return tip
}
// Public: destroys the tooltip and removes it from the DOM
//
// Returns a tip
tip.destroy = function() {
if(node) {
getNodeEl().remove();
node = null;
}
return tip;
}
function d3_tip_direction() { return 'n' }
function d3_tip_offset() { return [0, 0] }
function d3_tip_html() { return ' ' }
var direction_callbacks = {
n: direction_n,
s: direction_s,
e: direction_e,
w: direction_w,
nw: direction_nw,
ne: direction_ne,
sw: direction_sw,
se: direction_se
};
var directions = Object.keys(direction_callbacks);
function direction_n() {
var bbox = getScreenBBox()
return {
top: bbox.n.y - node.offsetHeight,
left: bbox.n.x - node.offsetWidth / 2
}
}
function direction_s() {
var bbox = getScreenBBox()
return {
top: bbox.s.y,
left: bbox.s.x - node.offsetWidth / 2
}
}
function direction_e() {
var bbox = getScreenBBox()
return {
top: bbox.e.y - node.offsetHeight / 2,
left: bbox.e.x
}
}
function direction_w() {
var bbox = getScreenBBox()
return {
top: bbox.w.y - node.offsetHeight / 2,
left: bbox.w.x - node.offsetWidth
}
}
function direction_nw() {
var bbox = getScreenBBox()
return {
top: bbox.nw.y - node.offsetHeight,
left: bbox.nw.x - node.offsetWidth
}
}
function direction_ne() {
var bbox = getScreenBBox()
return {
top: bbox.ne.y - node.offsetHeight,
left: bbox.ne.x
}
}
function direction_sw() {
var bbox = getScreenBBox()
return {
top: bbox.sw.y,
left: bbox.sw.x - node.offsetWidth
}
}
function direction_se() {
var bbox = getScreenBBox()
return {
top: bbox.se.y,
left: bbox.e.x
}
}
function initNode() {
var node = d3.select(document.createElement('div'))
node
.style('position', 'absolute')
.style('top', 0)
.style('opacity', 0)
.style('pointer-events', 'none')
.style('box-sizing', 'border-box')
return node.node()
}
function getSVGNode(el) {
el = el.node()
if(el.tagName.toLowerCase() === 'svg')
return el
return el.ownerSVGElement
}
function getNodeEl() {
if(node === null) {
node = initNode();
// re-add node to DOM
document.body.appendChild(node);
};
return d3.select(node);
}
// Private - gets the screen coordinates of a shape
//
// Given a shape on the screen, will return an SVGPoint for the directions
// n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest),
// sw(southwest).
//
// +-+-+
// | |
// + +
// | |
// +-+-+
//
// Returns an Object {n, s, e, w, nw, sw, ne, se}
function getScreenBBox() {
var targetel = target || d3.event.target;
while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) {
targetel = targetel.parentNode;
}
var bbox = {},
matrix = targetel.getScreenCTM(),
tbbox = targetel.getBBox(),
width = tbbox.width,
height = tbbox.height,
x = tbbox.x,
y = tbbox.y
point.x = x
point.y = y
bbox.nw = point.matrixTransform(matrix)
point.x += width
bbox.ne = point.matrixTransform(matrix)
point.y += height
bbox.se = point.matrixTransform(matrix)
point.x -= width
bbox.sw = point.matrixTransform(matrix)
point.y -= height / 2
bbox.w = point.matrixTransform(matrix)
point.x += width
bbox.e = point.matrixTransform(matrix)
point.x -= width / 2
point.y -= height / 2
bbox.n = point.matrixTransform(matrix)
point.y += height
bbox.s = point.matrixTransform(matrix)
return bbox
}
return tip
};
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css" integrity="sha384-B4dIYHKNBt8Bc12p+WXckhzcICo0wtJAoU8YZTY5qE0Id1GSseTk6S+L3BlXeVIU" crossorigin="anonymous">
<link rel="stylesheet" href="styles.css">
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="d3-tip.js"></script>
<script src="scripts.js"></script>
</head>
<body>
<div id="filters">
<select id="filter_drop" onchange="updateRoot(this)">
</select>
</div>
<button type="button" onclick="collapseAll()">Collapse All</button>
<button type="button" onclick="expandAll()">Expand All</button>
<div id="viz"></div>
<script type="text/javascript">
var flatData = [
{"name": "1", "parent": "" , "label": "L1", "size": 50, "color": "steelblue", "icon": "icon.png"},
{"name": "2", "parent": "1", "label": "L2", "size": 30, "color": "steelblue", "icon": "icon.png"},
{"name": "3", "parent": "1", "label": "L3", "size": 10, "color": "steelblue", "icon": "icon.png"},
{"name": "4", "parent": "3", "label": "L4", "size": 6, "color": "steelblue", "icon": "icon.png"},
{"name": "5", "parent": "3", "label": "L5", "size": 8, "color": "steelblue", "icon": "icon.png"},
{"name": "6", "parent": "1", "label": "L5", "size": 2, "color": "red", "icon": "icon.png"},
{"name": "7", "parent": "1", "label": "L7", "size": 1, "color": "red", "icon": "icon.png"},
{"name": "8", "parent": "7", "label": "L8", "size": 17, "color": "red", "icon": "icon.png"},
{"name": "9", "parent": "1", "label": "L9", "size": 7, "color": "red", "icon": "icon.png"},
{"name": "10", "parent": "2", "label": "L9", "size": 7, "color": "red", "icon": "icon.png"},
{"name": "11", "parent": "10", "label": "L9", "size": 7, "color": "red", "icon": "icon.png"},
{"name": "12", "parent": "10", "label": "L9", "size": 7, "color": "red", "icon": "icon.png"},
{"name": "13", "parent": "11", "label": "L9", "size": 7, "color": "red", "icon": "icon.png"},
{"name": "14", "parent": "12", "label": "L9", "size": 7, "color": "red", "icon": "icon.png"},
{"name": "15", "parent": "14", "label": "L9", "size": 7, "color": "red", "icon": "icon.png"}
];
// Assigns parent, children, height, depth
root = d3.stratify()
.id(function(d) { return d.name; })
.parentId(function(d) { return d.parent; })
(flatData);
// Add master node to filter nodes
filterNodes = showChildren(root)
filterNodes['master'] = root
console.log(filterNodes)
// creates filter buttons
var docFrag = document.createDocumentFragment();
for (var key in filterNodes) {
var elem = document.createElement('option');
elem.text = key;
elem.value = key;
if (key=='master'){elem.selected = 'selected';};
docFrag.appendChild(elem);
}
document.getElementById("filter_drop").appendChild(docFrag);
</script>
<script>
// Set the dimensions and margins of the diagram
var margin = {top: 50, right: 50, bottom: 30, left: 120},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.
append("g")
.attr("transform", "translate("
+ margin.left + "," + margin.top + ")");
var i = 0,
duration = 400; // animation duration
// declares a tree layout and assigns the size
var treemap = d3.tree().size([height, width]);
root.x0 = height / 2;
root.y0 = 0;
// Collapse after the second level
root.children.forEach(collapse);
update(root);
</script>
</body>
// Find out nodes after master node
function showChildren (node){
var nodes = {};
node.children.forEach(function(d){
if ( d.hasOwnProperty('children') ){
nodes[d.data.label] = d;
}});
return nodes;
};
// Change master root
function updateRoot(d){
var key = d.value
console.log(key)
root = filterNodes[key]
console.log(root)
if ( root.children == null ) {
root.children = root._children
}
var height = root.children.length * 50
root.x0 = height / 2;
root.y0 = 0;
treemap = d3.tree().size([height, width]);
// Collapse after the second level
root.children.forEach(collapse);
update(root);
};
// Collapse the node and all it's children
function collapse(d) {
if(d.children) {
d._children = d.children
d._children.forEach(collapse)
d.children = null
}
};
// Collapses the node in the tree
function collapseAll() {
root.children.forEach(collapse);
update(root);
};
// Expands the node d and all the children nodes of d
function expand(d) {
if (d._children) {
d.children = d._children;
d._children = null;
}
if (d.children) {
d.children.forEach(expand);
}
};
//Expands all the nodes in the tree
function expandAll() {
root.children.forEach(expand);
update(root);
};
// Update tree
function update(source) {
// Assigns the x and y position for the nodes
var treeData = treemap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function(d){ d.y = d.depth * 180});
// Tooltips
var tool_tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return d.data.label + "<br> <strong>Volume</strong> <span style='color:red'>" + d.data.size + "</span>";
});
svg.call(tool_tip);
// ****************** Nodes section ***************************
// Update the nodes...
var node = svg.selectAll('g.node')
.data(nodes, function(d) {return d.id || (d.id = ++i); });
// Enter any new modes at the parent's previous position.
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
// Add icon for the nodes
// nodeEnter.append("image")
// .attr("xlink:href", function(d) { return d.data.icon; })
// .attr("x", 5)
// .attr("y", -28)
// .attr("width", 50)
// .attr("height", 50);
// Add Circle for the nodes
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 1e-6)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
})
.on('mouseover', tool_tip.show)
.on('mouseout', tool_tip.hide);
// Add labels for the nodes
nodeEnter.append('text')
.attr("class", "name")
.attr("x", function(d) { return -3 * d.data.label.length; })
.attr("y", function(d) { return -22 - d.data.size/2; })
.text(function(d) { return (d.data.label.length > 8) ? "..."+d.data.label.slice(-8): d.data.label; })
.style("transform", "rotate(-45deg)");
// UPDATE
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Update the node attributes and style
nodeUpdate.select('circle.node')
.attr('r', 10)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
})
.attr("r", function(d){ return d.data.size/2;})
.attr('cursor', 'pointer');
// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle')
.attr('r', 1e-6);
// On exit reduce the opacity of text labels
nodeExit.select('text')
.style('fill-opacity', 1e-6);
// ****************** links section ***************************
// Update the links...
var link = svg.selectAll('path.link')
.data(links, function(d) { return d.id; });
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', "g")
.attr("class", "link")
.attr("distance", 300)
.style("stroke", function(d) { return d.data.color; })
.style("stroke-width", function(d) { return d.data.size; })
.attr('d', function(d){
var o = {x: source.x0, y: source.y0}
return diagonal(o, o)
});
// UPDATE
var linkUpdate = linkEnter.merge(link);
// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', function(d){ return diagonal(d, d.parent) });
// Remove any exiting links
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', function(d) {
var o = {x: source.x, y: source.y}
return diagonal(o, o)
})
.remove();
// Store the old positions for transition.
nodes.forEach(function(d){
d.x0 = d.x;
d.y0 = d.y;
});
};
// Creates a curved (diagonal) path from parent to the child nodes
function diagonal(s, d) {
path = `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x},
${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`
return path
};
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
};
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font: 12px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
.d3-tip {
line-height: 1;
padding: 6px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 4px;
font-size: 12px;
}
/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
content: "\25BC";
position: absolute;
text-align: center;
}
/* Style northward tooltips specifically */
.d3-tip.n:after {
margin: -2px 0 0 0;
top: 100%;
left: 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment