block by nitaku 317e231bdbcc0b266203

Multidimensional Duck

Full Screen

-

index.js

// Generated by CoffeeScript 1.10.0
(function() {
  var MARGIN, height, svg, width, zoom, zoomable_layer;

  MARGIN = 100;

  svg = d3.select('svg');

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

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

  zoomable_layer = svg.append('g');

  zoom = d3.behavior.zoom().scaleExtent([0.1, 400]).on('zoom', function() {
    zoomable_layer.attr({
      transform: "translate(" + (zoom.translate()) + ")scale(" + (zoom.scale()) + ")"
    });
    return zoomable_layer.selectAll('.semantic_zoom').attr({
      transform: "scale(" + (1 / zoom.scale()) + ")"
    });
  });

  svg.call(zoom);

  d3.json('//wafi.iit.cnr.it/webvis/tmp/dbpedia/galileo_matrix.json', function(data) {
    var enter_indicator_semzooms, enter_indicators, enter_points, enter_semzooms, indicators, keys, link_indicators, links, links_data, m, max_x, max_y, min_x, min_y, points, points_data, x, y;
    m = [];
    keys = [];
    data.forEach(function(e1) {
      var row;
      keys.push(e1.k1);
      row = [];
      m.push(row);
      return e1.similarities.forEach(function(e2) {
        var dist;
        dist = 1 - e2.sim;
        return row.push(dist);
      });
    });
    points_data = mds_classic(m);
    points_data.forEach(function(d) {
      d[0] = d[0] + 0.001 * Math.random();
      return d[1] = d[1] + 0.001 * Math.random();
    });
    min_x = d3.min(points_data, function(d) {
      return d[0];
    });
    max_x = d3.max(points_data, function(d) {
      return d[0];
    });
    min_y = d3.min(points_data, function(d) {
      return d[1];
    });
    max_y = d3.max(points_data, function(d) {
      return d[1];
    });
    x = d3.scale.linear().domain([min_x, max_x]).range([MARGIN, width - MARGIN]);
    y = d3.scale.linear().domain([min_y, max_y]).range([MARGIN, height - MARGIN]);
    links_data = [];
    points_data.forEach(function(p1, i1) {
      var array;
      array = [];
      points_data.forEach(function(p2, i2) {
        if (i1 !== i2) {
          return array.push({
            source: p1,
            target: p2,
            dist: m[i1][i2]
          });
        }
      });
      return links_data = links_data.concat(array);
    });
    links_data.forEach(function(d) {
      d.mul = d.dist / Math.sqrt(Math.pow(d.target[1] - d.source[1], 2) + Math.pow(d.target[0] - d.source[0], 2));
      d.indicator_x = x(d.source[0]) + d.mul * (x(d.target[0]) - x(d.source[0]));
      return d.indicator_y = y(d.source[1]) + d.mul * (y(d.target[1]) - y(d.source[1]));
    });
    links = zoomable_layer.selectAll('.link').data(links_data);
    links.enter().append('line').attr({
      "class": 'link',
      x1: function(d) {
        return x(d.source[0]);
      },
      y1: function(d) {
        return y(d.source[1]);
      },
      x2: function(d) {
        return x(d.target[0]);
      },
      y2: function(d) {
        return y(d.target[1]);
      }
    });
    link_indicators = zoomable_layer.selectAll('.link_indicator').data(links_data.filter(function(d) {
      return d.mul > 1;
    }));
    link_indicators.enter().append('line').attr({
      "class": 'link_indicator',
      x1: function(d) {
        return x(d.target[0]);
      },
      y1: function(d) {
        return y(d.target[1]);
      },
      x2: function(d) {
        return d.indicator_x;
      },
      y2: function(d) {
        return d.indicator_y;
      }
    });
    points = zoomable_layer.selectAll('.point').data(points_data);
    enter_points = points.enter().append('g').attr({
      "class": 'point',
      transform: function(d) {
        return "translate(" + (x(d[0])) + "," + (y(d[1])) + ")";
      }
    });
    enter_semzooms = enter_points.append('g').attr({
      "class": 'semantic_zoom'
    });
    enter_semzooms.append('circle').attr({
      r: 6,
      opacity: 0.3
    });
    enter_semzooms.append('circle').attr({
      r: 4
    });
    enter_semzooms.append('text').text(function(d, i) {
      return keys[i].replace('http://dbpedia.org/resource/', '').replace(/_/g, ' ');
    }).attr({
      y: 12,
      dy: '0.35em'
    });
    enter_points.append('title').text(function(d, i) {
      var name;
      name = keys[i].replace('http://dbpedia.org/resource/', '').replace(/_/g, ' ');
      return name + ("\n" + d[0] + ", " + d[1]);
    });
    indicators = zoomable_layer.selectAll('.indicator').data(links_data);
    enter_indicators = indicators.enter().append('g').attr({
      "class": 'indicator',
      transform: function(d) {
        return "translate(" + d.indicator_x + " " + d.indicator_y + ")";
      }
    });
    enter_indicator_semzooms = enter_indicators.append('g').attr({
      "class": 'semantic_zoom'
    });
    enter_indicator_semzooms.append('circle').attr({
      r: 5
    });
    return enter_points.on('click', function(d) {
      links.classed('visible', function(l) {
        return l.source === d;
      });
      indicators.classed('visible', function(l) {
        return l.source === d;
      });
      return link_indicators.classed('visible', function(l) {
        return l.source === d;
      });
    });
  });

}).call(this);

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="//d3js.org/d3.v3.min.js"></script>
    <script src="numeric-1.2.6.min.js"></script>
    <script src="mds.js"></script>
    <link rel="stylesheet" type="text/css" href="index.css">
    <title>Multidimensional Duck</title>
  </head>
  <body>
    <svg width="960px" height="500px"></svg>
    <script src="index.js"></script>
  </body>
</html>

index.coffee

MARGIN = 100

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

# append a group for zoomable content
zoomable_layer = svg.append('g')

# define a zoom behavior
zoom = d3.behavior.zoom()
  .scaleExtent([0.1,400])
  .on 'zoom', () ->
    # GEOMETRIC ZOOM
    zoomable_layer
      .attr
        transform: "translate(#{zoom.translate()})scale(#{zoom.scale()})"
    
    # SEMANTIC ZOOM
    # scale back all objects that have to be semantically zoomed
    zoomable_layer.selectAll('.semantic_zoom')
      .attr
        transform: "scale(#{1/zoom.scale()})"
    
svg.call(zoom)

d3.json '//wafi.iit.cnr.it/webvis/tmp/dbpedia/galileo_matrix.json', (data) ->
  m = []
  keys = []
  data.forEach (e1) ->
    keys.push e1.k1
    row = []
    m.push row
    e1.similarities.forEach (e2) ->
      dist = 1-e2.sim
      row.push dist

  points_data = mds_classic(m)
  
  # jitter to avoid overplotting
  # FIXME use collision detection
  points_data.forEach (d) ->
    d[0] = d[0] + 0.001*Math.random()
    d[1] = d[1] + 0.001*Math.random()
    
  min_x = d3.min points_data, (d) -> d[0]
  max_x = d3.max points_data, (d) -> d[0]
  min_y = d3.min points_data, (d) -> d[1]
  max_y = d3.max points_data, (d) -> d[1]

  x = d3.scale.linear()
    .domain([min_x, max_x])
    .range([MARGIN, width-MARGIN])

  y = d3.scale.linear()
    .domain([min_y, max_y])
    .range([MARGIN, height-MARGIN])
  

  links_data = []
  points_data.forEach (p1,i1) ->
    array = []
    points_data.forEach (p2,i2) ->
      if i1 isnt i2
        array.push {source: p1, target: p2, dist: m[i1][i2]}
    links_data = links_data.concat array

    
  links_data.forEach (d) ->
    d.mul = d.dist / Math.sqrt( Math.pow(d.target[1]-d.source[1],2) + Math.pow(d.target[0]-d.source[0],2) )
    d.indicator_x = x(d.source[0]) + d.mul*(x(d.target[0])-x(d.source[0]))
    d.indicator_y = y(d.source[1]) + d.mul*(y(d.target[1])-y(d.source[1]))
    
  # links
  
  links = zoomable_layer.selectAll('.link')
    .data(links_data)

  links.enter().append('line')
    .attr
      class: 'link'
      x1: (d) -> x(d.source[0])
      y1: (d) -> y(d.source[1])
      x2: (d) -> x(d.target[0])
      y2: (d) -> y(d.target[1])

  # link indicators
  link_indicators = zoomable_layer.selectAll('.link_indicator')
    .data(links_data.filter (d) -> d.mul > 1)

  link_indicators.enter().append('line')
    .attr
      class: 'link_indicator'
      x1: (d) -> x(d.target[0])
      y1: (d) -> y(d.target[1])
      x2: (d) -> d.indicator_x
      y2: (d) -> d.indicator_y
  
  # points

  points = zoomable_layer.selectAll('.point')
    .data(points_data)

  enter_points = points.enter().append('g')
    .attr
      class: 'point'
      transform: (d) -> "translate(#{x(d[0])},#{y(d[1])})"

  enter_semzooms = enter_points.append('g')
    .attr
      class: 'semantic_zoom'
        
  enter_semzooms.append('circle')
    .attr
      r: 6
      opacity: 0.3

  enter_semzooms.append('circle')
    .attr
      r: 4

  # FIXME use label placement
  enter_semzooms.append('text')
    .text (d,i)-> keys[i].replace('http://dbpedia.org/resource/', '').replace(/_/g,' ')
    .attr
      y: 12
      dy: '0.35em'

  enter_points.append('title')
    .text (d,i) ->
      name = keys[i].replace('http://dbpedia.org/resource/', '').replace(/_/g,' ')
      return name + "\n#{d[0]}, #{d[1]}"

  # distance indicators

  indicators = zoomable_layer.selectAll('.indicator')
    .data(links_data)

  enter_indicators = indicators.enter().append('g')
    .attr
      class: 'indicator'
      transform: (d) -> "translate(#{d.indicator_x} #{d.indicator_y})"

  enter_indicator_semzooms = enter_indicators.append('g')
    .attr
      class: 'semantic_zoom'
        
  enter_indicator_semzooms.append('circle')
    .attr
      r: 5
      
  # interaction

  enter_points
    .on 'click', (d) ->
      links.classed 'visible', (l) -> l.source is d
      indicators.classed 'visible', (l) -> l.source is d
      link_indicators.classed 'visible', (l) -> l.source is d

index.css

svg {
  background: #E6DFCC;
}
.point {
  fill: brown;
  opacity: 0.8;
  cursor: pointer;
}
.point:hover {
  opacity: 1;
}
.point text {
  fill: #333;
  text-anchor: middle;
  font-family: sans-serif;
  font-size: 10px;
}

.link, .indicator, .link_indicator {
  stroke: white;
  fill: none;
  visibility: hidden;
  pointer-events: none;
  vector-effect: non-scaling-stroke;
}
.link_indicator {
  stroke-dasharray: 2 2;
}
.visible.link, .visible.indicator, .visible.link_indicator {
  visibility: visible;
}

mds.js

// given a matrix of distances between some points, returns the
/// point coordinates that best approximate the distances
mds_classic = function(distances, dimensions) {
    dimensions = dimensions || 2;

    // square distances
    var M = numeric.mul(-.5, numeric.pow(distances, 2));

    // double centre the rows/columns
    function mean(A) { return numeric.div(numeric.add.apply(null, A), A.length); }
    var rowMeans = mean(M),
        colMeans = mean(numeric.transpose(M)),
        totalMean = mean(rowMeans);

    for (var i = 0; i < M.length; ++i) {
        for (var j =0; j < M[0].length; ++j) {
            M[i][j] += totalMean - rowMeans[i] - colMeans[j];
        }
    }

    // take the SVD of the double centred matrix, and return the
    // points from it
    var ret = numeric.svd(M),
        eigenValues = numeric.sqrt(ret.S);
    return ret.U.map(function(row) {
        return numeric.mul(row, eigenValues).splice(0, dimensions);
    });
};