Point clustering with the constraint that points should only be clustered within borders.
xxxxxxxxxx
<meta charset="utf-8">
<style>
path {
fill: #f4f4f4;
stroke: #666;
stroke-width: 1px;
}
circle {
stroke: none;
}
text {
fill: #444;
font-size: 12px;
font-family: sans-serif;
text-anchor: middle;
}
</style>
<body>
<script src="//d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/supercluster@4.1.1/dist/supercluster.min.js"></script>
<script>
const width = 480,
height = 500;
const color = d3
.scaleOrdinal()
.range(["#ef9a9a", "#9fa8da", "#ffe082", "#80cbc4"]);
const projection = d3.geoMercator();
const path = d3.geoPath().projection(projection);
const svg = d3
.select("body")
.append("svg")
.attr("width", width * 2)
.attr("height", height);
Promise.all([
d3.json("four-corners.geo.json"),
d3.json("random-points.geo.json")
]).then(([states, points]) => {
projection.fitExtent([[10, 10], [width - 10, height - 10]], states);
const left = svg.append("g");
const right = svg.append("g").attr("transform", "translate(" + width + ")");
// Draw background
[left, right].forEach(g =>
g
.selectAll("path")
.data(states.features)
.enter()
.append("path")
.attr("d", path)
);
// Original points on left
left
.selectAll("circle")
.data(points.features)
.enter()
.append("circle")
.attr("r", 2)
.attr(
"transform",
d => "translate(" + projection(d.geometry.coordinates) + ")"
)
.attr("fill", d => color(d.properties.state));
// Get a flat list of clusters within each state
// Each one is GeoJSON plus x, y, and r properties
const clusters = getClusters(points.features);
// Draw the clusters
const clustered = right
.selectAll("g")
.data(clusters)
.enter()
.append("g")
.attr("transform", d => "translate(" + d.x + " " + d.y + ")");
clustered
.append("circle")
.attr("r", d => d.r)
.attr("fill", d => color(d.properties.state));
// Label the clusters
clustered
.append("text")
.text(d => d.properties.point_count || 1)
.attr("dy", "0.35em");
// Clusters on the border might overlap, nudge them apart with a collision force
d3.forceSimulation(clusters)
.force(
"collide",
d3
.forceCollide()
.strength(0.8)
.radius(d => d.r)
)
.on("tick", () =>
clustered.attr("transform", d => "translate(" + d.x + " " + d.y + ")")
);
});
function getClusters(points) {
const allClusters = [];
// Group points by state
const byState = d3
.nest()
.key(d => d.properties.state)
.entries(points);
// Cluster each group individually
byState.forEach(entry => {
const index = supercluster({
radius: 50,
maxZoom: 5
});
index.load(entry.values);
index.getClusters([-180, -90, 180, 90], 5).forEach(cluster => {
// Add x, y, r, and state properties to each cluster
const [x, y] = projection(cluster.geometry.coordinates);
cluster.properties.state = entry.key;
cluster.x = x;
cluster.y = y;
cluster.r = cluster.properties.point_count ? 14 : 10;
allClusters.push(cluster);
});
});
return allClusters;
}
</script>
https://d3js.org/d3.v5.min.js
https://unpkg.com/supercluster@4.1.1/dist/supercluster.min.js