Skip to main content
Tutorials

JavaScript ヒートマップ実装チュートリアル:インタラクティブな密度マップを作る

JavaScript ヒートマップ チュートリアル:MapAtlas でインタラクティブな密度マップを構築します。データ整形、強度の重み付け、カラーグラデーション、歩行者トラフィックや配送需要など実例まで解説します。

Brent van der Heiden23 min read
#javascript heatmap#heatmap javascript tutorial#interactive heatmap web#data visualization#maps api#geospatial#javascript#density map
JavaScript ヒートマップ実装チュートリアル:インタラクティブな密度マップを作る

ヒートマップは、大量の座標ポイントを一目で読み取れる密度の面に変えます。暖色はイベントが集中している場所を、寒色はまばらな場所を示します。ユーザーはラベルを1つも読まないうちにパターンを把握できます。そのためヒートマップは、人流分析、配送需要の予測、犯罪発生レポートのツール、不動産価格の調査で定番の選択肢になっています。

このチュートリアルでは、MapAtlas SDKを使ってインタラクティブなヒートマップをゼロから構築します。ポイントデータを重み付きのGeoJSONとして構造化し、カスタムのカラーグラデーションで密度レイヤーを描画し、強度を調整するスライダーを接続し、最後に実際の配送需要の例で仕上げます。終わる頃には、どのJavaScriptやReactのプロジェクトにも組み込める再利用可能なパターンが手に入ります。

MapAtlas SDKを初めて使う場合は、先にウェブサイトにインタラクティブなマップを追加する方法をお読みください。インストール、マップの初期化、マーカーについて説明しています。このチュートリアルは、その続きから始まります。

ヒートマップを使うべき場面

ヒートマップが適しているのは、個々のポイントの位置データが大量にあり、それぞれが何かではなく、どこに集中しているかを伝えたい場合です。個別のマーカーは数百ポイントを超えると意味をなさなくなります。ヒートマップなら、ピンをいくら立てても見えてこない構造が浮かび上がります。

主なユースケースは次のとおりです。

  • 人流分析: 小売店の出店地選び、都市計画、イベントの混雑予測に使います。入口を通過した顧客ごとにGPSのpingを置くと、都市、ショッピングセンター、会場のどのエリアに最も人が集まっているかをヒートマップが示します。
  • 配送需要: 注文の発生地点を郵便番号や生の座標で集計し、需要が集中している場所を配車担当者に示します。これはそのままゾーン計画とドライバーの割り当てに活用できます。
  • 犯罪・インシデントデータ: 警察の分析ダッシュボード、保険のリスクマップ、公共安全のレポートツールはいずれも密度ヒートマップを使います。個別のマーカーでユーザーを圧倒せずに、空間的なリスクを伝えられるからです。
  • 不動産価格の分布: 重み付けした値と組み合わせると、都市のどこで価格が最も高いかをヒートマップで示せます。不動産向けのマップツールの構築については、不動産物件マップのチュートリアルをご覧ください。

「どこで最も多く起きているか」を知りたいなら、ヒートマップを使ってください。

データ形式: 重み付きのGeoJSONポイント

MapAtlasのヒートマップレイヤーは、Pointフィーチャーで構成された標準的なGeoJSONのFeatureCollectionを読み込みます。各フィーチャーには、密度の面への寄与度を調整するweightプロパティを持たせられます。10品目の配送注文は1品目の注文より多くの熱を生み、主要な乗り換え駅は裏通りより多くの人流を生みます。

const demandData = {
  type: "FeatureCollection",
  features: [
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [4.8952, 52.3702] },
      properties: { weight: 8, zone: "centrum" }
    },
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [4.9123, 52.3601] },
      properties: { weight: 3, zone: "oost" }
    },
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [4.8801, 52.3780] },
      properties: { weight: 12, zone: "west" }
    },
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [4.9041, 52.3540] },
      properties: { weight: 1, zone: "south" }
    }
  ]
};

weightの値には単位がありません。データセットの値の範囲に合わせて正規化してください。需要が最も多いゾーンで500件、最も少ないゾーンで10件の注文がある場合は、渡す前にそれらを1から10のスケールに変換します。こうすることで、絶対数が大きく異なるデータセットでも、ヒートマップの見た目に意味を持たせられます。

weightプロパティがない場合、すべてのポイントが同じだけ寄与し、ヒートマップは純粋な件数の密度を表します。

前提条件

始める前に、次のものを用意してください。

  • MapAtlasのAPIキー(無料で登録、クレジットカード不要)
  • npmベースのプロジェクトならNode.js 18以上、CDNを使う場合は通常のHTMLページ

ステップ1: SDKをインストールしてマップを初期化する

SDKをインストールします。

npm install @mapmetrics/mapmetrics-gl

または、通常のHTMLファイルでCDNから読み込みます。

<link
  rel="stylesheet"
  href="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css"
/>
<script src="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.js"></script>

高さを指定したコンテナを追加します。

<div id="map" style="width: 100%; height: 600px;"></div>

アカウントにあるダークスタイルでマップを初期化します。ダークなベースでは背景に対してヒートマップのカラーグラデーションが際立つため、密度の可視化にはこのベースがおすすめです。

import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';

const map = new mapmetricsgl.Map({
  container: 'map',
  style: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_DARK_STYLE_ID/style.json&token=YOUR_API_KEY',
  center: [4.9041, 52.3676], // アムステルダム
  zoom: 12,
});

ステップ2: ヒートマップレイヤーを追加する

GeoJSONをソースとして登録し、そのソースを読み込むヒートマップレイヤーを追加します。map.on('load', ...)コールバックを使うと、スタイルの読み込みが完了してから変更を加えられます。

map.on('load', () => {
  map.addSource('demand', {
    type: 'geojson',
    data: demandData,
  });

  map.addLayer({
    id: 'demand-heatmap',
    type: 'heatmap',
    source: 'demand',
    paint: {
      // 各ポイントを 'weight' プロパティで重み付けする(ない場合は 1)
      'heatmap-weight': [
        'interpolate', ['linear'],
        ['get', 'weight'],
        0, 0,
        12, 1
      ],
      // 半径(ピクセル)。大きいほど滑らかになるが精度は下がる
      'heatmap-radius': 30,
      // ヒートマップレイヤー全体の不透明度
      'heatmap-opacity': 0.85,
    },
  });
});

これで動作するヒートマップができました。青緑のエリアは密度が低く、デフォルトのグラデーションでは密度が高くなるにつれて黄色や赤に変わります。次のステップでは、デフォルトのパレットを意図した配色に置き換えます。

ステップ3: カラーグラデーションをカスタマイズする

heatmap-colorプロパティは、密度の値(0から1)を色に対応させます。使うのは、MapAtlasのペイントシステム全体で使われているのと同じinterpolate式です。密度0は透明にしておくと、何もないエリアではベースマップが透けて見えます。

map.addLayer({
  id: 'demand-heatmap',
  type: 'heatmap',
  source: 'demand',
  paint: {
    'heatmap-weight': [
      'interpolate', ['linear'],
      ['get', 'weight'],
      0, 0,
      12, 1
    ],
    'heatmap-radius': 30,
    'heatmap-opacity': 0.85,
    'heatmap-color': [
      'interpolate', ['linear'],
      ['heatmap-density'],
      0,    'rgba(0, 0, 255, 0)',   // 密度ゼロでは透明
      0.2,  'rgba(0, 128, 255, 0.6)',
      0.4,  'rgba(0, 230, 200, 0.7)',
      0.6,  'rgba(100, 230, 0, 0.8)',
      0.8,  'rgba(255, 200, 0, 0.9)',
      1.0,  'rgba(255, 50, 0, 1)'   // 密度のピークでは鮮やかな赤
    ],
  },
});

このグラデーションは、青(まばら)から緑と黄色を経て赤(密集)へと変化します。多くのユーザーが気象レーダーの地図やサーモグラフィーで慣れ親しんでいるイメージと一致します。アプリにブランドのカラーパレットがある場合は、RGBの値を独自の色の区切りに置き換えてください。滑らかな遷移は補間が自動で処理します。

デザインのヒント: プロ向けのダッシュボードや分析ツールでは、密度の低い側をほぼ透明にしておきましょう。静かなエリアではベースマップの読みやすさが保たれ、視線が自然に密度の高いゾーンへ向かいます。

ステップ4: 半径スライダーでインタラクティブに操作する

ヒートマップの半径は、各ポイントの影響がどこまで広がるかを決めます。小さい半径(10から15px)では細かなクラスターが見え、大きい半径(50から80px)ではより広く滑らかな面になります。ユースケースによって適切なデフォルト値は異なります。密集した都心部の人流には小さい半径が、国全体の配送需要には大きい半径が必要です。

ユーザーがその場で調整できるよう、スライダーを用意します。

<div id="controls" style="position: absolute; top: 16px; left: 16px; z-index: 1;
     background: white; padding: 12px 16px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.2);">
  <label style="font-size: 14px; font-weight: 600;">
    Radius: <span id="radius-value">30</span>px
  </label>
  <br />
  <input id="radius-slider" type="range" min="5" max="80" value="30" style="width: 180px; margin-top: 6px;" />
</div>

スライダーをsetPaintPropertyに接続します。これにより、レイヤーを追加し直さずに更新できます。

const slider = document.getElementById('radius-slider');
const radiusLabel = document.getElementById('radius-value');

slider.addEventListener('input', () => {
  const radius = Number(slider.value);
  radiusLabel.textContent = radius;
  map.setPaintProperty('demand-heatmap', 'heatmap-radius', radius);
});

setPaintPropertyはちらつきのないライブ更新です。ヒートマップは同じフレーム内でGPU上で再描画されます。このパターンは、不透明度、強度、色の区切りなど、あらゆるペイントプロパティに使えます。

ステップ5: ズームレベルに応じて強度を調整する

ズームレベルが低い(都市全体の表示)と、近くのポイントが大きく重なり、ヒートマップ全体が一様に飽和して見えることがあります。ズームレベルが高い(街路レベル)と、同じポイントが広がり、密度の面がまばらに見えます。ズームに連動して強度を補正すると、どのズームレベルでも読みやすい可視化を保てます。

paint: {
  // ...その他のペイントプロパティ...
  'heatmap-intensity': [
    'interpolate', ['linear'],
    ['zoom'],
    8,  1,   // 低ズーム: 通常の強度
    14, 3    // 高ズーム: ポイントの広がりを補うため強度を上げる
  ],
  'heatmap-radius': [
    'interpolate', ['linear'],
    ['zoom'],
    8,  20,  // 都市スケールでは小さい半径
    14, 50   // 街路スケールでは大きい半径
  ],
},

どちらのプロパティも、zoomに対する同じinterpolate式を使っています。区切りの間の値は線形に補間されるため、ユーザーがズームしても滑らかに変化します。

実例: 配送需要のヒートマップ

配送需要のダッシュボード向けに、単体で動く完全な実装を示します。注文データはAPIからGeoJSONのFeatureCollectionとして届きます。ユーザーが時間帯フィルターを変更するたびに、ヒートマップが更新されます。

import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';

const API_KEY = 'YOUR_API_KEY';

const map = new mapmetricsgl.Map({
  container: 'map',
  style: `https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_DARK_STYLE_ID/style.json&token=${API_KEY}`,
  center: [4.9041, 52.3676],
  zoom: 11,
});

async function loadOrders(hourFrom, hourTo) {
  const res = await fetch(
    `/api/orders/geojson?hour_from=${hourFrom}&hour_to=${hourTo}`
  );
  return res.json(); // GeoJSON の FeatureCollection を返す
}

map.on('load', async () => {
  const orders = await loadOrders(8, 12); // 朝のピーク

  map.addSource('orders', { type: 'geojson', data: orders });

  map.addLayer({
    id: 'order-heatmap',
    type: 'heatmap',
    source: 'orders',
    paint: {
      'heatmap-weight': [
        'interpolate', ['linear'], ['get', 'items'],
        0, 0, 20, 1
      ],
      'heatmap-color': [
        'interpolate', ['linear'], ['heatmap-density'],
        0,   'rgba(0, 0, 255, 0)',
        0.2, 'rgba(0, 128, 255, 0.5)',
        0.5, 'rgba(0, 230, 150, 0.8)',
        0.8, 'rgba(255, 200, 0, 0.9)',
        1.0, 'rgba(255, 50, 0, 1)'
      ],
      'heatmap-radius': [
        'interpolate', ['linear'], ['zoom'],
        8, 20, 14, 50
      ],
      'heatmap-intensity': [
        'interpolate', ['linear'], ['zoom'],
        8, 1, 14, 3
      ],
      'heatmap-opacity': 0.9,
    },
  });

  // 時間帯フィルターのボタン
  document.querySelectorAll('[data-hour-range]').forEach((btn) => {
    btn.addEventListener('click', async () => {
      const [from, to] = btn.dataset.hourRange.split('-').map(Number);
      const newOrders = await loadOrders(from, to);
      map.getSource('orders').setData(newOrders);
    });
  });
});

既存のソースに対してsetDataを呼ぶと、レイヤーを追加し直さずにGeoJSONを置き換えられます。ヒートマップは自動で再描画されます。このパターンは、時間帯、曜日、天候など、時間に基づくあらゆるフィルターに応用できます。

配送需要のヒートマップと組み合わせることの多いルート計画レイヤーについては、Route Optimization APIのチュートリアルをご覧ください。需要のヒートマップの上に最適化された配送ルートを重ねると、配車担当者は1つの画面で業務の全体像を把握できます。

大規模データセットでのパフォーマンスのコツ

MapAtlasのヒートマップレイヤーはWebGLを使って高速に描画しますが、規模が大きくなると、そこへデータを送るパイプラインがボトルネックになることがあります。

非常に大きなデータセットはサーバー側で事前集計する。 数百万件のGPSのpingがある場合、すべてをブラウザに送らないでください。サーバー側で空間集計(H3の六角形グリッド、四分木、または単純なグリッドへの丸め)を実行し、100万件の生のポイントをcountとweightフィールドを持つ1万個のグリッドセルに減らします。一般的なズームレベルではヒートマップの見た目はほぼ同じで、読み込み時間はごく一部で済みます。

更新は差分でストリーミングする。 ライブデータ(リアルタイムの人流や注文の発生)では、増え続けるGeoJSONオブジェクトに蓄積するのではなく、直近のポイントのローリングウィンドウでsetDataを使ってください。ソースのポイント数に上限を設け、古いレコードを削除します。

ソースにmaxzoomを設定する。 addSourceの呼び出しにmaxzoom: 14を追加すると、SDKはズーム14を超えるとタイルデータを要求しなくなります。ヒートマップレイヤーはタイル化されたデータではなく1つのフラットなGeoJSONソースを読むため、ほとんど影響はありませんが、高ズームでの不要な再処理を防げます。

ペイント式はシンプルに保つ。 heatmap-colorのグラデーションは、設定した時点で一度だけカラーランプのテクスチャに変換されるため、色の区切りを増やしてもフレームあたりのコストはほとんど増えません。モバイルでは、大きなheatmap-radiusと多いポイント数のほうがコストが大きいので、まずこの2つを調整してください。

マップを遅延初期化する。 マップの初期化全体をIntersectionObserverのコールバックで包み、マップのコンテナが画面に入ったときだけ実行されるようにします。これにより、SDKのバンドルの読み込みを最初のページ読み込みから後ろに回せます。マップがファーストビューより下にあるマーケティングページで特に効果があります。

遅延読み込みやクラスタリングを含むマップのパフォーマンスのパターンをさらに詳しく知りたい場合は、ウェブサイトにインタラクティブなマップを追加する方法の本番環境向けチェックリストに一覧があります。

Reactとの統合

ヒートマップをReactコンポーネントで包む方法は、他のMapAtlasのマップと同じパターンです。useEffectで初期化し、スライダーやフィルターの状態をuseStateで持ち、アンマウント時にクリーンアップします。

import { useEffect, useRef, useState } from 'react';
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';

export function DeliveryHeatmap({ geojson, apiKey }) {
  const containerRef = useRef(null);
  const mapRef       = useRef(null);
  const [radius, setRadius] = useState(30);

  useEffect(() => {
    const map = new mapmetricsgl.Map({
      container: containerRef.current,
      style: `https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_DARK_STYLE_ID/style.json&token=${apiKey}`,
      center: [4.9041, 52.3676],
      zoom: 11,
    });
    mapRef.current = map;

    map.on('load', () => {
      map.addSource('orders', { type: 'geojson', data: geojson });
      map.addLayer({
        id: 'order-heatmap',
        type: 'heatmap',
        source: 'orders',
        paint: {
          'heatmap-radius': radius,
          'heatmap-opacity': 0.9,
        },
      });
    });

    return () => map.remove();
  }, [apiKey]);

  // マップを再マウントせずに半径を更新する
  useEffect(() => {
    const map = mapRef.current;
    if (!map || !map.getLayer('order-heatmap')) return;
    map.setPaintProperty('order-heatmap', 'heatmap-radius', radius);
  }, [radius]);

  return (
    <div>
      <div style={{ padding: '8px 0' }}>
        <label>
          Radius: {radius}px
          <input
            type="range" min={5} max={80} value={radius}
            onChange={e => setRadius(Number(e.target.value))}
            style={{ marginLeft: 8, width: 160 }}
          />
        </label>
      </div>
      <div ref={containerRef} style={{ width: '100%', height: '600px' }} />
    </div>
  );
}

Next.jsでは、ブラウザ専用のSDKによるサーバーサイドレンダリングのエラーを避けるため、このコンポーネントをdynamic(() => import('./DeliveryHeatmap'), { ssr: false })でインポートしてください。

次に作るもの

これで、重み付きデータ、カスタムのカラーグラデーション、半径スライダー、ズーム連動の強度を備えたインタラクティブなヒートマップが動くようになりました。次の発展の方向は次のとおりです。

  • ヒートマップの上にルーティングレイヤーを重ね、需要の面に対して計画した配送ルートを表示します。Route Optimization APIのチュートリアルで実装の全体を説明しています。
  • 時間のアニメーションを追加します。setIntervalのループでソースのsetDataを呼び、1時間ごとのスナップショットを順に表示します。静的な密度マップが、1日の中で需要がどう動くかを示すタイムラプスに変わります。
  • 不動産価格のデータと組み合わせ、地区ごとの価格の分布を示します。不動産物件マップのチュートリアルでは、不動産プラットフォーム向けの重み付きデータのパターンを扱っています。
  • MapAtlasの料金ページで、本番環境のトラフィックに合ったプランを確認してください。

SDKの完全なリファレンスと追加のサンプルはdocs.mapatlas.xyzで公開しています。

よくある質問

ヒートマップとコロプレスマップの違いは何ですか?

ヒートマップは、近くのポイントを連続したカラーグラデーションに溶け込ませてポイントの密度を可視化します。GPSのpingやイベントの発生地点など、生の座標データに適しています。コロプレスマップは、国、郵便番号区域、国勢調査の区画など、あらかじめ決められた地理的エリアを統計値で塗り分けます。個別のポイントが大量にある場合はヒートマップを、データがすでに地域ごとに集計されている場合はコロプレスマップを使ってください。

JavaScriptのヒートマップはどのくらいのデータポイントを扱えますか?

MapAtlasのヒートマップレイヤーはWebGLを通じてGPU上で描画するため、一般的なハードウェアなら数万ポイントを滑らかに扱えます。数十万ポイント以上の、さらに大きなデータセットでは、サーバー側でデータを解像度の低いグリッドに事前集計し、GeoJSONのfill-extrusionレイヤーやcircleレイヤーに切り替えると、ローエンドのデバイスでのパフォーマンスが向上します。

MapAtlasのヒートマップは無料で使えますか?

はい。MapAtlasには、マップタイルの描画、GeoJSONレイヤーのサポート、ヒートマップレイヤーを含む無料プランがあります。無料プランは、開発と少量の本番利用に対応しています。プランの詳細はmapatlas.eu/pricingをご覧ください。

ヒートマップはモバイルブラウザで動作しますか?

はい。MapAtlas SDKは描画にWebGLを使っており、iOSのSafariやAndroidのChromeを含むすべての最新のモバイルブラウザでサポートされています。ローエンドのモバイル端末で非常に大きなデータセットを扱う場合は、ポイント数を減らすか、ヒートマップの半径を大きくすると、フレームレートを滑らかに保てます。


関連記事:

よくある質問

ヒートマップとコロプレスマップの違いは何ですか?

ヒートマップは、近くのポイントを連続したカラーグラデーションに溶け込ませてポイントの密度を可視化します。GPSのpingやイベントの発生地点など、生の座標データに適しています。コロプレスマップは、国、郵便番号区域、国勢調査の区画など、あらかじめ決められた地理的エリアを統計値で塗り分けます。個別のポイントが大量にある場合はヒートマップを、データがすでに地域ごとに集計されている場合はコロプレスマップを使ってください。

JavaScriptのヒートマップはどのくらいのデータポイントを扱えますか?

MapAtlasのヒートマップレイヤーはWebGLを通じてGPU上で描画するため、一般的なハードウェアなら数万ポイントを滑らかに扱えます。数十万ポイント以上の、さらに大きなデータセットでは、サーバー側でデータを解像度の低いグリッドに事前集計し、GeoJSONのfill-extrusionレイヤーやcircleレイヤーに切り替えると、ローエンドのデバイスでのパフォーマンスが向上します。

MapAtlasのヒートマップは無料で使えますか?

はい。MapAtlasには、マップタイルの描画、GeoJSONレイヤーのサポート、ヒートマップレイヤーを含む無料プランがあります。無料プランは、開発と少量の本番利用に対応しています。プランの詳細はmapatlas.eu/pricingをご覧ください。

ヒートマップはモバイルブラウザで動作しますか?

はい。MapAtlas SDKは描画にWebGLを使っており、iOSのSafariやAndroidのChromeを含むすべての最新のモバイルブラウザでサポートされています。ローエンドのモバイル端末で非常に大きなデータセットを扱う場合は、ポイント数を減らすか、ヒートマップの半径を大きくすると(より多くのポイントが視覚的にまとまります)、フレームレートを滑らかに保てます。

役に立ちましたか?シェアしてください。

著者について

Brent van der Heiden

著者

Brent van der Heiden

Co-Founder & CEO at MapAtlas

Brent built MapAtlas out of a conviction that developers deserve location APIs with fair pricing and genuine end-user privacy. Before that he co-founded MapMetrics, a community mapping and navigation app. He writes about geospatial infrastructure, map data, AI search visibility, and how location data powers the products people rely on every day.

すべての記事を見る →
ブログに戻る