Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active August 29, 2015 14:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nitaku/38e263524da7ef85e11f to your computer and use it in GitHub Desktop.
Save nitaku/38e263524da7ef85e11f to your computer and use it in GitHub Desktop.
Multivariate binning
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)
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;
}
<!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="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/d3.hexbin.v0.min.js?5c6e4f0"></script>
<script src="http://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>
(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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment