function ready(geo, userData) { /** * Build legend. * @param {Object} legendKey Selection to mount legend on. * @param {number} max Value maximum. * @param {function} scale Colour scale. * @param {string} legendText Legend title. * @return {undefined} DOM side effects. */ function buildKey(legendKey, max, scale, legendText) { const x = d3.scaleLinear() .domain([1, max]) .range([0, 120]); const xAxis = d3.axisBottom(x) .tickSize(13) .tickValues(scale.domain()); const g = legendKey.call(xAxis); g.select('.domain').remove(); const data = scale.range().map(color => { const d = scale.invertExtent(color); if (d[0] == null) d[0] = x.domain()[0]; if (d[1] == null) d[1] = x.domain()[1]; return d; }); g.selectAll('rect') .data( scale.range().map(color => { const d = scale.invertExtent(color); if (d[0] == null) d[0] = x.domain()[0]; if (d[1] == null) d[1] = x.domain()[1]; return d; }) ) .enter() .insert('rect', '.tick') .attr('height', 8) .attr('x', d => x(d[0])) .attr('width', d => x(d[1]) - x(d[0])) .attr('fill', d => scale(d[0])); g.append('text') .attr('fill', '#000') .attr('font-weight', 'bold') .attr('text-anchor', 'start') .attr('y', -6) .text(legendText); } // Set up 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.geoConicEqualArea() .fitSize([width, height], geo) .parallels([36, 66]); 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]; }); // Confine the global points to Europe. const poly = d3.geoPolygon(geo, projection); userData = d3.polygonPoints(userData, poly); // Set up clip paths. svg .append('defs') .append('clipPath') .attr('id', 'clip-it') .append('path') .attr('d', geoPath(geo)); svg .append('g') .attr('id', 'world') .append('path') .attr('d', geoPath(geo)) .attr('stroke', '#000') .attr('fill', 'none'); // Hexgrid generator. const hexgrid = d3.hexgrid() .extent([width, height]) .geography(geo) .projection(projection) .pathGenerator(geoPath) .hexRadius(7) .edgePrecision(1) .gridExtend(2) .geoKeys(['lng', 'lat']); // Hexgrid instance. const hex = hexgrid(userData); // Calculate Ckmeans based colour scale. const counts = hex.grid.layout .map(el => el.datapointsWt) .filter(el => el > 0); const ckBreaks = ss.ckmeans(counts, 4).map(clusters => clusters[0]); const colourScale = d3 .scaleThreshold() .domain(ckBreaks) .range(['#fff', '#e7e7e7', '#aaa', '#777', '#555']); // Clip. const gHex = svg .append('g') .attr('id', 'hexes') .attr('clip-path', 'url(#clip-it)'); // Draw. gHex .selectAll('.hex') .data(hex.grid.layout) .enter() .append('path') .attr('class', 'hex') .attr('transform', d => `translate(${d.x}, ${d.y})`) .attr('d', hex.hexagon()) .style('fill', d => colourScale(d.datapointsWt)) .style('stroke', '#999') .style('stroke-opacity', 0.4); // Build and mount legend. const legendKey = svg .append('g') .attr('class', 'legend') .attr('transform', `translate(${width - 120}, ${height})`) .call( buildKey, hex.grid.extentPointsWeighted[1], colourScale, 'Number of disputes' ); } // Data load. const geoData = d3.json( 'https://raw.githubusercontent.com/larsvers/map-store/master/europe_geo.json' ); const points = d3.csv( 'https://raw.githubusercontent.com/larsvers/data-store/master/military_disputes_world.csv' ); Promise.all([geoData, points]).then(res => { let [geoData, userData] = res; ready(geoData, userData); });