Skip to content

Instantly share code, notes, and snippets.

@djjupa
Last active July 29, 2016 01:10
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save djjupa/5655723 to your computer and use it in GitHub Desktop.
Collapsable force layout with nested selections

The json data has to have nested elements called "children"

{
"name": "flare",
"logowidth": "36",
"logoheight": "36",
"root": "true",
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
},
"children": [
{
"name": "analytics",
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1",
"id": 1 },
{ "attribute": "attribute2",
"type": "type2",
"id": 2},
{ "attribute": "attribute3",
"type": "type3",
"id": 3}
]
},
"children": [
{
"name": "cluster",
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
},
"children": [
{"name": "AgglomerativeCluster", "size": 3938,
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
}
},
{"name": "CommunityStructure", "size": 3812,
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
}
},
{"name": "HierarchicalCluster", "size": 6714,
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
}
},
{"name": "MergeEdge", "size": 743,
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
}
}
]
},
{
"name": "graph",
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
},
"children": [
{
"name": "BetweennessCentrality", "size": 3534,
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
}
},
{
"name": "SpanningTree", "size": 3416,
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
}
}
]
},
{
"name": "optimization",
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
},
"children": [
{
"name": "AspectRatioBanker", "size": 7074,
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
}
}
]
}
]
},
{
"name": "data",
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
},
"children": [
{
"name": "converters",
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
},
"children": [
{"name": "JSONConverter",
"size": 2220,
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
}
}
]
}
]
},
{
"name": "display",
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
},
"children": [
{"name": "LineSprite", "size": 1732,
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
}
},
{
"name": "RectSprite", "size": 3623,
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
}
},
{"name": "TextSprite", "size": 10066,
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
}
}
]
},
{
"name": "flex",
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
},
"children": [
{
"name": "FlareVis", "size": 4116,
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
}
},
{
"name": "scale",
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
},
"children": [
{
"name": "IScaleMap", "size": 2105,
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
}
},
{
"name": "LinearScale", "size": 1316,
"personaldata": {
"children": [
{ "attribute": "attribute1",
"type": "type1" },
{ "attribute": "attribute2",
"type": "type2" },
{ "attribute": "attribute3",
"type": "type3" }
]
}
}
]
}
]
}
]
}
<meta charset="utf-8" />
<link href='http://fonts.googleapis.com/css?family=Life+Savers|Didact+Gothic' rel='stylesheet' type='text/css'>
<meta charset="utf-8" />
<style>
.node {
stroke: #009900;
stroke-width: 1.5px;
color: #009900;
}
.node text {
pointer-events: none;
font: 15px sans-serif;
stroke-width: 0px;
}
.link {
stroke: #999;
/* stroke-opacity: 2.6; */
}
path.link {
fill: none;
stroke-width: 2px;
}
marker#end {
fill: #999;
}
line {
stroke: #000;
stroke-width: 1.5px;
}
</style>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<body>
<script>
var w = 1200,
h = 800,
maxNodeSize = 50,
root;
var vis;
var force = d3.layout.force();
vis = d3.select("body").append("svg");
d3.json("collapsableforcelayoutwithicons.json", function(json) {
root = json;
root.fixed = true;
root.x = w / 2;
root.y = 120;
// Build the arrow
var defs = vis.insert("svg:defs").selectAll("marker")
.data(["end"]);
defs.enter().append("svg:marker")
.attr("id", "end") // As explained here: http://www.d3noob.org/2013/03/d3js-force-directed-graph-example-basic.html
//.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
update();
});
/**
*
*/
function update() {
console.log('entered update');
nodes = flatten(root),
links = d3.layout.tree().links(nodes);
// Start the force layout.
force.nodes(nodes)
.links(links)
.gravity(0.05)
.charge(-1500)
.linkDistance(100)
.friction(0.5)
.linkStrength(function(l, i) {return 1; })
.size([w, h])
.on("tick", tick)
.start();
var i = 0;
// Update the paths
var path = vis.selectAll("path.link")
.data(links, function(d) { return d.target.id; });
path.enter().append("svg:path")
.attr("class", "link")
.attr("marker-end", "url(#end)")
.style("stroke", "#ccc");
// Exit any old paths.
path.exit().remove();
// Update the nodes…
var node = vis.selectAll("g.node")
.data(nodes, function(d) { return d.id; });
// Enter any new nodes.
var nodeEnter = node.enter().append("svg:g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.on("click", click)
//.on("mouseover", showNodeDetails)
//.on("mouseout", hideNodeDetails)
.call(force.drag)
.each(getPersonalDataNodes)
;
// Add the circles to the nodes
nodeEnter.append("svg:circle")
.attr("r", setCircleRadius)
.style("fill", "white")
.style("stroke", color)
.style("stroke-width", function(d) { return d.root ? 0 : 1.5})
.style("opacity", function(d) { return d.root ? 0 : 1})
;
// Add text to the node (as defined by the json file)
nodeEnter.append("svg:text")
.attr("class", "nodetext")
.attr("text-anchor", "middle")
.attr("dx", function(d) { return (d.logowidth/2)|| 16/2; })
.attr("dy", function(d) { return (d.logoheight/2 + 15) || 16/2 + 15; })
.text(function(d) { return d.name; });
//Add an image to the node
nodeEnter.append("svg:image")
.attr("xlink:href", function(d) { return d.logo;})
.attr("x", function(d) { return (0 - (d.logowidth/2)) || -16/2;})
.attr("y", function(d) { return (0 - (d.logoheight/2)) || -16/2;})
.attr("height", function(d) { return d.logoheight || 16;})
.attr("width", function(d) { return d.logowidth || 16;})
//.attr("height", 26)
//.attr("width", 26);
;
// Exit any old nodes.
node.exit().remove();
// Re-select for update.
path = vis.selectAll("path.link");
node = vis.selectAll("g.node");
/*
* The tick function for the un-nested nodes
*/
function tick() {
// Draw curved paths
path.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy); //*(Math.random() < 0.5 ? 1 : -1)*(Math.random()/10);
return "M" + d.source.x + ","
+ d.source.y
+ "A" + dr + ","
+ dr + " 0 0,1 "
+ d.target.x + ","
+ d.target.y;
});
node.attr("transform", nodeTransform );
}
}
/**
* Display the children of the nested element:
* - Position and fix the root node
* - Flatten the array of nodes (store it on )
*
*/
function getPersonalDataNodes(treenode, i) {
//console.log('entered getPersonalDataNodes');
var vis2 = d3.select(this);
// Set the fix root
treenode.personaldata.fixed = true;
treenode.personaldata.x = 0;
treenode.personaldata.y = 0;
var icon_size = 16;
// Flatten the structure - get the array of personaldata objects
var personaldataNodes = flatten(treenode.personaldata);
// Create the links from the nodes to the root - using the "tree"
// https://github.com/mbostock/d3/wiki/Tree-Layout
var sublinks = d3.layout.tree().links(personaldataNodes);
// Start the force layout.
var forcepii = d3.layout.force()
.nodes(personaldataNodes)
.links(sublinks)
.gravity(-0.005)
.linkDistance(50)
.start();
//
var piilinks = vis2.selectAll("line.piilink")
.data(sublinks, function(d) { return d.target.id; }); // link nodes according to the nodes created during flattning
// Attach a line
piilinks.enter().append("svg:line")
.attr("class", "piilink")
.style("stroke", "#ccc");
// Exit any old paths.
piilinks.exit().remove();
// Select the nodes
var pii = vis2.selectAll("circle.pii")
.data(personaldataNodes, function(d) { return d.id; });
// Group them together
var piiEnter = pii.enter().append("svg:g")
.attr("class", "piinode")
.attr("transform", function(treenode) { return "translate(" + treenode.x + "," + treenode.y + ")"; })
.call(forcepii.drag)
;
// Add a circle
piiEnter.append("svg:circle")
.attr("class", "pii")
.attr("r", 5)
.style("fill", "lightblue")
.style("stroke", "black")
;
// Add text to the node (as defined by the json file)
piiEnter.append("svg:text")
.attr("class", "piinodetext")
.attr("text-anchor", "middle")
.attr("dx", function(treenode) { return 16/2; })
.attr("dy", function(treenode) { return 16/2 + 15; })
.text(function(treenode) { return treenode.type; });
pii.exit().remove();
// Restart
sublinks = vis2.selectAll("line.piilinks");
personaldataNodes = vis2.selectAll("circle.pii");
/*
* Tick function for the nested arrays
*/
forcepii.on("tick", function(treenode){
piilinks.attr("x1", function(treenode) { return treenode.source.x; })
.attr("y1", function(treenode) { return treenode.source.y; })
.attr("x2", function(treenode) { return treenode.target.x; })
.attr("y2", function(treenode) { return treenode.target.y; });
pii.attr("transform", function(treenode) { return "translate(" + treenode.x + "," + treenode.y + ")"; })
});
}
/**
* Color leaf nodes dark blue #3182bd, and packages white or blue.
* dark blue #3182bd
* light blue #c6dbef
* orange #fd8d3c
*/
function color(d) {
return d._children ? "#3182bd" : d.children ? "c6dbef" : "fd8d3c";
}
/**
* Toggle children on click.
*/
function click(d) {
//console.log('entered click');
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update();
}
/**
* Returns a list of all nodes under the root.
*/
function flatten(root) {
var nodes = [];
var i = 0;
function recurse(node) {
if (node.children)
node.children.forEach(recurse);
if (!node.id)
node.id = ++i;
nodes.push(node);
}
recurse(root);
return nodes;
}
/**
* Gives the coordinates of the border for keeping the nodes inside a frame
* http://bl.ocks.org/mbostock/1129492
*/
function nodeTransform(d) {
d.x = Math.max(maxNodeSize, Math.min(w - (d.logowidth/2 || 16/2), d.x));
d.y = Math.max(maxNodeSize, Math.min(h - (d.logoheight/2 || 16/2), d.y));
return "translate(" + d.x + "," + d.y + ")";
}
/**
*
* @param {Object} d
*/
function setCircleRadius(d) {
if(d.logowidth)
radius = d.logowidth/2 + 15;
else
radius = 16/2 + 15;
d.radius = radius;
//console.log('d.id=' + d.id + 'radius=' + radius);
return d.radius;
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment