Deck.gl の世界へようこそ！ ３歩でわかる お手軽 データビジュアライゼーション

今年も [Advent Calendar](https://qiita.com/advent-calendar/2021) の季節がやってきましたね。私も恒例の社内 Advent Calendar 向けに記事を書かせていただきます。[2019年](https://qiita.com/dsudo/items/42da9c7f45ed6147395e)，[2020年](https://qiita.com/dsudo/items/43c8c443ced83f657e14) に引き続き，今年は Deck.gl をご紹介します。


# Deck.glとは
+ https://deck.gl/
+ Uber 発のオープンソース
    - MITライセンス
+ データビジュアライゼーション フレームワーク
    - レイヤーを使ったアプローチ
+ React フレンドリー
    - リアクティブ プログラミング パラダイム
+ WebGL ベース
    - GPU で大量のデータを効率的に可視化
+ ベースマッププロバイダーとの統合
    - ArcGIS, CART, Google Map, Mapbox など


# 登場人物たちを見てみよう！

～ Deck.gl の世界に登場する人たちをご紹介するコーナー ～

## [Deck](https://deck.gl/docs/api-reference/core/deck)
+ Deck.gl のコアクラス
+ レイヤーやビューポートを受け取り，イベントを処理する

## [Layer](https://deck.gl/docs/api-reference/core/layer) / レイヤー
+ レイヤーは Deck.gl のコアコンセプト
+ データムのコレクションから各オブジェクトのプロパティへマッピング  
  マップ上にレンダリングする
+ 複数のレイヤーを組み合わせ可能

<details><summary>Layer の種類</summary><div>

### Core Layers
一般的なデータビジュアライゼーションを目的としたレイヤー

+ [ArcLayer](https://deck.gl/docs/api-reference/layers/arc-layer)
    - From - To の座標を結ぶ 3D のアーチ
+ [BitmapLayer](https://deck.gl/docs/api-reference/layers/bitmap-layer)
    - 境界に合わせたビットマップ
+ [GeoJsonLayer](https://deck.gl/docs/api-reference/layers/geojson-layer)
    - ポイント，ライン，ポリゴン，テキストなど
    - GeoJSON 形式のデータを元にする
+ [IconLayer](https://deck.gl/docs/api-reference/layers/icon-layer)
    - ラスターアイコン
+ [LineLayer](https://deck.gl/docs/api-reference/layers/line-layer)
    - From - To の座標を結ぶフラットライン
+ [PathLayer](https://deck.gl/docs/api-reference/layers/path-layer)
    - 座標リストの各要素をつなぐパス
    - [Miter joint](https://en.wikipedia.org/wiki/Miter_joint)
+ [PointCloudLayer](https://deck.gl/docs/api-reference/layers/point-cloud-layer)
    - 3D の球体
+ [PolygonLayer](https://deck.gl/docs/api-reference/layers/polygon-layer)
    - ポリゴン
+ [ScatterplotLayer](https://deck.gl/docs/api-reference/layers/scatterplot-layer)
    - 2D の円
    - 散布図を表現
+ [TextLayer](https://deck.gl/docs/api-reference/layers/text-layer)
    - テキストラベル
    - IconLayer の拡張

### Aggregation Layers
入力データを集約し，グリッドによるヒートマップなどを表現するレイヤー

+ [ContourLayer](https://deck.gl/docs/api-reference/aggregation-layers/contour-layer)
    - Isoline
        - しきい値の上下を分ける線分
    - Isoband
        - しきい値の範囲を塗りつぶす
+ [GridLayer](https://deck.gl/docs/api-reference/aggregation-layers/grid-layer)
    - 3D 四角形グリッドのヒートマップ
    - GPU 集約のサポート有無によって GPUGridLayer または CPUGridLayer
+ [HexagonLayer](https://deck.gl/docs/api-reference/aggregation-layers/hexagon-layer)
    - 3D 六角形グリッドのヒートマップ
+ [ScreenGridLayer](https://deck.gl/docs/api-reference/aggregation-layers/screen-grid-layer)
    - 2D グリッド
    - 座標の配列をヒストグラムビンに集約 (GPU または CPU)
    - GridLayer はマップを拡大縮小しても，オブジェクトは合わせて拡大縮小されるが，ScreenGridLayer は各ビンに分類される値の数が再集約される
+ [HeatmapLayer](https://deck.gl/docs/api-reference/aggregation-layers/heatmap-layer)
    - ヒートマップ
    - [ガウスカーネル密度推定](https://en.wikipedia.org/wiki/Kernel_%28statistics%29#Kernel_functions_in_common_use) を内部的に実装
        - 何を言ってるんだ...?
    - すべてのブラウザでサポートされているわけではない

### Geo Layers
具体的な地理空間可視化ユースケースを実現するレイヤー

+ [H3ClusterLayer](https://deck.gl/docs/api-reference/geo-layers/h3-cluster-layer) / [H3HexagonLayer](https://deck.gl/docs/api-reference/geo-layers/h3-hexagon-layer)
    - 3D 六角形グリッド
    - H3 (https://h3geo.org/) を利用
        - Apache2.0 ライセンス
        - 六角形のグリッドに分割・集計するための地理空間インデックスシステム
+ [S2Layer](https://deck.gl/docs/api-reference/geo-layers/s2-layer)
    - ポリゴン
    - S2 Geometry Library (http://s2geometry.io/) を利用
        - Apache2.0 ライセンス
        - データを 2D ではなく，3D の球体で表現する
+ [TileLayer](https://deck.gl/docs/api-reference/geo-layers/tile-layer)
    - 分割されたタイル
    - 現在の Viewport に表示されているものだけをロードする
    - Google Map みたいな感じ
+ [Trips Layer](https://deck.gl/docs/api-reference/geo-layers/trips-layer)
    - 車両の移動を表すアニメーション化されたパス
    - [Example](https://deck.gl/examples/trips-layer/)
+ [TerrainLayer](https://deck.gl/docs/api-reference/geo-layers/terrain-layer)
    - 立体的な地形
    - [Example](https://deck.gl/examples/terrain-layer/)
+ [MVTLayer](https://deck.gl/docs/api-reference/geo-layers/mvt-layer)
    - 分割されたタイル
    - MVTs ([Mapbox Vector Tiles](https://docs.mapbox.com/vector-tiles/specification/)) を利用

### Mesh Layers
3D モデルを可視化するレイヤー

+ [SimpleMeshLayer](https://deck.gl/docs/api-reference/mesh-layers/simple-mesh-layer)
    - 任意の数の 3D オブジェクト
    - e.g. マップ上の位置と方向を持つ 3D の車
+ [ScenegraphLayer](https://deck.gl/docs/api-reference/mesh-layers/scenegraph-layer)
    - シーングラフ
    - luma.gl (https://luma.gl/) を利用
        - PhiloGL の後継
        - MIT ライセンス
    - 通常は glTF ファイルをロードする
        - glTF (GL Transmission Format)
            - JSONによって3Dモデルやシーンを表現するフォーマット 
    - プログラムでもシーングラフの作成が可能

</div></details>

## [Viewport](https://deck.gl/docs/api-reference/core/viewport) / ビューポート
+ 典型的な 3D ライブラリの「カメラ」クラスの地理空間対応バージョン
+ 3D 座標のプロジェクションなどの数値計算を行う
+ 通常は Viewport インスタンスを直接操作することはない

## [View](https://deck.gl/docs/api-reference/core/view) / ビュー
+ HTML の canvas 上の位置と範囲に対応
+ [ViewState](https://deck.gl/docs/developer-guide/view-state-transitions) / ビューステート
    - ビューインスタンスの状態を表す
    - カメラの位置，向き，ズームやコントローラーなどの設定

<details><summary>View の種類</summary><div>

+ [MapView](https://deck.gl/docs/api-reference/core/map-view)
    - [Webメルカトル図法](https://en.wikipedia.org/wiki/Web_Mercator_projection)を実装する
    - デフォルトのビュー
+ [GlobeView (Experimental)](https://deck.gl/docs/api-reference/core/globe-view)
    - 地球を3D地球儀に投影する
+ [FirstPersonView](https://deck.gl/docs/api-reference/core/first-person-view)
    - 一人称視点
    - FPS (First-person shooter) みたいなやつ
    - e.g. 車両ログをドライバーの視点からレンダリング
+ [OrthographicView](https://deck.gl/docs/api-reference/core/orthographic-view)
    - 平行投影／正投影
    - 地理空間以外のユースケースで 2D チャートをレンダリング
+ [OrbitView](https://deck.gl/docs/api-reference/core/orbit-view)
    - 透視投影
    - 地理空間以外のユースケースで 3D オブジェクトをレンダリング

</div></details>

## [Controller](https://deck.gl/docs/api-reference/core/controller) / コントローラー
+ ユーザーの操作とカメラをインタラクティブにする
+ ポインターイベントとタッチジェスチャーをリッスン  
  ユーザーの入力をビューステートの変更に変換する

<details><summary>Controller の種類</summary><div>

※ View の種類にそれぞれ対応

+ [MapController](https://deck.gl/docs/api-reference/core/map-controller)
+ [GlobeController](https://deck.gl/docs/api-reference/core/globe-controller)
+ [FirstPersonController](https://deck.gl/docs/api-reference/core/first-person-controller)
+ [OrthographicController](https://deck.gl/docs/api-reference/core/orthographic-controller)
+ [OrbitController](https://deck.gl/docs/api-reference/core/orbit-controller)

</div></details>

## その他
+ [Point Light](https://deck.gl/docs/api-reference/core/point-light) などのライト
+ [Lighting Effect](https://deck.gl/docs/api-reference/core/lighting-effect) などの視覚効果


# 簡単 3 Step で体験してみよう！

～ サンプルを使って Deck.gl の世界を体験できるコーナー ～

今回は React を使わず，素の JavaScript で実装します。

## Step 1
まずは，地図を表示してみましょう。

[DeckGL (Scripting Interface)](https://deck.gl/docs/api-reference/core/deckgl) は Mapbox などの外部サービスとの統合をサポートする機能でコアの Deck クラスを拡張したクラスです。Step1 では，このクラスを利用してベースマップを表示します。

Mapbox のサービスを利用するには [アクセストークン](https://docs.mapbox.com/help/getting-started/access-tokens/) が必要になります。※ Mapbox GL JS は ver 2 からオープンソースではなくなりました。無料枠はあります。MapLibre (https://maplibre.org/) というプロジェクトが ver 1 系をフォークしてオープンソースとして引き継いでいるそうです。

それでは，コードを書いていきましょう。

Deck.gl ライブラリを読み込んでおく必要があります。HTML の Script タグに追加すれば OK です。今回は CDN から取得することにします。

``` html
<script src='https://unpkg.com/deck.gl@8.6.4/dist.min.js'></script>
```

Mapbox の API を利用するためのスタイルとスクリプトも読み込みましょう。[^1]

[^1]: 古いバージョンのライブラリだとアクセストークンなしでも表示できてしまうみたいですが...

``` html
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.0/mapbox-gl.css' rel='stylesheet' />
```

Deck が使う canvas 要素はプロパティで指定することもできますが，省略すると自動生成されます。自動生成される親要素は省略すると `document.body` になります。
画面全体にマップを表示するように CSS を設定します。

``` css
html, body {
  padding: 0;
  margin: 0;
  width: 100%;
  height: 100%;
}
```

DeckGL のインスタンスを生成します。

+ `map`
    - デフォルト window.mapboxgl
+ `mapStyle`
    - map のスタイル
+ `mapboxApiAccessToken`
    - Mapbox の API アクセストークン
+ `initialViewState`
    - 初期のビューステート
+ `views`
    - デフォルト MapView
+ `controller`
    - デフォルト null （無効）
    - true を指定すると，ビューのデフォルトコントローラー
    - デフォルトのビューが MapView なので，その場合は MapController

``` js
const deckgl = new deck.DeckGL({
  mapStyle: 'https://api.maptiler.com/maps/streets/style.json',
  initialViewState: {
    longitude: 137, // 経度
    latitude: 37,   // 緯度
    zoom: 5,        // ズーム
    minZoom: 2,     // 最小ズーム
    maxZoom: 8,     // 最大ズーム
    pitch: 30,      // 傾き
    bearing: 10,    // 回転
  },
  controller: true,
  layers: []
});
```

これで地図が表示されました！
<img src="https://user-images.githubusercontent.com/24204273/144743278-db3b3db5-67f7-454d-a2b4-61468754f267.png" width="480" alt="step1.png" />

DeckGL オブジェクトの `setProps` メソッドを使えば，動的に `viewState` や `layers` の値を変更することもできます。

``` js
deckgl.setProps({...});
```

## Step 2
次に，ベースマップではなく TileLayer を使って OpenStreetMap の地図タイルを表示してみましょう。こちらの方法であればアクセストークンは不要です。

+ OpenStreetMap
    - https://www.openstreetmap.org/about
    - [Open Data Commons Open Database License (ODbL)](https://opendatacommons.org/licenses/odbl/)

ライセンスの規約に則り，クレジット（出典）を表示するための HTML 要素を追加しましょう。

``` html
<div class="attribution">
  © <a href="http://www.openstreetmap.org/about/" target="_blank" rel="noopener noreferrer">OpenStreetMap</a> contributors
</div>
```

<details>
<summary>style</summary>
<div>

``` css
.attribution {
  position: absolute;
  bottom: 0;
  right: 0;
  z-index: 9;
  padding: 2px 4px;
  background-color: rgba(255, 255, 255, 0.5);
  font-size: 11px;
  pointer-events: auto;
}

.attribution a {
  color: rgba(0, 0, 0, 0.75);
  text-decoration: none;
}
```

</div>
</details>

地図タイルを表示するための [TileLayer](https://deck.gl/docs/api-reference/geo-layers/tile-layer) のインスタンスを生成します。

``` js
const tileLayer = new deck.TileLayer({
  data: "https://c.tile.openstreetmap.org/{z}/{x}/{y}.png",
  minZoom: 0,
  maxZoom: 19,
  tileSize: 256,
  renderSubLayers: props => new deck.BitmapLayer(props, {
    data: null,
    image: props.data,
    bounds: (({ west, south, east, north }) => [west, south, east, north])(props.tile.bbox),
  }),
});
```

Step2 では，拡張版ではないコアの [Deck](https://deck.gl/docs/api-reference/core/deck) オブジェクトを使用します。`layers` を指定してインスタンスを生成します。

``` js
const deckgl = new deck.Deck({
  initialViewState: {
    ... // Step1 と同じ
  },
  controller: true,
  layers: [
    tileLayer,
  ]
});
```

地図タイルが表示できましたね🗺 [^2]
<img src="https://user-images.githubusercontent.com/24204273/144743314-21ccd8ab-9130-4dc7-a3bf-af54714dbf23.png" width="480" alt="step2.png" />

[^2]: ただ，カメラの位置判定で，ズームアウトしたときに一般的な世界地図（日本が右端にあるやつ）の区切りあたりで途切れてしまうみたいですね... 

## Step 3
いよいよ地図上にデータを表示してみましょう。今回は ScatterplotLayer を使って，日本の都市の人口に比例したサイズの円を該当の座標位置にそれぞれ表示します。

まずは，適当なデータを用意します。

+ 人口は Wikipedia から
    - https://ja.wikipedia.org/wiki/日本の市の人口順位
+ 座標は Google Map で調べました...

``` js
const data = [
  {p: [141.3521727, 43.0620842], v: 1952356}, // sapporo
  {p: [140.8677736, 38.2686018], v: 1082159}, // sendai
  {p: [139.5196066, 35.4937089], v: 3724844}, // yokohama
  {p: [136.9043778, 35.1814347], v: 2295638}, // nagoya
  {p: [135.7696242, 35.0118413], v: 1475183}, // kyouto
  {p: [135.5023064, 34.6936616], v: 2691185}, // oosaka
  {p: [135.1959010, 34.6894819], v: 1537272}, // koube
  {p: [132.4556497, 34.3849809], v: 1194034}, // hiroshima
  {p: [130.4017854, 33.5902214], v: 1538681}, // fukuoka
];
```

この `data` を使って [ScatterplotLayer](https://deck.gl/docs/api-reference/layers/scatterplot-layer) のインスタンスを生成します。

+ `opacity`
    - 不透明度
+ `stroked`
    - アウトラインを描画するかどうか
    - デフォルト false
+ `filled`
    - 領域を塗りつぶすかどうか
    - デフォルト true
+ `getPosition`
    - データムから座標を取得する関数
+ `getRadius`
    - データムから半径を取得する関数
+ `getFillColor`
    - データムから塗りつぶしの色を取得する関数
    - ドキュメントでは `getColor` となっているが古いプロパティらしく，警告がでる  
    ```
    >  `ScatterplotLayer: getColor` is deprecated and will be removed in a later version. Use `getFillColor/getLineColor` instead  
    ```

``` js
const scatterplotLayer = new deck.ScatterplotLayer({
  data,
  opacity: 0.5,
  getPosition : ({p}) => p,
  getRadius   : ({v}) => Math.sqrt(v) * 12,
  getFillColor: ({v}) => [Math.sqrt(v) / 10, 20, 60],
});
```

Deck のインスタンスを生成します。先ほどの tileLayer に加えて scatterplotLayer も設定します。

``` js
const deckgl = new deck.Deck({
  initialViewState: {
    ... // Step1 と同じ
  },
  controller: true,
  layers: [
    tileLayer,
    scatterplotLayer,
  ]
});
```

データに合わせたサイズ・色の円が表示されました！
<img src="https://user-images.githubusercontent.com/24204273/144743461-4d4c7b5b-548d-4ee7-92f0-1e6eb56e54e4.png" width="480" alt="step2.png"/>

ということで，簡単にデータビジュアライゼーションができましたね😉

[サンプル](https://daisukesudo.github.io/deck-gl-sample/)

+ ドラッグ : 平行移動
+ CTRL + ドラッグ : で回転
+ スクロール : ズームイン・アウト


# おしまい
はじめはオープンデータを使ったデータビジュアライゼーションを構想していたのですが，多くの場合は何かしらの加工が必要で，なかなかそのまますぐに使えそうなデータを見つけることができなかったため，今回は断念しました。。もう少し調べて再チャレンジしたいです。

Deck.gl の [Examples](https://deck.gl/examples) では，GIS (Geographic Information System) 分野に留まらない幅広い魅力に触れることができますので，一度ご覧になってみてはいかがでしょうか。


# 参考
以下，大変参考にさせていただきました。

+ Uber社の全部盛りデータ可視化ツールスイート「Vis.gl」一覧  
  https://gunmagisgeek.com/blog/deck-gl/6435
+ deck.glでmapboxサービス以外のベースマップを使用する  
  https://gunmagisgeek.com/blog/deck-gl/6431
+ Mapbox GL JS ver.2はオープンソースではなくなってしまいました。  
  https://gunmagisgeek.com/blog/mapboxgl/7029