Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active March 8, 2016 03:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nitaku/dd4dbb5dd80cad8662d0 to your computer and use it in GitHub Desktop.
Save nitaku/dd4dbb5dd80cad8662d0 to your computer and use it in GitHub Desktop.
Treemap navigator II (dbpedia)

An improved treemap navigator, displaying the ontology of DBpedia.

Besides some DBpedia-specific customization (e.g. colors), the navigator now features a better drag behavior and a way to collapse open subtrees (it is sufficient to click on an exploded node to collapse it back).

global = {}
global.subtrees_data = []
# layout, behaviors and scales
SCALE = 200
PADDING = 0
TIP = 2
color = d3.scale.ordinal()
.domain(['Person', 'Organisation', 'Place', 'Work', 'Species', 'Event'])
.range(['#E14E5F', '#A87621', '#43943E', '#AC5CC4', '#2E99A0', '#2986EC'])
treemap = d3.layout.treemap()
.size([SCALE, SCALE])
.round(false)
.value((node) -> node.size)
# define a drag behavior
drag = d3.behavior.drag()
.origin((d) -> d)
drag.on 'dragstart', () ->
# silence other listeners (disable pan when dragging)
# see https://github.com/mbostock/d3/wiki/Drag-Behavior
d3.event.sourceEvent.stopPropagation()
drag.on 'drag', (d) ->
# update the datum of the dragged node
d.x = d3.event.x
d.y = d3.event.y
redraw()
svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height
# 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([0.1,10]) # 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(#{-SCALE/2},#{-SCALE/2})"
d3.json 'http://wafi.iit.cnr.it/webvis/tmp/dbpedia/realOntologySunburst2.json', (tree) ->
treemap.nodes(tree)
subtree_data = {tree: tree, x: 0, y: 0}
tree.subtree = subtree_data
global.subtrees_data.push subtree_data
redraw()
redraw = (duration) ->
duration = if duration? then duration else 0
# links
links = vis.selectAll('.link')
.data(global.subtrees_data.filter((subtree_data) -> subtree_data.tree.depth isnt 0))
links.enter().append('path')
.attr
class: 'link'
links.transition().duration(duration)
.attr
d: (subtree_data) ->
parent = subtree_data.tree.parent.subtree
x1 = parent.x+subtree_data.tree.x+subtree_data.tree.dx/2
y1 = parent.y+subtree_data.tree.y+subtree_data.tree.dy/2
x2 = subtree_data.x+subtree_data.tree.x+subtree_data.tree.dx/2
y2 = subtree_data.y+subtree_data.tree.y+subtree_data.tree.dy/2
alpha = Math.atan2(y1-y2,x2-x1)
tip = Math.min(TIP, subtree_data.tree.dx/2, subtree_data.tree.dy/2)
return "M#{x1} #{y1} L#{x2-tip*Math.sin(alpha)} #{y2-tip*Math.cos(alpha)} L#{x2+tip*Math.sin(alpha)} #{y2+tip*Math.cos(alpha)}"
links.exit()
.remove()
# subtrees
subtrees = vis.selectAll('.subtree')
.data(global.subtrees_data, (st) -> st.tree.depth + '_' + st.tree.name) # FIXME not universally valid
enter_subtrees = subtrees.enter().append('g')
.call(drag)
.attr
class: 'subtree'
enter_subtrees.append('rect')
.attr
class: 'handle'
x: (subtree_data) -> subtree_data.tree.x - PADDING
y: (subtree_data) -> subtree_data.tree.y - PADDING
width: (subtree_data) -> subtree_data.tree.dx + 2*PADDING
height: (subtree_data) -> subtree_data.tree.dy + 2*PADDING
.append('title')
.text((st) -> get_info(st.tree))
enter_subtrees.append('text')
.text((subtree_data) -> subtree_data.tree.name + if subtree_data.tree.parent? and subtree_data.tree.name is subtree_data.tree.parent.name then ' alone' else '')
.attr
class: 'label'
x: (subtree_data) -> subtree_data.tree.x + subtree_data.tree.dx/2
y: (subtree_data) -> subtree_data.tree.y
dy: '-0.35em'
subtrees.transition().duration(duration)
.attr
transform: (subtree_data) -> "translate(#{subtree_data.x},#{subtree_data.y})"
subtrees.exit()
.remove()
nodes = subtrees.selectAll('.node')
.data((subtree_data) -> if subtree_data.tree.children? then subtree_data.tree.children else [])
enter_nodes = nodes.enter().append('rect')
.attr
class: 'node'
x: (node) -> node.x
y: (node) -> node.y
width: (node) -> node.dx
height: (node) -> node.dy
fill: (node) -> get_color(node)
enter_nodes.on 'click', (node) ->
# disable exploding when dragging
# see https://github.com/mbostock/d3/wiki/Drag-Behavior
return if d3.event.defaultPrevented
if node.exploded is true
collapse(node)
redraw()
return
node.exploded = true
x = d3.select(this.parentNode).datum().x
y = d3.select(this.parentNode).datum().y
subtree_data = {tree: node, x: x, y: y}
node.subtree = subtree_data
global.subtrees_data.push subtree_data
redraw()
# push new subtrees away from the point of creation
subtree_data.x += 30
subtree_data.y += 30
window.requestAnimationFrame () -> redraw(1000)
enter_nodes.append('title')
.text((node) -> get_info(node))
nodes.classed('exploded', (node) -> node.exploded)
nodes.exit()
.remove()
collapse = (node) ->
if node.children?
node.children.filter((n) -> n.exploded).forEach (n) -> collapse(n)
node.exploded = false
global.subtrees_data = global.subtrees_data.filter (st) -> st.tree isnt node
get_color = (node) ->
if not node.parent?
return '#7E7F7E'
if node.name not in color.domain()
return get_color(node.parent)
return color(node.name)
get_info = (node) ->
if node.children?
subclasses = node.children.reduce(((count, n) -> if n.name isnt node.name then count+1 else count), 0)
else
subclasses = 'no'
return node.name + (if node.parent? and node.name is node.parent.name then ' alone' else '') + '\n' + subclasses + ' subclasses'+ '\n' + d3.format(',')(node.size) + ' instances'
svg {
background: white;
}
.node {
stroke-width: 1px;
shape-rendering: crispEdges;
vector-effect: non-scaling-stroke;
stroke: white;
fill-opacity: 0.8;
}
.handle {
stroke-width: 1px;
shape-rendering: crispEdges;
vector-effect: non-scaling-stroke;
fill: white;
stroke: #333;
}
.label {
text-anchor: middle;
font-family: sans-serif;
/*text-shadow: -1px 0 white, 0 1px white, 1px 0 white, 0 -1px white;*/
text-shadow: -2px 0 white, 0 2px white, 2px 0 white, 0 -2px white, -1px -1px white, 1px -1px white, 1px 1px white, -1px 1px white;
cursor: move;
}
.link {
fill: rgba(0,0,0,0.6);
stroke: none;
}
.node.exploded {
fill-opacity: 0.2;
}
.node:hover {
fill-opacity: 0.6;
cursor: pointer;
}
.handle:hover {
stroke-width: 2px;
cursor: move;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="description" content="Treemap navigator II (dbpedia)" />
<title>Treemap navigator II (dbpedia)</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<svg height="500" width="960"></svg>
<script src="index.js"></script>
</body>
</html>
(function() {
var PADDING, SCALE, TIP, collapse, color, drag, get_color, get_info, global, height, redraw, svg, treemap, vis, width, zoom, zoomable_layer,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
global = {};
global.subtrees_data = [];
SCALE = 200;
PADDING = 0;
TIP = 2;
color = d3.scale.ordinal().domain(['Person', 'Organisation', 'Place', 'Work', 'Species', 'Event']).range(['#E14E5F', '#A87621', '#43943E', '#AC5CC4', '#2E99A0', '#2986EC']);
treemap = d3.layout.treemap().size([SCALE, SCALE]).round(false).value(function(node) {
return node.size;
});
drag = d3.behavior.drag().origin(function(d) {
return d;
});
drag.on('dragstart', function() {
return d3.event.sourceEvent.stopPropagation();
});
drag.on('drag', function(d) {
d.x = d3.event.x;
d.y = d3.event.y;
return redraw();
});
svg = d3.select('svg');
width = svg.node().getBoundingClientRect().width;
height = svg.node().getBoundingClientRect().height;
svg.attr({
viewBox: "" + (-width / 2) + " " + (-height / 2) + " " + width + " " + height
});
zoomable_layer = svg.append('g');
zoom = d3.behavior.zoom().scaleExtent([0.1, 10]).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(" + (-SCALE / 2) + "," + (-SCALE / 2) + ")"
});
d3.json('http://wafi.iit.cnr.it/webvis/tmp/dbpedia/realOntologySunburst2.json', function(tree) {
var subtree_data;
treemap.nodes(tree);
subtree_data = {
tree: tree,
x: 0,
y: 0
};
tree.subtree = subtree_data;
global.subtrees_data.push(subtree_data);
return redraw();
});
redraw = function(duration) {
var enter_nodes, enter_subtrees, links, nodes, subtrees;
duration = duration != null ? duration : 0;
links = vis.selectAll('.link').data(global.subtrees_data.filter(function(subtree_data) {
return subtree_data.tree.depth !== 0;
}));
links.enter().append('path').attr({
"class": 'link'
});
links.transition().duration(duration).attr({
d: function(subtree_data) {
var alpha, parent, tip, x1, x2, y1, y2;
parent = subtree_data.tree.parent.subtree;
x1 = parent.x + subtree_data.tree.x + subtree_data.tree.dx / 2;
y1 = parent.y + subtree_data.tree.y + subtree_data.tree.dy / 2;
x2 = subtree_data.x + subtree_data.tree.x + subtree_data.tree.dx / 2;
y2 = subtree_data.y + subtree_data.tree.y + subtree_data.tree.dy / 2;
alpha = Math.atan2(y1 - y2, x2 - x1);
tip = Math.min(TIP, subtree_data.tree.dx / 2, subtree_data.tree.dy / 2);
return "M" + x1 + " " + y1 + " L" + (x2 - tip * Math.sin(alpha)) + " " + (y2 - tip * Math.cos(alpha)) + " L" + (x2 + tip * Math.sin(alpha)) + " " + (y2 + tip * Math.cos(alpha));
}
});
links.exit().remove();
subtrees = vis.selectAll('.subtree').data(global.subtrees_data, function(st) {
return st.tree.depth + '_' + st.tree.name;
});
enter_subtrees = subtrees.enter().append('g').call(drag).attr({
"class": 'subtree'
});
enter_subtrees.append('rect').attr({
"class": 'handle',
x: function(subtree_data) {
return subtree_data.tree.x - PADDING;
},
y: function(subtree_data) {
return subtree_data.tree.y - PADDING;
},
width: function(subtree_data) {
return subtree_data.tree.dx + 2 * PADDING;
},
height: function(subtree_data) {
return subtree_data.tree.dy + 2 * PADDING;
}
}).append('title').text(function(st) {
return get_info(st.tree);
});
enter_subtrees.append('text').text(function(subtree_data) {
return subtree_data.tree.name + ((subtree_data.tree.parent != null) && subtree_data.tree.name === subtree_data.tree.parent.name ? ' alone' : '');
}).attr({
"class": 'label',
x: function(subtree_data) {
return subtree_data.tree.x + subtree_data.tree.dx / 2;
},
y: function(subtree_data) {
return subtree_data.tree.y;
},
dy: '-0.35em'
});
subtrees.transition().duration(duration).attr({
transform: function(subtree_data) {
return "translate(" + subtree_data.x + "," + subtree_data.y + ")";
}
});
subtrees.exit().remove();
nodes = subtrees.selectAll('.node').data(function(subtree_data) {
if (subtree_data.tree.children != null) {
return subtree_data.tree.children;
} else {
return [];
}
});
enter_nodes = nodes.enter().append('rect').attr({
"class": 'node',
x: function(node) {
return node.x;
},
y: function(node) {
return node.y;
},
width: function(node) {
return node.dx;
},
height: function(node) {
return node.dy;
},
fill: function(node) {
return get_color(node);
}
});
enter_nodes.on('click', function(node) {
var subtree_data, x, y;
if (d3.event.defaultPrevented) {
return;
}
if (node.exploded === true) {
collapse(node);
redraw();
return;
}
node.exploded = true;
x = d3.select(this.parentNode).datum().x;
y = d3.select(this.parentNode).datum().y;
subtree_data = {
tree: node,
x: x,
y: y
};
node.subtree = subtree_data;
global.subtrees_data.push(subtree_data);
redraw();
subtree_data.x += 30;
subtree_data.y += 30;
return window.requestAnimationFrame(function() {
return redraw(1000);
});
});
enter_nodes.append('title').text(function(node) {
return get_info(node);
});
nodes.classed('exploded', function(node) {
return node.exploded;
});
return nodes.exit().remove();
};
collapse = function(node) {
if (node.children != null) {
node.children.filter(function(n) {
return n.exploded;
}).forEach(function(n) {
return collapse(n);
});
}
node.exploded = false;
return global.subtrees_data = global.subtrees_data.filter(function(st) {
return st.tree !== node;
});
};
get_color = function(node) {
var _ref;
if (node.parent == null) {
return '#7E7F7E';
}
if (_ref = node.name, __indexOf.call(color.domain(), _ref) < 0) {
return get_color(node.parent);
}
return color(node.name);
};
get_info = function(node) {
var subclasses;
if (node.children != null) {
subclasses = node.children.reduce((function(count, n) {
if (n.name !== node.name) {
return count + 1;
} else {
return count;
}
}), 0);
} else {
subclasses = 'no';
}
return node.name + ((node.parent != null) && node.name === node.parent.name ? ' alone' : '') + '\n' + subclasses + ' subclasses' + '\n' + d3.format(',')(node.size) + ' instances';
};
}).call(this);
@nitaku
Copy link
Author

nitaku commented Sep 26, 2014

Update: Setting round to false is apparently needed to solve a bad behavior of d3's treemap layout. When left to its default value, the layout produces incorrect representations (a region in the upper left corner is depicted smaller than it actually should be). I don't know if this is a known limitation of the rounding feature or a bug in d3 code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment