見出し画像

Reactアプリ上にDeck.gl×Google Mapでデータを可視化してみた


はじめに

今回はDeck.glを使ってGoogle Map上にデータを可視化しようということでやっていきたいと思います。
Deck.gl自体はReactとの親和性が高く作られているので、今回はReactを使っていきたいと思います。

今回やること

今回はdeck.glを使って、以下の2つのデータ可視化をしてみたいと思います。

  1. サンプルデータを用いたTrips Layerでの移動データの可視化

  2. 東京都の1kmメッシュ内の滞在人口をPolygon Layerで可視化

使用するBase Mapについては今回はGoogle Mapを利用していきたいと思います。

前提

以下については事前に準備できているものとして進めます。

  1. Reactプロジェクトの立ち上げ(今回はTypeScriptを利用)

  2. Google Maps APIの取得(こちらを参考に設定)

ライブラリのインストール

今回ReactでGoogle Mapを扱うにあたっては「@react-google-maps/api」のライブラリを利用します。

yarn add @react-google-maps/api

Deck.glのライブラリについては「deck.gl」から必要なものだけ追加しておきます。

yarn add @deck.gl/google-maps @deck.gl/core @deck.gl/geo-layers @deck.gl/layers

サンプルデータを用いたTrips Layerでの移動データの可視化

まずはこちらからやっていきます。
今回使うTrips Layerの詳細はこちらのページで確認できます。
可視化するデータについてはGoogleの公式サンプルでも使っているデータ(ニューヨーク市での車両移動データ)を利用します。

それでは実装していきます。
こちらを参考に、まずはGoogle Mapを表示するところまでやってみます(公式のドキュメントページがなくなっている?ので、READMEを見てやってます)。
事前に取得したAPIキーはこちらで使います。

import { GoogleMap, useJsApiLoader } from '@react-google-maps/api';

const containerStyle = {
  height: "100vh",
  width: "100%",
};

type Map = google.maps.Map;

const App = () => {
  const { isLoaded } = useJsApiLoader({
    id: "google-map",
    googleMapsApiKey: "取得したAPIキー",
  });
  
  const onLoad = (map: Map) => {
    // 地図の中心を設定する
    map.setCenter({
      lat: 40.7127,
      lng: -74.0059,
    });
    map.setZoom(14);
  };


  return (
    isLoaded ? (
      <GoogleMap
        mapContainerStyle={containerStyle}
        onLoad={onLoad}
      />
    ) : (<></>)
  );
}

export default App;

無事表示できました!
この後表示するデータはニューヨーク付近のものなので、地図の中心地はそのあたりになるようにあらかじめ設定しています。

では本題で、Deck.glを組み合わせて移動データを可視化していきます。
Google公式のやり方を参考にしながらやっていきます。

Google Map上にDeck.glを表示させるためには、まずはGoogleMapsOverlayインスタンスを作成(①)し、その中にDeck.glのLayerを設定する(②)ようです。
また、今回は時間ごとに移動データをアニメーションさせる(③)必要があるので、requestAnimationFrameを使っています。

上記の2つを組み合わせると以下のようになります。

// ①・・・GoogleMapsOverlayインスタンスの作成
const deckOverlay = new GoogleMapsOverlay({});

let currentTime = 0;

// ③・・・requestAnimationFrameに渡す関数
const animate = () => {
  currentTime = (currentTime + 1) % loopLength;

  const tripsLayer = new TripsLayer({
    id: "trips",
    data: DATA_URL,
    getPath: (d: Data) => d.path,
    getTimestamps: (d: Data) => d.timestamps,
    getColor: (d: Data) => VENDOR_COLORS[d.vendor],
    opacity: 1,
    widthMinPixels: 2,
    trailLength: 180,
    currentTime,
    shadowEnabled: false,
  });

  // ②・・・GoogleMapsOverlayにDeck.glのLayerを設定する
  deckOverlay.setProps({
    layers: [tripsLayer],
  });

  window.requestAnimationFrame(animate);
};
animate();

これでアニメーション付きのTripsが描画されるはずなので、動かしてみましょう。
以下コードの全体になります。

//App.tsx

import { GoogleMap, useJsApiLoader } from '@react-google-maps/api';
import { GoogleMapsOverlay } from '@deck.gl/google-maps/typed';
import { TripsLayer } from '@deck.gl/geo-layers/typed';

// データについてはサンプルデータを利用
const DATA_URL = 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/trips/trips-v7.json' // eslint-disable-line

const containerStyle = {
  height: "100vh",
  width: "100%",
};

// 読み込ませるデータのプロパティによって描画する際の色を変える
const VENDOR_COLORS: number[][] | Uint8Array = [
  [255, 0, 0, 255], // vendor #0
  [0, 0, 255, 255], // vendor #1
];

interface Data {
  vendor: number;
  path: [number, number][];
  timestamps: number[];
}

type Map = google.maps.Map;

const App = ({
  loopLength = 1800,
}) => {
  const { isLoaded } = useJsApiLoader({
    id: "google-map",
    googleMapsApiKey: "取得したAPIキー",
  });

  const deckOverlay = new GoogleMapsOverlay({});

  let currentTime = 0;
  const animate = () => {
    currentTime = (currentTime + 1) % loopLength;

    const tripsLayer = new TripsLayer({
      id: "trips",
      data: DATA_URL,
      getPath: (d: Data) => d.path,
      getTimestamps: (d: Data) => d.timestamps,
      getColor: (d: Data) => VENDOR_COLORS[d.vendor],
      opacity: 1,
      widthMinPixels: 2,
      trailLength: 180,
      currentTime,
      shadowEnabled: false,
    });

    deckOverlay.setProps({
      layers: [tripsLayer],
    });

    window.requestAnimationFrame(animate);
  };
  animate();
  
  const onLoad = (map: Map) => {
    map.setCenter({
      lat: 40.7127,
      lng: -74.0059,
    });
    map.setZoom(14);
    deckOverlay.setMap(map);
  };


  return (
    isLoaded ? (
      <GoogleMap
        mapContainerStyle={containerStyle}
        onLoad={onLoad}
      />
    ) : (<></>)
  );
}

export default App;

いいですね!無事表示されました。
ここでは画像になっていますが、実際はアニメーションで赤と青の線が動いている状態です。
こちらと同様の動きをしています)

これでDeck.glをでうまくGoogle Map上で表示させることができました。

東京都の1kmメッシュ内の滞在人口をPolygon Layerで可視化

せっかくなので違うパターンもやってみます。
こちらのPolygon Layerを使ってみたいと思います。

今回使うデータはG空間情報センターが提供している1kmメッシュ別の滞在人口データ(出典:「全国の人流オープンデータ」(国土交通省)(https://www.geospatial.jp/ckan/dataset/mlit-1km-fromto))です。

このデータは2019年から2021年までの日本全国の月別滞在人口が格納されており、集計の期間として平日、休日、全日の平休日の区分と、昼、深夜、終日の時間帯区分を持っています。
今回は東京都の2021年1月における全日、終日の滞在人口データを使って、対象データの滞在人口を地図上に可視化していきたいと思います。

今回はDeck.glのPolygon Layerを利用するので、まずはどのようなデータ形式で読み込ませることができるのか確認します。
こちらから見てみましょう

/**
   * Data format:
   * [
   *   {
   *     // Simple polygon (array of coords)
   *     contour: [[-122.4, 37.7], [-122.4, 37.8], [-122.5, 37.8], [-122.5, 37.7], [-122.4, 37.7]],
   *     zipcode: 94107,
   *     population: 26599,
   *     area: 6.11
   *   },
   *   {
   *     // Complex polygon with holes (array of rings)
   *     contour: [
   *       [[-122.4, 37.7], [-122.4, 37.8], [-122.5, 37.8], [-122.5, 37.7], [-122.4, 37.7]],
   *       [[-122.45, 37.73], [-122.47, 37.76], [-122.47, 37.71], [-122.45, 37.73]]
   *     ],
   *     zipcode: 94107,
   *     population: 26599,
   *     area: 6.11
   *   },
   *   ...
   * ]
   */

確認できました。
配列の中に描画したい各Polygonのデータを格納している形のようです。

次に、この形式に合わせて滞在人口データを加工していきます。
滞在データについては以下のようなデータ定義になっています。
(以下データカラム補足)
dayflag : 0(休日)、1(平日)、2(全日)
timezone : 0(昼)、1(深夜)、2(終日)
population : 滞在人口

monthly_mdp_mesh1km.csv

また、mesh1kmidと紐づく形でメッシュの緯度経度情報が以下の定義で提供されています。

attribute_mesh1km_2020.csv

欲しいデータは各メッシュのPolygonと滞在人口数となるので、「lon_max」「lat_max」「lon_min」「lat_min」からメッシュ(四角形のPolygonになる)の頂点座標を取得していきます。
例えば、idが53394519のメッシュについては以下のように4つの座標配列を作成します。

[[139.75,35.6749992],[139.75,35.6833344],[139.737503,35.6833344],[139.737503,35.6749992]]

ただここで注意なのですが、最終的にDeck.glのLayerに読み込ませる時には、一方向かつ、最初と最後の座標は同じにする必要があります。上記のデータは一方向ではありますが、最後の座標がないので追加します(ちゃんと図形を閉じましょうということですね)。

[[139.75,35.6749992],[139.75,35.6833344],[139.737503,35.6833344],[139.737503,35.6749992],[139.75,35.6749992]]

こちらでOKです。
本筋とは違うのでここではデータの加工処理の詳細は割愛しますが、以下のようなデータが完成しました。今回はこちらのJSON形式のデータを読み込ませて地図上に描画していきます。

// tokyo.json

[
    {
        "mesh1kmid":53394519,
        "population":15200,
        "polygon":[
            [139.75,35.6749992],
            [139.75,35.6833344],
            [139.737503,35.6833344],
            [139.737503,35.6749992],
            [139.75,35.6749992]
        ]
    },
    {
        "mesh1kmid":53394528,
        "population":31477,
        "polygon":[
            [139.737503,35.6833344],
            [139.737503,35.6916656],
            [139.725006,35.6916656],
            [139.725006,35.6833344],
            [139.737503,35.6833344]
        ]
    }
    ...
]

それでは実装していきます。

前回同様にまずはGoogle Mapを表示させましょう。
今回は東京のデータになるので、中心点は東京付近に設定しておきます。

import { GoogleMap, useJsApiLoader } from '@react-google-maps/api';

const containerStyle = {
  height: "100vh",
  width: "100%",
};

type Map = google.maps.Map;

const App = () => {
  const { isLoaded } = useJsApiLoader({
    id: "google-map",
    googleMapsApiKey: "取得したAPIキー",
  });

  const onLoad = (map: Map) => {
    map.setCenter({
      lat: 35.681236,
      lng: 139.767125,
    });
    map.setZoom(10);
  };


  return (
    isLoaded ? (
      <GoogleMap
        mapContainerStyle={containerStyle}
        onLoad={onLoad}
      />
    ) : (<></>)
  );
}

export default App;

無事表示できました!

では本題で、Deck.glを組み合わせて滞在量を可視化していきます。
見やすいように滞在人口が多いものほど高さを出す&オレンジ色っぽく描画していきたいと思います。

Tripsの例ではアニメーションを使う関係で後からGoogleMapsOverlayにDeck.glのLayer情報を入れていたのですが、今回はそれがないのでインスタンス作成時に設定しています。
getElevationプロパティでPolygonの高さを設定できるので、滞在量をそのまま設定します(getElevationを利用するときはextrudedがtrueである必要があるので気をつけましょう)。

import { GoogleMapsOverlay } from '@deck.gl/google-maps/typed';
import { PolygonLayer } from '@deck.gl/layers/typed';

import populationData from "./data/tokyo.json"

const deckOverlay = new GoogleMapsOverlay({
  layers: [
    new PolygonLayer({
      id: 'polygon-layer',
      data: populationData,
      pickable: true,
      stroked: true,
      filled: true,
      wireframe: true,
      lineWidthMinPixels: 1,
      getPolygon: d => d.polygon,
      getElevation: d => d.population, // 高さを出す
      getFillColor: d => [d.population / 60, 140, 0, 200],
      extruded: true,
    }),
  ]
});

そしてGoogle Mapがロードされた時にGoogleMapsOverlayのsetMap関数にGoogle Mapのインスタンスを渡して描画させます。
onLoad関数の中に、deckOverlay.setMap(map);を追加して描画処理を行います。

  const onLoad = (map: Map) => {
    map.setCenter({
      lat: 35.681236,
      lng: 139.767125,
    });
    map.setZoom(10);
    // 高さが見やすいように視点が斜めからになるように設定を追加
    map.setTilt(45);
    deckOverlay.setMap(map);
  };

以下が全体コードになります。

//App.tsx

import { GoogleMap, useJsApiLoader } from '@react-google-maps/api';
import { GoogleMapsOverlay } from '@deck.gl/google-maps/typed';
import { PolygonLayer } from '@deck.gl/layers/typed';

import populationData from "./data/tokyo.json"


const containerStyle = {
  height: "100vh",
  width: "100%",
};

type Map = google.maps.Map;

const App = () => {
  const { isLoaded } = useJsApiLoader({
    id: "google-map",
    googleMapsApiKey: "取得したAPIキー",
  });

  const deckOverlay = new GoogleMapsOverlay({
    layers: [
      new PolygonLayer({
        id: 'polygon-layer',
        data: populationData,
        pickable: true,
        stroked: true,
        filled: true,
        wireframe: true,
        lineWidthMinPixels: 1,
        getPolygon: d => d.polygon,
        getElevation: d => d.population,
        getFillColor: d => [d.population / 60, 140, 0, 200],
        extruded: true,
      }),
    ]
  });

  const onLoad = (map: Map) => {
    map.setCenter({
      lat: 35.681236,
      lng: 139.767125,
    });
    map.setZoom(10);
    map.setTilt(45);
    deckOverlay.setMap(map);
  };


  return (
    isLoaded ? (
      <GoogleMap
        mapContainerStyle={containerStyle}
        onLoad={onLoad}
      />
    ) : (<></>)
  );
}

export default App;

それでは実行していきます。

描画自体はされましたが、高さが出てないですね、、視点も真上からになっています。

結構ハマったんですが、Googleの公式ドキュメントを見ているとこんなページを見つけました。

WebGL オーバーレイ表示を使用するには、ベクターマップを有効にした状態で、マップ ID を使用して地図を読み込む必要があります。

https://developers.google.com/maps/documentation/javascript/webgl/webgl-overlay-view?hl=ja

はい、やってないですね、、
公式を参考に設定していきます。

作成できるとマップIDが払い出されるので控えておいてください。
ここで地図のスタイルも変更できるようなので、せっかくなので黒っぽい色に変えてみます。

ダークを選択して保存。
名前は今回は「dark」としました。

すると先ほど作成したマップIDのページから地図のスタイルを選択できるようになっているので「dark」を選択して保存します。

これでマップIDの設定は完了です。
ソースコードの反映していきます。

やり方は簡単で、GoogleMapコンポーネントのoptionプロパティに設定するだけです。
全体のコードも合わせて記載しておきます。

// Polygonレイヤー
import { GoogleMap, useJsApiLoader } from '@react-google-maps/api';
import { GoogleMapsOverlay } from '@deck.gl/google-maps/typed';
import { PolygonLayer } from '@deck.gl/layers/typed';

import populationData from "./data/tokyo.json"


const containerStyle = {
  height: "100vh",
  width: "100%",
};

type Map = google.maps.Map;

const App = () => {
  const { isLoaded } = useJsApiLoader({
    id: "google-map",
    googleMapsApiKey: "取得したAPIキー",
  });

  const deckOverlay = new GoogleMapsOverlay({
    layers: [
      new PolygonLayer({
        id: 'polygon-layer',
        data: populationData,
        pickable: true,
        stroked: true,
        filled: true,
        wireframe: true,
        lineWidthMinPixels: 1,
        getPolygon: d => d.polygon,
        getElevation: d => d.population, // 高さを出す
        getFillColor: d => [d.population / 60, 140, 0, 200],
        extruded: true,
      }),
    ]
  });

  const onLoad = (map: Map) => {
    map.setCenter({
      lat: 35.681236,
      lng: 139.767125,
    });
    map.setZoom(10);
    map.setTilt(45);
    deckOverlay.setMap(map);
  };


  return (
    isLoaded ? (
      <GoogleMap
        options={{mapId: "取得したマップID"}}
        mapContainerStyle={containerStyle}
        onLoad={onLoad}
      />
    ) : (<></>)
  );
}

export default App;

実行してみます。

いいですね!期待通りのものを表示することができました!

おわりに

今回はDeck.glを使ってTrips LayerとPolygon Layerでデータの可視化をしてみましたが、データの羅列が実際に地図上に表示するだけですごく価値のあるものに変わったんじゃないかなと思います!
Deck.glは他にも様々なLayerを表示することができるので、その時々によって最適なものを利用できたらと思います。

また、React × Google MapやReact × Deck.glの記事はたくさんあったのですが、React × Google Map × Deck.glの情報が意外と少なかったので、この3つを組み合わせたものを作りたいと思っている方は参考にしていただけたらと思います。


この記事が気に入ったらサポートをしてみませんか?