Triangular binning a set of 2D points. Maps area to the density of points within a triangle.
Essentially a fork of this block that does hexagonal binning. Uses the d3.triangleBin plugin. See this block which maps density to color instead of area.
Triangular binning a set of 2D points. Maps area to the density of points within a triangle.
Essentially a fork of this block that does hexagonal binning. Uses the d3.triangleBin plugin. See this block which maps density to color instead of area.
<html> | |
<head> | |
<style> | |
body { | |
font: 12px sans-serif; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #000; | |
} | |
.triangle { | |
fill: none; | |
stroke: #ddd; | |
stroke-width: 0.5px; | |
} | |
</style> | |
</head> | |
<body> | |
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
<script src="triangle-bin.js"></script> | |
<script> | |
var margin = { top: 10, left: 40, bottom: 30, right: 10 }, | |
width = 960 - margin.left - margin.right, | |
height = 500 - margin.top - margin.bottom, | |
maxSideLength = 35, | |
maxArea = Math.pow(maxSideLength, 2) * Math.sqrt(3)/4; | |
var points = d3.range(2000) | |
.map(function() { | |
return [ | |
d3.random.normal(width/2, 80)(), | |
d3.random.normal(height/2, 80)() | |
]; | |
}); | |
var xScale = d3.scale.identity().domain([0, width]), | |
yScale = d3.scale.linear() | |
.domain([0, height]) | |
.range([height, 0]); | |
var xAxis = d3.svg.axis().scale(xScale).orient("bottom"), | |
yAxis = d3.svg.axis().scale(yScale).orient("left"); | |
var areaScale = d3.scale.linear() | |
.domain([0, 35]) | |
.range([0, maxArea]); | |
var sideLengthScale = function(d) { | |
var A = areaScale(d); | |
return Math.sqrt(4 * A / Math.sqrt(3)); | |
}; | |
var svg = d3.select("body").append("svg") | |
.attr("width", width + margin.left + margin.right) | |
.attr("height", height + margin.top + margin.bottom) | |
.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
var tribin = d3.triangleBin() | |
.size([width, height]) | |
.sideLength(maxSideLength); | |
svg.append("clipPath") | |
.attr("id", "clip") | |
.append("rect") | |
.attr("width", width) | |
.attr("height", height); | |
svg.append("g") | |
.attr("clip-path", "url(#clip)") | |
.selectAll(".triangle") | |
.data(tribin(points)) | |
.enter().append("path") | |
.attr("class", "triangle") | |
.attr("transform", function(d) { | |
return "translate(" + d.x + "," + d.y + ")"; | |
}) | |
.attr("d", function(d) { | |
var sideLength = sideLengthScale(d.length); | |
return tribin.triangle(d.orientation, sideLength); | |
}) | |
.style("fill", "steelblue"); | |
svg.append("g").call(xAxis) | |
.attr("class", "x axis") | |
.attr("transform", "translate(0," + height + ")"); | |
svg.append("g").call(yAxis) | |
.attr("class", "y axis"); | |
</script> | |
</body> | |
</html> |
d3.triangleBin = function() { | |
var size = [400, 300], | |
sideLength = 30, | |
x = function(d) { return d[0]; }, | |
y = function(d) { return d[1]; }; | |
function triangleBin(data) { | |
var points = data.map(function(d) { return [x(d), y(d)]; }); | |
var triangles = createTriangleGrid(size, sideLength) | |
.map(function(d) { d.points = []; return d; }); | |
// TODO: Optimize binning. This brute force search is slow. | |
// Bin points in triangles | |
points.forEach(function(point, i) { | |
for (var i = 0; i < triangles.length; i++) { | |
if (pointInTriangle(triangles[i], point)) triangles[i].points.push(point); | |
} | |
}); | |
return triangles | |
.map(function(d) { | |
var center = d.center, | |
orientation = d.orientation; | |
d = d.points; | |
d.x = center[0]; | |
d.y = center[1]; | |
d.orientation = orientation; | |
return d.length > 0 ? d : null; | |
}) | |
.filter(function(d) { return d !== null; }); | |
} | |
triangleBin.size = function(_) { | |
if (!arguments.length) return size; | |
size = _; | |
return triangleBin; | |
}; | |
triangleBin.sideLength = function(_) { | |
if (!arguments.length) return sideLength; | |
sideLength = _; | |
return triangleBin; | |
}; | |
triangleBin.x = function(_) { | |
if (!arguments.length) return x; | |
x = _; | |
return triangleBin; | |
}; | |
triangleBin.y = function(_) { | |
if (!arguments.length) return y; | |
y = _; | |
return triangleBin; | |
}; | |
triangleBin.triangle = function(orientation, length) { | |
length = length || sideLength; | |
var points = createTriangle([0, 0], length, orientation); | |
return "M" + points.join("L") + "Z"; | |
}; | |
return triangleBin; | |
// Creates an array of triangles that covers the area of the canvas | |
function createTriangleGrid(size, sideLength) { | |
var triangles = [], | |
rc = sideLength / Math.sqrt(3), // maximum radius of circumscribing circle | |
ri = rc / 2; // maximum radius of inscribing circle | |
// upward pointing triangle | |
for (var x = sideLength/2; x <= size[0] + sideLength; x += sideLength) { | |
for (var y = rc - ri; y <= size[1] + sideLength; y += rc + ri) { | |
var triangle = createTriangle([x, y], sideLength, "up"); | |
triangles.push(triangle); | |
} | |
} | |
// downward pointing triangles | |
for (var x = 0; x <= size[0] + sideLength; x += sideLength) { | |
for (var y = 0; y <= size[1] + sideLength; y += rc + ri) { | |
var triangle = createTriangle([x, y], sideLength, "down"); | |
triangles.push(triangle); | |
} | |
} | |
return triangles; | |
} | |
// Create equilateral triangle (with counterclockwise vertices) | |
function createTriangle(center, sideLength, orientation) { | |
var cx = center[0], | |
cy = center[1], | |
rc = sideLength / Math.sqrt(3), // maximum radius of circumscribing circle | |
ri = rc / 2; // maximum radius of inscribing circle | |
// Add vertices | |
if (orientation === "up") { | |
var triangle = [ | |
[cx, cy - rc], | |
[cx - sideLength/2, cy + ri], | |
[cx + sideLength/2, cy + ri] | |
]; | |
} | |
else if (orientation === "down") { | |
var triangle = [ | |
[cx, cy + rc], | |
[cx + sideLength/2, cy - ri], | |
[cx - sideLength/2, cy - ri] | |
]; | |
} | |
triangle.center = center; | |
triangle.orientation = orientation; | |
return triangle; | |
} | |
// identify which side of a line and given point is | |
function sideOfLine(line, point) { | |
// TODO: clean up naming | |
var x1 = line[0][0], | |
y1 = line[0][1], | |
x2 = line[1][0], | |
y2 = line[1][1], | |
x = point[0], | |
y = point[1]; | |
return (y2 - y1) * (x - x1) + (-x2 + x1) * (y - y1); | |
} | |
// identify if a point is in a triangle | |
function pointInTriangle(triangle, point) { | |
// triangle points must be counterclockwise | |
// TODO: clean up naming | |
var x1 = triangle[0][0], | |
y1 = triangle[0][1], | |
x2 = triangle[1][0], | |
y2 = triangle[1][1], | |
x3 = triangle[2][0], | |
y3 = triangle[2][1], | |
x = point[0], | |
y = point[1]; | |
var checkSide1 = sideOfLine([[x1, y1], [x2, y2]], [x, y]) >= 0, | |
checkSide2 = sideOfLine([[x2, y2], [x3, y3]], [x, y]) >= 0, | |
checkSide3 = sideOfLine([[x3, y3], [x1, y1]], [x, y]) >= 0; | |
return checkSide1 && checkSide2 && checkSide3; | |
} | |
} |