|
| 1 | +<p id="map-text"> |
| 2 | + Loading... |
| 3 | +</p> |
| 4 | +<div id="map-container"></div> |
| 5 | +<div id="map-tooltip" class="tooltip"></div> |
| 6 | + |
| 7 | +<script type="module"> |
| 8 | + import * as d3 from "https://cdn.jsdelivr.net/npm/d3@v7/+esm"; |
| 9 | + import escape from "https://cdn.jsdelivr.net/npm/escape-html/+esm"; |
| 10 | + |
| 11 | + const container = document.getElementById("map-container"); |
| 12 | + const text = document.getElementById("map-text"); |
| 13 | + const tooltip = d3.select("#map-tooltip"); |
| 14 | + |
| 15 | + const width = 640; |
| 16 | + const height = 400; |
| 17 | + |
| 18 | + // create a new svg for d3.js to draw on |
| 19 | + const svg = d3.create("svg").attr("width", width).attr("height", height); |
| 20 | + container.append(svg.node()); |
| 21 | + |
| 22 | + // grab the data |
| 23 | + Promise.all([ |
| 24 | + d3.json("http://localhost:8787/locations.geojson"), |
| 25 | + d3.json("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson") |
| 26 | + ]).then(([pointData, worldData]) => { |
| 27 | + // update map text |
| 28 | + text.innerText = `${pointData.features.length} hackers online!`; |
| 29 | + |
| 30 | + // get the bounds of locations (min & max coordinates) |
| 31 | + const bounds = d3.geoBounds(pointData); |
| 32 | + const [[x0, y0], [x1, y1]] = bounds; |
| 33 | + |
| 34 | + const dx = x1 - x0; |
| 35 | + const dy = y1 - y0; |
| 36 | + const x = ((x0 + x1) / 2) || 0; |
| 37 | + const y = ((y0 + y1) / 2) || 0; |
| 38 | + |
| 39 | + // calculate scale and translation based on the bounds |
| 40 | + const scale = Math.max(10, Math.min(dx / width, dy / height)) * 100; |
| 41 | + const translate = [width / 2 - scale * x, height / 2 - scale * y]; |
| 42 | + const padding = 100; |
| 43 | + |
| 44 | + // create a new projection, centered at the middle of the locations |
| 45 | + let projection = d3.geoNaturalEarth1() |
| 46 | + .center([x, y]) |
| 47 | + .translate([width / 2, height / 2]); |
| 48 | + |
| 49 | + // if there's more than 1 location, fit the map to show all locations nicely |
| 50 | + if (pointData.features.length > 1) { |
| 51 | + projection = projection.fitExtent([[0 + padding, 0 + padding], [width - padding, height - padding]], pointData); |
| 52 | + } |
| 53 | + |
| 54 | + // clamp the map scaling to 2000 so countries can be seen still |
| 55 | + projection = projection.scale(Math.min(projection.scale(), 2000)); |
| 56 | + |
| 57 | + const pathGenerator = d3.geoPath(projection).pointRadius(4); |
| 58 | + |
| 59 | + // draw countries |
| 60 | + svg.append("path") |
| 61 | + .datum(worldData) |
| 62 | + .attr("d", pathGenerator) |
| 63 | + .attr("fill", "#e6e7e8") |
| 64 | + .attr("stroke", "white"); |
| 65 | + |
| 66 | + // draw points for each hacker location |
| 67 | + svg.selectAll("circle") |
| 68 | + .data(pointData.features) |
| 69 | + .enter() |
| 70 | + .append("circle") |
| 71 | + .attr("cx", (d) => projection(d.geometry.coordinates)[0]) |
| 72 | + .attr("cy", (d) => projection(d.geometry.coordinates)[1]) |
| 73 | + .attr("r", 4) |
| 74 | + .attr("fill", "blue") |
| 75 | + .on("mouseover", (event, d) => { |
| 76 | + tooltip.style("display", "block") |
| 77 | + .html(`<b>${escape(d.properties.name || "Anonymous")}</b><br>${escape(d.properties.locationName)}`) |
| 78 | + .style("left", (event.pageX + 10) + "px") |
| 79 | + .style("top", (event.pageY - 10) + "px"); |
| 80 | + }).on("mouseout", () => { |
| 81 | + tooltip.style("display", "none"); |
| 82 | + }); |
| 83 | + }); |
| 84 | +</script> |
0 commit comments