Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active November 8, 2019 07:12
Show Gist options
  • Save nitaku/142545ea7bac1866327a to your computer and use it in GitHub Desktop.
Save nitaku/142545ea7bac1866327a to your computer and use it in GitHub Desktop.
Cascaded treemap

A modified implementation of Cascaded Treemaps. With respect to the original algorithm, this implementation has a better (in my opinion) cascading effect, which ensures no zig-zag behaviors in corners.

Unfortunately, this method introduces a very ugly layout error (visible on the bottom right corner, where an elongated rectangle is displayed as outside its parent container). A possible solution would be to reintroduce the same fix described in the paper linked above, but this would also reintroduce the zig-zag behavior in such corner cases.

OFFSET = 4
HEADER = 8
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.size)
.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.size, b.size)
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,5])
.range([d3.hcl(320,0,20), d3.hcl(200,70,80)])
.interpolate(d3.interpolateHcl)
d3.json 'http://wafi.iit.cnr.it/webvis/tmp/flare.json', (tree) ->
# needed for sorting
aggregate = (node) ->
if node.children?
node.children.forEach aggregate
node.size = d3.sum node.children, (d) -> d.size
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) -> d.dx - 2*OFFSET*d.height_r # custom algorithm, part II
height: (d) -> d.dy - 2*OFFSET*d.height_b
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.name
.attr
class: 'label'
x: (d) -> d.x
y: (d) -> d.y
dx: 2
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: 8px;
fill: white;
pointer-events: none;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Cascaded treemaps</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<svg></svg>
<script src="index.js"></script>
</body>
</html>
// Generated by CoffeeScript 1.10.0
(function() {
var HEADER, OFFSET, color, h, height, svg, treemap, vis, w, width, zoom, zoomable_layer;
OFFSET = 4;
HEADER = 8;
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.size;
}).padding([OFFSET + HEADER, OFFSET, OFFSET, OFFSET]).sort(function(a, b) {
h = d3.ascending(a.height, b.height);
if (h === 0) {
return d3.ascending(a.size, b.size);
}
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, 5]).range([d3.hcl(320, 0, 20), d3.hcl(200, 70, 80)]).interpolate(d3.interpolateHcl);
d3.json('http://wafi.iit.cnr.it/webvis/tmp/flare.json', function(tree) {
var aggregate, cells, compute_height, compute_heights, data, labels;
aggregate = function(node) {
if (node.children != null) {
node.children.forEach(aggregate);
return node.size = d3.sum(node.children, function(d) {
return d.size;
});
}
};
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);
cells.enter().append('rect').attr({
"class": 'cell',
x: function(d) {
return d.x;
},
y: function(d) {
return d.y;
},
width: function(d) {
return d.dx - 2 * OFFSET * d.height_r;
},
height: function(d) {
return d.dy - 2 * OFFSET * d.height_b;
},
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;
});
labels = vis.selectAll('.label').data(data.filter(function(d) {
return (d.children != null) && d.children.length > 0;
}));
return labels.enter().append('text').text(function(d) {
return d.name;
}).attr({
"class": 'label',
x: function(d) {
return d.x;
},
y: function(d) {
return d.y;
},
dx: 2,
dy: '1em'
});
});
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment