Create a gist now

Instantly share code, notes, and snippets.

Fog of War IV

Fog of war is a technique used in video games for nearly four decades--first and often seen in complex tactical and strategy games. Like many data visualization techniques found in video games, it hasn't been adopted in more traditional data visualization. Here is a simple implementation. There are a set of points displayed on-screen with a set of hexes overlaid upon them. By clicking on a hex, you reveal what is underneath, and if there are no points underneath that hex, nearby empty hexes are also revealed. This hearkens to the mechanics of another, simpler game: minesweeper.

Other examples:

(function() {
d3.hexbin = function() {
var width = 1,
height = 1,
r,
x = d3_hexbinX,
y = d3_hexbinY,
dx,
dy;
function hexbin(points) {
var binsById = {};
points.forEach(function(point, i) {
var py = y.call(hexbin, point, i) / dy, pj = Math.round(py),
px = x.call(hexbin, point, i) / dx - (pj & 1 ? .5 : 0), pi = Math.round(px),
py1 = py - pj;
if (Math.abs(py1) * 3 > 1) {
var px1 = px - pi,
pi2 = pi + (px < pi ? -1 : 1) / 2,
pj2 = pj + (py < pj ? -1 : 1),
px2 = px - pi2,
py2 = py - pj2;
if (px1 * px1 + py1 * py1 > px2 * px2 + py2 * py2) pi = pi2 + (pj & 1 ? 1 : -1) / 2, pj = pj2;
}
var id = pi + "-" + pj, bin = binsById[id];
if (bin) bin.push(point); else {
bin = binsById[id] = [point];
bin.i = pi;
bin.j = pj;
bin.x = (pi + (pj & 1 ? 1 / 2 : 0)) * dx;
bin.y = pj * dy;
}
});
return d3.values(binsById);
}
function hexagon(radius) {
var x0 = 0, y0 = 0;
return d3_hexbinAngles.map(function(angle) {
var x1 = Math.sin(angle) * radius,
y1 = -Math.cos(angle) * radius,
dx = x1 - x0,
dy = y1 - y0;
x0 = x1, y0 = y1;
return [dx, dy];
});
}
hexbin.x = function(_) {
if (!arguments.length) return x;
x = _;
return hexbin;
};
hexbin.y = function(_) {
if (!arguments.length) return y;
y = _;
return hexbin;
};
hexbin.hexagon = function(radius) {
if (arguments.length < 1) radius = r;
return "m" + hexagon(radius).join("l") + "z";
};
hexbin.hexagonArray = function(radius) {
if (arguments.length < 1) radius = r;
return hexagon(radius);
};
hexbin.centers = function() {
var centers = [];
for (var y = 0, odd = false, j = 0; y < height + r; y += dy, odd = !odd, ++j) {
for (var x = odd ? dx / 2 : 0, i = 0; x < width + dx / 2; x += dx, ++i) {
var center = [x, y];
center.i = i;
center.j = j;
centers.push(center);
}
}
return centers;
};
hexbin.mesh = function() {
var fragment = hexagon(r).slice(0, 4).join("l");
return hexbin.centers().map(function(p) { return "M" + p + "m" + fragment; }).join("");
};
hexbin.size = function(_) {
if (!arguments.length) return [width, height];
width = +_[0], height = +_[1];
return hexbin;
};
hexbin.radius = function(_) {
if (!arguments.length) return r;
r = +_;
dx = r * 2 * Math.sin(Math.PI / 3);
dy = r * 1.5;
return hexbin;
};
return hexbin.radius(1);
};
var d3_hexbinAngles = d3.range(0, 2 * Math.PI, Math.PI / 3),
d3_hexbinX = function(d) { return d[0]; },
d3_hexbinY = function(d) { return d[1]; };
})();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Fog of War with Hexbin</title>
<style>
</style>
</head>
<body>
<svg width="500" height="500" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.16/d3.min.js" charset="utf-8" type="text/javascript"></script>
<script src="d3.hexbin.js" charset="utf-8" type="text/javascript"></script>
<script type="text/javascript">
vertices = [[303,348],[354,364],[261,285],[368,403],[315,330],[435,120],[121,275],[98,257],[299,474],[388,369],[216,256],[309,286],[38,287],[131,132],[230,241],[260,334],[189,187],[247,192],[229,219],[224,473],[180,211],[101,157],[198,235],[137,221],[139,228],[130,219],[294,251],[110,189],[201,277],[145,251],[176,261],[170,263],[180,259],[178,264],[83,416],[365,185],[313,196],[198,333],[255,227],[287,171],[333,318],[155,453],[120,242],[82,211],[76,263],[98,234],[142,190],[346,151],[466,220],[250,25],[467,302]];
hexbin = d3.hexbin()
.size([500,500])
.radius(25)
.x(function (d) {return d.x})
.y(function (d) {return d.y})
fullpoints = [];
vertices.forEach(function (d) {
fullpoints.push({x: d[0], y: d[1], type: "data"})
})
d3.range(20).forEach(function (d) {
d3.range(20).forEach(function (p) {
fullpoints.push({x: d * 25, y: p * 25, type: "hex"})
})
})
hexdata = hexbin(fullpoints);
console.log(hexdata)
d3.select("svg").selectAll("circle")
.data(fullpoints.filter(function (d) {return d.type === "data"}))
.enter()
.append("circle")
.attr("cx", function (d) {return d.x})
.attr("cy", function (d) {return d.y})
.style("fill", "#fff3b1")
.style("stroke-width", "2px")
.style("stroke", "#766400")
.attr("r", 3);
d3.select("svg").selectAll("path")
.data(hexdata)
.enter()
.append("path")
.attr("d", function (d) {return hexbin.hexagon(26); })
.attr("transform", function(d) { console.log(d); return "translate(" + d.x + "," + d.y + ")"; })
.style("fill", "#001276")
.style("stroke", "#001276")
.style("stroke-width", "10px")
.style("stroke-opacity", 0.5)
.on("click", defog)
function defog(d) {
d3.selectAll("path")
.filter(function (p) {
return Math.abs(p.x - d.x) < 65 && Math.abs(p.y - d.y) < 65 && d.filter(function (d) {return d.type === "data"}).length === 0 && p.filter(function (p) {return p.type === "data"}).length === 0;
})
.transition()
.duration(1000)
.style("opacity", 0);
d3.select(this)
.transition()
.duration(500)
.style("fill", "#fff3b1")
.style("opacity", 0.5)
.transition()
.duration(1000)
.style("opacity", 0);
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment