A cascaded treemap showing the hierarchy computed by a power graph analysis of a graph containing about 50K nodes and 100K links. The resulting treemap contains many flat regions (i.e., regions with many nodes at the same depth).
// Generated by CoffeeScript 1.10.0
(function() {
var HEADER, OFFSET, color, h, height, svg, treemap, vis, w, width, zoom, zoomable_layer;
OFFSET = 0.1;
HEADER = 0;
svg = d3.select('svg');
width = svg.node().getBoundingClientRect().width;
height = svg.node().getBoundingClientRect().height;
w = width - 40;
h = height - 40;
treemap = d3.layout.treemap().size([w, h]).value(function(node) {
return node.value;
}).padding([OFFSET + HEADER, OFFSET, OFFSET, OFFSET]).sort(function(a, b) {
h = d3.ascending(a.height, b.height);
if (h === 0) {
return d3.ascending(a.value, b.value);
}
return h;
});
svg.attr({
viewBox: (-width / 2) + " " + (-height / 2) + " " + width + " " + height
});
zoomable_layer = svg.append('g');
zoom = d3.behavior.zoom().scaleExtent([-Infinity, Infinity]).on('zoom', function() {
return zoomable_layer.attr({
transform: "translate(" + (zoom.translate()) + ")scale(" + (zoom.scale()) + ")"
});
});
svg.call(zoom);
vis = zoomable_layer.append('g').attr({
transform: "translate(" + (-w / 2) + "," + (-h / 2) + ")"
});
color = d3.scale.linear().domain([0, 17]).range([d3.hcl(320, 0, 20), d3.hcl(200, 70, 80)]).interpolate(d3.interpolateHcl);
d3.json('isolario_power.json', function(tree) {
var aggregate, cells, compute_height, compute_heights, data;
aggregate = function(node) {
if (node.children != null) {
node.children.forEach(aggregate);
return node.value = d3.sum(node.children, function(d) {
return d.value;
});
}
};
aggregate(tree);
compute_height = function(node) {
if (node.children != null) {
node.children.forEach(compute_height);
return node.height = 1 + d3.max(node.children, function(d) {
return d.height;
});
} else {
return node.height = 0;
}
};
compute_height(tree);
data = treemap.nodes(tree);
compute_heights = function(node) {
var bchildren, bmax, rchildren, rmax;
if (node.children != null) {
node.children.forEach(compute_heights);
rmax = d3.max(node.children, function(c) {
return c.x + c.dx;
});
rchildren = node.children.filter(function(d) {
return (d.x + d.dx) >= rmax;
});
node.height_r = 1 + d3.max(rchildren, function(d) {
return d.height_r;
});
bmax = d3.max(node.children, function(c) {
return c.y + c.dy;
});
bchildren = node.children.filter(function(d) {
return (d.y + d.dy) >= bmax;
});
return node.height_b = 1 + d3.max(bchildren, function(d) {
return d.height_b;
});
} else {
node.height_r = 0;
return node.height_b = 0;
}
};
compute_heights(tree);
data.sort(function(a, b) {
return d3.ascending(a.depth, b.depth);
});
cells = vis.selectAll('.cell').data(data);
return cells.enter().append('rect').attr({
"class": 'cell',
x: function(d) {
return d.x;
},
y: function(d) {
return d.y;
},
width: function(d) {
w = d.dx - 2 * OFFSET * d.height_r;
if (w < 0) {
return 0;
} else {
return w;
}
},
height: function(d) {
h = d.dy - 2 * OFFSET * d.height_b;
if (h < 0) {
return 0;
} else {
return h;
}
},
fill: function(d) {
return color(d.depth);
},
stroke: function(d) {
return color(d.depth + 0.5);
}
}).classed('leaf', function(d) {
return (d.children == null) || d.children.length === 0;
});
});
}).call(this);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Isolario Powergraph Treemap</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="//d3js.org/d3.v3.min.js"></script>
</head>
<body>
<svg></svg>
<script src="index.js"></script>
</body>
</html>
OFFSET = 1
HEADER = 0
svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height
w = width - 40
h = height - 40
treemap = d3.layout.treemap()
.size([w, h])
.value((node) -> node.value)
.padding([OFFSET+HEADER,OFFSET,OFFSET,OFFSET])
.sort (a,b) ->
# taller subtrees first, then larger subtrees first
h = d3.ascending(a.height, b.height)
if h is 0
return d3.ascending(a.value, b.value)
return h
# translate the viewBox to have (0,0) at the center of the vis
svg
.attr
viewBox: "#{-width/2} #{-height/2} #{width} #{height}"
# append a group for zoomable content
zoomable_layer = svg.append('g')
# define a zoom behavior
zoom = d3.behavior.zoom()
.scaleExtent([-Infinity,Infinity]) # min-max zoom
.on 'zoom', () ->
# GEOMETRIC ZOOM
zoomable_layer
.attr
transform: "translate(#{zoom.translate()})scale(#{zoom.scale()})"
# bind the zoom behavior to the main SVG
svg.call(zoom)
# group the visualization
vis = zoomable_layer.append('g')
.attr
transform: "translate(#{-w/2},#{-h/2})"
color = d3.scale.linear()
.domain([0,17])
.range([d3.hcl(320,0,20), d3.hcl(200,70,80)])
.interpolate(d3.interpolateHcl)
d3.json 'isolario_power.json', (tree) ->
# needed for sorting
aggregate = (node) ->
if node.children?
node.children.forEach aggregate
node.value = d3.sum node.children, (d) -> d.value
aggregate(tree)
# needed for sorting
compute_height = (node) ->
if node.children?
node.children.forEach compute_height
node.height = 1+d3.max node.children, (d) -> d.height
else
node.height = 0
compute_height(tree)
data = treemap.nodes(tree)
# needed for the layout (custom algorithm, part I)
compute_heights = (node) ->
if node.children?
node.children.forEach compute_heights
rmax = d3.max node.children, (c) -> c.x+c.dx
rchildren = node.children.filter (d) -> (d.x+d.dx) >= rmax
node.height_r = 1+d3.max rchildren, (d) -> d.height_r
bmax = d3.max node.children, (c) -> c.y+c.dy
bchildren = node.children.filter (d) -> (d.y+d.dy) >= bmax
node.height_b = 1+d3.max bchildren, (d) -> d.height_b
else
node.height_r = 0
node.height_b = 0
compute_heights(tree)
# Lu and Fogarty algorithm (sort of)
# walk = (node) ->
# if node.children? and node.children.length > 0
# node.children.forEach walk
# node.x = d3.min(node.children, (d) -> d.x)
# node.y = d3.min(node.children, (d) -> d.y)
# node.dx = d3.max(node.children, (d) -> d.x+d.dx) - node.x
# node.dy = d3.max(node.children, (d) -> d.y+d.dy) - node.y# + HEADER
# node.x -= OFFSET
# node.y -= OFFSET# + HEADER
# walk(tree)
data.sort (a,b) -> d3.ascending(a.depth, b.depth)
cells = vis.selectAll '.cell'
.data data
cells.enter().append 'rect'
.attr
class: 'cell'
x: (d) -> d.x
y: (d) -> d.y
width: (d) ->
w = d.dx - 2*OFFSET*d.height_r # custom algorithm, part II
return if w < 0 then 0 else w
height: (d) ->
h = d.dy - 2*OFFSET*d.height_b
return if h < 0 then 0 else h
fill: (d) -> color(d.depth)
stroke: (d) -> color(d.depth+0.5)
.classed 'leaf', (d) -> not d.children? or d.children.length is 0
# labels = vis.selectAll '.label'
# .data data.filter (d) -> d.children? and d.children.length > 0
#
# labels.enter().append 'text'
# .text (d) -> d.data.name
# .attr
# class: 'label'
# x: (d) -> d.x
# y: (d) -> d.y
# dx: 0.1
# dy: '1em'
html, body {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
svg {
width: 100%;
height: 100%;
background: black;
}
.cell {
stroke-width: 0;
}
.leaf {
stroke-width: 1;
vector-effect: non-scaling-stroke;
}
.label {
font-family: sans-serif;
font-size: 1px;
fill: black;
pointer-events: none;
}