block by nitaku d7cbf8fd96ce42fed36d

Duck Islands (Pink Floyd)

Full Screen

A suggestive experiment leveraging the duck similarity to create “islands” of similar RDF resources.

The technique make use of a fixed random seed, an offline force directed layout simulation to displace the nodes, a Voronoi tessellation and SVG ClipPath to create the cells.

The example depicts the neighborhood of the Pink Floyd resource within DBpedia. Songs and albums are somewhat mixed in the left side of the map, while people (far right), lables (bottom right) are isolated.

index.js

// Generated by CoffeeScript 1.4.0

/* define a fixed random seed, to avoid to have a different layout on each page reload. change the string to randomize
*/


(function() {
  var distance, graph_layer, height, land, panel, relations_layer, sea, svg, voronoi, width, zoom;

  Math.seedrandom('abcde');

  panel = d3.select('#panel');

  svg = d3.select('svg');

  width = svg.node().getBoundingClientRect().width;

  height = svg.node().getBoundingClientRect().height;

  voronoi = d3.geom.voronoi().x(function(d) {
    return d.x;
  }).y(function(d) {
    return d.y;
  });

  distance = d3.scale.linear().domain([0, 1]).range([100, 0]);

  /* store the graph in a zoomable layer
  */


  graph_layer = svg.append('g');

  /* define a zoom behavior
  */


  zoom = d3.behavior.zoom().scaleExtent([0.01, 10]).on('zoom', function() {
    /* whenever the user zooms,
    */

    /* modify translation and scale of the zoom group accordingly
    */
    return graph_layer.attr('transform', "translate(" + (zoom.translate()) + ")scale(" + (zoom.scale()) + ")");
  });

  /* bind the zoom behavior to the main SVG
  */


  svg.call(zoom);

  /* create groups for land and sea
  */


  sea = graph_layer.append('g');

  /* cover the sea with a pattern
  */


  graph_layer.append('rect').attr('id', 'sea_pattern').attr('width', 10000).attr('height', 10000).attr('x', -5000).attr('y', -5000);

  land = graph_layer.append('g');

  relations_layer = graph_layer.append('g');

  d3.json('pink_floyd_matrix.json', function(data) {
    var cells_data, deep_cells, deep_clips, enter_land_cells, force, graph, i, l, land_cells, land_clips, links, n, nodes, shallow_cells, shallow_clips, _i, _j, _k, _len, _len1, _ref, _ref1;
    graph = {
      nodes: data.map(function(n) {
        return {
          uri: n.k1
        };
      }),
      links: []
    };
    data.forEach(function(e1) {
      return e1.similarities.forEach(function(e2) {
        return graph.links.push({
          source: e1.k1,
          target: e2.k2,
          similarity: e2.sim
        });
      });
    });
    /* objectify the graph
    */

    /* resolve node IDs (not optimized at all!)
    */

    _ref = graph.links;
    for (_i = 0, _len = _ref.length; _i < _len; _i++) {
      l = _ref[_i];
      _ref1 = graph.nodes;
      for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
        n = _ref1[_j];
        if (l.source === n.uri) {
          l.source = n;
        }
        if (l.target === n.uri) {
          l.target = n;
        }
      }
    }
    /* run the force layout for a fixed number of iterations
    */

    force = d3.layout.force().size([width, height]).charge(-10000).linkDistance(function(l) {
      return 5 * distance(l.similarity);
    }).chargeDistance(1000);
    force.nodes(graph.nodes).links(graph.links).start();
    for (i = _k = 0; _k <= 5000; i = ++_k) {
      force.tick();
    }
    force.stop();
    /* update deep clip paths
    */

    deep_clips = graph_layer.selectAll('.deep_clip').data(graph.nodes);
    deep_clips.enter().append('clipPath').attr('class', 'deep_clip').attr('id', function(d, i) {
      return "deep_clip-" + i;
    }).append('circle').attr('r', 52);
    deep_clips.select('circle').attr('cx', function(n) {
      return n.x;
    }).attr('cy', function(n) {
      return n.y;
    });
    /* update shallow clip paths
    */

    shallow_clips = graph_layer.selectAll('.shallow_clip').data(graph.nodes);
    shallow_clips.enter().append('clipPath').attr('class', 'shallow_clip').attr('id', function(d, i) {
      return "shallow_clip-" + i;
    }).append('circle').attr('r', 26);
    shallow_clips.select('circle').attr('transform', function(n) {
      return "translate(" + n.x + "," + n.y + ")";
    });
    /* update land clip paths
    */

    land_clips = graph_layer.selectAll('.land_clip').data(graph.nodes);
    land_clips.enter().append('clipPath').attr('class', 'land_clip').attr('id', function(d, i) {
      return "land_clip-" + i;
    }).append('path').attr('d', 'M8,16 L18,4 L14,-11 L0,-18 L-14,-11 L-18,4 L-8,16 z');
    land_clips.select('path').attr('transform', function(n) {
      return "translate(" + n.x + "," + n.y + ")";
    });
    /* update cells
    */

    cells_data = voronoi(graph.nodes);
    deep_cells = sea.selectAll('.deep_cell').data(cells_data);
    deep_cells.enter().append('path').attr('class', 'deep_cell').attr('clip-path', function(d, i) {
      return "url(#deep_clip-" + i + ")";
    });
    deep_cells.attr('d', function(d) {
      return "M" + d.join(",") + "Z";
    });
    shallow_cells = sea.selectAll('.shallow_cell').data(cells_data);
    shallow_cells.enter().append('path').attr('class', 'shallow_cell').attr('clip-path', function(d, i) {
      return "url(#shallow_clip-" + i + ")";
    });
    shallow_cells.attr('d', function(d) {
      return "M" + d.join(",") + "Z";
    });
    land_cells = land.selectAll('.land_cell').data(cells_data);
    enter_land_cells = land_cells.enter().append('path').attr('class', 'land_cell').attr('clip-path', function(d, i) {
      return "url(#land_clip-" + i + ")";
    }).on('click', function(d) {
      /* highlight selection
      */
      land_cells.classed('selected', false);
      d3.select(this).classed('selected', true);
      /* query DBpedia for details
      */

      return d3.json(d.point.uri.replace('/resource/', '/data/') + '.json', function(e) {
        /* load details into the panel
        */

        var en_a, en_l, relations, relations_data;
        en_l = _.find(e[d.point.uri]['http://www.w3.org/2000/01/rdf-schema#label'], function(l) {
          return l.lang === 'en';
        });
        en_a = _.find(e[d.point.uri]['http://dbpedia.org/ontology/abstract'], function(a) {
          return a.lang === 'en';
        });
        panel.select('header').text(en_l != null ? en_l.value : '[no english label found]');
        panel.select('section').text(en_a != null ? en_a.value : '[no english abstract found]');
        /* show relational links
        */

        relations_data = [];
        /* outgoing links
        */

        _.map(e[d.point.uri], function(p, p_uri) {
          return _.map(p, function(o) {
            var obj;
            if (!o.type === ('uri' != null)) {
              return;
            }
            obj = _.find(graph.nodes, function(n) {
              return n.uri === o.value;
            });
            if (!(obj != null)) {
              return;
            }
            return relations_data.push({
              's': d.point,
              'p': p_uri,
              'o': obj
            });
          });
        });
        /* incoming links
        */

        _.map(e, function(s, s_uri) {
          if (s_uri === d.point.uri) {
            return;
          }
          return _.map(s, function(p, p_uri) {
            var subj;
            subj = _.find(graph.nodes, function(n) {
              return n.uri === s.value;
            });
            if (!(subj != null)) {
              return;
            }
            return _.map(p, function(o) {
              return relations_data.push({
                's': subj,
                'p': p_uri,
                'o': d.point
              });
            });
          });
        });
        relations = relations_layer.selectAll('.relation').data(relations_data.filter(function(r) {
          return r.s !== r.o;
        }), function(r) {
          return "" + r.s.uri + ">>>" + r.p + ">>>" + r.o.uri;
        });
        relations.enter().append('path').attr('class', 'relation').attr('d', function(r) {
          return "M" + r.s.x + " " + r.s.y + " C" + r.s.x + " " + (r.s.y - 120) + " " + r.o.x + " " + (r.o.y - 120) + " " + r.o.x + " " + r.o.y;
        });
        return relations.exit().remove();
      });
    });
    enter_land_cells.append('title').text(function(n) {
      return n.point.uri.replace('http://dbpedia.org/resource/', '');
    });
    land_cells.attr('d', function(d) {
      return "M" + d.join(",") + "Z";
    });
    /* update links
    */

    links = graph_layer.selectAll('.link').data(graph.links);
    links.enter().append('line').attr('class', 'link').attr('opacity', function(l) {
      return l.similarity;
    });
    links.attr('x1', function(l) {
      return l.source.x;
    }).attr('y1', function(l) {
      return l.source.y;
    }).attr('x2', function(l) {
      return l.target.x;
    }).attr('y2', function(l) {
      return l.target.y;
    });
    /* update nodes
    */

    nodes = graph_layer.selectAll('.node').data(graph.nodes);
    nodes.enter().append('circle').attr('class', 'node').attr('r', 2);
    return nodes.attr('cx', function(n) {
      return n.x;
    }).attr('cy', function(n) {
      return n.y;
    });
  });

}).call(this);

index.html

<!DOCTYPE html>
<html>
  <head>
	<meta charset="utf-8">
	<title>Duck Islands (Pink Floyd)</title>
	<link type="text/css" href="index.css" rel="stylesheet"/>
	<script src="//d3js.org/d3.v3.min.js"></script>
	<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
	<script src="//davidbau.com/encode/seedrandom-min.js"></script>
	<script src="//wafi.iit.cnr.it/webvis/tmp/clusterfck.js"></script>
  </head>
  <body>
	<svg height="500" width="960">
	  <defs>
		<pattern id="sea" x="0" y="0" width="30" height="30" patternUnits="userSpaceOnUse">
		  <path d="M0 0.5 L 10 0.5" stroke="white"/>
		  <path d="M15 15.5 L 25 15.5" stroke="white"/>
		  <!--<circle cx="2" cy="2" r="1" fill="white"/>
		  <circle cx="13" cy="22" r="1" fill="white"/>-->
		</pattern>
	  </defs>
    </svg><div id="panel"><header></header><section></section></div>
	<script src="index.js"></script>
  </body>
</html>

index.coffee

### define a fixed random seed, to avoid to have a different layout on each page reload. change the string to randomize ###
Math.seedrandom('abcde')

panel = d3.select('#panel')
svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height

