Mapbox Newsletter WEEKLY TIPSの解説 -「クラスターの作成と設定」
はじめに
この記事は、先日配信されたMapbox NewsletterのWEEKLY TIPSで紹介されていた「クラスターの作成と設定」についての解説です。このサンプルではクラスターの使い方を例示しています。また、Newsletterの購読はこちらからお申し込みいただけます。
以下が本サンプルのデモです。
コードを確認
まずExamplesのコードを見に行きましょう。
日本語サイト
英語サイト
基本的に同じコードですが、英語版はスタイルがMapbox Dark v11にアップグレードされているのでこちらを使用します。Mapbox Dark v10ではデフォルトのプロジェクション(地図投影法)がWebメルカトルであるのに対し、Mapbox Dark v11ではGlobe(3D表示された地球)なので、印象がかなり異なります。また、英語版はMapbox GL JS v3が使用されています。
HTML/CSS
まずHTMLを見ていきましょう。
以下は地図を表示するエレメントです。
<div id="map"></div>
Mapの作成
次にJavaScriptのコードを見ていきます。以下のコードはいつも通り、Mapオブジェクトを作成しています。container
で地図を表示するHTMLエレメントのidを指定します。
const map = new mapboxgl.Map({
container: 'map',
// Choose from Mapbox's core styles, or make your own style with Mapbox Studio
style: 'mapbox://styles/mapbox/dark-v11',
center: [-103.5917, 40.6699],
zoom: 3
});
loadイベント
addSource
およびaddLayer
はスタイルの読み込み後に実行される必要があります。そこで、load
イベント(map.on('load', () => {})
の中身)で以降の処理を記述しています。
ソースの作成
https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson から地震に関するGeoJSONデータを取得し、ソースを作成します。このデータはPointデータです。クラスターを作成するためにはソースにクラスターに関する設定が必要です。これは、クラスターデータを動的に生成しているというソース内部動作に起因します。設定項目はこちらにあります。
map.addSource('earthquakes', {
type: 'geojson',
// Point to GeoJSON data. This example visualizes all M1.0+ earthquakes
// from 12/22/15 to 1/21/16 as logged by USGS' Earthquake hazards program.
data: 'https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson',
cluster: true,
clusterMaxZoom: 14, // Max zoom to cluster points on
clusterRadius: 50 // Radius of each cluster when clustering points (defaults to 50)
});
サンプルの設定項目は以下のとおりです。
項目 | 説明 |
---|---|
cluster | クラスターを使用する際にtrue を設定 |
clusterMaxZoom | 設定したズームレベルまでクラスターデータを生成 |
clusterRadius | 指定したピクセル半径内のデータをクラスタリング |
レイヤーの作成
3種類のレイヤーを作成します。それぞれ見ていきます。
地震発生件数を示す円
1つ目は地震発生件数を示す円です。Circleレイヤーで作成します。cluster
をtrue
にしたソースではclusterRadius
半径内に存在するPointデータを結合した新たなPointoデータが自動的に作成されています。このデータはpoint_count
というプロパティを持っているので、それのあるなしでクラスターにより作成された新しいデータか元の地震のPointデータかを区別できます (filter: ['has', 'point_count']
の部分)。
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'earthquakes',
filter: ['has', 'point_count'],
paint: {
...後述...
}
});
次にpaint
の中身を見ます。point_count
にはクラスタリングされたPointの個数が入っていますが、この個数に合わせて円の色と大きさを変更します。円の色はcircle-color
、円の大きさはcircle-radius
で指定します。それぞれ、step
を使い、閾値ごとに色・大きさを指定しています。
'circle-color': [
'step',
['get', 'point_count'],
'#51bbd6',
100,
'#f1f075',
750,
'#f28cb1'
],
'circle-radius': [
'step',
['get', 'point_count'],
20,
100,
30,
750,
40
]
step
は以下のような書式です。例えば入力値が閾値1未満の場合は出力値0、入力値が閾値2未満の場合は出力値1が出力されます。今回は入力値としてpoint_count
を使用するのでget
を用いて値を取得しています。
["step",
入力値,
出力値0,
閾値1,
出力値1,
閾値2,
出力値2,
...
]
step
はその名の通り、閾値前後で出力値が切り替わります。代わりにinterpolate
を使うと徐々に色が変わるのでこれも面白いです。
地震発生件数の表示
2つ目は地震発生件数の表示です。数値を表示するのでSymbolレイヤーを使用します。1つ目と同様のfilter
を用いてクラスターデータを取得します。また、表示する数値は['get', 'point_count_abbreviated']
で取得します。point_count_abbreviated
はpoint_count
と同じデータですが、概数として格納されています例えば、1234
であれば1.2k
のような値になっています。
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'earthquakes',
filter: ['has', 'point_count'],
layout: {
'text-field': ['get', 'point_count_abbreviated'],
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 12
}
});
元の地震データの表示
ズームをしていくと、クラスターから外れて元の地震データとして取得できる点が現れてきます。これを表示するためのCircleレイヤーが3つ目のレイヤーです。クラスターデータではないのでfilter
の条件が逆転しています(filter: ['!', ['has', 'point_count']],
)。
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'earthquakes',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 4,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff'
}
});
クリックの処理
クラスター、元の地震データそれぞれのクリックに対する処理が記述されています。
クラスターデータのクリック処理
Map#on
のclick
イベントは第2引数にレイヤーIDを指定できます。ここでは1つ目のレイヤーのID、clusters
を指定しています。これにより、clusters
レイヤーのフィーチャーがクリックされたときにだけこのイベントが発火します。
map.on('click', 'clusters', (e) => {
...後述...
});
イベントが発火するとまずMap#queryRenderedFeatures
を実行してクリックした地点にあるclusters
レイヤーのフィーチャーを取得します。
const features = map.queryRenderedFeatures(e.point, {
layers: ['clusters']
});
次にそのクラスタのフィーチャーのcluster_id
を取得し、Map#getClusterExpansionZoom
を呼び出します。getClusterExpansionZoom
は指定したcluster_id
のフィーチャーが展開されるズームレベルをコールバック関数に返します。そこで、コールバック関数の中でそのズームレベルまでカメラをズームさせる処理を記述しています。
const clusterId = features[0].properties.cluster_id;
map.getSource('earthquakes').getClusterExpansionZoom(
clusterId,
(err, zoom) => {
if (err) return;
map.easeTo({
center: features[0].geometry.coordinates,
zoom: zoom
});
}
);
元データの地震データのクリック処理
同様にクリックイベントの処理です。ここでは3つ目のレイヤーのID、unclustered-point
を指定しています。
map.on('click', 'unclustered-point', (e) => {
...後述...
});
クリックしたフィーチャーの座標、マグニチュード、津波の有無を取得します。
const coordinates = e.features[0].geometry.coordinates.slice();
const mag = e.features[0].properties.mag;
const tsunami =
e.features[0].properties.tsunami === 1 ? 'yes' : 'no';
地図が繰り返されるプロジェクションを使用しているときの処理です(こちらの記事と同じ処理です)。
if (['mercator', 'equirectangular'].includes(map.getProjection().name)) {
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
}
最後に取得した情報を使ってポップアップを表示します。
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(
`magnitude: ${mag}<br>Was there a tsunami?: ${tsunami}`
)
.addTo(map);
マウスカーソルの変更
1つ目のレイヤーにマウスカーソルが乗ったときにマウスカーソルを変更します。また、外れたときには元に戻します。
map.on('mouseenter', 'clusters', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'clusters', () => {
map.getCanvas().style.cursor = '';
});
まとめ
多数のデータがありそのまま表示すると見にくくなるケースで、クラスターを使うと視覚的にわかりやすい地図が作成できることがわかりました。
Discussion