m
map-gl-js-spiderfy
Fan out overlapping clustered points
Maintained by Nazka Mapps
A spiderfy plugin for MapLibre GL JS and Mapbox GL JS: clicking a cluster of points that share a location fans them out on legs in a circle or spiral, so each one can be clicked.
Live demo
map-gl-js-spiderfy running on MapLibre GL JS in your browser
Loading the map…
Demo code
import Spiderfy from "@nazka/map-gl-js-spiderfy";
import type { PluginDemo } from "../types";
export default {
center: [-73.9855, 40.758], // Times Square
zoom: 15,
async run({ map, maplibregl }) {
// A round marker icon, drawn on a canvas (the plugin works with symbol layers).
const canvas = Object.assign(document.createElement("canvas"), { width: 28, height: 28 });
const ctx = canvas.getContext("2d")!;
ctx.arc(14, 14, 11, 0, 2 * Math.PI);
Object.assign(ctx, { fillStyle: "#e4572e", strokeStyle: "#fff", lineWidth: 3 });
ctx.fill();
ctx.stroke();
map.addImage("venue-dot", ctx.getImageData(0, 0, 28, 28), { pixelRatio: 2 });
// Twelve points at the same spot, which clustering alone can never pull apart.
const points = Array.from({ length: 12 }, (_, i) => ({
type: "Feature" as const,
properties: { name: `Venue ${i + 1}` },
geometry: { type: "Point" as const, coordinates: [-73.9855, 40.758] },
}));
map.addSource("venues", {
type: "geojson",
data: { type: "FeatureCollection", features: points },
cluster: true,
});
// The plugin copies this layer's layout to the fanned-out points, so keep it to constants.
map.addLayer({
id: "venues",
type: "symbol",
source: "venues",
layout: { "icon-image": "venue-dot", "icon-allow-overlap": true },
});
map.addLayer({
id: "venue-count",
type: "symbol",
source: "venues",
filter: ["has", "point_count"],
layout: {
"text-field": ["get", "point_count_abbreviated"],
"text-font": ["Noto Sans Bold"],
"text-size": 10,
"text-allow-overlap": true,
},
paint: { "text-color": "#fff" },
});
// Click a cluster to fan its points out.
const spiderfy = new Spiderfy(map, {
forceSpiderifyMinZoom: 0,
spiderLegsColor: "#e4572e",
spiderLegsWidth: 2,
});
spiderfy.applyTo("venues");
// Once the plugin is set up (on the next "idle"), click the cluster for the reader.
await map.once("idle");
const { x, y } = map.project([-73.9855, 40.758]);
const { left, top } = map.getCanvas().getBoundingClientRect();
map.fire(
new maplibregl.MapMouseEvent(
"click",
map,
new MouseEvent("click", { clientX: left + x, clientY: top + y }),
),
);
},
} satisfies PluginDemo;