voronoi = d3.geom.voronoi()
  .x((d) -> d.x)
  .y((d) -> d.y)
  
distance = d3.scale.linear()
  .domain([0,1])
  .range([100,0])

### store the graph in a zoomable layer ###
graph_layer = svg.append('g')

### define a zoom behavior ###
zoom = d3.behavior.zoom()
    .scaleExtent([0.01,10]) # min-max zoom
    .on 'zoom', () ->
      ### whenever the user zooms, ###
      ### modify translation and scale of the zoom group accordingly ###
      graph_layer.attr('transform', "translate(#{zoom.translate()})scale(#{zoom.scale()})")
      
### bind the zoom behavior to the main SVG ###
svg.call(zoom)

### create groups for land and sea ###
sea = graph_layer.append('g')

### cover the sea with a pattern ###
graph_layer.append('rect')
  .attr('id', 'sea_pattern')
  .attr('width', 10000)
  .attr('height', 10000)
  .attr('x', -5000)
  .attr('y', -5000)
  
land = graph_layer.append('g')

relations_layer = graph_layer.append('g')
  
d3.json 'pink_floyd_matrix.json', (data) ->
  graph = {
    nodes: (data.map (n) -> {uri: n.k1}),
    links: []
  }
  
  data.forEach (e1) ->
    e1.similarities.forEach (e2) ->
      graph.links.push {source: e1.k1, target: e2.k2, similarity: e2.sim}
  
  ### objectify the graph ###
  ### resolve node IDs (not optimized at all!) ###
  for l in graph.links
      for n in graph.nodes
          if l.source is n.uri
              l.source = n
          
          if l.target is n.uri
              l.target = n
  
  ### run the force layout for a fixed number of iterations ###
  force = d3.layout.force()
      .size([width, height])
      .charge(-10000)
      .linkDistance((l) -> 5*distance(l.similarity))
      .chargeDistance(1000)
      
  force
      .nodes(graph.nodes)
      .links(graph.links)
      .start()
      
  for i in [0..5000]
    force.tick()
  
  force.stop()
  
  ### update deep clip paths ###
  deep_clips = graph_layer.selectAll('.deep_clip')
      .data(graph.nodes)
      
  deep_clips.enter().append('clipPath')
      .attr('class', 'deep_clip')
      .attr('id', (d,i) -> "deep_clip-#{i}")
    .append('circle')
      .attr('r', 52)
      
  deep_clips.select('circle')
      .attr('cx', (n) -> n.x)
      .attr('cy', (n) -> n.y)
  
  ### update shallow clip paths ###
  shallow_clips = graph_layer.selectAll('.shallow_clip')
      .data(graph.nodes)
      
  shallow_clips.enter().append('clipPath')
      .attr('class', 'shallow_clip')
      .attr('id', (d,i) -> "shallow_clip-#{i}")
    .append('circle')
      .attr('r', 26)
      
  shallow_clips.select('circle')
      .attr('transform', (n) -> "translate(#{n.x},#{n.y})")
      
  ### update land clip paths ###
  land_clips = graph_layer.selectAll('.land_clip')
      .data(graph.nodes)
      
  land_clips.enter().append('clipPath')
      .attr('class', 'land_clip')
      .attr('id', (d,i) -> "land_clip-#{i}")
  #  .append('circle')
  #    .attr('r', 18)
  #  .append('path')
  #    .attr('d', 'M16,-9 L0,-18 L-16,-9 L-16,9 L0,18 L16,9 z')
    .append('path')
      .attr('d', 'M8,16 L18,4 L14,-11 L0,-18 L-14,-11 L-18,4 L-8,16 z')
      
  land_clips.select('path')
      .attr('transform', (n) -> "translate(#{n.x},#{n.y})")
      
  ### update cells ###
  cells_data = voronoi(graph.nodes)
  
  
  deep_cells = sea.selectAll('.deep_cell')
      .data(cells_data)
      
  deep_cells.enter().append('path')
      .attr('class', 'deep_cell')
      .attr('clip-path', (d,i) -> "url(#deep_clip-#{i})")
  
  deep_cells.attr('d', (d) -> "M" + d.join(",") + "Z")
  
  
  shallow_cells = sea.selectAll('.shallow_cell')
      .data(cells_data)
  
  shallow_cells.enter().append('path')
      .attr('class', 'shallow_cell')
      .attr('clip-path', (d,i) -> "url(#shallow_clip-#{i})")
  
  shallow_cells.attr('d', (d) -> "M" + d.join(",") + "Z")
  
  
  land_cells = land.selectAll('.land_cell')
      .data(cells_data)
      
  enter_land_cells = land_cells.enter().append('path')
      .attr('class', 'land_cell')
      .attr('clip-path', (d,i) -> "url(#land_clip-#{i})")
    .on 'click', (d) ->
      ### highlight selection ###
      land_cells.classed('selected', false)
      d3.select(this).classed('selected', true)
      
      ### query DBpedia for details ###
      d3.json d.point.uri.replace('/resource/','/data/')+'.json', (e) ->
        ### load details into the panel ###
        en_l = _.find e[d.point.uri]['http://www.w3.org/2000/01/rdf-schema#label'], (l) -> l.lang is 'en'
        en_a = _.find e[d.point.uri]['http://dbpedia.org/ontology/abstract'], (a) -> a.lang is 'en'
        panel.select('header')
          .text(if en_l? then en_l.value else '[no english label found]')
        panel.select('section')
          .text(if en_a? then en_a.value else '[no english abstract found]')
        
        ### show relational links ###
        relations_data = []
        
        ### outgoing links ###
        _.map e[d.point.uri], (p, p_uri) ->
          _.map p, (o) ->
            if not o.type is 'uri'?
              return
            
            obj = _.find graph.nodes, (n) -> n.uri is o.value
            if not obj?
              return
            
            relations_data.push {'s': d.point, 'p': p_uri, 'o': obj}
        
        ### incoming links ###
        _.map e, (s, s_uri) ->
          if s_uri is d.point.uri
            return
          
          _.map s, (p, p_uri) ->
            subj = _.find graph.nodes, (n) -> n.uri is s.value
            if not subj?
              return
            
            _.map p, (o) ->
              relations_data.push {'s': subj, 'p': p_uri, 'o': d.point}
              
        relations = relations_layer.selectAll('.relation')
          .data(relations_data.filter((r) -> r.s isnt r.o), (r) -> "#{r.s.uri}>>>#{r.p}>>>#{r.o.uri}")
          
        relations.enter().append('path')
          .attr('class', 'relation')
          .attr('d', (r) -> "M#{r.s.x} #{r.s.y} C#{r.s.x} #{r.s.y-120} #{r.o.x} #{r.o.y-120} #{r.o.x} #{r.o.y}")
          
        relations.exit()
          .remove()
      
  enter_land_cells.append('title')
    .text((n) -> n.point.uri.replace('http://dbpedia.org/resource/',''))
  
  land_cells.attr('d', (d) -> "M" + d.join(",") + "Z")
      
  ### update links ###
  links = graph_layer.selectAll('.link')
    .data(graph.links)
  
  links.enter().append('line')
    .attr('class', 'link')
    .attr('opacity', (l) -> l.similarity)
  
  links
      .attr('x1', (l) -> l.source.x)
      .attr('y1', (l) -> l.source.y)
      .attr('x2', (l) -> l.target.x)
      .attr('y2', (l) -> l.target.y)
      
  ### update nodes ###
  nodes = graph_layer.selectAll('.node')
    .data(graph.nodes)
    
  nodes.enter().append('circle')
    .attr('class', 'node')
    .attr('r', 2)
    
  nodes
    .attr('cx', (n) -> n.x)
    .attr('cy', (n) -> n.y)
    

