block by nitaku 38e263524da7ef85e11f

Multivariate binning

Full Screen

-

index.js

(function() {
  var angle, apothem, arc_generator, bins, bottom_left, bottom_right, classes, color, distributions, dots, hexbin, max, max_tot, outer_polygon_generator, pie_layout, pies, points, polygon_generator, radius, radius_scale, side, subplots, svg, upper_left, upper_middle, upper_right;

  side = 320;

  distributions = [
    d3.range(1500).map(function() {
      return {
        x: d3.random.normal(side / 2, 80)(),
        y: d3.random.normal(side / 2, 80)()
      };
    }), d3.range(1500).map(function() {
      return {
        x: d3.random.normal(side / 2, 80)() - 50,
        y: 100 + d3.random.normal(side / 2, 80)()
      };
    }), d3.range(1000).map(function() {
      return {
        x: d3.random.normal(3.5 * side / 4, 80)(),
        y: d3.random.normal(side / 2, 80)()
      };
    })
  ];

  classes = distributions.length;

  points = _.chain(distributions).map(function(distribution, klass) {
    distribution.forEach(function(point) {
      return point["class"] = klass;
    });
    return distribution;
  }).flatten(true).value();

  radius = 26;

  apothem = Math.sqrt(3) / 2 * radius;

  hexbin = d3.hexbin().size([side, side]).radius(radius).x(function(d) {
    return d.x;
  }).y(function(d) {
    return d.y;
  });

  bins = _.chain(hexbin(points)).forEach(function(bin) {
    return bin.classes_count = _.chain(_.range(classes)).map(function(klass) {
      return bin.filter(function(point) {
        return point["class"] === klass;
      }).length;
    }).value();
  }).value();

  max_tot = d3.max(bins, function(bin) {
    return bin.length;
  });

  max = d3.max(bins, function(bin) {
    return d3.max(bin.classes_count);
  });

  angle = 2 * Math.PI / classes;

  svg = d3.select('svg');

  upper_left = svg.append('g').attr('id', 'pies').attr('clip-path', 'url(#square_window)');

  upper_middle = svg.append('g').attr('id', 'dots').attr('clip-path', 'url(#square_window)').attr('transform', "translate(" + side + ",0)");

  upper_right = svg.append('g').attr('id', 'radar').attr('clip-path', 'url(#square_window)').attr('transform', "translate(" + (2 * side) + ",0)");

  bottom_left = svg.append('g').attr('id', 'polar_length').attr('clip-path', 'url(#square_window)').attr('transform', "translate(" + (side / 2) + "," + side + ")");

  bottom_right = svg.append('g').attr('id', 'polar_area').attr('clip-path', 'url(#square_window)').attr('transform', "translate(" + (3 * side / 2) + "," + side + ")");

  svg.append('line').attr({
    "class": 'separator',
    x1: 0,
    x2: 3 * side,
    y1: side,
    y2: side
  });

  svg.append('line').attr({
    "class": 'separator',
    x1: side,
    x2: side,
    y1: 0,
    y2: side
  });

  svg.append('line').attr({
    "class": 'separator',
    x1: 2 * side,
    x2: 2 * side,
    y1: 0,
    y2: side
  });

  svg.append('line').attr({
    "class": 'separator',
    x1: side / 2,
    x2: side / 2,
    y1: side,
    y2: 2 * side
  });

  svg.append('line').attr({
    "class": 'separator',
    x1: 3 * side / 2,
    x2: 3 * side / 2,
    y1: side,
    y2: 2 * side
  });

  svg.append('line').attr({
    "class": 'separator',
    x1: 5 * side / 2,
    x2: 5 * side / 2,
    y1: side,
    y2: 2 * side
  });

  color = d3.scale.category10();

  dots = upper_middle.selectAll('.dot').data(points);

  dots.enter().append('circle').attr({
    "class": 'dot',
    r: 1,
    cx: function(p) {
      return p.x;
    },
    cy: function(p) {
      return p.y;
    },
    fill: function(d) {
      return color(d["class"]);
    }
  });

  subplots = upper_left.selectAll('.subplot').data(bins);

  subplots.enter().append('g').attr({
    "class": 'subplot',
    transform: function(bin) {
      return "translate(" + bin.x + "," + bin.y + ")";
    }
  });

  pie_layout = d3.layout.pie().sort(null);

  radius_scale = d3.scale.sqrt().domain([0, max_tot]).range([0, apothem]);

  arc_generator = d3.svg.arc().outerRadius(function(count) {
    return radius_scale(d3.select(this.parentNode).datum().length);
  }).innerRadius(0);

  pies = subplots.selectAll('.pie').data(function(bin) {
    return pie_layout(bin.classes_count);
  });

  pies.enter().append('path').attr({
    "class": 'pie',
    d: arc_generator,
    fill: function(count, klass) {
      return color(klass);
    }
  });

  subplots = upper_right.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, apothem]);

  outer_polygon_generator = d3.svg.line().x(function(count, klass) {
    return apothem * Math.cos(klass * angle - Math.PI / 2);
  }).y(function(count, klass) {
    return apothem * Math.sin(klass * angle - Math.PI / 2);
  });

  polygon_generator = d3.svg.line().x(function(count, klass) {
    return radius_scale(count) * Math.cos(klass * angle - Math.PI / 2);
  }).y(function(count, klass) {
    return radius_scale(count) * Math.sin(klass * angle - Math.PI / 2);
  });

  subplots.append('path').attr({
    "class": 'outer_polygon',
    d: function(bin) {
      return outer_polygon_generator(bin.classes_count) + 'z';
    }
  });

  subplots.append('path').attr({
    "class": 'polygon',
    d: function(bin) {
      return polygon_generator(bin.classes_count) + 'z';
    }
  });

  subplots = bottom_left.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, apothem]);

  arc_generator = d3.svg.arc().innerRadius(0).outerRadius(function(count) {
    return radius_scale(count);
  }).startAngle(function(count, klass) {
    return klass * angle - angle / 2;
  }).endAngle(function(count, klass) {
    return klass * angle + angle / 2;
  });

  pies = subplots.selectAll('.pie').data(function(bin) {
    return bin.classes_count;
  });

  pies.enter().append('path').attr({
    "class": 'pie',
    d: arc_generator,
    fill: function(count, klass) {
      return color(klass);
    }
  });

  subplots = bottom_right.selectAll('.subplot').data(bins);

  subplots.enter().append('g').attr({
    "class": 'subplot',
    transform: function(bin) {
      return "translate(" + bin.x + "," + bin.y + ")";
    }
  });

  radius_scale = d3.scale.sqrt().domain([0, max]).range([0, apothem]);

  arc_generator = d3.svg.arc().innerRadius(0).outerRadius(function(count) {
    return radius_scale(count);
  }).startAngle(function(count, klass) {
    return klass * angle - angle / 2;
  }).endAngle(function(count, klass) {
    return klass * angle + angle / 2;
  });

  pies = subplots.selectAll('.pie').data(function(bin) {
    return bin.classes_count;
  });

  pies.enter().append('path').attr({
    "class": 'pie',
    d: arc_generator,
    fill: function(count, klass) {
      return color(klass);
    }
  });

}).call(this);

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="description" content="Multivariate binning" />
  <title>Multivariate 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="//cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
</head>
<body>
  <svg height="640" width="960">
    <defs>
      <clipPath id="square_window">
        <rect x="0" y="0" width="320.5" height="320.5" />
      </clipPath>
    </defs>
  </svg>
  <script src="index.js"></script>
</body>
</html>

index.coffee

side = 320

# DATA
distributions = [
  d3.range(1500).map( () -> {x: d3.random.normal(side/2, 80)(), y: d3.random.normal(side/2, 80)()} ),
  d3.range(1500).map( () -> {x: d3.random.normal(side/2, 80)() - 50, y: 100+d3.random.normal(side/2, 80)()} ),
  #d3.range(1500).map( () -> {x: d3.random.normal(side/2, 80)(), y: d3.random.normal(side/2, 80)() - 100} ),
  d3.range(1000).map( () -> {x: d3.random.normal(3.5*side/4, 80)(), y: d3.random.normal(side/2, 80)()} ),
  #d3.range(1000).map( () -> {x: d3.random.normal(side/4, 80)(), y: d3.random.normal(side/2, 80)()} )
]

classes = distributions.length

points = _.chain(distributions)
  .map( (distribution, klass) ->
    distribution.forEach (point) -> point.class = klass
    return distribution
  )
  .flatten(true)
  .value()
  
# hexagonal binning
radius = 26
apothem = Math.sqrt(3)/2 * radius

hexbin = d3.hexbin()
  .size([side, side])
  .radius(radius)
  .x((d) -> d.x)
  .y((d) -> d.y)
  
bins = _.chain(hexbin(points))
  .forEach( (bin) ->
    bin.classes_count = _.chain(_.range(classes))
      .map( (klass) -> bin.filter( (point) -> point.class is klass ).length )
      .value()
  )
  .value()

max_tot = d3.max(bins, (bin) -> bin.length)
max = d3.max(bins, (bin) -> d3.max(bin.classes_count) )
angle = 2*Math.PI / classes


svg = d3.select('svg')

upper_left = svg.append('g')
    .attr('id', 'pies')
    .attr('clip-path', 'url(#square_window)')
  
upper_middle = svg.append('g')
    .attr('id', 'dots')
    .attr('clip-path', 'url(#square_window)')
    .attr('transform', "translate(#{side},0)")
    
upper_right = svg.append('g')
    .attr('id', 'radar')
    .attr('clip-path', 'url(#square_window)')
    .attr('transform', "translate(#{2*side},0)")
    
bottom_left = svg.append('g')
    .attr('id', 'polar_length')
    .attr('clip-path', 'url(#square_window)')
    .attr('transform', "translate(#{side/2},#{side})")
    
