Skip to content

Instantly share code, notes, and snippets.

@dagrende
Last active March 3, 2016 06:32
Show Gist options
  • Save dagrende/267ce65d2e4bb9e1dd49 to your computer and use it in GitHub Desktop.
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

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.

<!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