index.css

html, body {
  padding: 0;
  margin: 0;
}
svg {
  background: #404040;
  display: inline-block;
}
#panel {
  position: absolute;
  left: 660px;
  top: 0;
  width: 300px;
  height: 500px;
  background: rgba(255, 255, 255, 0.75);
  vertical-align: top;
  padding: 12px;
  box-sizing: border-box;
  color: #333;
  overflow-y: auto;
}
#panel header {
  font-size: 20px;
  font-weight: bold;
  color: black;
  margin-bottom: 12px;
}
#sea_pattern {
  opacity: 0.07;
  fill: url(#sea);
}
.link {
  stroke: orange;
  vector-effect: non-scaling-stroke;
  display: none;
}
.node {
  fill: #DDD;
  stroke: #777;
  vector-effect: non-scaling-stroke;
  display: none;
}
.deep_cell {
  fill: #484444;
}
.shallow_cell {
  fill: #755;
}
.land_cell {
  fill: #f4a582;
  stroke: #977;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
.land_cell:hover {
  fill: #ff7777;
  cursor: pointer;
}
.selected.land_cell {
  fill: #DDD;
}
.selected.land_cell:hover {
  fill: white;
}
.relation {
  stroke: white;
  stroke-width: 0.5;
  vector-effect: non-scaling-stroke;
  fill: none;
  pointer-events: none;
}