Example of how to use the d3-hexgrid plugin with a legend and a dash of interactivity.
More examples... Farmers Markets I: SVG • Military disputes: SVG with clipping • Number of cities: Canvas • Post boxes: • all examples on Observable
license: mit | |
height: 500 | |
border: no |
.DS_Store |
Example of how to use the d3-hexgrid plugin with a legend and a dash of interactivity.
More examples... Farmers Markets I: SVG • Military disputes: SVG with clipping • Number of cities: Canvas • Post boxes: • all examples on Observable
function ready(geo, userData) { | |
// Container SVG. | |
const margin = { top: 30, right: 30, bottom: 30, left: 30 }, | |
width = 900 - margin.left - margin.right, | |
height = 500 - margin.top - margin.bottom; | |
const 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})`); | |
// Projection and path. | |
const projection = d3.geoAlbers().fitSize([width, height], geo); | |
const geoPath = d3.geoPath().projection(projection); | |
// Prep user data. | |
userData.forEach(site => { | |
const coords = projection([+site.lng, +site.lat]); | |
site.x = coords[0]; | |
site.y = coords[1]; | |
}); | |
// Hexgrid generator. | |
const hexgrid = d3.hexgrid() | |
.extent([width, height]) | |
.geography(geo) | |
.pathGenerator(geoPath) | |
.projection(projection) | |
.hexRadius(5); | |
// Hexgrid instance. | |
const hex = hexgrid(userData); | |
// Create exponential colorScale. | |
const scaleExponent = 10; | |
const colourScale = d3.scaleSequential(t => { | |
var tNew = Math.pow(t, scaleExponent); | |
return d3.interpolateViridis(tNew); | |
}) | |
.domain([...hex.grid.extentPointDensity].reverse()); | |
// Draw the hexes. | |
svg | |
.append('g') | |
.selectAll('.hex') | |
.data(hex.grid.layout) | |
.enter() | |
.append('path') | |
.attr('class', 'hex') | |
.attr('d', hex.hexagon()) | |
.attr('transform', d => `translate(${d.x} ${d.y})`) | |
.style('fill', d => (!d.pointDensity ? '#fff' : colourScale(d.pointDensity))) | |
.style('stroke', '#F7E76E') | |
.style('stroke-opacity', 0.5); | |
// Tooltip. | |
const formatNum = d3.format('.2'); | |
const tip = d3.select('.tooltip'); | |
d3.selectAll('.hex') | |
.on('mouseover', mouseover) | |
.on('mouseout', mouseout); | |
// Handler. | |
function mouseover(d) { | |
tip | |
.style('opacity', 1) | |
.style('top', `${d3.event.pageY - 20}px`) | |
.style('left', `${d3.event.pageX + 10}px`); | |
tip.html(`cover: ${formatNum(d.cover)}<br> | |
points: ${d.datapoints}<br> | |
points wt: ${formatNum(d.datapointsWt)}<br> | |
density: ${formatNum(d.pointDensity)}`); | |
} | |
function mouseout() { | |
tip.style('opacity', 0); | |
} | |
// Legend... | |
// Values. | |
const legendScale = 8 / hex.radius(); | |
// Get legend data. | |
const equalRange = n => d3.range(n).map(d => d / (n - 1)); | |
const densityDist = hex.grid.layout | |
.map(d => d.pointDensity) | |
.sort(d3.ascending) | |
.filter(d => d); | |
const splitRange = equalRange(11); | |
const indeces = splitRange.map(d => Math.floor(d * (densityDist.length - 1))); | |
const densityPick = indeces.map(d => densityDist[d]); | |
const legendData = densityPick.map(d => ({ | |
density: d, | |
colour: colourScale(d) | |
})); | |
// Build legend. | |
const gLegend = svg | |
.append('g') | |
.attr('class', 'legend') | |
.attr('transform', `translate(0, ${height})`); | |
gLegend | |
.append('text') | |
.text(`Point density (scale exponent: ${scaleExponent})`) | |
.attr('fill', '#555') | |
.attr('font-family', 'sans-serif') | |
.attr('font-size', '0.55rem') | |
.attr('font-weight', 'bold') | |
.attr('dy', 19) | |
.attr('dx', -4); | |
const legend = gLegend | |
.selectAll('.legend__key') | |
.data(legendData) | |
.enter() | |
.append('g') | |
.attr('class', 'legend__key') | |
.attr('transform', (d, i) => `translate(${i * Math.sqrt(3) * hexgrid.hexRadius() * legendScale}, 0)`); | |
legend | |
.append('g') | |
.attr('transform', `scale(${legendScale})`) | |
.append('path') | |
.attr('d', hex.hexagon()) | |
.style('fill', d => d.colour) | |
.style('stroke-width', 0.5) | |
.style('stroke', '#fff'); | |
legend | |
.append('text') | |
.text( | |
(d, i, n) => (i == 0 || i == n.length - 1 ? formatNum(d.density) : '') | |
) | |
.attr('fill', '#555') | |
.attr('font-family', 'sans-serif') | |
.attr('font-size', '0.7rem') | |
.attr('font-weight', 'bold') | |
.attr('text-anchor', 'middle') | |
.attr('dy', -10); | |
} | |
// Data load. | |
const geoData = d3.json( | |
'https://raw.githubusercontent.com/larsvers/map-store/master/us_mainland_geo.json' | |
); | |
const points = d3.json( | |
'https://raw.githubusercontent.com/larsvers/data-store/master/farmers_markets_us.json' | |
); | |
Promise.all([geoData, points]).then(res => { | |
let [geoData, userData] = res; | |
ready(geoData, userData); | |
}); |
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<title>Farmers Markets</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<!-- d3-hexgrid script comes first. --> | |
<script src="//unpkg.com/d3-hexgrid"></script> | |
<script src="//unpkg.com/d3"></script> | |
<style type="text/css"> | |
.tooltip { | |
position: absolute; | |
opacity: 0; | |
font-family: Nunito, sans-serif; | |
pointer-events: none; | |
background-color: #eee; | |
padding: 0.5em; | |
box-shadow: 1px 2px 4px #888; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="container"></div> | |
<div class="tooltip"></div> | |
<script src="app.js"></script> | |
</body> | |
</html> |