bottom_right = svg.append('g')
    .attr('id', 'polar_area')
    .attr('clip-path', 'url(#square_window)')
    .attr('transform', "translate(#{3*side/2},#{side})")
    
svg.append('line')
  .attr
    class: 'separator'
    x1: 0
    x2: 3*side
    y1: side
    y2: side
  
svg.append('line')
  .attr
    class: 'separator'
    x1: side
    x2: side
    y1: 0
    y2: side
    
svg.append('line')
  .attr
    class: 'separator'
    x1: 2*side
    x2: 2*side
    y1: 0
    y2: side
    
svg.append('line')
  .attr
    class: 'separator'
    x1: side/2
    x2: side/2
    y1: side
    y2: 2*side
    
svg.append('line')
  .attr
    class: 'separator'
    x1: 3*side/2
    x2: 3*side/2
    y1: side
    y2: 2*side
    
svg.append('line')
  .attr
    class: 'separator'
    x1: 5*side/2
    x2: 5*side/2
    y1: side
    y2: 2*side
  
#color = d3.scale.ordinal()
#  .domain([0, classes])
#  .range(["#1b9e77","#d95f02","#66a61e","#e6ab02","#e7298a","#a6761d","#666666"])
color = d3.scale.category10()

# dot density plot
dots = upper_middle.selectAll('.dot')
  .data(points)

dots.enter().append('circle')
  .attr
    class: 'dot'
    r: 1
    cx: (p) -> p.x
    cy: (p) -> p.y
    fill: (d) -> color(d.class)
    

# pie chart subplotting
subplots = upper_left.selectAll('.subplot')
  .data(bins)
  
subplots.enter().append('g')
  .attr
    class: 'subplot'
    transform: (bin) -> "translate(#{bin.x},#{bin.y})"
    
pie_layout = d3.layout.pie()
  .sort(null)
  
radius_scale = d3.scale.sqrt()
  .domain([0, max_tot])
  .range([0, apothem])
  
arc_generator = d3.svg.arc()
  .outerRadius((count) -> radius_scale( d3.select(this.parentNode).datum().length ))
  .innerRadius(0)
    
pies = subplots.selectAll('.pie')
  .data((bin) -> pie_layout(bin.classes_count))
  
pies.enter().append('path')
  .attr
    class: 'pie'
    d: arc_generator
    fill: (count, klass) -> color(klass)


# radar chart subplotting
subplots = upper_right.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, apothem])
      
outer_polygon_generator = d3.svg.line()
  .x((count, klass) -> apothem * Math.cos(klass*angle-Math.PI/2))
  .y((count, klass) -> apothem * Math.sin(klass*angle-Math.PI/2))
  
polygon_generator = d3.svg.line()
  .x((count, klass) -> radius_scale(count) * Math.cos(klass*angle-Math.PI/2))
  .y((count, klass) -> radius_scale(count) * Math.sin(klass*angle-Math.PI/2))
  
subplots.append('path')
  .attr
    class: 'outer_polygon'
    d: (bin) -> outer_polygon_generator(bin.classes_count) + 'z'
    
subplots.append('path')
  .attr
    class: 'polygon'
    d: (bin) -> polygon_generator(bin.classes_count) + 'z'
    

# polar length chart subplotting
subplots = bottom_left.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, apothem])

arc_generator = d3.svg.arc()
  .innerRadius(0)
  .outerRadius((count) -> radius_scale(count))
  .startAngle((count, klass) -> klass*angle - angle/2)
  .endAngle((count, klass) -> klass*angle + angle/2)
  
pies = subplots.selectAll('.pie')
  .data((bin) -> bin.classes_count)
  
pies.enter().append('path')
  .attr
    class: 'pie'
    d: arc_generator
    fill: (count, klass) -> color(klass)


# polar area chart subplotting
subplots = bottom_right.selectAll('.subplot')
  .data(bins)
  
subplots.enter().append('g')
  .attr
    class: 'subplot'
    transform: (bin) -> "translate(#{bin.x},#{bin.y})"
    
radius_scale = d3.scale.sqrt()
  .domain([0, max])
  .range([0, apothem])

arc_generator = d3.svg.arc()
  .innerRadius(0)
  .outerRadius((count) -> radius_scale(count))
  .startAngle((count, klass) -> klass*angle - angle/2)
  .endAngle((count, klass) -> klass*angle + angle/2)
  
pies = subplots.selectAll('.pie')
  .data((bin) -> bin.classes_count)
  
pies.enter().append('path')
  .attr
    class: 'pie'
    d: arc_generator
    fill: (count, klass) -> color(klass)

index.css

svg {
  background-color: white;
}
.separator {
  stroke: #DEDEDE;
  fill: none;
  shape-rendering: crispEdges;
}
.outer_polygon {
  fill: #EEE;
}

.pie {
  stroke: white;
  stroke-width: 0.5;
}

#pies .pie {
  stroke: none;
}