2023/09/04
React + D3.jsで日本地図の描画をする
こちらの記事で紹介されている日本地図をReactコンポーネント化してみた際の記録です
D3.jsインストール
pnpm install d3
日本地図コンポーネント
components/japan-map.tsx
"use client"; // Next.js App Routerを使う場合はここ追加
import React, { useEffect, memo } from "react";
import * as d3 from "d3";
import geoJson from "@/lib/japan.json";
import { useMounted } from "@/hooks";
type List = {
name: string;
count: number;
};
const getTarget = ({
prefName,
list,
}: {
prefName: string;
list: List[];
}): List | null => {
const pref = prefName.toLowerCase();
let target: List | null = null;
list.map((e: any) => {
if (e.name === pref) target = e;
});
return target;
};
const JapanMap = ({ list }: { list: List[] }) => {
const mounted = useMounted();
async function main() {
const width = 500; // 描画サイズ: 幅
const height = 500; // 描画サイズ: 高さ
const centerPos = [137.0, 38.2]; // 地図のセンター位置
const scale = 1000; // 地図のスケール
const color = "#2566CC"; // 地図の色
const colorActive = "#ebfd2a" // ホバーした時の色
// 地図設定
const projection = d3
.geoMercator()
.center(centerPos)
.translate([width / 2, height / 2])
.scale(scale);
// 地図をpathに投影(変換)
const path = d3.geoPath().projection(projection);
// SVG要素を追加
const svg = d3
.select(`#map-container`)
.append(`svg`)
.attr(`viewBox`, `0 0 ${width} ${height}`)
.attr(`width`, `100%`)
.attr(`height`, `100%`);
// 都道府県の領域データをpathで描画
svg
.selectAll(`path`)
.data(geoJson.features)
.enter()
.append(`path`)
.attr(`d`, path)
.attr(`stroke`, `#666`)
.attr(`stroke-width`, 0.25)
.attr(`fill`, color)
.attr(`cursor`, (item: any) => {
// カーソルの設定
const t = getTarget({ list, prefName: item.properties.name });
if(!t || t.count === 0) return 'not-allowed';
return 'pointer';
})
.attr(`fill-opacity`, (item: any) => {
// 透明度の設定
const t = getTarget({ list, prefName: item.properties.name });
if (!t || t.count === 0) return 0;
return t.count * 0.05;
})
/**
* 都道府県領域の click イベントハンドラ
*/
.on(`click`, function(item: any, target: any) {
// クリックイベントを追加したい場合はこちらに記述
console.log({ item, target });
})
/**
* 都道府県領域の MouseOver イベントハンドラ
*/
.on(`mouseover`, async function(item: any, target: any) {
// ラベル用のグループ
const group = svg.append(`g`).attr(`id`, `label-group`);
const t = getTarget({ list, prefName: item.properties.name });
const count = t ? t.count : 0;
// ラベルに表示する文字
const label = `${target.properties.name_ja}(${count}人)`;
// 矩形を追加: テキストの枠
const rectElement = group
.append(`rect`)
.attr(`id`, `label-rect`)
.attr(`stroke`, `#666`)
.attr(`stroke-width`, 0.5)
.attr(`fill`, `#fff`);
// テキストを追加
const textElement = group
.append(`text`)
.attr(`id`, `label-text`)
.text(label);
// テキストのサイズから矩形のサイズを調整
const padding = {
x: 5,
y: 0,
};
const textSize = textElement.node().getBBox();
rectElement
.attr(`x`, textSize.x - padding.x)
.attr(`y`, textSize.y - padding.y)
.attr(`width`, textSize.width + padding.x * 2)
.attr(`height`, textSize.height + padding.y * 2);
// @ts-ignore
d3.select(this).attr(`fill`, colorActive);
// @ts-ignore
d3.select(this).attr(`stroke-width`, `1`);
})
/**
* 都道府県領域の MouseMove イベントハンドラ
*/
.on("mousemove", function(item: any) {
// テキストのサイズ情報を取得
const textSize = svg
.select("#label-text")
.node()
.getBBox();
// マウス位置からラベルの位置を指定
const labelPos = {
x: item.offsetX - textSize.width,
y: item.offsetY - textSize.height,
};
// ラベルの位置を移動
svg
.select("#label-group")
.attr(`transform`, `translate(${labelPos.x}, ${labelPos.y})`);
})
/**
* 都道府県領域の MouseOut イベントハンドラ
*/
.on(`mouseout`, function(item: any) {
// ラベルグループを削除
svg.select("#label-group").remove();
// @ts-ignore
d3.select(this).attr(`fill`, color);
// @ts-ignore
d3.select(this).attr(`stroke-width`, `0.25`);
});
}
useEffect(() => {
(async () => {
if(mounted) await main();
})();
return () => {
const target = document.getElementById(`map-container`);
if (target) target.innerHTML = "";
};
}, [mounted]);
return (
<div id="map-container" className="w-[500px] h-[500px]" />
);
};
export default memo(JapanMap);
上記の参考記事内で紹介されているmain
関数をuseEffect
内で呼び出し、アンマウント時にリセットしています。
次にコンポーネント内で読み込んでいるjapan.json
とuseMounted
について説明します。
japan.json
japan.json
ファイルは上記の参考記事で紹介されていた通りに制作し、都道府県名などを少し編集したものです。
このファイルをコピペしてそのまま使えます。
useMounted hook
useMounted
はコンポーネントがマウントされたか判定するフックです。
react-use
などのライブラリを使うか、以下をコピペしてそのまま使ってください
import * as React from "react";
export function useMounted() {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
return mounted;
}
#map-container
が読み込まれてからmain()
を発火させる必要がある為、このhookを使っています。
コンポーネント呼び出し例
page.tsx
import JapanMap from '@/components/japan-map';
export default async function Page() {
const list = [
{ name: "tokyo", count: 20 },
{ name: "osaka", count: 15 },
{ name: "aichi", count: 12 },
];
return (
<JapanMap list={list} />
)
}
JapanMap
コンポーネントに渡す引数list
は以下のような形になります
const list = [
{ name: "tokyo", count: 37 },
{ name: "osaka", count: 20 },
{ name: "aichi", count: 15 },
];
name
に都道府県名がアルファベットで入り、count
に任意の数値が入ります。
このcount
の数を地図上の色の濃淡で表せるようになっています
こんな感じで表現されます。この画像では東京をホバーしているので、東京がハイライトされラベルに設定した文字が表示されていますね☺️