A first step towards the creation of a map of DBpedia. This experiment shows the first-level classes of the DBpedia ontology and the links between them. You can zoom in, hover and click on links and nodes to obtain more detailed information.
A node’s area grows linearly with the amount of entities of the relative class, while link thickness is representative of the amount of triples that have entities of the corresponding classes as subject and object (the encoding is nonlinear to make the diagram readable - so the thickness is exaggerated). Directionality is conveyed with animation (as in this example).
Because hierarchical links are not depicted, this is an implicit visualization (Schulz et al. 2010) of the ontology tree. Only the first level is shown for the moment - deeper nodes will be drawn inside their parents (circular treemap). At the same time, it is an explicit visualization of the graph defined by the triples. We decided to trade some compactness (treemaps’ best feature) with plenty of room to show the important information carried by links. Moreover, because links are bundled and attracted to the center, the hierarchy is more explicitly shown.
Many issues are still open:
(function() {
var height, svg, vis, width, zoom;
svg = d3.select('svg');
width = svg.node().getBoundingClientRect().width;
height = svg.node().getBoundingClientRect().height;
svg.attr({
viewBox: "" + (-width / 2) + " " + (-height / 2 - 40) + " " + width + " " + height
});
vis = svg.append('g');
zoom = d3.behavior.zoom().scaleExtent([1, 8]).on('zoom', function() {
vis.attr({
transform: "translate(" + (zoom.translate()) + ")scale(" + (zoom.scale()) + ")"
});
return vis.selectAll('.semantic_zoom').attr({
transform: "scale(" + (1 / zoom.scale()) + ")"
});
});
svg.call(zoom);
d3.json('http://wafi.iit.cnr.it/webvis/tmp/DBpediaClassRelations.json', function(graph_data) {
var MAX_WIDTH, circular, circular_layout, compute_degree, labels, link_thickness, links, links_layer, list_links, max, new_link_elements, new_node_elements, nodes, nodes_layer, objectify, radius, sankey, tension;
graph_data.links = graph_data.links.filter(function(l) {
return l.weight > 0;
});
objectify = function(graph) {
return graph.links.forEach(function(l) {
return graph.nodes.forEach(function(n) {
if (l.source === n.id) {
l.source = n;
}
if (l.target === n.id) {
return l.target = n;
}
});
});
};
objectify(graph_data);
list_links = function(graph) {
return graph.nodes.forEach(function(n) {
n.links = graph.links.filter(function(link) {
return link.source === n || link.target === n;
});
return n.links.sort(function(a, b) {
var a_dir, b_dir, cmp, other_a_id, other_b_id;
if (a.source === n) {
other_a_id = a.target.id;
a_dir = 'target';
} else {
other_a_id = a.source.id;
a_dir = 'source';
}
if (b.source === n) {
other_b_id = b.target.id;
b_dir = 'target';
} else {
other_b_id = b.source.id;
b_dir = 'source';
}
cmp = other_a_id - other_b_id;
if (cmp === 0) {
if (a_dir === 'target') {
return 1;
} else {
return -1;
}
} else {
return cmp;
}
});
});
};
list_links(graph_data);
compute_degree = function(graph) {
return graph.nodes.forEach(function(n) {
return n.degree = d3.sum(n.links, function(link) {
return link.weight;
});
});
};
compute_degree(graph_data);
MAX_WIDTH = 18;
max = d3.max(graph_data.nodes, function(n) {
return n.degree;
});
link_thickness = d3.scale.sqrt().domain([0, max]).range([0.2, MAX_WIDTH]);
sankey = function(graph) {
return graph.nodes.forEach(function(n) {
var acc;
acc = 0;
n.links.forEach(function(link) {
if (link.source === n) {
return link.sankey_source = {
start: acc,
middle: acc + link_thickness(link.weight) / 2,
end: acc += link_thickness(link.weight)
};
} else if (link.target === n) {
return link.sankey_target = {
start: acc,
middle: acc + link_thickness(link.weight) / 2,
end: acc += link_thickness(link.weight)
};
}
});
return n.sankey_tot = acc;
});
};
sankey(graph_data);
circular_layout = function() {
var delta_theta, rho, self, theta, theta_0;
rho = function(d, i, data) {
return 100;
};
theta_0 = function(d, i, data) {
return -Math.PI / 2;
};
delta_theta = function(d, i, data) {
return 2 * Math.PI / data.length;
};
theta = function(d, i, data) {
return theta_0(d, i, data) + i * delta_theta(d, i, data);
};
self = function(data) {
data.forEach(function(d, i) {
d.rho = rho(d, i, data);
d.theta = theta(d, i, data);
d.x = d.rho * Math.cos(d.theta);
return d.y = d.rho * Math.sin(d.theta);
});
return data;
};
self.rho = function(x) {
if (x != null) {
if (typeof x === 'function') {
rho = x;
} else {
rho = function() {
return x;
};
}
return self;
}
return rho;
};
self.theta_0 = function(x) {
if (x != null) {
if (typeof x === 'function') {
theta_0 = x;
} else {
theta_0 = function() {
return x;
};
}
return self;
}
return theta_0;
};
self.delta_theta = function(x) {
if (x != null) {
if (typeof x === 'function') {
delta_theta = x;
} else {
delta_theta = function() {
return x;
};
}
return self;
}
return delta_theta;
};
self.theta = function(x) {
if (x != null) {
if (typeof x === 'function') {
theta = x;
} else {
theta = function() {
return x;
};
}
return self;
}
return theta;
};
return self;
};
circular = circular_layout().rho(160);
circular(graph_data.nodes);
links_layer = vis.append('g');
nodes_layer = vis.append('g');
radius = d3.scale.sqrt().domain([
0, d3.min(graph_data.nodes, function(n) {
return n.size;
})
]).range([0, MAX_WIDTH / 2]);
nodes = nodes_layer.selectAll('.node').data(graph_data.nodes);
new_node_elements = nodes.enter().append('circle').attr({
"class": 'node',
r: function(node) {
return radius(node.size);
},
cx: function(node) {
return node.x + (4 + radius(node.size)) * Math.cos(node.theta);
},
cy: function(node) {
return node.y + (4 + radius(node.size)) * Math.sin(node.theta);
}
});
new_node_elements.append('title').text(function(node) {
return node.name + ' class\n' + d3.format(',')(node.size) + ' entities';
});
labels = nodes_layer.selectAll('.label').data(graph_data.nodes);
labels.enter().append('g').attr('transform', function(node) {
return "translate(" + (node.x + (4 + radius(node.size)) * Math.cos(node.theta)) + " " + (node.y + (4 + radius(node.size)) * Math.sin(node.theta)) + ")";
}).append('text').text(function(node) {
return node.name;
}).attr({
"class": 'label semantic_zoom',
dy: '0.35em'
});
links = links_layer.selectAll('.link').data(graph_data.links);
tension = 0.3;
new_link_elements = links.enter().append('path').attr({
"class": 'link flowline',
'stroke-width': function(link) {
return link_thickness(link.weight);
}
});
new_link_elements.append('title').text(function(link) {
return link.source.name + ' -> ' + link.target.name + '\n' + d3.format(',')(link.weight) + ' links';
});
links.attr({
d: function(link) {
var cxs, cxt, cys, cyt, sankey_ds, sankey_dt, sankey_dxs, sankey_dxt, sankey_dys, sankey_dyt, xs, xsi, xt, xti, ys, ysi, yt, yti;
sankey_ds = link.source.sankey_tot / 2 - link.sankey_source.middle;
sankey_dt = link.target.sankey_tot / 2 - link.sankey_target.middle;
sankey_dxs = sankey_ds * Math.cos(link.source.theta + Math.PI / 2);
sankey_dys = sankey_ds * Math.sin(link.source.theta + Math.PI / 2);
sankey_dxt = sankey_dt * Math.cos(link.target.theta + Math.PI / 2);
sankey_dyt = sankey_dt * Math.sin(link.target.theta + Math.PI / 2);
xs = link.source.x + sankey_dxs;
ys = link.source.y + sankey_dys;
xt = link.target.x + sankey_dxt;
yt = link.target.y + sankey_dyt;
xsi = xs + (4 + radius(link.source.size)) * Math.cos(link.source.theta);
ysi = ys + (4 + radius(link.source.size)) * Math.sin(link.source.theta);
xti = xt + (4 + radius(link.target.size)) * Math.cos(link.target.theta);
yti = yt + (4 + radius(link.target.size)) * Math.sin(link.target.theta);
cxs = xs - link.source.x * tension;
cys = ys - link.source.y * tension;
cxt = xt - link.target.x * tension;
cyt = yt - link.target.y * tension;
return "M" + xsi + " " + ysi + " L" + xs + " " + ys + " C" + cxs + " " + cys + " " + cxt + " " + cyt + " " + xt + " " + yt + " L" + xti + " " + yti;
}
});
nodes.on('click', function(n) {
links.classed('blurred', function(link) {
return link.source !== n && link.target !== n;
});
return d3.event.stopPropagation();
});
svg.on('click', function() {
if (d3.event.defaultPrevented) {
return;
}
return links.classed('blurred', false);
});
return links.on('click', function(l) {
links.classed('blurred', function(link) {
return link !== l;
});
return d3.event.stopPropagation();
});
});
}).call(this);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="description" content="DBpedia Map - First-level classes of the ontology (with links)" />
<title>DBpedia Map - First-level classes of the ontology (with links)</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="//d3js.org/d3.v3.min.js"></script>
</head>
<body>
<svg height="500" width="960"></svg>
<script src="index.js"></script>
</body>
</html>
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-40} #{width} #{height}"
# append a group for zoomable content
vis = svg.append('g')
# define a zoom behavior
zoom = d3.behavior.zoom()
.scaleExtent([1,8]) # min-max zoom - a value of 1 represent the initial zoom
.on 'zoom', () ->
# GEOMETRIC ZOOM
# whenever the user zooms,
# modify translation and scale of the zoomable layer accordingly
vis
.attr
transform: "translate(#{zoom.translate()})scale(#{zoom.scale()})"
# SEMANTIC ZOOM
# scale back all objects that have to be semantically zoomed
vis.selectAll('.semantic_zoom')
.attr
transform: "scale(#{1/zoom.scale()})"
# bind the zoom behavior to the main SVG (this is needed to have pan work on empty space - a group would pan only when dragging child elements)
svg.call(zoom)
d3.json 'http://wafi.iit.cnr.it/webvis/tmp/DBpediaClassRelations.json', (graph_data) ->
# remove 0-weight links
graph_data.links = graph_data.links.filter (l) -> l.weight > 0
# objectify the graph
# resolve node IDs (not optimized at all!)
objectify = (graph) ->
graph.links.forEach (l) ->
graph.nodes.forEach (n) ->
if l.source is n.id
l.source = n
if l.target is n.id
l.target = n
objectify(graph_data)
# create link arrays for each node
list_links = (graph) ->
graph.nodes.forEach (n) ->
n.links = graph.links.filter (link) -> link.source is n or link.target is n
n.links.sort (a,b) ->
if a.source is n
other_a_id = a.target.id
a_dir = 'target'
else
other_a_id = a.source.id
a_dir = 'source'
if b.source is n
other_b_id = b.target.id
b_dir = 'target'
else
other_b_id = b.source.id
b_dir = 'source'
cmp = other_a_id - other_b_id
if cmp is 0
if a_dir is 'target'
return 1
else
return -1
else
return cmp
list_links(graph_data)
# compute node weighted degrees (sankey totals)
compute_degree = (graph) ->
graph.nodes.forEach (n) ->
n.degree = d3.sum n.links, (link) -> link.weight
compute_degree(graph_data)
# sankeify the graph
MAX_WIDTH = 18
max = d3.max graph_data.nodes, (n) -> n.degree
link_thickness = d3.scale.sqrt()
.domain([0, max])
.range([0.2, MAX_WIDTH])
sankey = (graph) ->
graph.nodes.forEach (n) ->
acc = 0
n.links.forEach (link) ->
if link.source is n
link.sankey_source = {
start: acc,
middle: acc + link_thickness(link.weight)/2,
end: acc += link_thickness(link.weight)
}
else if link.target is n
link.sankey_target = {
start: acc,
middle: acc + link_thickness(link.weight)/2,
end: acc += link_thickness(link.weight)
}
n.sankey_tot = acc
sankey(graph_data)
# layout
circular_layout = () ->
rho = (d, i, data) -> 100
theta_0 = (d, i, data) -> -Math.PI/2 # start from the angle pointing north
delta_theta = (d, i, data) -> 2*Math.PI/data.length
theta = (d, i , data) -> theta_0(d, i, data) + i*delta_theta(d, i, data)
self = (data) ->
data.forEach (d, i) ->
d.rho = rho(d, i, data)
d.theta = theta(d, i, data)
d.x = d.rho * Math.cos(d.theta)
d.y = d.rho * Math.sin(d.theta)
return data
self.rho = (x) ->
if x?
if typeof(x) is 'function'
rho = x
else
rho = () -> x
return self
# else
return rho
self.theta_0 = (x) ->
if x?
if typeof(x) is 'function'
theta_0 = x
else
theta_0 = () -> x
return self
# else
return theta_0
self.delta_theta = (x) ->
if x?
if typeof(x) is 'function'
delta_theta = x
else
delta_theta = () -> x
return self
# else
return delta_theta
self.theta = (x) ->
if x?
if typeof(x) is 'function'
theta = x
else
theta = () -> x
return self
# else
return theta
return self
# apply the layout
circular = circular_layout()
.rho(160)
circular(graph_data.nodes)
# draw nodes above links
links_layer = vis.append('g')
nodes_layer = vis.append('g')
radius = d3.scale.sqrt()
.domain([0, d3.min graph_data.nodes, (n) -> n.size])
.range([0, MAX_WIDTH/2])
nodes = nodes_layer.selectAll('.node')
.data(graph_data.nodes)
new_node_elements = nodes.enter().append('circle')
.attr
class: 'node'
r: (node) -> radius(node.size)
cx: (node) -> node.x + (4+radius(node.size))*Math.cos(node.theta)
cy: (node) -> node.y + (4+radius(node.size))*Math.sin(node.theta)
# tooltips
new_node_elements.append('title')
.text((node) -> node.name + ' class\n' + d3.format(',')(node.size) + ' entities')
# draw node labels
labels = nodes_layer.selectAll('.label')
.data(graph_data.nodes)
labels.enter().append('g')
.attr('transform', (node) -> "translate(#{node.x + (4+radius(node.size))*Math.cos(node.theta)} #{node.y + (4+radius(node.size))*Math.sin(node.theta)})")
.append('text')
.text((node) -> node.name)
.attr
class: 'label semantic_zoom'
dy: '0.35em'
links = links_layer.selectAll('.link')
.data(graph_data.links)
tension = 0.3
new_link_elements = links.enter().append('path')
.attr
class: 'link flowline'
'stroke-width': (link) -> link_thickness(link.weight)
# tooltips
new_link_elements.append('title')
.text((link) -> link.source.name + ' -> ' + link.target.name + '\n' + d3.format(',')(link.weight) + ' links')
links
.attr
d: (link) ->
sankey_ds = link.source.sankey_tot/2 - link.sankey_source.middle
sankey_dt = link.target.sankey_tot/2 - link.sankey_target.middle
sankey_dxs = sankey_ds*Math.cos(link.source.theta+Math.PI/2)
sankey_dys = sankey_ds*Math.sin(link.source.theta+Math.PI/2)
sankey_dxt = sankey_dt*Math.cos(link.target.theta+Math.PI/2)
sankey_dyt = sankey_dt*Math.sin(link.target.theta+Math.PI/2)
xs = link.source.x + sankey_dxs
ys = link.source.y + sankey_dys
xt = link.target.x + sankey_dxt
yt = link.target.y + sankey_dyt
xsi = xs + (4+radius(link.source.size))*Math.cos(link.source.theta)
ysi = ys + (4+radius(link.source.size))*Math.sin(link.source.theta)
xti = xt + (4+radius(link.target.size))*Math.cos(link.target.theta)
yti = yt + (4+radius(link.target.size))*Math.sin(link.target.theta)
cxs = xs-link.source.x*tension
cys = ys-link.source.y*tension
cxt = xt-link.target.x*tension
cyt = yt-link.target.y*tension
return "M#{xsi} #{ysi} L#{xs} #{ys} C#{cxs} #{cys} #{cxt} #{cyt} #{xt} #{yt} L#{xti} #{yti}"
# node hover
nodes.on 'click', (n) ->
links.classed('blurred', (link) -> link.source isnt n and link.target isnt n)
d3.event.stopPropagation()
svg.on 'click', () ->
return if (d3.event.defaultPrevented)
links.classed('blurred', false)
# link hover
links.on 'click', (l) ->
links.classed('blurred', (link) -> link isnt l)
d3.event.stopPropagation()
svg {
background: white;
}
.node {
fill: lightgray;
stroke: gray;
stroke-width: 2px;
vector-effect: non-scaling-stroke;
}
.node:hover {
stroke-width: 3px;
stroke: #555;
}
.link {
stroke: #004;
fill: none;
opacity: 0.3;
}
.blurred.link {
opacity: 0.025;
}
.link:hover {
opacity: 0.5;
}
.blurred.link:hover {
opacity: 0.1;
}
.label {
text-anchor: middle;
font-size: 16px;
fill: #444;
font-weight: bold;
font-family: sans-serif;
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;
pointer-events: none;
}
.flowline {
stroke-dasharray: 78, 2;
animation: flow 6s linear infinite;
-webkit-animation: flow 6s linear infinite;
}
@keyframes flow {
from {
stroke-dashoffset: 80;
}
to {
stroke-dashoffset: 0;
}
}
@-webkit-keyframes flow {
from {
stroke-dashoffset: 80;
}
to {
stroke-dashoffset: 0;
}
}