何の話か
地図サービスを作っていると、必ずぶつかる問題があります。
ピンが多すぎて、地図が見えない。
自分が運営しているバイク情報サイトの地図機能でも、都心を開くと駐車場・ショップ・ガソリンスタンドのピンが200件以上重なって、地図が「ピンの絨毯」になっていました。1つ1つのピンをタップするどころか、下の地図がどこなのかも分からない。
これを、いわゆる「Googleマップでよくあるアレ」——ズームアウトすると数字のクラスタにまとまり、ズームインすると個別ピンに割れる——で解決した話です。使うのは Leaflet 定番プラグインの Leaflet.markercluster。
実装自体は素直ですが、**「本番で事故らないための小さな判断」**がいくつかあったので、そこを中心にまとめます。
Before / After
-
Before: 全ピンを
L.markerで個別に地図へ addTo。ズームアウトすると重なって潰れる。描画も重い。 -
After: 全ピンを1つの
markerClusterGroupに載せる。ズームレベルに応じて自動でクラスタ化 / 個別化。
最小実装
1. 読み込み
markercluster は CSS 2本 + JS 1本。Leaflet 本体の後・自分の地図スクリプトの前に置きます。
<!-- Leaflet 本体 -->
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<!-- markercluster(Leaflet の後・自分の map.js の前) -->
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.Default.css" />
<script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster.js"></script>
<!-- 自分の地図スクリプト -->
<script src="/js/map.js"></script>
読み込み順を間違えると L.markerClusterGroup is not a function で落ちるので、この順序が肝です。
2. グループを差し替えるだけ
これまで L.layerGroup() に marker を addTo していたところを、L.markerClusterGroup() に変えるだけです。
// Before
const layer = L.layerGroup().addTo(map);
markers.forEach((m) => L.marker([m.lat, m.lng]).addTo(layer));
// After
const layer = L.markerClusterGroup();
markers.forEach((m) => L.marker([m.lat, m.lng]).addTo(layer));
map.addLayer(layer);
これだけで、ズームアウト時は数字クラスタ、ズームインで個別ピンに割れるようになります。クラスタをクリックすれば自動でズームイン(zoomToBounds)、個別ピンのクリックは従来どおりポップアップ、という挙動も標準で付いてきます。
本番で効いた「小さな判断」
ここからが本題です。ライブラリを入れるだけなら5分ですが、運用中のサービスに入れるときに効いた判断が3つあります。
判断1: 種類ごとにクラスタしない(種類横断で1つにまとめる)
地図には複数の種類のピンがありました(駐車場・ショップ・GS・…)。最初は「種類ごとにクラスタグループを分ける」ことも考えましたが、やめました。
理由: 都心では種類の違うピン同士が近接しているため、種類別にクラスタすると、同じ場所に複数の数字バッジが重なって、結局ごちゃつく。
なので 全種類を1つの markerClusterGroup にまとめる(Googleマップと同じく種類横断でまとまる)方が、体験がきれいでした。個別ピンに割れたときの色(=種類の区別)はマーカー側で維持されるので、種類が分からなくなることもありません。
クラスタは「密度を減らすため」の機能。種類の区別は個別ピンの見た目に任せる、と割り切るのが正解でした。
判断2: CDN が落ちても地図は動かす(フォールバック)
markercluster を CDN から読み込む以上、CDN が一時的に落ちる / ブロックされる可能性があります。そのとき地図ごと真っ白になるのは最悪です。
なので、L.markerClusterGroup が読めているかを確認し、無ければ従来の L.layerGroup にフォールバックするようにしました。
const makeLayer = () =>
typeof L.markerClusterGroup === "function"
? L.markerClusterGroup()
: L.layerGroup(); // CDN 失敗時は素の layerGroup(クラスタ無しに劣化するだけ)
const layer = makeLayer();
これで最悪の場合も「クラスタ無しの地図」に劣化するだけで済み、地図が死ぬことはありません。新しい依存を足すときは、それが無くても本体が動く設計にしておくと安心です。
判断3: フィルタ切替時に再クラスタする
「駐車場だけ表示」のようなフィルタがある場合、フィルタ後に表示中のピンだけで再クラスタしないと、消したはずのピンがクラスタの数に残ってしまいます。
やることは単純で、フィルタ時に clearLayers() してから、有効なマーカーだけを addLayer し直すだけです。
function applyFilter(activeTypes) {
layer.clearLayers();
markers
.filter((m) => activeTypes.includes(m.type))
.forEach((m) => layer.addLayer(L.marker([m.lat, m.lng])));
}
クラスタの数字が常に「今表示されているピンの数」と一致するので、ユーザーが混乱しません。
モバイルでの副次効果
余談ですが、このクラスタ化はモバイルで特に効きました。狭い画面で200個のピンが重なると操作不能でしたが、クラスタ化で「まず数字 → タップで拡大 → 個別ピン」という自然な段階が生まれ、片手でも扱えるようになりました。
地図UIをモバイルで作るなら、クラスタリングは「あったら便利」ではなく「ほぼ必須」だと感じます。
まとめ
- 大量ピンの地図は、
L.layerGroupをL.markerClusterGroupに差し替えるだけでクラスタ化できる。 - 読み込み順(Leaflet 本体 → markercluster → 自分のスクリプト)に注意。
- 運用サービスに入れるなら、種類横断で1クラスタ / CDN 失敗時のフォールバック / フィルタ後の再クラスタ の3点を押さえると事故らない。
「ピンが多すぎて地図が見えない」で悩んでいる人の参考になれば。
この実装は、バイクの中古車検索・ツーリング情報サイト MotoHub の地図機能で使っています。
