Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active February 15, 2019 06:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nitaku/be7d7ed64d548453711b to your computer and use it in GitHub Desktop.
Save nitaku/be7d7ed64d548453711b to your computer and use it in GitHub Desktop.
DBpedia Map - First-level classes of the ontology (with links)

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:

  • Zooming into a node should reveal its inner structure.
  • The relative scale of nodes should not be too large, to avoid to give the feeling of navigating a huge, mostly empty space.
  • The map can either have a coherent node scale in the whole space, or have vast empty spaces. This is due to the fact that the sum of the areas of children nodes has to be much lower than the area of the parent, because we saved a lot of space for depicting links.
  • Nodes should be color-coded as in previous experiments.
  • We have to think what to do exactly with the "other" class (a dummy class we used to merge many first-level classes together)
  • Ingoing and outgoing links should form two parallel "roads", rather than overlap. This is a nontrivial problem, because offsetting a bezier curve is not easy.
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;
}
}
<!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="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 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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment