本文へ移動
cccskills
無料GitHub で公開

web-maps-leaflet

Leaflet interactive maps. Use when building a 2D map with tile layers, markers, popups, GeoJSON, layer control, custom controls or marker clustering.

インストール方法を見る

含まれるファイル(5)

  • SKILL.md12.6 KB
  • examples/advanced.md10.2 KB
  • examples/core.md11.3 KB
  • metadata.yaml459 B
  • reference.md3.4 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Leaflet Interactive Map Patterns

Quick Guide: Leaflet is a small 2D mapping library: L.map initializes against a DOM element, L.tileLayer supplies the base map, and everything drawn on top — markers, popups, GeoJSON, controls — is a layer added to and removed from the map independently. L.geoJSON does most of the data work through its pointToLayer, onEachFeature, style and filter callbacks. Marker count is the decision that shapes the rest: past a hundred, DOM markers stop scaling and the work moves to clustering or the canvas renderer. Current: v1.9.4, with types in @types/leaflet.

Detailed Resources:

  • examples/core.md — map setup, tile providers, markers and icons, GeoJSON, layer groups and control, events
  • examples/advanced.md — custom controls, clustering, TypeScript, canvas rendering, bounds and viewport
  • reference.md — L.map methods, L.geoJSON and cluster options, event table, install checklist

<critical_requirements>

Before writing Leaflet code

Call map.remove() when the map goes away. It is the one call that tears down the resize observers, animation frames and DOM listeners Leaflet attached; without it a re-mount on the same element throws "Map container is already initialized".

Give every tile layer an attribution. OpenStreetMap and most other providers require it in their terms, and there is no default.

Move past DOM markers at around a hundred points — L.markerClusterGroup, or L.circleMarker on the canvas renderer. Each L.marker is an element in the document, and the page slows in proportion.

</critical_requirements>


Auto-detection: Leaflet, L.map, L.tileLayer, L.marker, L.popup, L.geoJSON, L.control.layers, L.layerGroup, L.featureGroup, L.icon, L.divIcon, L.circleMarker, L.polyline, L.polygon, L.Control.extend, L.DomUtil, L.DomEvent, markerClusterGroup, leaflet.markercluster, @types/leaflet, leaflet.css, addTo(map), bindPopup, bindTooltip, onEachFeature, pointToLayer, invalidateSize, flyTo, fitBounds, latLngBounds

Applies to:

  • Interactive 2D maps with markers, popups, tooltips and overlays
  • GeoJSON points, lines and polygons, styled and filtered from their own properties
  • Base-layer switching and overlay toggling through a layer control
  • Custom controls built on L.Control.extend
  • Large marker datasets, through clustering or the canvas renderer
  • Map, marker and layer events, and camera movement

Handled elsewhere:

  • Where the tiles come from — Leaflet renders any XYZ raster endpoint, and choosing a provider and meeting its terms is a separate decision
  • Where the GeoJSON comes from — the layer takes an object, and fetching, caching and paging it are not the map's concern
  • How markers, popups and controls look — the map hands you class names and containers, and the CSS inside them is settled by whatever owns styling
  • 3D terrain, globe projection and GPU-rendered vector tiles — this is a 2D raster library with an SVG or canvas vector layer over it

<philosophy>

Everything on the map is a layer. Tiles, markers, GeoJSON, even controls — each is added and removed independently, which is why toggling a dataset is map.removeLayer(group) rather than a rebuild.

The core is deliberately small (~42KB gzipped) and covers the common map. Clustering, heatmaps, drawing and vector tiles are plugins, and a plugin is how the library expects those needs to be met.

Methods return this, so setup reads as a chain: L.marker(pos).addTo(map).bindPopup(html).

Interaction is events. Maps, markers and layers all emit; .on() subscribes and .off() unsubscribes, and map.off() with no arguments is part of teardown.

</philosophy>

<decision_framework>

Marker strategy, by count

< 100     → L.marker with L.icon or L.divIcon
100 – 10K → L.markerClusterGroup
10K – 50K → L.markerClusterGroup with chunkedLoading, and L.circleMarker rather than L.marker
50K+      → canvas rendering, or pre-tiled vector data

Which layer type

One coordinate               → L.marker (with an icon) or L.circleMarker (for data viz)
A path                       → L.polyline
An area                      → L.polygon, or L.circle for a radius in metres
A GeoJSON dataset            → L.geoJSON, which handles every geometry type
A group you need to toggle   → L.layerGroup, or L.featureGroup where you need getBounds()/bindPopup()

Which icon

The default pin          → L.marker() with no icon option
A custom image           → L.icon({ iconUrl, iconSize, iconAnchor })
Several image variants   → L.Icon.extend({ options }), then construct per variant
HTML or CSS content      → L.divIcon({ html, className, iconSize })
A data point, many of    → L.circleMarker — a vector shape, not a DOM element

</decision_framework>


<patterns>

Core patterns

Pattern 1: Map initialization and tile layers

Target a DOM element, set the view, add a base layer with its attribution.

import L from "leaflet";
import "leaflet/dist/leaflet.css";

const map = L.map("map").setView([51.505, -0.09], 13);

L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
  maxZoom: 19,
  attribution:
    '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(map);

The CSS import is not optional — without it controls, popups and markers render unpositioned.

Full code: examples/core.md


Pattern 2: Markers, popups and tooltips

const marker = L.marker([51.5, -0.09]).addTo(map);
marker.bindPopup("<b>Hello</b><br>I am a popup.");
marker.bindTooltip("Hover text", { direction: "top" });

L.popup().setLatLng([51.513, -0.09]).setContent("Standalone").openOn(map);

openOn(map) closes whatever popup was open; addTo(map) leaves it, which is the difference between one-at-a-time and several.

Full code: examples/core.md


Pattern 3: GeoJSON layers

Four callbacks cover most of what a dataset needs, and none of them requires touching the data.

const geoLayer = L.geoJSON(geojsonData, {
  pointToLayer: (feature, latlng) => L.circleMarker(latlng, { radius: 8 }),
  onEachFeature: (feature, layer) => layer.bindPopup(feature.properties?.name),
  style: (feature) => ({ color: feature?.properties?.color ?? "#3388ff" }),
  filter: (feature) => feature?.properties?.visible !== false,
}).addTo(map);

filter excludes a feature before it is rendered, which is cheaper than rendering and hiding it.

Full code: examples/core.md


Pattern 4: Layer groups and layer control

const overlays = { Cities: L.layerGroup([markerA, markerB]), Parks: parkGroup };
L.control.layers({ Street: osm, Satellite: satellite }, overlays).addTo(map);

Base layers are radio buttons and overlays are checkboxes. L.featureGroup where the group needs getBounds(), bindPopup() or setStyle(); L.layerGroup where it is only a container.

Full code: examples/core.md


Pattern 5: Events and interaction

map.on("click", (e: L.LeafletMouseEvent) => {
  const { lat, lng } = e.latlng;
});

map.on("moveend", () => map.getBounds()); // load data for the new viewport

const handler = () => {};
map.on("zoomend", handler);
map.off("zoomend", handler); // .off() needs the same reference

Full code: examples/core.md


Pattern 6: Custom controls

L.Control.extend returns a constructor; onAdd builds and returns the container element.

const InfoControl = L.Control.extend({
  options: { position: "bottomleft" as L.ControlPosition },
  onAdd(): HTMLElement {
    const container = L.DomUtil.create("div", "info-control");
    L.DomEvent.disableClickPropagation(container);
    return container;
  },
});

new InfoControl().addTo(map);

disableClickPropagation is what stops a click on the control also being a click on the map, and disableScrollPropagation does the same for a scrollable control.

Full code: examples/advanced.md


Pattern 7: Marker clustering

leaflet.markercluster replaces one DOM element per point with one per visible cluster.

const clusterGroup = L.markerClusterGroup({
  maxClusterRadius: 50,
  disableClusteringAtZoom: 18,
  chunkedLoading: true,
});

clusterGroup.addLayers(markers); // bulk add, not one addLayer per marker
map.addLayer(clusterGroup);

chunkedLoading keeps a bulk add off the main thread long enough for the UI to stay responsive, and disableClusteringAtZoom hands back individual markers once the user is close enough to want them.

Full code: examples/advanced.md


Pattern 8: TypeScript

Types ship separately, in @types/leaflet and @types/leaflet.markercluster.

import L, { type LatLngExpression, type MapOptions } from "leaflet";

const options: MapOptions = { center: [51.505, -0.09], zoom: 13 };
const map = L.map("map", options);

Full code: examples/advanced.md


Pattern 9: Teardown

function destroyMap(map: L.Map): void {
  map.off(); // every listener
  map.remove(); // the map, its layers, and the DOM Leaflet created
}

Skipping this leaves listeners and animation frames alive, and a second L.map() call against the same element throws.

</patterns>
<performance>

Performance

GeoJSON: use filter rather than rendering and hiding; simplify geometry server-side for overview zooms; add large datasets in chunks with addData(); and where a dataset changes entirely, clearLayers() and re-add rather than restyling feature by feature.

Vector layers: preferCanvas: true on the map, or a per-layer L.canvas() renderer, moves circles and polylines off SVG — worth it past about a thousand shapes. Canvas-rendered layers take no CSS styling and no SVG filters, so hover effects have to come from Leaflet events.

Markers: L.circleMarker is a vector shape rather than a DOM element, so it costs far less than L.marker for a data point that does not need an icon.

Popups: set large popup content lazily on the popupopen event rather than building it for every marker up front.

Containers: call map.invalidateSize() after the container changes size — a CSS transition, an accordion, a tab switch — or the map keeps rendering to its old dimensions.

</performance>

<red_flags>

Red flags

Breaks at runtime:

  • No map.remove() on teardown — listeners and animation frames survive, and re-initializing on the same element throws "Map container is already initialized"
  • map.fitBounds on an empty FeatureGroup — throws — check bounds.isValid() first
  • leaflet/dist/leaflet.css not imported — controls, popups and markers render unpositioned, which looks like a layout bug rather than a missing import
  • Default marker icons under a bundler — the CSS-relative image paths no longer resolve and markers render broken — set them explicitly through L.Icon.Default.mergeOptions
  • A tile layer with no attribution — breaches the terms of OpenStreetMap and most other providers
  • L.marker for a few hundred points — one DOM element each, and the page degrades steadily — cluster, or use L.circleMarker

Surprising behaviour:

  • A container that changed size renders grey tiles and mis-targeted clicks until invalidateSize() is called
  • openOn(map) closes the previously open popup; only addTo(map) leaves several open
  • Clicks on a custom control also reach the map unless the container went through L.DomEvent.disableClickPropagation
  • GeoJSON coordinates are [longitude, latitude] while L.latLng takes [latitude, longitude] — the commonest coordinate bug, and L.geoJSON flips them for you so only hand-built coordinates are at risk
  • flyTo and panTo cancel each other when called in quick succession
  • L.Control.extend({...}) returns a constructor rather than an instance — Leaflet's own class system, not ES classes, so it is new at the call site
  • A tile layer's maxZoom limits tile availability and the map's maxZoom limits the user; set the map higher than the tiles and the extra zoom levels are grey
  • refreshClusters() has to be called after changing a marker's icon or data — clusters do not notice on their own

</red_flags>

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Hugging Face Inference SDK patterns for TypeScript/Node.js — InferenceClient setup, chat completion, text generation, streaming, embeddings, image generation, audio transcription, translation, summarization, and Inference Endpoints

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

LiteLLM proxy server setup, TypeScript client patterns via OpenAI SDK, model routing, fallbacks, load balancing, spend tracking, virtual keys, and production deployment

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

Serverless GPU compute platform for AI model deployment — web endpoints, GPU functions, model serving, and TypeScript client patterns

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

Local LLM inference with the Ollama JavaScript client -- chat, streaming, tool calling, vision, embeddings, structured output, model management, and OpenAI-compatible endpoint

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

Replicate SDK patterns for TypeScript/Node.js -- client setup, predictions, streaming, webhooks, file handling, model versioning, deployments, and training

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

Together AI SDK patterns for TypeScript — client setup, chat completions, streaming, structured output, function calling, embeddings, image generation, fine-tuning, and OpenAI-compatible endpoints

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

agents-inc のスキルをすべて見る

このスキルの問題を報告する