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> |