MapLibre GL JSを使って地図上に大量のマーカー(地点データ)を表示する際、「ピンが重なって見づらい」「データが多すぎてブラウザの動作が重い」といった課題に直面していないでしょうか?
このような問題は、近接するマーカーを1つのまとまりとしてグループ化する「クラスタリング」を導入することで解決できます。
この記事では、MapLibre GL JSを使ったクラスタリングの実装方法を解説します。
cluster: trueを使った最小構成の基本設定から、件数に応じた色・サイズの変更、クリック時のズームアニメーション、さらにclusterPropertiesを用いた実務レベルの応用的なデータ集計まで、コピペして試せるコードとともに順番に解説します。
この記事を読むことで、大量のデータを軽快かつ視覚的に分かりやすく地図上に表現できるようになります。
WEBCOACH|副業・フリーランス特化型のオンラインWebデザインスクールMapLibreのクラスタリングとは?大量の地点をまとめて表示する仕組み
結論:MapLibre GL JSでは標準機能を利用して、距離が近い複数の地点データを1つの「クラスタ(円やグループ)」としてまとめて表示できます。
数千件、数万件といった大量の地点データをそのまま地図に描画すると、マーカー同士が重なり合ってしまいユーザーが情報を読み取れません。さらに、DOM要素や描画処理が急増することでブラウザのメモリを圧迫し、フリーズや動作遅延の原因となります。
クラスタリングを適用すれば、地図の拡大・縮小(ズームレベル)に合わせて適切な数に地点が自動でグループ化されるため、視認性の向上とパフォーマンスの最適化を同時に実現できます。


MapLibre GL JSの標準機能だけでクラスタリングできる理由
MapLibre GL JSでクラスタリングを行う場合、追加の外部ライブラリをわざわざインストールする必要はありません。
MapLibreにはデータソースを扱う機能が内蔵されており、地図にデータを読み込ませる際の「GeoJSON Source」に対して、オプションとして cluster: true という1行を追加するだけで、自動的にクラスタリングが有効になります。これにより、開発者は複雑な計算処理を自作することなく、手軽にマーカーのグループ化を実装できます。
GeoJSONのPointデータを近接地点ごとにまとめる仕組み
クラスタリングは、GeoJSONデータの中の「Point(点)」データを対象に行われます。
MapLibreは、画面上のピクセル距離を基準にして近接地点を判定します。具体的には clusterRadius(デフォルトは50ピクセル)という設定値に基づき、指定されたピクセル半径内にあるPointデータを1つのクラスタに集約します。
地図をズームアウトして広域を表示すると、画面上の距離が縮まるため多くの地点が1つの大きなクラスタにまとまります。逆にズームインして詳細を表示していくと、ピクセル距離が離れるためクラスタが分割され、最終的には個別のマーカー(Point)として表示される仕組みです。
MapLibreのクラスタリングとSuperclusterの関係
MapLibre GL JSが標準機能だけで数万件ものデータを瞬時にクラスタリングできるのは、内部の計算処理にSupercluster(スーパークラスタ)という非常に高速なJavaScriptライブラリを採用しているからです。
Superclusterは、巨大なGeoJSONデータを階層的なインデックス構造(KDツリー)で管理し、各ズームレベルにおけるクラスタの状態をミリ秒単位で高速に算出するオープンソースのライブラリです。
MapLibreのGeoJSON Sourceは内部的にこのSuperclusterを利用(ラップ)しているため、私たちは直接Superclusterの複雑なAPIを操作しなくても、cluster: true と設定するだけでその強力なパフォーマンスの恩恵を受けることができます。クライアント側(ブラウザ側)の処理だけで、実用十分な速度で大量データのクラスタリングが可能なのはこのためです。
MapLibre GL JSでクラスタリングする基本準備
MapLibre GL JSでクラスタリングを実装するための第一歩として、最小構成の基本設定を解説します。
クラスタリングを実現するには、「GeoJSONデータの準備」「Source(データ元)へのクラスタ設定」「Layer(見た目)の追加」という3つのステップが必要です。

クラスタリング実装に必須となるGeoJSON(Point)のデータ形式
MapLibreでクラスタリングを行うには、地点データがGeoJSONフォーマットで記述されている必要があります。具体的には、Point(点)ジオメトリを持つFeatureをまとめたFeatureCollectionという形式を使用します。
クラスタリングは座標(経度・緯度)を基準にして距離を計算するため、LineString(線)やPolygon(面)ではなく、Pointデータである必要があります。
// クラスタリングに必要なGeoJSONの基本構造
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [139.767125, 35.681236] // [経度, 緯度]の順
},
"properties": {
"name": "東京駅",
"id": 1
}
},
// ... 他の地点データが続く
]
}
Sourceプロパティに「cluster: true」と「clusterMaxZoom」を設定する基礎コード
GeoJSONデータを用意したら、MapLibreの地図にデータソースとして追加します。このとき、map.addSource()のオプションとしてcluster: trueを設定するだけで、クラスタリングが有効になります。
実務では、あわせてclusterMaxZoomとclusterRadiusを設定するのが一般的です。
map.addSource('my-locations', {
type: 'geojson',
data: 'https://example.com/data.geojson', // GeoJSONのURL、またはオブジェクトを直接指定
cluster: true, // クラスタリングを有効化
clusterMaxZoom: 14, // クラスタリングを行う最大のズームレベル
clusterRadius: 50 // 近接地点をクラスタにまとめる半径(ピクセル単位。デフォルトは50)
});
- clusterMaxZoom: このズームレベルを超えて拡大(ズームイン)すると、どんなに距離が近くてもクラスタリングが解除され、個別の地点として表示されます。
- clusterRadius: クラスタとしてまとめる範囲の広さをピクセルで指定します。数値を大きくすると、より広範囲の地点が1つにまとまりやすくなります。
HTML・CSS・JavaScriptを含めたサンプルプログラム
ここでは、ブラウザでそのまま動かせる完全な最小構成のサンプルコードを提示します。
「GeoJSONを読み込み、クラスタリングを有効にし、地図上に円として描画する」までの流れを確認してください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>MapLibre クラスタリング基本サンプル</title>
<!-- MapLibre GL JSのCSSとJSを読み込み -->
<link href="https://unpkg.com/maplibre-gl@^5.6.2/dist/maplibre-gl.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-gl@^5.6.2/dist/maplibre-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
/* 地図を画面いっぱいに表示 */
#map { position: absolute; top: 0; bottom: 0; width: 100%; }</style>
</head>
<body>
<div id="map"></div>
<script>
// 地図の初期化
const map = new maplibregl.Map({
container: 'map', // HTMLのdiv要素のID
style: 'https://tiles.openfreemap.org/styles/liberty', // 地図のスタイル設定
center: [139.767, 35.681], // 初期表示の経度・緯度
zoom: 1 // 全体が見えるように初期ズームレベルを低めに設定
});
map.on('load', () => {
// 1. データソースの追加とクラスタリングの有効化
map.addSource('earthquakes', {
type: 'geojson',
// サンプル用データ(世界の地震データ)
data: 'https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson',
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
// 2. クラスタ(まとまり)を表示するレイヤー
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'earthquakes',
filter: ['has', 'point_count'], // クラスタ化されたデータのみ抽出
paint: {
'circle-color': '#51bbd6', // クラスタの円の色
'circle-radius': 20, // クラスタの円の大きさ
'circle-stroke-width': 2,
'circle-stroke-color': '#fff'
}
});
// 3. 個別ポイント(クラスタ化されていない地点)を表示するレイヤー
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'earthquakes',
filter: ['!', ['has', 'point_count']], // クラスタ化されていないデータのみ抽出
paint: {
'circle-color': '#f28cb1',
'circle-radius': 6,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff'
}
});
});</script>
</body>
</html>
実際の表示
See the Pen maplibre-clustering-sample-01 by watashi-xyz (@watashi-xyz) on CodePen.
このコードをコピーしてHTMLファイルとして保存し、ブラウザで開くと地図が表示されます。青い円(クラスタ)が表示されており、地図を拡大していくとクラスタが分割され、最終的にピンクの小さな円(個別ポイント)になる動作を確認できます。
クラスタの件数と個別マーカーを正しく表示する方法
基本設定でクラスタの円を描画できたら、次は「そのクラスタの中に何件の地点が含まれているか」の数値を円の上に重ねて表示します。
また、MapLibreではクラスタ(まとまり)と個別ポイント(単一の地点)を明確に区別してレイヤーを定義する必要があるため、正しく表示を分けるためのフィルター設定についても解説します。

point_countでクラスタ内の地点数を表示する
クラスタの中に含まれる地点の正確な総数を表示するには、point_countプロパティを使用します。
GeoJSON Sourceでcluster: trueを設定すると、MapLibreの内部処理(Supercluster)によって、生成されたクラスタデータに対して自動的にpoint_countというプロパティが付与されます。
数字を表示するためには、円を描画するcircleレイヤーとは別に、テキストを描画するためのsymbolレイヤーを追加し、テキストフィールドにこのpoint_countを読み込ませます。
// クラスタの件数を表示するテキストレイヤー
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'earthquakes', // 先ほど追加したGeoJSONソース
filter: ['has', 'point_count'], // クラスタのみを対象とする
layout: {
// {point_count} と記述することで件数プロパティを文字列として展開
'text-field': '{point_count}',
'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold'], // 利用環境に合わせてフォントを指定
'text-size': 12
},
paint: {
'text-color': '#ffffff' // テキストの色を白に設定
}
});
実際の表示(マーク上に数字が表示されています)
See the Pen maplibre-clustering-sample-02 by watashi-xyz (@watashi-xyz) on CodePen.
point_count_abbreviatedで「1.2k」のように件数を省略表示する
数千件、数万件といった大量の地点データが1つのクラスタにまとまる場合、数値の桁数が多くなりすぎてクラスタの円からテキストがはみ出してしまうことがあります。このような場合は、point_count_abbreviatedを使用するのが便利です。
MapLibreはpoint_countに加えて、数値を読みやすくフォーマットしたpoint_count_abbreviatedというプロパティも自動生成します。これを利用すると、例えば「1200」は「1.2k」、「10000」は「10k」といった省略形式で表示されます。
実装方法は、先ほどのtext-fieldの指定を変更するだけです。
layout: {
// 桁数が多い場合は省略形(例: 1.2k)で表示する
'text-field': '{point_count_abbreviated}',
'text-size': 12
}
データ量が1,000件を超えるような実務のプロジェクトでは、UIの崩れを防ぐためにpoint_count_abbreviatedをデフォルトとして採用することをおすすめします。
['has', 'point_count']でクラスタと個別ポイントを判別する
同じGeoJSONデータソースから「クラスタ」と「クラスタ化されていない個別の地点」を分けて描画するためには、各レイヤーのfilterプロパティに['has', 'point_count']を使った条件判定を記述します。
なぜこの記述が必要かというと、クラスタリング機能は元のGeoJSONのPointデータを上書きしているわけではなく、ズームレベルに応じて動的に「クラスタ要素」と「非クラスタ要素(個別の元データ)」を混在させて出力しているからです。
クラスタ要素には必ずpoint_countプロパティが存在し、個別の元データには存在しない、という仕様を利用して両者を振り分けます。
// 1. クラスタ用の円を描画するレイヤー
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'my-locations',
// 「point_countプロパティを持っている(=クラスタである)」データだけを表示
filter: ['has', 'point_count'],
paint: { /* ... */ }
});
// 2. 個別ポイント(単一の地点)を描画するレイヤー
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'my-locations',
// 「!(否定)」を使って、「point_countプロパティを持っていない(=個別地点である)」データだけを表示
filter: ['!', ['has', 'point_count']],
paint: { /* ... */ }
});
【注意点:似た用語の混同に注意】
クラスタリング実装時によく似た用語が登場しますが、役割を明確に区別してください。
cluster(真偽値):ソースのオプション設定(cluster: true)で主に使用します。cluster_id(数値):後述するクリックイベント時などに、特定のクラスタを識別するための固有IDです。フィルター用の判定基準としては使いません。point_count(数値):レイヤーの表示・非表示を振り分けるためのフィルター(['has', 'point_count'])として使用する、最も確実なプロパティです。

件数に応じてクラスタの色・サイズを変更する方法
実務の地図アプリケーションでは、クラスタに含まれる件数(地点の密集度)をユーザーが一目で直感的に把握できるよう、円の色やサイズを動的に変えるのが一般的です。
MapLibre GL JSでは、JavaScript側で複雑な条件分岐や再描画のロジックを書かなくても、「式(Expressions)」というレイヤーのスタイル設定を活用するだけで、表示を自在に切り替えることができます。
ここでは、実務でよく使われるstep式とinterpolate式という2つの代表的な設定方法と、それぞれの使い分けについて解説します。

step式で「1〜10件・11〜50件・51件以上」を段階表示する
データ規模を「小・中・大」のように明確なグループに分けてパキッと見せたい場合は、step式を使用します。
step式は、指定した数値の閾値(ステップ)を超えるごとに、色やサイズを段階的に切り替える記述方法です。
以下のコードは、クラスタの件数(point_count)を取得し、「10件未満」「10〜49件」「50件以上」の3段階でクラスタの色とサイズを切り替える実用的な例です。
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'my-locations',
filter: ['has', 'point_count'],
paint: {
// 件数に応じて色を段階的に切り替える
'circle-color': [
'step',
['get', 'point_count'], // 判断基準となるプロパティを取得
'#51bbd6', // デフォルトの色(10件未満)
10, '#f1f075', // 10件以上の場合の色
50, '#f28cb1' // 50件以上の場合の色
],
// 件数に応じて円のサイズ(半径)を段階的に切り替える
'circle-radius': [
'step',
['get', 'point_count'],
15, // 10件未満のサイズ(15px)
10, 20, // 10件以上のサイズ(20px)
50, 25 // 50件以上のサイズ(25px)
]
}
});
実際の表示
See the Pen maplibre-clustering-sample-03 by watashi-xyz (@watashi-xyz) on CodePen.
【コードのポイント】
['get', 'point_count']を使って、クラスタ内の地点数を評価しています。閾値を変更したい場合は、コード内の数値(10や50)をプロジェクトのデータ規模に合わせて調整するだけで、簡単に3段階以上の表示も可能です。
interpolate式でcircle-radiusを滑らかに変化させる
グループ分けではなく、件数に応じてサイズや色をグラデーションのように「無段階で滑らかに」変化させたい場合は、interpolate(補間)式を使用します。
ヒートマップのような連続的なデータの強弱を視覚的に表現したい場面で非常に効果的です。
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'my-locations',
filter: ['has', 'point_count'],
paint: {
'circle-color': '#51bbd6',
// 最小件数から最大件数にかけて、円のサイズを線形(linear)に滑らかに拡大する
'circle-radius': [
'interpolate',
['linear'], // 線形補間を指定
['get', 'point_count'],
1, 15, // 1件のときは半径15px
1000, 40 // 1000件のときは半径40px(間の件数は自動計算)
]
}
});
実際の表示
See the Pen maplibre-clustering-sample-04 by watashi-xyz (@watashi-xyz) on CodePen.
【コードのポイント】
['linear']を指定することで、1件〜1000件の間の数値をMapLibreが自動的に計算し、例えば500件であれば中間程度のサイズで描画してくれます。
「少数のときは明確に分けたいが、数が増えたら連続的に大きくしたい」といった場合には、step式よりもinterpolate式が適しています。
circle-color・枠線・透明度を使って見た目を調整する
ベースとなる円の大きさと色が決まったら、背景の地図(ベースマップ)や他のマーカーと見分けがつきやすいように、枠線(stroke)や透明度(opacity)を調整してUIを洗練させます。
大量の地点が密集するエリアでは、クラスタ同士が重なってしまうことがあります。このとき、塗りつぶし色を半透明にしつつ、枠線を白などの不透明色でくっきりと描画すると、重なり具合がユーザーに伝わりやすくなります。
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'my-locations',
filter: ['has', 'point_count'],
paint: {
// 1. rgbaを使って塗りつぶし色を半透明(80%)にする
'circle-color': 'rgba(81, 187, 214, 0.8)',
// (またはプロパティで指定する場合)
// 'circle-color': '#51bbd6',
// 'circle-opacity': 0.8,
// 2. 枠線の太さと色を設定して視認性を高める
'circle-stroke-width': 2, // 枠線を2pxに
'circle-stroke-color': '#ffffff' // 枠線を白にする
}
});
【コードのポイント】
circle-stroke-width(枠線の太さ)とcircle-stroke-color(枠線の色)を組み合わせることで、クラスタが海の上にあっても建物の上にあっても、背景色に埋もれることなく確実に視認できるようになります。これはアクセシビリティの観点でも重要な設定です。
実際の表示
See the Pen Untitled by watashi-xyz (@watashi-xyz) on CodePen.
TVCMで話題の【ココナラ】無料会員登録はこちらクラスタをクリックして地点を詳しく表示する方法
基本設定を終えて地図上にクラスタを表示しただけでは、ユーザーがクラスタ(円)をクリックしても何も起こりません。
実務のアプリケーションでは、クリックした際に「地図をズームしてクラスタを分割する」あるいは「クラスタ内の施設一覧をサイドバーに表示する」といったインタラクションが必須になります。
MapLibre GL JSには、GeoJSON Sourceに対してクラスタの中身を操作・取得するための便利なAPIが用意されています。ここでは、特定のクラスタを識別する固有ID(cluster_id)を利用して、代表的な3つのメソッドを実装する方法を解説します。

getClusterExpansionZoom()で分割される位置まで自動ズームする
クラスタをクリックした際に、そのクラスタがちょうど分割される(バラバラになる)ズームレベルまで滑らかに地図を拡大させるには、getClusterExpansionZoom()を使用します。
このメソッドを使うと、ユーザー自身がマウスホイールやピンチインで何度も拡大する手間を省き、ワンクリックで次の階層をスムーズに表示できます。実装するには、クリックしたレイヤーからcluster_idを取得し、メソッドに渡してズームレベルを計算させます。
// クラスタレイヤーに対するクリックイベントを設定
map.on('click', 'clusters', async (e) => {
// クリックした要素のプロパティから固有の cluster_id を取得
const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
const clusterId = features[0].properties.cluster_id;
// 地図のデータソースを取得
const source = map.getSource('my-locations');
try {
// クラスタが分割されるズームレベルを非同期で計算して取得
const zoom = await source.getClusterExpansionZoom(clusterId);
// 取得したズームレベルへ滑らかにアニメーション移動(easeTo)
map.easeTo({
center: features[0].geometry.coordinates,
zoom: zoom
});
} catch (err) {
console.error('ズームレベルの計算に失敗しました:', err);
}
});
実際の表示
See the Pen maplibre-clustering-sample-06 by watashi-xyz (@watashi-xyz) on CodePen.
【注意点】
最新のMapLibre GL JSでは、これらのクラスタ操作メソッドはPromiseを返すため、async/await構文を使ってスッキリ記述するのが実務的なベストプラクティスです。
getClusterChildren()で直下のクラスタと地点を取得する
クラスタの中身がどのような構成になっているか、「1階層下(直下)」の情報を知りたい場合はgetClusterChildren()を使用します。
大規模なデータをクラスタリングした場合、1つの巨大なクラスタの中には「さらに小さなクラスタ群」と「個別の地点」が混在しています。このメソッドは最終的な全地点ではなく、あくまでツリー構造の直下にある要素(子要素)だけを配列として返します。
map.on('click', 'clusters', async (e) => {
const clusterId = e.features[0].properties.cluster_id;
const source = map.getSource('my-locations');
// 直下の子要素(クラスタまたは個別ポイント)を取得
const children = await source.getClusterChildren(clusterId);
console.log(children);
// 出力例: [{ properties: { cluster: true, point_count: 5, ... } }, { properties: { name: "店舗A", ... } }]
});
直下要素のプロパティを確認し、それが再びクラスタ(cluster: true)であればさらに掘り下げる、といった再帰的な処理やデバッグを行いたい場面で役立ちます。
getClusterLeaves()でクラスタ内の最終的な地点一覧を取得する
階層の深さに関係なく、クラスタ内に格納されている「最終的な元の地点データ(個別ポイント)」をすべて取得したい場合は、getClusterLeaves()を使用します。
実務で最も利用頻度が高いのはこのメソッドです。たとえば「クリックしたクラスタに含まれる店舗の名前を、ポップアップやサイドバーのリストに一覧表示したい」という機能はこれを使って実装します。
引数には cluster_id の他に、取得する最大件数(limit)と、取得を開始するオフセット(offset)を指定します。これは、数万件のデータが入ったクラスタを一括取得してブラウザがフリーズするのを防ぐためのページネーション機能として働きます。
map.on('click', 'clusters', async (e) => {
const clusterId = e.features[0].properties.cluster_id;
const source = map.getSource('my-locations');
// getClusterLeaves(clusterId, limit, offset)
// 例: 対象クラスタから最大10件の元データを0番目から取得
const leaves = await source.getClusterLeaves(clusterId, 10, 0);
// 取得した元データ(Leaf = 葉)の名前をコンソールに表示
leaves.forEach(leaf => {
console.log(leaf.properties.name);
});
// 実務ではここでDOMを操作し、取得したリストを画面のサイドバーなどに描画します
});
See the Pen maplibre-clustering-sample-07 by watashi-xyz (@watashi-xyz) on CodePen.
【注意点】
limitに意図せず非常に大きな値(またはInfinity)を指定してしまうと、超巨大クラスタをクリックした際に計算負荷が跳ね上がり、パフォーマンスの低下を招きます。ユーザーが一度に閲覧できる適切な件数(10〜100件程度)に設定し、必要に応じてoffsetを加算して追加読み込みさせるUI設計が重要です。
MapLibreのクラスタと個別ポイントの操作を分ける方法
地図を操作するユーザーにとって、「複数の地点がまとまったクラスタ」と「単一の個別ポイント(マーカー)」では、期待する動作が異なります。
クラスタをクリックした場合は「ズームして中身を見たい」はずですし、個別ポイントをクリックした場合は「その地点の店舗名や詳細情報を見たい」はずです。
MapLibre GL JSでは、イベントリスナー(map.on)の第2引数に対象となるレイヤーIDを指定することで、クラスタ用レイヤーと個別ポイント用レイヤーの処理を簡単に分離できます。
ここでは、実務で標準的に使われる操作の切り分け方と、ホバー時のマウスカーソル(ポインター)の変更方法を解説します。

クラスタだけにクリックイベントを設定する
クラスタレイヤー(円)をクリックした時のみ発火するイベントを設定します。
前のセクションで解説した自動ズーム処理(getClusterExpansionZoom)と組み合わせるとともに、マウスオーバー時にカーソルを「指のマーク(pointer)」に変更して、クリック可能であることをユーザーに伝えます。
// クラスタ(clustersレイヤー)をクリックしたときの処理
map.on('click', 'clusters', async (e) => {
const clusterId = e.features[0].properties.cluster_id;
const source = map.getSource('my-locations');
try {
const zoom = await source.getClusterExpansionZoom(clusterId);
map.easeTo({
center: e.features[0].geometry.coordinates,
zoom: zoom
});
} catch (err) {
console.error(err);
}
});
// クラスタにホバー(マウスカーソルが乗った)したとき、カーソルをポインターに変更
map.on('mouseenter', 'clusters', () => {
map.getCanvas().style.cursor = 'pointer';
});
// クラスタからマウスが外れたらカーソルを元に戻す
map.on('mouseleave', 'clusters', () => {
map.getCanvas().style.cursor = '';
});
このように特定のレイヤーID(ここでは'clusters')を指定することで、地図上の何もない場所をクリックしてもエラーにならず、クラスタに触れた時だけ処理が走るようになります。
実際の表示
See the Pen maplibre-clustering-sample-08 by watashi-xyz (@watashi-xyz) on CodePen.
個別マーカーだけにポップアップを表示する
次に、クラスタ化されていない個別の地点(単一のマーカー)をクリックした際の処理を書きます。
ここでは、対象レイヤーを'unclustered-point'に指定し、MapLibre標準のmaplibregl.Popup()を使って地点の詳細情報(プロパティ)を画面に表示させます。
// 個別ポイント(unclustered-pointレイヤー)をクリックしたときの処理
map.on('click', 'unclustered-point', (e) => {
// console.log(e.features[0].geometry.coordinates);
// 元のGeoJSONのプロパティからデータを取得
const coordinates = e.features[0].geometry.coordinates.slice();
const lat = coordinates[0];
const lon = coordinates[1];
// クリック位置にポップアップを表示
new maplibregl.Popup()
.setLngLat(coordinates)
.setHTML(`<p>緯度:${lat}<br>経度:${lon}</p>`)
.addTo(map);
});
// 個別ポイントホバー時のカーソル変更
map.on('mouseenter', 'unclustered-point', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'unclustered-point', () => {
map.getCanvas().style.cursor = '';
});
実際の表示
See the Pen maplibre-clustering-sample-09 by watashi-xyz (@watashi-xyz) on CodePen.
【注意点】
クラスタと個別ポイントで別々のレイヤーIDを設定しているため、これで両者のクリック処理は完全に独立して動きます。クラスタレイヤーではポップアップは開かず、個別ポイントレイヤーではズーム処理は発火しません。
queryRenderedFeatures()でクリックしたクラスタを取得する
MapLibreでクリックイベント(map.on('click', e))が発生した際、e.featuresを通じてクリックされた要素を取得するのが一般的ですが、複雑なレイヤー構成(例えば、クラスタの円と件数テキストが別レイヤーで重なっている場合など)では、queryRenderedFeatures()メソッドを手動で実行する方が確実な場面があります。
queryRenderedFeatures()は、指定した画面上のピクセル位置に存在する特定のレイヤーの要素(Feature)を配列として返します。
// 地図全体に対するクリックイベント
map.on('click', (e) => {
// クリックした位置(e.point)にある「clusters」レイヤーの要素を取得
const features = map.queryRenderedFeatures(e.point, {
layers: ['clusters']
});
// 取得した要素がない(クラスタ以外をクリックした)場合は処理を終了
if (!features.length) {
return;
}
// 以下、取得したクラスタ要素を使った処理
const clusterId = features[0].properties.cluster_id;
const pointCount = features[0].properties.point_count;
console.log(`クリックしたクラスタのIDは ${clusterId}、件数は ${pointCount} 件です。`);
});
このメソッドを使用することで、「クリックしたポイントが確実にクラスタのレイヤーであるか」を厳密に判定できるため、複数のイベントが干渉してしまうような複雑なUI開発において、より安全に操作を制御できるようになります。
clusterPropertiesでクラスタ内のデータを集計する方法
クラスタリングの基本機能では、クラスタに含まれる「地点の総数(point_count)」のみが自動計算されます。しかし実務では、「このクラスタの中にコンビニとスーパーが何件ずつあるのか」「クラスタ内の店舗の売上合計はいくらか」といった、独自の属性データに基づいた集計が求められることが多くあります。
MapLibre GL JSでは、GeoJSON SourceにclusterPropertiesというオプションを設定することで、クラスタ化の処理と同時に任意のプロパティを集計できます。ここでは、単純な件数表示から一歩進んだ応用的なデータ集計の方法を解説します。

カテゴリ別の店舗数・施設数をクラスタ単位で集計する
施設や店舗の「カテゴリ別」の件数をクラスタ内に保持するには、clusterProperties内でMapLibreの「式(Expressions)」を使って独自の集計ロジックを定義します。
たとえば、以下のようなGeoJSONデータがあり、categoryプロパティに「convenience(コンビニ)」と「supermarket(スーパー)」が混在しているとします。
// 集計対象のGeoJSONデータのイメージ
{
"type": "Feature",
"properties": {
"name": "店舗A",
"category": "convenience", // カテゴリ
"stock": 150 // 在庫数などの数値
},
"geometry": { "type": "Point", "coordinates": [139.7, 35.6] }
}
これをカテゴリ別に集計するためのSource設定は以下のようになります。
map.addSource('my-shops', {
type: 'geojson',
data: 'https://example.com/shops.geojson',
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50,
// clusterPropertiesで独自の集計ルールを定義する
clusterProperties: {
// 'convenience_count' という新しいプロパティを作成して集計
'convenience_count': [
'+', // ① アクション:足し算を行う
['case', ['==', ['get', 'category'], 'convenience'], 1, 0] // ② 条件:categoryがconvenienceなら1、違えば0を返す
],
// 'supermarket_count' という新しいプロパティを作成して集計
'supermarket_count': [
'+',
['case', ['==', ['get', 'category'], 'supermarket'], 1, 0]
]
}
});
【コードのポイント】
clusterPropertiesは、「どのアクションで(例:+)」「どのような条件の数値を(例:case式)」まとめるかを定義します。これにより、生成されたクラスタのプロパティに convenience_count: 5 のような集計結果が自動的に付与されるようになります。
売上合計・在庫数・災害件数をクラスタに表示する
特定の条件で「1」を足すだけでなく、GeoJSONが持っている「数値データ」そのものをクラスタ単位で合計(Sum)することも可能です。
売上合計、商品の在庫数、被害総額、災害件数などの数値をクラスタに可視化したい場合に利用します。
前項と同じclusterPropertiesの中に、以下のように数値を直接足し合わせる式を追加します。
clusterProperties: {
// 各地点が持つ 'stock' プロパティの数値をそのまま合計する
'total_stock': [
'+', // 足し算を行う
['get', 'stock'] // 元データのstockプロパティを取得
]
}
集計した数値は、通常のpoint_countと同じようにテキストレイヤーのtext-fieldで呼び出して地図上に表示できます。
// 集計した「在庫合計」をクラスタ上にテキスト表示するレイヤー
map.addLayer({
id: 'cluster-stock-label',
type: 'symbol',
source: 'my-shops',
filter: ['has', 'point_count'],
layout: {
// {total_stock} で集計結果を展開して表示
'text-field': '在庫: {total_stock}個',
'text-size': 14
}
});
クラスタ内の集計値を凡例やドーナツチャートで可視化する
clusterPropertiesで集計したカテゴリ別のデータ(例:コンビニ5件、スーパー3件)は、地図上で可視化して初めてユーザーにとって価値のある情報になります。
可視化のアプローチとして、最もシンプルかつ実用的なのは、データ駆動型のスタイル(step式など)を用いて「クラスタ内で最も件数が多いカテゴリの色」にクラスタ全体の色を塗り分ける手法です。これと画面端に配置したHTMLの「凡例(レジェンド)」を連動させることで、そのエリアの主要な施設傾向が一目で分かります。
さらに高度なUIとして、クラスタの円を「カテゴリ別の割合を示したドーナツチャート(円グラフ)」にして表示したいという要件が実務ではよく発生します。
しかし、MapLibre標準のcircleレイヤーは単一の円しか描画できないため、複雑なドーナツチャートをスタイル式だけで描画することは困難です。
ドーナツチャートや複数アイコンの配置など、複雑なデザインのクラスタを実現するためには、次のセクションで解説する「HTML要素を使ったカスタムマーカー(HTML Marker)」と連動させるアプローチが一般的です。集計したconvenience_countなどのデータは、HTML Marker側から参照することで自由なグラフ描画のソースとして活用できます。
HTML・SVGでクラスタのデザインをカスタマイズする方法
MapLibre GL JSの標準機能(Circle LayerやSymbol Layer)は、WebGL(GPU)を利用して描画されるため非常に高速ですが、「複雑なレイアウト」「外部の画像アイコンとの組み合わせ」「アニメーション」などの柔軟なデザイン表現には限界があります。
デザイン要件が厳密なプロジェクトや、前述の「ドーナツチャート」のような高度な可視化を行いたい場合は、MapLibre標準のレイヤー機能に加えて「HTML要素(maplibregl.Marker)」を使ってクラスタを描画するアプローチを採用します。
ここでは、HTMLやSVGを駆使して完全にオリジナルのクラスタUIを構築する方法を解説します。

HTML要素を使って件数やアイコンを自由に配置する
結論から言うと、JavaScriptの標準的なDOM操作で作成したHTML要素(divやimgなど)を、そのまま地図上のクラスタマーカーとして配置することができます。
CSSのFlexboxやGridを使って文字とアイコンの配置を自由にコントロールできるため、「中心に文字、右上に赤い通知バッジを表示する」といった要件も簡単に実現できます。
// HTMLベースのカスタムマーカーを作成する関数
function createCustomClusterMarker(coordinates, pointCount){
// 1. マーカーの土台となるdiv要素を作成
const el = document.createElement('div');
el.className = 'custom-html-cluster';
// 2. 自由にHTMLを構築(例:アイコン画像と件数テキストの組み合わせ)
el.innerHTML = `
<div style="background: #ffffff; border-radius: 50%; padding: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.3); text-align: center;">
<img src="icon-shop.png" width="20" height="20" alt="店舗アイコン"><br>
<span style="font-weight: bold; color: #333;">${pointCount}件</span>
</div>
`;
// 3. MapLibreのMarkerクラスに要素を渡し、地図に追加する
new maplibregl.Marker({ element: el })
.setLngLat(coordinates)
.addTo(map);
}
【注意点:パフォーマンスとのトレードオフ】
HTML Marker(DOM要素)は、Circle Layer(WebGL)に比べてブラウザのレンダリング負荷が格段に高くなります。数百〜数千のクラスタをすべてHTML要素で描画すると、スマートフォン等で地図をスクロールした際にカクつき(FPS低下)が発生する原因になります。
HTML化は「デザインの自由度」と「描画パフォーマンス」のトレードオフであることを理解し、データ量が極端に多い場合はWebGLの標準機能(Circle/Symbol)を優先的に検討してください。
SVG・Canvasでカテゴリ別の独自クラスタを描画する
clusterPropertiesで集計した「コンビニの数」と「スーパーの数」の割合を、1つのクラスタの上に「ドーナツチャート(円グラフ)」として描画したい場合、HTMLマーカーの中に動的に生成したSVG要素を埋め込む手法が最も実用的です。
SVGであれば、JavaScript側で数値を元にパス(path)の角度や色を計算し、自由なグラフを描画できます。
// SVGドーナツチャートを生成する概念コード
function createDonutChartCluster(convenienceCount, supermarketCount){
const total = convenienceCount + supermarketCount;
// 割合の計算(実務ではここで角度やSVGのpath文字列を計算します)
const convPercent = (convenienceCount / total) * 100;
const el = document.createElement('div');
// 簡易的にCSSのconic-gradientを使ってドーナツチャート風の円を表現する例
el.style.width = '40px';
el.style.height = '40px';
el.style.borderRadius = '50%';
el.style.background = `conic-gradient(#51bbd6 0% ${convPercent}%, #f1f075 ${convPercent}% 100%)`;
// 中央をくり抜く(ドーナツ化)
el.innerHTML = `
<div style="width: 24px; height: 24px; background: white; border-radius: 50%; margin: 8px auto; text-align: center; line-height: 24px; font-size: 10px;">
${total}
</div>
`;
return el; // これを new maplibregl.Marker(el) に渡す
}
Canvas要素を用いることも可能ですが、DOM内に直接インラインで記述でき、解像度に依存せず綺麗に拡大縮小できるSVGや最新のCSS(conic-gradientなど)を活用する方が、コードの見通しが良くなり管理が容易です。
クラスタの色・サイズと凡例を連動させる
地図上に独自デザインのクラスタや複数の色を配置した場合、その色が何を意味しているのかをユーザーに伝える「凡例(レジェンド)」の設置が不可欠です。
MapLibreの地図コンテナ(<div id="map"></div>)の外部、もしくは上に重なるように絶対配置(position: absolute)でHTMLの凡例UIを作成します。
このとき、MapLibreのstep式などで指定したレイヤーの色定義コードと、HTML側の凡例の色(CSS)を手動で記述すると、仕様変更時に修正漏れが発生しやすくなります。実務では、設定を変数として切り出し、双方で共通のカラーパレットを参照するように設計するのがベストプラクティスです。
// 1. カラーパレットと条件を共通の定数として定義する
const CLUSTER_STYLES = [
{ min: 0, color: '#51bbd6', label: '10件未満' },
{ min: 10, color: '#f1f075', label: '10〜49件' },
{ min: 50, color: '#f28cb1', label: '50件以上' }
];
// 2. この定数を使って、HTML側に凡例を自動生成する
const legendDiv = document.getElementById('my-legend');
CLUSTER_STYLES.forEach(style => {
const item = document.createElement('div');
item.innerHTML = `<span style="display:inline-block; width:15px; height:15px; background:${style.color}; border-radius:50%; margin-right:5px;"></span>${style.label}`;
legendDiv.appendChild(item);
});
// 3. MapLibreのスタイル(step式)もこの定数を元に組み立てる
// (※実装時には定数配列を展開して、MapLibreが読み込めるフラットな配列形式に変換します)
このように設計することで、デザイン変更があった場合でも定数を書き換えるだけで地図上のクラスタカラーと凡例が完全に連動して更新され、保守性の高い堅牢なアプリケーションになります。
コストパフォーマンスに優れた高性能なレンタルサーバー
【Hostinger】よくある質問(FAQ)
-
MapLibre GL JSでクラスタリングできますか?
-
はい、可能です。外部ライブラリを追加インストールしなくても、地図にデータを追加するGeoJSON Sourceのオプションに
cluster: trueを設定するだけで、標準機能としてクラスタリングを実装できます。
-
MapLibreとSuperclusterは何が違いますか?
-
MapLibre GL JSは地図描画API全体を指し、SuperclusterはそのMapLibreに標準で内蔵されている「クラスタリング計算用の高速ライブラリ」です。開発者は直接SuperclusterのAPIを叩かなくても、MapLibreで
cluster: trueを設定するだけで自動的にその高速な計算エンジンの恩恵を受けることができます。
-
clusterRadiusは何を設定する値ですか?
-
画面上で地点を1つのクラスタとしてまとめる基準となる「ピクセル単位の半径」です。デフォルト値は50(ピクセル)です。この数値を100などに大きくすると広範囲のポイントが1つの大きなクラスタにまとまりやすくなり、20などに小さくするとクラスタがより細かく分割されます。
-
MapLibreでクラスタリングできるデータ件数に上限はありますか?
-
API仕様としての明確な上限はありませんが、実用的にブラウザ(クライアント側)で処理できるのは数万〜数十万件程度が目安となります。GeoJSONデータを一度ブラウザのメモリに読み込んで計算するため、ユーザーのPCやスマートフォンのスペックに依存します。数百万件を超える大規模データの場合は、サーバー側(PostGISやVector Tile等)での事前クラスタリング処理を検討してください。
-
クラスタをクリックしてズームさせることはできますか?
-
はい、可能です。クラスタのレイヤーに対してクリックイベント(
map.on('click', ...))を設定し、その中でgetClusterExpansionZoom()メソッドを呼び出すことで、クラスタが自然に分割される適切なズームレベルを取得し、自動的に拡大させることができます。
-
MapLibreで同じ座標のマーカーをSpiderfy(蜘蛛の巣状に展開)できますか?
-
いいえ、MapLibre GL JSの標準機能にはSpiderfy機能は含まれていません。ビルやマンションなど完全に同じ座標に複数の地点が重なっている場合、ズームしてもクラスタは分割されません。これを展開表示したい場合は、クリック時に
getClusterLeaves()で内包される地点データを取得し、HTML Markerを使ってJavaScript側で自作の円状UIとして再配置するなどの独自実装が必要です。
-
cluster: trueを設定してもクラスタ(円や数字)が表示されない原因は何ですか?
-
レイヤーの表示フィルター(
filter)の設定漏れ、またはGeoJSONフォーマットのエラーが主な原因です。クラスタを描画するレイヤーにfilter: ['has', 'point_count']が指定されているか確認してください。また、データがLineやPolygonではなく「Point」ジオメトリのFeatureCollection形式になっている必要があります。
まとめ
MapLibre GL JSでのクラスタリングは、大量の地点データを扱うWeb地図アプリケーションにおいて、ブラウザのパフォーマンス低下を防ぎ、ユーザーにとっての視認性を劇的に向上させるために欠かせない機能です。
まずは、本記事で紹介した「最小構成のサンプルプログラム」をコピーして、手元のブラウザで動かしてみてください。そこから少しずつ、クリックイベントやフィルターの切り分け、スタイル設定を追加していくことで、実務で求められる見やすく軽快な地図UIを確実に完成させることができるはずです。
WEBCOACH|副業・フリーランス特化型のオンラインWebデザインスクール

