The main focus when producing hexbin maps showing a full hex-cover of the resepctive country is on the data prep. The key steps of this approach:
Draw the map
Draw a point grid covering at least the map's bounding box
Only keep points within country polygon (w/ d3.polygonContains()
)
Merge point grid data with data to visualise (walmart data from here)
Calculate hexbins with d3.hexbin()
Draw hexbin map
Built with blockbuilder.org
xxxxxxxxxx
<html lang="en">
<head>
<title>hex</title>
<meta charset="utf-8">
<script src="//d3js.org/d3.v4.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="//d3js.org/d3-color.v1.min.js"></script>
<script src="//d3js.org/d3-interpolate.v1.min.js"></script>
<script src="//d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="//d3js.org/d3-hexbin.v0.2.min.js"></script>
<style type="text/css">
body {
font-family: Avenir, sans-serif;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
/* Globals and SVG */
/* --------------- */
var projection,
hexRadius,
hexbin,
colourScale;
var margin = { top: 30, right: 30, bottom: 30, left: 30 },
width = 850 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var svg = d3.select('#container')
.append('svg')
.attr('width', width + margin.left + margin.top)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
/* Functions */
/* --------- */
function drawHexmap(points) {
var hexes = svg.append('g').attr('id', 'hexes')
.selectAll('.hex')
.data(points)
.enter().append('path')
.attr('class', 'hexes')
.attr('transform', function(d) { return 'translate(' + d.x + ', ' + d.y + ')'; })
.attr('d', hexbin.hexagon())
.style('fill', '#ddd')
.style('fill', function(d) { return colourScale(d.datapoints); })
.style('stroke', '#999')
.style('stroke-width', 1);
} // drawHexmap()
function getHexpointsWithCount(data) {
var maxCount = 0; // for colourScale
data.forEach(function(el) {
var count = 0;
el.forEach(function(elt) {
if (elt.datapoint === 1) count++;
})
el.datapoints = count;
maxCount = Math.max(maxCount, count); // for colourScale
});
// create colourScale as soon as maximum number of datapoints is determined
colourScale = d3.scaleLinear().domain([0, maxCount]).range(['#fff', '#0085DB']).interpolate(d3.interpolateHcl);
return data;
} // getHexpointsWithCount()
function getHexpoints(points) {
hexbin = d3.hexbin() // note: global
.radius(hexRadius)
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
var hexPoints = hexbin(points);
return hexPoints;
} // getHexpoints()
function getDatapoints(data) {
var dataPoints = []
data.forEach(function(el) {
var obj = {};
obj.x = projection([+el.x, +el.y])[0];
obj.y = projection([+el.x, +el.y])[1];
obj.datapoint = 1;
dataPoints.push(obj);
});
return dataPoints;
} // getDatapoints()
function keepPointsInPolygon(points, polygon) {
var pointsInPolygon = [];
points.forEach(function(el) {
var inPolygon = d3.polygonContains(polygon, [el.x, el.y]);
if (inPolygon) pointsInPolygon.push(el);
});
return pointsInPolygon;
} // keepPointsInPolygon()
function drawPointGrid(data) {
svg.append('g').attr('id', 'circles')
.selectAll('.dot')
.data(data)
.enter().append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', 1)
.attr('fill', 'tomato');
} // drawPointGrid()
function getPolygonPoints(data) {
var features = data.features[0].geometry.coordinates[7][0];
var polygonPoints = []
features.forEach(function(el) {
polygonPoints.push(projection(el));
});
return polygonPoints;
} // getPolygonPoints()
function createPointGrid(rowNum, colNum) {
var rows = rowNum,
columns = colNum;
hexRadius = Math.min(width/((columns + 0.5) * Math.sqrt(3)), height/((rows + 1/3) * 1.5)); // note: global
var points = [],
index = -1;
d3.range(rows).forEach(function(row) {
d3.range(columns).forEach(function(col) {
obj = {};
// obj.x = hexRadius * col * Math.sqrt(3);
obj.x = hexRadius * col * 1.7;
obj.y = hexRadius * row * 1.5;
obj.datapoint = 0;
points.push(obj)
});
});
return points;
} // createPointGrid()
function drawGeo(data) {
projection = d3.geoAlbersUsa() // note: global
.scale(1000).translate([width/2, height/2]);
var geoPath = d3.geoPath()
.projection(projection);
svg
.append('path')
.datum(data)
.attr('d', geoPath)
.attr('fill', 'none')
} // drawGeo()
function prepData(topo) {
var geo = topojson.feature(topo, topo.objects.us);
return geo;
} // prepData()
/* Load and algorithm */
/* ------------------ */
// Run algoritm
function ready(error, us, waltest) {
if (error) throw error;
var us = prepData(us);
drawGeo(us);
var points = createPointGrid(90, 140);
var polygonPoints = getPolygonPoints(us);
var usPoints = keepPointsInPolygon(points, polygonPoints);
var dataPoints = getDatapoints(waltest)
var mergedPoints = usPoints.concat(dataPoints)
var hexPoints = getHexpoints(mergedPoints);
var hexPointsWithCount = getHexpointsWithCount(hexPoints);
// drawPointGrid(mergedPoints);
drawHexmap(hexPoints);
}
// Load data sources
d3.queue()
.defer(d3.json, 'us.json')
.defer(d3.tsv, 'waltest.tsv')
.await(ready);
</script>
</html>
https://d3js.org/d3.v4.js
https://d3js.org/topojson.v1.min.js
https://d3js.org/d3-color.v1.min.js
https://d3js.org/d3-interpolate.v1.min.js
https://d3js.org/d3-scale-chromatic.v1.min.js
https://d3js.org/d3-hexbin.v0.2.min.js