The json data has to have nested elements called "children"
Last active
July 29, 2016 01:10
Collapsable force layout with nested selections
This file contains 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
{ | |
"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" } | |
] | |
} | |
} | |
] | |
} | |
] | |
} | |
] | |
} |
This file contains 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
<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