Clustering points using D3 and supercluster. Red circles are clusters with point counts. Green circles are individual points. Zoom in and out to see the clustering effect.
Last active
March 13, 2022 23:49
-
-
Save r-suen/bc74cf986e9198daaf6d9ee32e93313b to your computer and use it in GitHub Desktop.
D3 + supercluster
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<title>D3 + supercluster</title> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.0/topojson.min.js"></script> | |
<script src="https://unpkg.com/supercluster@7.1.2/dist/supercluster.min.js"></script> | |
</head> | |
<style> | |
body { | |
margin: 0; | |
overflow: hidden; | |
} | |
rect { | |
fill: #fff; | |
} | |
.cluster { | |
fill: #ff6666; | |
stroke: #ff0000; | |
stroke-width: 0; | |
} | |
.cluster-label { | |
fill: #000; | |
dominant-baseline: middle; | |
text-anchor: middle; | |
font-family: sans-serif; | |
font-size: 14px; | |
} | |
.point { | |
fill: #17677b; | |
stroke: #f8f3f6; | |
stroke-width: 2; | |
} | |
</style> | |
<body> | |
<div id="map"></div> | |
<script> | |
const style = { | |
countriesColor: "#555", | |
countriesStrokeColor: "#ddd", | |
countriesStrokeWidth: 1, | |
clusterRadius: 20, | |
clusterStrokeWidth: 0, | |
pointRadius: 8, | |
pointStrokeWidth: 2, | |
}; | |
const convertZoomLevelToMercator = (zoomLevel) => (2 ** 8 + zoomLevel) / 2 / Math.PI; | |
const convertZoomLevelFromMercator = (zoomLevelInMercator) => | |
Math.log(zoomLevelInMercator * 2 * Math.PI) / Math.LN2 - 8; | |
const width = window.innerWidth; | |
const height = window.innerHeight; | |
const bbox = [-180, -85, 180, 85]; | |
let prevZoom = 1; | |
const index = new Supercluster({ | |
radius: 50, | |
maxZoom: 14, | |
}); | |
const svg = d3.select("#map").append("svg").attr("width", width).attr("height", height); | |
svg.append("rect").attr("width", width).attr("height", height); | |
const g = svg.append("g"); | |
const projection = d3 | |
.geoMercator() | |
.scale(convertZoomLevelToMercator(1)) // 256px tile equivalent to zoom level 1 | |
.translate([width / 2, height / 2]); | |
const path = d3.geoPath(projection); | |
const drawClusters = (zoomLevel) => { | |
const data = index.getClusters(bbox, zoomLevel - 1); // subtract 1 zoom level based on mapbox 512px tile | |
const clusters = g.selectAll("clusters").data(data).enter(); | |
clusters | |
.append("circle") | |
.attr("cx", (d) => projection(d.geometry.coordinates)[0]) | |
.attr("cy", (d) => projection(d.geometry.coordinates)[1]) | |
.attr("r", (d) => (d.properties.cluster ? style.clusterRadius : style.pointRadius)) | |
.attr("class", (d) => (d.properties.cluster ? "cluster" : "point")); | |
clusters | |
.append("text") | |
.attr("x", (d) => projection(d.geometry.coordinates)[0]) | |
.attr("y", (d) => projection(d.geometry.coordinates)[1]) | |
.text((d) => d.properties.point_count_abbreviated || null) | |
.attr("class", (d) => (d.properties.cluster ? "cluster-label" : "point-label")); | |
}; | |
const zoomed = () => { | |
const t = d3.event.transform; | |
const currentZoom = Math.floor(convertZoomLevelFromMercator(t.k * projection.scale())); | |
if (currentZoom !== prevZoom) { | |
d3.selectAll("circle").remove(); | |
d3.selectAll("text").remove(); | |
drawClusters(currentZoom); | |
prevZoom = currentZoom; | |
} | |
g.attr("transform", t); | |
d3.selectAll("path").attr("stroke-width", style.countriesStrokeWidth / t.k); | |
d3.selectAll("circle") | |
.attr("r", (d) => (d.properties.cluster ? style.clusterRadius / t.k : style.pointRadius / t.k)) | |
.style("stroke-width", (d) => | |
d.properties.cluster ? style.clusterStrokeWidth / t.k : style.pointStrokeWidth / t.k | |
); | |
d3.selectAll("text").style("font-size", 14 / t.k); | |
}; | |
const zoom = d3 | |
.zoom() | |
.scaleExtent([0.5, 2 ** 10]) // 256px tile equivalent to zoom range 0-11 | |
.on("zoom", zoomed); | |
svg.call(zoom); | |
d3.json("world-110m.json", (error, data) => { | |
if (error) throw error; | |
g.selectAll("path") | |
.data(topojson.feature(data, data.objects.countries).features) | |
.enter() | |
.append("path") | |
.attr("d", path) | |
.attr("fill", style.countriesColor) | |
.attr("stroke", style.countriesStrokeColor) | |
.attr("stroke-width", style.countriesStrokeWidth); | |
d3.json("amtrak_stations.geojson", (err, d) => { | |
if (err) throw err; | |
index.load(d.features); | |
drawClusters(1); | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment