block by nitaku 8053186

Entity recognition graph

Full Screen

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.

index.js

(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);

index.html

<!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>

index.coffee

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()
      

index.css

.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;
}

index.sass

.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