block by nitaku be7d7ed64d548453711b

DBpedia Map - First-level classes of the ontology (with links)

Full Screen

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:

index.js

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

index.html

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

index.coffee

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

index.css

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