The renderHierarchy function below, makes it easy to render any data structure as a hierarchy of svg elements. The example code renders the same nodes from two data structures that inpmements the same hierarchy - one natural hierarchy and one flat list where a parent attribute is used to define the hierarchy.
Last active
March 3, 2016 06:32
-
-
Save dagrende/267ce65d2e4bb9e1dd49 to your computer and use it in GitHub Desktop.
Render d3 node hierarchy from any data structure and a first layout manager sketch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
rect, circle { | |
fill: none; | |
stroke: black; | |
} | |
</style> | |
<body> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/lodash/4.5.1/lodash.min.js"></script> | |
<script> | |
var margin = {top: 10, right: 30, bottom: 30, left: 30}, | |
width = 960 - margin.left - margin.right, | |
height = 500 - margin.top - margin.bottom; | |
var svg = d3.select("body").append("svg") | |
.attr("width", width + margin.left + margin.right) | |
.attr("height", height + margin.top + margin.bottom) | |
.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
// hierarchical data | |
var hierData = [ | |
{id: '1', type: 'a', | |
children: [ | |
{id: '2', type: 'b', | |
children: [ | |
{id: '3', type: 'a'}, | |
{id: '4', type: 'b'} | |
] | |
}, | |
{id: '5', type: 'b'} | |
] | |
} | |
]; | |
// flat data with the same hierarchy expressed by parent attribute | |
var flatData = [ | |
{id: '1', type: 'a'}, | |
{id: '2', type: 'b', parent: '1'}, | |
{id: '3', type: 'a', parent: '2'}, | |
{id: '4', type: 'b', parent: '2'}, | |
{id: '5', type: 'b', parent: '1'}, | |
]; | |
var defaultLayout = hboxLayout(); | |
renderHierarchy(svg, getHierChildren, getRenderer); | |
//renderHierarchy(svg, getFlatChildren, getRenderer); // render the same for flat data | |
// works like _.groupBy | |
function groupBy(a, f) { | |
var m = new Map(); | |
_.forEach(a, function(d) { | |
var fd = f(d); | |
var mfd = m[fd]; | |
if (mfd) { | |
mfd.push(d); | |
} else { | |
m[fd] = [d]; | |
} | |
}); | |
return m; | |
} | |
// Renders all objects under the d3 wrapped parentElement. | |
// getChildren(parentData) returns all data elements that are immediate children of parentData, or top level data items if parentData is falsy (undefined, null or false). | |
// getRenderer(dataObject) returns a renderer for dataObject | |
function renderHierarchy(parentElement, getChildren, getRenderer) { | |
function renderChildren(parentEl, parent) { | |
var dataByType = groupBy(getChildren(parent), getRenderer); | |
_.forEach(dataByType, (typeData) => { | |
var nodes = getRenderer(typeData[0])(parentEl, typeData); | |
nodes.each(function(d) { | |
renderChildren(d3.select(this), d); | |
this.layout && this.layout(d3.select(this)); | |
}); | |
}); | |
} | |
renderChildren(parentElement, undefined); | |
} | |
// Returns a renderer for data object d, that creates, updates or removes elements. | |
// The call renderer(parentElement, dataObjectArray) should return a d3 selection of elements - both new and existing. | |
// parentElement is a d3 selection of one parent element, and | |
// dataObjectArray is all data objects to be rendered by this renderer. | |
function getRenderer(d) { | |
return {'a': renderRectangleNodes, 'b': renderRoundedRectangleNodes}[d.type]; | |
} | |
// Returns children of parentData, | |
// or top level objects if no parentData. | |
function getHierChildren(parentData) { | |
if (parentData) { | |
return parentData.children | |
} else { | |
return hierData; | |
} | |
} | |
// Returns all objects with parent attribute equal to id of parentData, | |
// or all objects with no parent attribute if no parentData. | |
function getFlatChildren(parentData) { | |
if (parentData) { | |
return _.filter(flatData, d => (d.parent == parentData.id)); | |
} else { | |
return _.filter(flatData, d => !d.parent); | |
} | |
} | |
// Renders rectangular elements under parentElement corresponding to dataArray items. | |
// Assumes each data item has an id attribute. | |
function renderRectangleNodes(parentElement, dataArray) { | |
var nodes = parentElement.selectAll('.a') | |
.data(dataArray, d => d.id); | |
nodes.enter().append('g') | |
.attr('class', 'node a') | |
.each(function(d) { | |
this.size = defaultSizeSetter; | |
this.layout = hboxLayout().fill(true); | |
}) | |
.append('rect') | |
.attr('width', 50) | |
.attr('height', 30); | |
nodes.exit().remove(); | |
return nodes; | |
} | |
// Renders rounded rectangle elements under parentElement corresponding to dataArray items. | |
// Assumes each data item has an id attribute. | |
function renderRoundedRectangleNodes(parentElement, data) { | |
var nodes = parentElement.selectAll('.b') | |
.data(data, d => d.id); | |
nodes.enter().append('g') | |
.attr('class', 'node b') | |
.each(function(d) { | |
this.size = defaultSizeSetter; | |
this.layout = vboxLayout().margin(10); | |
}) | |
.append('rect') | |
.attr('width', 50) | |
.attr('height', 30) | |
.attr('rx', 5) | |
.attr('ry', 5); | |
nodes.exit().remove(); | |
return nodes; | |
} | |
// sets size of node to size | |
// node is d3 wrapped, size is {width: , height: } | |
function defaultSizeSetter(node, size) { | |
var rectNode = node.select('rect'); | |
if (!size) { | |
return {width: rectNode.attr('width'), height: rectNode.attr('height')}; | |
} | |
rectNode.attr(size); | |
} | |
// returns a function layout(node), that when called with a d3 g element, lines up its children g elements horisontally and sizes its rect around them | |
function hboxLayout() { | |
var margin = 5, fill = false, positionSetter = defaultPositionSetter; | |
function layout(node) { | |
var x = margin, maxHeight = 0; | |
var gChildren = d3.selectAll(_.filter(node.node().childNodes, function(node) {return node.tagName == 'g'})); | |
gChildren.each(function(d) { | |
positionSetter(d3.select(this), {x: x, y: margin}); | |
var bBox = this.getBBox(); | |
x += bBox.width + margin; | |
maxHeight = Math.max(maxHeight, bBox.height); | |
}); | |
if (fill) { | |
gChildren.each(function(d) { | |
this.size(d3.select(this), {height: maxHeight}); | |
}); | |
} | |
node.node().size(node, | |
{width: Math.max(30, Math.max(30, x)), | |
height: Math.max(30, Math.max(30, margin + maxHeight + margin))}); | |
} | |
layout.positionSetter = function(ps) { | |
if (!ps) {return positionSetter;} | |
positionSetter = ps; | |
return layout; | |
}; | |
layout.margin = function(size) { | |
if (!size) {return margin;} | |
margin = size; | |
return layout; | |
}; | |
layout.fill = function(t) { // true sets all child height to max child height | |
if (!t) {return fill;} | |
fill = t; | |
return layout; | |
}; | |
return layout; | |
} | |
// get or set position of node to pos | |
// node is d3 wrapped, pos is {x: , y: } | |
function defaultPositionSetter(node, pos) { | |
if (!pos) { | |
var tr = node.attr('transform'); | |
var p = utils.getTranslation(pos); | |
return p || {x: 0, y: 0}; | |
} | |
node.attr("transform", "translate(" + pos.x + "," + pos.y + ")") | |
} | |
// returns a function layout(node), that when called with a d3 g element, lines up its children g elements horisontally and sizes its rect around them | |
function vboxLayout() { | |
var margin = 5, fill = false, positionSetter = defaultPositionSetter; | |
function layout(node) { | |
var y = margin, maxWidth = 0; | |
var gChildren = d3.selectAll(_.filter(node.node().childNodes, function(node) {return node.tagName == 'g'})); | |
gChildren.each(function(d) { | |
positionSetter(d3.select(this), {x: margin, y: y}); | |
var bBox = this.getBBox(); | |
y += bBox.height + margin; | |
maxWidth = Math.max(maxWidth, bBox.height); | |
}); | |
if (fill) { | |
gChildren.each(function(d) { | |
this.size(d3.select(this), {width: maxWidth}); | |
}); | |
} | |
node.node().size(node, | |
{width: Math.max(30, margin + maxWidth + margin), | |
height: Math.max(30, y)}); | |
} | |
layout.positionSetter = function(ps) { | |
if (!ps) {return positionSetter;} | |
positionSetter = ps; | |
return layout; | |
}; | |
layout.margin = function(size) { | |
if (!size) {return margin;} | |
margin = size; | |
return layout; | |
}; | |
layout.fill = function(t) { // true sets all child widths to max child width | |
if (!t) {return fill;} | |
fill = t; | |
return layout; | |
}; | |
return layout; | |
} | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment