👋

D3.jsでクリッカブルマップを作成する

2021/11/17に公開

経緯

キャップ野球情報局というサービスを開発しています。
https://cap-baseball.com

全国にあるキャップ野球チームを網羅しているのですが、地域ごとに目次があった方が便利だなーと常々思っていました。クリックできる地図があるともっと便利だと。
調べてみるとD3.jsを使うとクリッカブルマップが作れそう...

D3.jsそういえば触ったことあったなぁ...
https://ckoshien.hatenablog.jp/entry/2020/01/26/232305

D3.jsの導入

前に使っていたとはいえ、Nextjsに移行したときに廃止した機能で、package.jsonからも消えていたので新たに入れ直しました。

yarn add d3
yarn add @types/d3 -D

注意した方がいいこと
最新のv7をNext.jsにインポートするにはダイナミックインポートを使う必要があります。

dynamic(()=>import('d3'))

私はv6を使うことにしました。 "d3": "^6.3.1"

実装

昔のコードはreactのclass構文のものだったので多少手を加える必要がありました。基本的にはuseEffectuseRefを使えばいいかと思います。
https://stackoverflow.com/a/65642208

json地図データの入手・加工は割愛します。

item.properties.idは都道府県コード(prefCode)が入っており、returnTeamsは地域別のチーム一覧を返しています。returnPrefは都道府県コードから都道府県名を返すようにしています。fill-opacityでチーム数に応じて色の濃淡が出るようにopacityの調整を行っています。

const mapRef = useRef(null);
useEffect(() => {
    var w = 320;
    var h = 350;
    // 地図の投影図法を設定する.
    var projection = d3
      .geoMercator()
      //中心座標
      .center([136.5, 37.5])
      .scale(1300)
      .translate([w / 2, h / 2]);

    // GeoJSONからpath要素を作る.
    var path = d3.geoPath().projection(projection);

    const jpn = mapJson;
    const svg = d3.select(mapRef.current);
    const selection = svg
      .attr("height", h)
      .attr("width", w)
      .selectAll("path")
      .data(jpn.features)
      .enter()
      .append("path")
      .attr("class", (d) =>
        d.properties.region
          ? `region ${d.properties.region.toLowerCase()}`
          : "region"
      )
      .attr("class", function (d) {
        return "pref ";
      })
      .attr("d", path)
      .attr(`stroke`, `#666`)
      .attr(`stroke-width`, 0.25)
      .style(`fill`, `#FF0000`)
      .style(`fill-opacity`, (item) => {
        return returnTeams([item.properties.id]).length / 20;
      })
      .on(`mouseover`, (ev, d) => {
        // マウス位置の都道府県領域を赤色に変更
        d3.select(ev.currentTarget).attr(`fill`, `#CC4C39`);
        d3.select(ev.currentTarget).attr(`stroke-width`, `1`);
      })
      .on(`mouseout`, (ev, d) => {
        d3.select(ev.currentTarget).attr(`fill`, `#FF0000`);
        d3.select(ev.currentTarget).attr(`stroke-width`, `0.25`);
      })
      .on(`click`, (ev, d) => {
        // マウス位置の都道府県領域を赤色に変更
        d3.select(ev.currentTarget).attr(`fill`, `#CC4C39`);
        d3.select(ev.currentTarget).attr(`stroke-width`, `1`);
        // スクロール用要素取得
        const rect = document.getElementById(returnPref(Number(d.properties.id))).getBoundingClientRect();
        scrollTo(0, rect.y)
      });
  }, [mapRef]);
  return (
    <div>
      <svg ref={mapRef} />
    </div>
  );

参考

https://qiita.com/cieloazul310/items/e604535e27fa4db539ba
https://qiita.com/alclimb/items/31d4360c74a8f8935256
https://qiita.com/chai222222/items/112199e08d3634d3c81b

Discussion