Skip to content
Make with
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;