block by nitaku fc0db597d941956e6489

OpeNER - Places with categories (polar chart binning)

Full Screen

-

index.js

(function() {
  var categories, classes, colorify, data_url, geo_url, height, path_generator, projection, radius, svg, width, zoom, zoom_group;

  width = 960;

  height = 500;

  radius = 2;

  svg = d3.select('svg');

  zoom_group = svg.append('g');

  zoom = d3.behavior.zoom().scaleExtent([1, 4]).on('zoom', function() {
    zoom_group.attr('transform', "translate(" + (zoom.translate()) + ")scale(" + (zoom.scale()) + ")");
    return zoom_group.selectAll('.place').attr('r', radius / zoom.scale());
  });

  svg.call(zoom);

  projection = d3.geo.azimuthalEqualArea().clipAngle(180 - 1e-3).scale(160000).rotate([-10.277475, -42.789034, 0]).translate([width / 2, height / 2]).precision(0.1);

  path_generator = d3.geo.path().projection(projection);

  categories = ['restaurant', 'attraction', 'poi', 'accommodation'];

  classes = categories.length;

  colorify = d3.scale.category10().domain(categories);

  data_url = 'http://wafi.iit.cnr.it/webvis/tmp/elba_places.json';

  geo_url = 'http://wafi.iit.cnr.it/webvis/italiastat/data/istat/2011/reg2011.topo.json';

  d3.json(geo_url, function(geo) {
    var regions;

    regions = topojson.feature(geo, geo.objects.reg2011);
    zoom_group.selectAll('.region').data(regions.features).enter().append('path').attr('class', 'region').attr('d', path_generator);
    return d3.json(data_url, function(points) {
      var angle, apothem, arc_generator, bins, hexbin, max, max_tot, pies, radius_scale, subplots;

      radius = 22;
      apothem = Math.sqrt(3) / 2 * radius;
      hexbin = d3.hexbin().size([width, height]).radius(radius).x(function(d) {
        return projection([d.lng, d.lat])[0];
      }).y(function(d) {
        return projection([d.lng, d.lat])[1];
      });
      bins = _.chain(hexbin(points)).forEach(function(bin) {
        return bin.classes_count = _.chain(categories).map(function(klass) {
          var count;

          count = bin.filter(function(point) {
            return point.category === klass;
          }).length;
          return {
            count: count,
            "class": klass
          };
        }).value();
      }).value();
      max_tot = d3.max(bins, function(bin) {
        return bin.length;
      });
      max = d3.max(bins, function(bin) {
        return d3.max(bin.classes_count, function(count) {
          return count.count;
        });
      });
      angle = 2 * Math.PI / classes;
      subplots = zoom_group.selectAll('.subplot').data(bins);
      subplots.enter().append('g').attr({
        "class": 'subplot',
        transform: function(bin) {
          return "translate(" + bin.x + "," + bin.y + ")";
        }
      });
      radius_scale = d3.scale.linear().domain([0, max]).range([0, radius]);
      arc_generator = d3.svg.arc().innerRadius(0).outerRadius(function(count) {
        return radius_scale(count.count);
      }).startAngle(function(count, i) {
        return i * angle - angle / 2;
      }).endAngle(function(count, i) {
        return i * angle + angle / 2;
      });
      pies = subplots.selectAll('.pie').data(function(bin) {
        return bin.classes_count;
      });
      return pies.enter().append('path').attr({
        "class": 'pie',
        d: arc_generator,
        fill: function(count) {
          return colorify(count["class"]);
        }
      });
    });
  });

}).call(this);

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="description" content="OpeNER - Places with categories (polar chart binning)" />
    <title>OpeNER - Places with categories (polar chart binning)</title>
    <link rel="stylesheet" href="index.css">
    <script src="//d3js.org/d3.v3.min.js"></script>
    <script src="//d3js.org/d3.hexbin.v0.min.js?5c6e4f0"></script>
    <script src="//d3js.org/topojson.v1.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
</head>
<body>
  <svg height="500" width="960"></svg>
  
  <script src="index.js"></script>
</body>
</html>

index.coffee

# Setup
width = 960
height = 500
radius = 2

svg = d3.select('svg')

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

# define a zoom behavior
zoom = d3.behavior.zoom()
    .scaleExtent([1,4]) # min-max zoom
    .on('zoom', () ->
      # whenever the user zooms,
      # modify translation and scale of the zoom group accordingly
      zoom_group.attr('transform', "translate(#{zoom.translate()})scale(#{zoom.scale()})")
      
      # semantic zooming
      zoom_group.selectAll('.place')
        .attr('r', radius/zoom.scale())
    )

# bind the zoom behavior to the main SVG
svg.call(zoom)

# Lambert equal-area projection - EU standard for statistical maps
projection = d3.geo.azimuthalEqualArea()
  .clipAngle(180 - 1e-3)
  .scale(160000)
  .rotate([-10.277475, -42.789034, 0])
  .translate([width / 2, height / 2])
  .precision(0.1)

path_generator = d3.geo.path()
  .projection(projection)

  
categories = ['restaurant', 'attraction', 'poi', 'accommodation']
classes = categories.length
colorify = d3.scale.category10()
  .domain(categories)


data_url = 'http://wafi.iit.cnr.it/webvis/tmp/elba_places.json'
geo_url = 'http://wafi.iit.cnr.it/webvis/italiastat/data/istat/2011/reg2011.topo.json'

d3.json geo_url, (geo) ->
  regions = topojson.feature(geo, geo.objects.reg2011)
  
  zoom_group.selectAll('.region')
        .data(regions.features)
      .enter().append('path')
        .attr('class', 'region')
        .attr('d', path_generator)
  
  d3.json data_url, (points) ->
    
    # hexagonal binning
    radius = 22
    apothem = Math.sqrt(3)/2 * radius
    
    hexbin = d3.hexbin()
      .size([width, height])
      .radius(radius)
      .x((d) -> projection([d.lng, d.lat])[0] )
      .y((d) -> projection([d.lng, d.lat])[1] )
      
    bins = _.chain(hexbin(points))
      .forEach( (bin) ->
        bin.classes_count = _.chain(categories)
          .map( (klass) ->
            count = bin.filter( (point) -> point.category is klass ).length
            return {
              count: count,
              class: klass
            }
          )
          .value()
      )
      .value()
    
    max_tot = d3.max(bins, (bin) -> bin.length)
    max = d3.max(bins, (bin) -> d3.max(bin.classes_count, (count) -> count.count) )
    angle = 2*Math.PI / classes
    
    # polar area chart subplotting
    subplots = zoom_group.selectAll('.subplot')
      .data(bins)
      
    subplots.enter().append('g')
      .attr
        class: 'subplot'
        transform: (bin) -> "translate(#{bin.x},#{bin.y})"
        
    radius_scale = d3.scale.linear()
      .domain([0, max])
      .range([0, radius])
    
    arc_generator = d3.svg.arc()
      .innerRadius(0)
      .outerRadius((count) -> radius_scale(count.count))
      .startAngle((count, i) -> i*angle - angle/2)
      .endAngle((count, i) -> i*angle + angle/2)
      
    pies = subplots.selectAll('.pie')
      .data((bin) -> bin.classes_count)
      
    pies.enter().append('path')
      .attr
        class: 'pie'
        d: arc_generator
        fill: (count) -> colorify(count.class)
    
    
    #zoom_group.selectAll('.place')
    #    .data(points)
    #  .enter().append('circle')
    #    .attr('class', 'place')
    #    .attr('r', radius)
    #    .attr('fill', (d) -> colorify(d.category) )
    #    .attr('transform', (d) ->
    #      p = projection([d.lng, d.lat])
    #      return "translate(#{p[0]},#{p[1]})"
    #    )
    

index.css

body {
    margin: 0;
    padding: 0;
}
svg {
    background: #BEDDF5;
}

.region {
  fill: white;
}
.graticule {
  fill: none;
  stroke-width: 1px;
  stroke: gray;
  vector-effect: non-scaling-stroke;
}

.pie {
  stroke: white;
  stroke-width: 0.5;
  vector-effect: non-scaling-stroke;
}