This node-link diagram shows entities (circles) automatically extracted from web pages (squares). A link indicates that a certain entity has been found in a certain web page. Entities are colored according to their category (people, telephone numbers, etc.). Use the mouse wheel to zoom.
The main code is adapted from this example on node-link diagrams.
Many improvements can be made to this visualization: choice of colors, better interaction, a more stable layout or a persistence layer to save the force layout status. The example is intended as a baseline visualization for this kind of data structures.
(function() {
var global, height, update, width;
width = 960;
height = 500;
/* SELECTION - store the selected node
*/
/* EDITING - store the drag mode (either 'drag' or 'add_link')
*/
global = {
selection: null
};
window.main = function() {
return d3.json('entities-pages.json', function(error, graph) {
var container, svg;
global.graph = graph;
global.graph.objectify = function() {
/* resolve node IDs (not optimized at all!)
*/
var l, n, _i, _len, _ref, _results;
_ref = global.graph.links;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
l = _ref[_i];
_results.push((function() {
var _j, _len2, _ref2, _results2;
_ref2 = global.graph.nodes;
_results2 = [];
for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) {
n = _ref2[_j];
if (l.source === n.id) {
l.source = n;
continue;
}
if (l.target === n.id) {
l.target = n;
continue;
} else {
_results2.push(void 0);
}
}
return _results2;
})());
}
return _results;
};
global.graph.objectify();
/* create the SVG
*/
svg = d3.select('body').append('svg').attr('width', width).attr('height', height);
/* ZOOM and PAN
*/
/* create container elements
*/
container = svg.append('g');
container.call(d3.behavior.zoom().scaleExtent([0.25, 6]).on('zoom', (function() {
return global.vis.attr('transform', "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
})));
global.vis = container.append('g');
/* create a rectangular overlay to catch events
*/
/* WARNING rect size is huge but not infinite. this is a dirty hack
*/
global.vis.append('rect').attr('class', 'overlay').attr('x', -500000).attr('y', -500000).attr('width', 1000000).attr('height', 1000000).on('click', (function(d) {
/* SELECTION
*/ global.selection = null;
d3.selectAll('.node').classed('selected', false);
return d3.selectAll('.link').classed('selected', false);
}));
/* END ZOOM and PAN
*/
global.colorify = d3.scale.category10();
/* initialize the force layout
*/
global.force = d3.layout.force().size([width, height]).charge(-800).linkDistance(30).on('tick', (function() {
/* update nodes and links
*/ global.vis.selectAll('.node').attr('transform', function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
return global.vis.selectAll('.link').attr('x1', function(d) {
return d.source.x;
}).attr('y1', function(d) {
return d.source.y;
}).attr('x2', function(d) {
return d.target.x;
}).attr('y2', function(d) {
return d.target.y;
});
}));
/* DRAG
*/
global.drag = global.force.drag().on('dragstart', function(d) {
return d.fixed = true;
});
return update();
});
};
update = function() {
/* update the layout
*/
var links, new_nodes, nodes;
global.force.nodes(global.graph.nodes).links(global.graph.links).start();
/* create nodes and links
*/
/* (links are drawn with insert to make them appear under the nodes)
*/
/* also define a drag behavior to drag nodes
*/
/* dragged nodes become fixed
*/
nodes = global.vis.selectAll('.node').data(global.graph.nodes, function(d) {
return d.id;
});
new_nodes = nodes.enter().append('g').attr('class', 'node').on('click', (function(d) {
/* SELECTION
*/ global.selection = d;
d3.selectAll('.node').classed('selected', function(d2) {
return d2 === d;
});
return d3.selectAll('.link').classed('selected', false);
}));
links = global.vis.selectAll('.link').data(global.graph.links, function(d) {
return "" + d.source.id + "->" + d.target.id;
});
links.enter().insert('line', '.node').attr('class', 'link').on('click', (function(d) {
/* SELECTION
*/ global.selection = d;
d3.selectAll('.link').classed('selected', function(d2) {
return d2 === d;
});
return d3.selectAll('.node').classed('selected', false);
}));
links.exit().remove();
new_nodes.call(global.drag);
new_nodes.filter(function(d) {
return d.type !== 'page';
}).append('circle').attr('r', 9).attr('stroke', function(d) {
return global.colorify(d.type);
}).attr('fill', function(d) {
return d3.hcl(global.colorify(d.type)).brighter(3);
});
new_nodes.filter(function(d) {
return d.type === 'page';
}).append('rect').attr('x', -8).attr('y', -8).attr('width', 16).attr('height', 16).attr('stroke', function(d) {
return global.colorify(d.type);
}).attr('fill', function(d) {
return d3.hcl(global.colorify(d.type)).brighter(3);
});
/* draw the label
*/
new_nodes.append('text').text(function(d) {
return d.value;
}).attr('dy', '0.35em');
return nodes.exit().remove();
};
}).call(this);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Entity recognition diagram</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="d3.v3.min.js"></script>
<script src="index.js"></script>
</head>
<body onload="main()">
</body>
</html>
width = 960
height = 500
### SELECTION - store the selected node ###
### EDITING - store the drag mode (either 'drag' or 'add_link') ###
global = {
selection: null
}
window.main = () ->
d3.json 'entities-pages.json', (error, graph) ->
global.graph = graph
global.graph.objectify = () ->
### resolve node IDs (not optimized at all!) ###
for l in global.graph.links
for n in global.graph.nodes
if l.source is n.id
l.source = n
continue
if l.target is n.id
l.target = n
continue
global.graph.objectify()
### create the SVG ###
svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
### ZOOM and PAN ###
### create container elements ###
container = svg.append('g')
container.call(d3.behavior.zoom().scaleExtent([0.25, 6]).on('zoom', (() -> global.vis.attr('transform', "translate(#{d3.event.translate})scale(#{d3.event.scale})"))))
global.vis = container.append('g')
### create a rectangular overlay to catch events ###
### WARNING rect size is huge but not infinite. this is a dirty hack ###
global.vis.append('rect')
.attr('class', 'overlay')
.attr('x', -500000)
.attr('y', -500000)
.attr('width', 1000000)
.attr('height', 1000000)
.on('click', ((d) ->
### SELECTION ###
global.selection = null
d3.selectAll('.node').classed('selected', false)
d3.selectAll('.link').classed('selected', false)
))
### END ZOOM and PAN ###
global.colorify = d3.scale.category10()
### initialize the force layout ###
global.force = d3.layout.force()
.size([width, height])
.charge(-800)
.linkDistance(30)
.on('tick', (() ->
### update nodes and links ###
global.vis.selectAll('.node')
.attr('transform', (d) -> "translate(#{d.x},#{d.y})")
global.vis.selectAll('.link')
.attr('x1', (d) -> d.source.x)
.attr('y1', (d) -> d.source.y)
.attr('x2', (d) -> d.target.x)
.attr('y2', (d) -> d.target.y)
))
### DRAG ###
global.drag = global.force.drag()
.on('dragstart', (d) -> d.fixed = true)
update()
update = () ->
### update the layout ###
global.force
.nodes(global.graph.nodes)
.links(global.graph.links)
.start()
### create nodes and links ###
### (links are drawn with insert to make them appear under the nodes) ###
### also define a drag behavior to drag nodes ###
### dragged nodes become fixed ###
nodes = global.vis.selectAll('.node')
.data(global.graph.nodes, (d) -> d.id)
new_nodes = nodes
.enter().append('g')
.attr('class', 'node')
.on('click', ((d) ->
### SELECTION ###
global.selection = d
d3.selectAll('.node').classed('selected', (d2) -> d2 is d)
d3.selectAll('.link').classed('selected', false)
))
links = global.vis.selectAll('.link')
.data(global.graph.links, (d) -> "#{d.source.id}->#{d.target.id}")
links
.enter().insert('line', '.node')
.attr('class', 'link')
.on('click', ((d) ->
### SELECTION ###
global.selection = d
d3.selectAll('.link').classed('selected', (d2) -> d2 is d)
d3.selectAll('.node').classed('selected', false)
))
links
.exit().remove()
new_nodes.call(global.drag) # DRAG
new_nodes.filter((d) -> d.type isnt 'page').append('circle')
.attr('r', 9)
.attr('stroke', (d) -> global.colorify(d.type))
.attr('fill', (d) -> d3.hcl(global.colorify(d.type)).brighter(3))
new_nodes.filter((d) -> d.type is 'page').append('rect')
.attr('x', -8)
.attr('y', -8)
.attr('width', 16)
.attr('height', 16)
.attr('stroke', (d) -> global.colorify(d.type))
.attr('fill', (d) -> d3.hcl(global.colorify(d.type)).brighter(3))
### draw the label ###
new_nodes.append('text')
.text((d) -> d.value)
.attr('dy', '0.35em')
nodes
.exit().remove()
.node > :not(text) {
stroke-width: 2px;
}
.node > text {
pointer-events: none;
font-family: sans-serif;
font-size: 8px;
text-anchor: middle;
fill: #555555;
text-shadow: -1px 0 1px white, 0 1px white, 1px 0 white, 0 -1px white;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
.link {
stroke-width: 2px;
stroke: gray;
opacity: 0.3;
}
.selected > :not(text) {
stroke-width: 4px;
}
.overlay {
fill: transparent;
}
.node > :not(text)
stroke-width: 2px
.node > text
pointer-events: none
font-family: sans-serif
font-size: 8px
text-anchor: middle
fill: #555
text-shadow: -1px 0 1px white, 0 1px white, 1px 0 white, 0 -1px white
// prevent text selection
-webkit-user-select: none
-moz-user-select: none
-ms-user-select: none
-o-user-select: none
user-select: none
.link
stroke-width: 2px
stroke: gray
opacity: 0.3
// selection
.selected > :not(text)
stroke-width: 4px
// zoom and pan
.overlay
fill: transparent