Before getting started, keep in mind drawing layouts with D3 is divided in two parts:
- Layout data creation and binding (e.g. position of the points) with
d3.layout()
functions - Using
SVG
element to visually represent the layout (often a<path>
element) withd3.svg
functions
Layouts are very-well documented:
- D3's documentation on layouts
- I presented some of D3s layouts in the previous talk. Look at the end of this document for other examples.
- Look at the documentation in the D3 wiki
Let's see how a force-layout works in D3.
- (Re)-Look at the design variations in nodes.html
- Investigate how nodes
graph.nodes
andgraph.links
are created - Look at the layout function
d3.layout.force()
applied to the nodes- It creates
x
andy
attributes (if they don't exist) - And updates them until the simulation reaches a stable state
- It creates
Adding more nodes
- To dynamically add new nodes:
- Update the
graph.nodes
array - Update the
force.nodes()
layout - Re-bind the data and add new circles
- Update the
- For fun, let's create new nodes for each mouse move at the pointer position
d3.select("svg").on("mousemove", function(d, i) {
graph.nodes.push({x:d3.mouse(this)[0], y:d3.mouse(this)[1], cat:Math.floor(nb_cat*Math.random())})
force.nodes(graph.nodes).start();
node = node.data(graph.nodes);
node.enter()
.append("g").attr("class", "node")
.append("circle")
.attr("r", 5)
})
- Color the nodes by category
.style("fill", function(d) { return fill(d.cat); })
The .tick()
function
- Computes the velocity decay and position; can be manually triggered
- Run in the console
force.start();force.tick();force.stop();
- Test a
.tick()
loop at the init of the page to pre-generate the layout
- Run in the console
var n = 50;
force.start();
for (var i = 0; i < n; ++i) force.tick();
force.stop();
Layout division into foci
- The
.tick()
function can also be overrided for custom layouts - Example of a multi-foci layout:
var foci = [{x: 150, y: 150}, {x: 350, y: 250}, {x: 700, y: 400}];
function tick(e) {
var k = .2 * e.alpha;
graph.nodes.forEach(function(d, i) {
d.x += (foci[d.id%foci.length].x - d.x) * k;
d.y += (foci[d.id%foci.length].y - d.y) * k;
});
graph_update(0);
}
alpha
is the cooling parameter of the layout- Disable links to let nodes propagate correctly
- Show nodes color to reveal categories cluster
d3.selectAll("circle").style("fill", function(d) { return fill(d.cat); })
- As mentionned earlier, generating the layout is different than drawing it
- For instance, look at this tree layout
Understanding SVG
shape drawing functions
- Documentation for all the SVG shapes D3 can draw
- Most of them return
<path>
elements - As a reminder, a line in SVG:
svg.selectAll(".line")
.data([{source: {x: 10, y: 10}, target:{x: 300, y: 300}}])
.enter().append("line")
.attr("stroke", "steelblue")
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; })
- Alternatively, using the D3 SVG function to draw a line:
var data = [{source: {x: 10, y: 10}, target:{x: 300, y: 300}}];
var line = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("linear");
svg.append("path")
.attr("d", line(d3.values(data[0])))
.attr("stroke", "red")
.attr("stroke-width", 1)
.attr("fill", "none");
- Using the diagonal function, but we need to provide it with some data
- The path generator expects the source and target points of the point
var diagonal = d3.svg.diagonal()
.source(data[0].source)
.target(data[0].target)
svg.append("path")
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("d", diagonal)
- You can also bind the data
var diagonal = d3.svg.diagonal()
svg.selectAll(".diag").data(data).enter().append("path").attr("class", "diag")
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("d", diagonal)
- Let's use those SVG functions to draw the tree
- But before, generate a tree-layout with the points
Data Structure
- D3 layouts requires some specific data structure. For trees:
{
"name": "Root",
"children": [
{
"name": "AA",
"children": [
{"name": "AB", "size": 1,
"children": [
{"name": "AAA", "size": 2},
{"name": "AAB", "size": 3},
{"name": "AAC", "size": 4}
]},
{"name": "C", "size": 5},
{"name": "D", "size": 6},
{"name": "E", "size": 7}
]
}]
};
- Note that the root node is the first node of the data
- Let's generate the nodes layout
var tree = d3.layout.tree()
.size([300,250]);
var nodes = tree.nodes(data);
var links = tree.links(nodes);
- Oh wait, didn't we just generate data??
- Let's bind them to circles!
var node = svg.selectAll("g.node")
.data(nodes)
.enter().append("g")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
node.append("circle")
.attr("r", function(d) { return d.children ? d.children.length : d.size;})
.attr("fill", "steelblue")
- Let's now link the nodes with a SVG line
var line = svg.selectAll(".line")
.data(links)
.enter().append("line")
.attr("class", "line")
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; })
- Better use a
d3.svg.diagonal()
SVG function as seen before - The magic happens within
tree.links(nodes)
var link = svg.selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", diagonal)
Vertical/Horizontal tree layout
- Change both layout and SVG drawing parameters to make it horizontal
- Flip the x and y coordinate for the circle nodes
- For the path, it's more tricky
- Create an accessor function
var diagonal_rotated = d3.svg.diagonal().projection(function(d) { return [d.y, d.x]});
link.transition().delay(1000).duration(1000)
.attr("d", diagonal_rotated)
- Adjust the node labels, show the
d.depth
Radial tree layout
- Switch to a radial layout with a
.separation()
var tree = d3.layout.tree()
.size([360, diameter / 2 - 120])
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
- Update the nodes with a
rotate()
transform
node.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ") translate(" + d.y + ")";})
- Update the diagonal function as well
var diagonal = d3.svg.diagonal.radial()
.projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
- Your turn: Implement the transition with the non-radial layout!
- Test with
d3.layout.cluster()
layout (instead ofd3.layout.tree()
)
Nested layouts, they belong to the hierarchical layouts family
- Treemap
- Circle packing
- Example
- Documentation
- To use the
d3.layout.pack()
data need to have a value attribute - Radius of circles can encode various things: depth, ..
- You can make a bubble chart by keeping the leave nodes
.attr("opacity", function(d) { return d.children ? 0 : .2;})