Tessellating equilateral triangles. Move mouse to adjust the size of the triangles. Click to invert the sizing.
Could be extended for two-dimensional binning, useful when there are a lot of data points. Other shapes that tesselate well are squares and hexagons.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Trianglar Tessellation</title>
</head>
<body>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
var width = 960,
height = 500,
trianglesAcross = 25; // # of triangles on the top row
var color = d3.scale.linear()
.domain([0, 1])
.range(["#1c9099", "#ece2f0"])
.interpolate(d3.interpolateLab);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.call(draw, function(d) { return proximity(d, width/2, height/2)});
var inverse = false;
svg
.on("mousemove", mousemove)
.on("click", click);
function mousemove() {
var mouse = d3.mouse(this)
svg.call(draw, function(d) {
return inverse ?
inverseProximity(d, mouse[0], mouse[1]) :
proximity(d, mouse[0], mouse[1]);
});
}
function click() {
var mouse = d3.mouse(this)
inverse = !inverse;
svg
.on("mousemove", null)
.call(draw, function(d) {
return inverse ?
inverseProximity(d, mouse[0], mouse[1]) :
proximity(d, mouse[0], mouse[1]);
}, 1000);
// Hacky way of preventing "mousemove" from interrupting animation
setTimeout(function() {
svg.on("mousemove", mousemove);
}, 1000);
}
function proximity(d, x, y) {
var dist = 1 - Math.sqrt(Math.pow(d.cx - x, 2) + Math.pow(d.cy - y, 2))/width;
return Math.pow(dist, 6);
}
function inverseProximity(d, x, y) {
var dist = Math.sqrt(Math.pow(d.cx - x, 2) + Math.pow(d.cy - y, 2))/width;
return Math.pow(dist, 2);
}
function draw(selection, area, duration) {
if (duration === undefined) duration = 0;
var data = createTriangleData(width, height, trianglesAcross, area);
var triangles = selection.selectAll("path").data(data);
triangles.enter().append("path");
triangles
.transition().duration(duration)
.attr("d", trianglePath)
.style("fill", function(d) { return color(d.p); });
triangles.exit().remove();
}
function trianglePath(d) {
var alpha = Math.sqrt(d.p * Math.pow(d.alpha, 2)),
ri = alpha * Math.sqrt(3) / 6,
rc = alpha / Math.sqrt(3);
var points = null;
if (d.pointing == "up") {
points = [
[d.cx - alpha/2, d.cy - ri],
[d.cx, d.cy + rc],
[d.cx + alpha/2, d.cy - ri]
];
}
else if (d.pointing == "down") {
points = [
[d.cx - alpha/2, d.cy + ri],
[d.cx + alpha/2, d.cy + ri],
[d.cx, d.cy - rc]
];
}
return d3.svg.line()(points);
}
function createTriangleData(width, height, trianglesAcross, area) {
// Source of geometric definitions:
// https://en.wikipedia.org/wiki/Equilateral_triangle
var alpha = width/trianglesAcross, // maximum side length
ri = alpha * Math.sqrt(3) / 6, // maximum radius of inscribing circle
rc = alpha / Math.sqrt(3); // maximum radius of circumscribing circle
var data = [];
// Upward pointing triangles
for (var x = alpha/2; x <= (width + alpha); x += alpha) {
for (var y = ri, i = 0; y <= (height + alpha); y += (ri+rc), i++) {
data.push({
cx: x - (i % 2 == 0 ? 0 : alpha/2),
cy: y,
pointing: "up",
alpha: alpha
});
}
}
// Downward pointing triangles
for (var x = 0; x <= (width + alpha); x += alpha) {
for (var y = rc, i = 0; y <= (height + alpha); y += (ri+rc), i++) {
data.push({
cx: x - (i % 2 == 0 ? 0 : alpha/2),
cy: y,
pointing: "down",
alpha: alpha
});
}
}
// p in [0, 1] maps the triangle's area from 0 to its maximum area
data = data.map(function(d) { d.p = area(d); return d; });
return data;
}
</script>
</body>
</html>