都道府県別コロプレス地図の作成方法
D3.jsNext.js
コロプレス地図(choropleth map)は、地理的な領域をデータの値に応じて色分けした地図です。この記事では、Next.jsとD3.jsを組み合わせて、インタラクティブな日本地図コンポーネントを実装する方法を解説します。
コンポーネントの概要
このJapanMapコンポーネントは以下の特徴を持っています:
- Next.jsのクライアントコンポーネントとして実装
- D3.jsを使用した地図の描画とインタラクション
- TopoJSONデータを使用した日本地図の表示
- 都道府県別データに基づく色分け表示
- ダークモード対応
- ホバー時のツールチップ表示
- カラースキームの切り替え機能
注意
これは警告メッセージです。重要な注意事項を書きます。
コード全体を表示
'use client';
import { useRef, useEffect, useState } from 'react';
import * as d3 from 'd3';
import { feature } from 'topojson-client';
import { Topology, GeometryObject, GeometryCollection } from 'topojson-specification';
import { Feature, FeatureCollection, Geometry, GeoJsonProperties } from 'geojson';
import { GeoPath, GeoPermissibleObjects } from 'd3-geo';
import { useTheme } from 'next-themes';
// 都道府県コードと名前のマッピング
const PREFECTURE_MAP: Record<string, string> = {
"01": "北海道", "02": "青森県", "03": "岩手県", "04": "宮城県", "05": "秋田県",
"06": "山形県", "07": "福島県", "08": "茨城県", "09": "栃木県", "10": "群馬県",
"11": "埼玉県", "12": "千葉県", "13": "東京都", "14": "神奈川県", "15": "新潟県",
"16": "富山県", "17": "石川県", "18": "福井県", "19": "山梨県", "20": "長野県",
"21": "岐阜県", "22": "静岡県", "23": "愛知県", "24": "三重県", "25": "滋賀県",
"26": "京都府", "27": "大阪府", "28": "兵庫県", "29": "奈良県", "30": "和歌山県",
"31": "鳥取県", "32": "島根県", "33": "岡山県", "34": "広島県", "35": "山口県",
"36": "徳島県", "37": "香川県", "38": "愛媛県", "39": "高知県", "40": "福岡県",
"41": "佐賀県", "42": "長崎県", "43": "熊本県", "44": "大分県", "45": "宮崎県",
"46": "鹿児島県", "47": "沖縄県"
};
// 都道府県名と都道府県コードのマッピング(逆引き用)
const PREFECTURE_NAME_TO_CODE: Record<string, string> = Object.entries(PREFECTURE_MAP)
.reduce((acc, [code, name]) => ({ ...acc, [name]: code }), {});
// タイトル、データ、単位を外部で定義
const MAP_TITLE = "都道府県別人口(2023年)";
const MAP_UNIT = "万人";
// 全都道府県のデータ
const PREFECTURE_DATA = [
{ prefName: "北海道", value: 520 },
{ prefName: "青森県", value: 124 },
{ prefName: "岩手県", value: 121 },
{ prefName: "宮城県", value: 230 },
{ prefName: "秋田県", value: 96 },
{ prefName: "山形県", value: 107 },
{ prefName: "福島県", value: 184 },
{ prefName: "茨城県", value: 287 },
{ prefName: "栃木県", value: 194 },
{ prefName: "群馬県", value: 195 },
{ prefName: "埼玉県", value: 730 },
{ prefName: "千葉県", value: 624 },
{ prefName: "東京都", value: 1400 },
{ prefName: "神奈川県", value: 920 },
{ prefName: "新潟県", value: 223 },
{ prefName: "富山県", value: 105 },
{ prefName: "石川県", value: 114 },
{ prefName: "福井県", value: 77 },
{ prefName: "山梨県", value: 81 },
{ prefName: "長野県", value: 206 },
{ prefName: "岐阜県", value: 199 },
{ prefName: "静岡県", value: 364 },
{ prefName: "愛知県", value: 750 },
{ prefName: "三重県", value: 179 },
{ prefName: "滋賀県", value: 141 },
{ prefName: "京都府", value: 259 },
{ prefName: "大阪府", value: 880 },
{ prefName: "兵庫県", value: 550 },
{ prefName: "奈良県", value: 135 },
{ prefName: "和歌山県", value: 93 },
{ prefName: "鳥取県", value: 56 },
{ prefName: "島根県", value: 67 },
{ prefName: "岡山県", value: 190 },
{ prefName: "広島県", value: 282 },
{ prefName: "山口県", value: 137 },
{ prefName: "徳島県", value: 73 },
{ prefName: "香川県", value: 97 },
{ prefName: "愛媛県", value: 135 },
{ prefName: "高知県", value: 70 },
{ prefName: "福岡県", value: 510 },
{ prefName: "佐賀県", value: 82 },
{ prefName: "長崎県", value: 134 },
{ prefName: "熊本県", value: 175 },
{ prefName: "大分県", value: 114 },
{ prefName: "宮崎県", value: 108 },
{ prefName: "鹿児島県", value: 160 },
{ prefName: "沖縄県", value: 146 }
];
// TopoJSONの型定義
interface JapanTopoJSON extends Topology {
objects: {
[key: string]: GeometryObject | GeometryCollection;
};
}
interface JapanMapProps {
colorScheme?: 'blue' | 'green' | 'red' | 'purple' | 'orange';
}
export default function PrefectureRankTotalPopulationMap({
colorScheme = 'blue'
}: JapanMapProps) {
const svgRef = useRef<SVGSVGElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const { resolvedTheme } = useTheme();
const isDarkMode = resolvedTheme === 'dark';
const [topoData, setTopoData] = useState<JapanTopoJSON | null>(null);
// カラースキームの設定
const getColorInterpolator = (scheme: string, isDark: boolean) => {
switch (scheme) {
case 'green': return isDark ? d3.interpolateGnBu : d3.interpolateGreens;
case 'red': return isDark ? d3.interpolateRdPu : d3.interpolateReds;
case 'purple': return isDark ? d3.interpolatePuRd : d3.interpolatePurples;
case 'orange': return isDark ? d3.interpolateOrRd : d3.interpolateOranges;
case 'blue':
default: return isDark ? d3.interpolateBlues : d3.interpolateBlues;
}
};
// TopoJSONデータの読み込み
useEffect(() => {
fetch('/data/japan-topojson.json')
.then(response => response.json())
.then(data => {
setTopoData(data);
})
.catch(error => {
console.error('地図データの読み込みに失敗しました:', error);
});
}, []);
// 地図の描画
useEffect(() => {
if (!svgRef.current || !topoData) return;
try {
const svg = d3.select(svgRef.current);
const container = containerRef.current;
if (!container) return;
// コンテナのサイズを取得
const width = container.clientWidth;
const height = width * 0.8; // アスペクト比を設定
// SVGのサイズを設定
svg.attr('viewBox', `0 0 ${width} ${height}`);
svg.selectAll("*").remove();
// 地図の投影法
const projection = d3.geoMercator()
.center([137, 38]) // 日本の中心あたり
.scale(width * 1.2)
.translate([width / 2, height / 2]);
// パスジェネレータ
const path = d3.geoPath().projection(projection);
// TopoJSONをGeoJSONに変換
const japan = feature(
topoData,
topoData.objects.japan as GeometryObject
) as FeatureCollection;
// データの結合
const prefectureData = new Map<string, number>();
// データを都道府県コードとマッピング
PREFECTURE_DATA.forEach(d => {
const prefCode = PREFECTURE_NAME_TO_CODE[d.prefName];
if (prefCode) {
prefectureData.set(prefCode, d.value);
}
});
// カラースケール
const colorInterpolator = getColorInterpolator(colorScheme, isDarkMode);
const colorScale = d3.scaleSequential()
.domain([0, d3.max(Array.from(prefectureData.values())) || 0])
.interpolator(colorInterpolator);
// ツールチップの設定
const tooltip = d3.select(tooltipRef.current);
// 地図の描画
svg.selectAll('path')
.data(japan.features)
.join('path')
.attr('d', path as GeoPath<any, GeoPermissibleObjects>)
.attr('fill', (d, i) => {
const index = i;
const prefCode = String(index + 1).padStart(2, '0');
return prefectureData.has(prefCode)
? colorScale(prefectureData.get(prefCode) || 0)
: '#ccc';
})
.attr('stroke', isDarkMode ? '#555' : 'white')
.attr('stroke-width', 0.5)
.attr('class', 'prefecture')
.on('mouseover', function(event, d) {
// 型アサーションを使用
const feature = d as Feature<Geometry, GeoJsonProperties>;
const index = japan.features.indexOf(feature);
const prefCode = String(index + 1).padStart(2, '0');
const prefName = PREFECTURE_MAP[prefCode] || '不明';
// 要素をハイライト
d3.select(this)
.attr('stroke', '#333')
.attr('stroke-width', 2);
// 値を取得
let value = '該当データなし';
if (prefectureData.has(prefCode)) {
value = (prefectureData.get(prefCode) || 0) + MAP_UNIT;
}
// コンテナの位置を取得
const containerRect = containerRef.current?.getBoundingClientRect();
if (containerRect) {
const mouseX = event.clientX - containerRect.left;
const mouseY = event.clientY - containerRect.top;
// ツールチップを表示
tooltip
.style('display', 'block')
.style('opacity', 1)
.style('left', `${mouseX + 10}px`)
.style('top', `${mouseY - 40}px`)
.html(`
<div class="font-bold">${prefName}</div>
<div>${value}</div>
`);
}
})
.on('mousemove', function(event) {
// マウス移動時の処理
const containerRect = containerRef.current?.getBoundingClientRect();
if (containerRect) {
const mouseX = event.clientX - containerRect.left;
const mouseY = event.clientY - containerRect.top;
tooltip
.style('left', `${mouseX + 10}px`)
.style('top', `${mouseY - 40}px`);
}
})
.on('mouseout', function() {
// マウスアウト時の処理
d3.select(this)
.attr('stroke', isDarkMode ? '#555' : 'white')
.attr('stroke-width', 0.5);
// ツールチップを非表示
tooltip
.style('opacity', 0)
.style('display', 'none');
});
// ツールチップのスタイル調整
tooltip
.style('background-color', isDarkMode ? '#333' : 'white')
.style('color', isDarkMode ? 'white' : 'black')
.style('border-color', isDarkMode ? '#555' : '#ddd');
// データがある場合のみ凡例を表示
if (PREFECTURE_DATA.length > 0) {
// 縦凡例の設定
const legendWidth = 20;
const legendHeight = 120;
const legendX = width - legendWidth - 50;
const legendY = height - legendHeight - 40;
// 凡例用のスケール
const legendScale = d3.scaleLinear()
.domain([d3.max(Array.from(prefectureData.values())) || 0, 0])
.range([0, legendHeight]);
// 凡例の目盛りを固定値に設定
const legendTicks = [0, 500, 1000, 1500];
const maxValue = d3.max(Array.from(prefectureData.values())) || 0;
const usableTicks = legendTicks.filter(tick => tick <= maxValue);
if (usableTicks[usableTicks.length - 1] < maxValue) {
usableTicks.push(maxValue);
}
const legendAxis = d3.axisRight(legendScale)
.tickValues(usableTicks)
.tickSize(2)
.tickFormat(d => {
// 数値を適切なフォーマットで表示
const value = +d;
if (value >= 1000) {
return `${(value / 1000).toFixed(1)}k`;
}
return `${value}`;
});
// グラデーション定義
const defs = svg.append('defs');
const linearGradient = defs.append('linearGradient')
.attr('id', 'legend-gradient')
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '0%')
.attr('y2', '100%');
linearGradient.selectAll('stop')
.data(d3.range(0, 1.01, 0.1))
.join('stop')
.attr('offset', d => d * 100 + '%')
.attr('stop-color', d => colorScale((1 - d) * (d3.max(Array.from(prefectureData.values())) || 0)));
// 凡例の矩形
svg.append('rect')
.attr('x', legendX)
.attr('y', legendY)
.attr('width', legendWidth)
.attr('height', legendHeight)
.style('fill', 'url(#legend-gradient)');
// 凡例の軸
const legendAxisG = svg.append('g')
.attr('transform', `translate(${legendX + legendWidth}, ${legendY})`)
.call(legendAxis as d3.Axis<d3.NumberValue>);
// 凡例の軸のテキストサイズを小さく
legendAxisG.selectAll('text')
.attr('font-size', '8px')
.attr('dx', '1px');
// 凡例のタイトル
svg.append('text')
.attr('x', legendX + legendWidth / 2)
.attr('y', legendY - 6)
.attr('text-anchor', 'middle')
.attr('font-size', '8px')
.attr('fill', isDarkMode ? '#e5e7eb' : '#333')
.text(MAP_UNIT);
}
// タイトルを追加
svg.append('text')
.attr('x', width / 2)
.attr('y', 20)
.attr('text-anchor', 'middle')
.attr('font-size', '14px')
.attr('font-weight', 'bold')
.attr('fill', isDarkMode ? '#e5e7eb' : '#333')
.text(MAP_TITLE);
} catch (error) {
console.error('地図の描画中にエラーが発生しました:', error);
}
}, [topoData, isDarkMode, colorScheme]);
return (
<div ref={containerRef} className="relative my-8">
<svg ref={svgRef} className="w-full h-auto"></svg>
<div
ref={tooltipRef}
className={`absolute p-2 rounded shadow-lg border text-sm pointer-events-none opacity-0 z-10 ${
isDarkMode ? 'bg-gray-800 text-white border-gray-600' : 'bg-white text-gray-800 border-gray-200'
}`}
style={{
display: 'none',
left: 0,
top: 0,
minWidth: '120px'
}}
></div>
</div>
);
}
実装の基本構造
必要なライブラリとインポート
'use client';
import { useRef, useEffect, useState } from 'react';
import * as d3 from 'd3';
import { feature } from 'topojson-client';
import { Topology, GeometryObject, GeometryCollection } from 'topojson-specification';
import { Feature, FeatureCollection, Geometry, GeoJsonProperties } from 'geojson';
import { GeoPath, GeoPermissibleObjects } from 'd3-geo';
import { useTheme } from 'next-themes';
- 'use client' ディレクティブでクライアントコンポーネントとして宣言
- Reactのフックとして useRef, useEffect, useState を使用
- D3.jsとTopoJSON関連のライブラリをインポート
- Next.jsの next-themes からダークモード対応のための useTheme フックをインポート
都道府県データの定義
// 都道府県コードと名前のマッピング
const PREFECTURE_MAP: Record<string, string> = {
"01": "北海道", "02": "青森県", "03": "岩手県", "04": "宮城県", "05": "秋田県",
"06": "山形県", "07": "福島県", "08": "茨城県", "09": "栃木県", "10": "群馬県",
"11": "埼玉県", "12": "千葉県", "13": "東京都", "14": "神奈川県", "15": "新潟県",
"16": "富山県", "17": "石川県", "18": "福井県", "19": "山梨県", "20": "長野県",
"21": "岐阜県", "22": "静岡県", "23": "愛知県", "24": "三重県", "25": "滋賀県",
"26": "京都府", "27": "大阪府", "28": "兵庫県", "29": "奈良県", "30": "和歌山県",
"31": "鳥取県", "32": "島根県", "33": "岡山県", "34": "広島県", "35": "山口県",
"36": "徳島県", "37": "香川県", "38": "愛媛県", "39": "高知県", "40": "福岡県",
"41": "佐賀県", "42": "長崎県", "43": "熊本県", "44": "大分県", "45": "宮崎県",
"46": "鹿児島県", "47": "沖縄県"
};
// 都道府県名と都道府県コードのマッピング(逆引き用)
const PREFECTURE_NAME_TO_CODE: Record<string, string> = Object.entries(PREFECTURE_MAP)
.reduce((acc, [code, name]) => ({ ...acc, [name]: code }), {});
// タイトル、データ、単位を外部で定義
const MAP_TITLE = "都道府県別人口(2023年)";
const MAP_UNIT = "万人";
// 全都道府県のデータ
const PREFECTURE_DATA = [
{ prefName: "北海道", value: 520 },
{ prefName: "青森県", value: 124 },
{ prefName: "岩手県", value: 121 },
{ prefName: "宮城県", value: 230 },
{ prefName: "秋田県", value: 96 },
{ prefName: "山形県", value: 107 },
{ prefName: "福島県", value: 184 },
{ prefName: "茨城県", value: 287 },
{ prefName: "栃木県", value: 194 },
{ prefName: "群馬県", value: 195 },
{ prefName: "埼玉県", value: 730 },
{ prefName: "千葉県", value: 624 },
{ prefName: "東京都", value: 1400 },
{ prefName: "神奈川県", value: 920 },
{ prefName: "新潟県", value: 223 },
{ prefName: "富山県", value: 105 },
{ prefName: "石川県", value: 114 },
{ prefName: "福井県", value: 77 },
{ prefName: "山梨県", value: 81 },
{ prefName: "長野県", value: 206 },
{ prefName: "岐阜県", value: 199 },
{ prefName: "静岡県", value: 364 },
{ prefName: "愛知県", value: 750 },
{ prefName: "三重県", value: 179 },
{ prefName: "滋賀県", value: 141 },
{ prefName: "京都府", value: 259 },
{ prefName: "大阪府", value: 880 },
{ prefName: "兵庫県", value: 550 },
{ prefName: "奈良県", value: 135 },
{ prefName: "和歌山県", value: 93 },
{ prefName: "鳥取県", value: 56 },
{ prefName: "島根県", value: 67 },
{ prefName: "岡山県", value: 190 },
{ prefName: "広島県", value: 282 },
{ prefName: "山口県", value: 137 },
{ prefName: "徳島県", value: 73 },
{ prefName: "香川県", value: 97 },
{ prefName: "愛媛県", value: 135 },
{ prefName: "高知県", value: 70 },
{ prefName: "福岡県", value: 510 },
{ prefName: "佐賀県", value: 82 },
{ prefName: "長崎県", value: 134 },
{ prefName: "熊本県", value: 175 },
{ prefName: "大分県", value: 114 },
{ prefName: "宮崎県", value: 108 },
{ prefName: "鹿児島県", value: 160 },
{ prefName: "沖縄県", value: 146 }
];
- 都道府県コードと名前の対応マップを定義
- 都道府県名からコードを逆引きするためのマップも用意
- 地図のタイトル、単位、データをコンポーネント外で定義
コンポーネントの型定義
// TopoJSONの型定義
interface JapanTopoJSON extends Topology {
objects: {
[key: string]: GeometryObject | GeometryCollection;
};
}
interface JapanMapProps {
colorScheme?: 'blue' | 'green' | 'red' | 'purple' | 'orange';
}
- TopoJSONデータの型を定義
- コンポーネントのプロップスとして、カラースキームを受け取るように定義
コンポーネントの実装
基本構造とフック
export default function PrefectureRankTotalPopulationMap({
colorScheme = 'blue'
}: JapanMapProps) {
const svgRef = useRef<SVGSVGElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const { resolvedTheme } = useTheme();
const isDarkMode = resolvedTheme === 'dark';
const [topoData, setTopoData] = useState<JapanTopoJSON | null>(null);
// ...
}
- SVG、ツールチップ、コンテナ要素への参照を useRef で管理
- useTheme フックでダークモードの状態を取得
- TopoJSONデータを useState で管理
カラースキームの設定
const getColorInterpolator = (scheme: string, isDark: boolean) => {
switch (scheme) {
case 'green': return isDark ? d3.interpolateGnBu : d3.interpolateGreens;
case 'red': return isDark ? d3.interpolateRdPu : d3.interpolateReds;
case 'purple': return isDark ? d3.interpolatePuRd : d3.interpolatePurples;
case 'orange': return isDark ? d3.interpolateOrRd : d3.interpolateOranges;
case 'blue':
default: return isDark ? d3.interpolateBlues : d3.interpolateBlues;
}
};
- 指定されたカラースキームとダークモードの状態に基づいて、適切なD3のカラーインターポレーターを返す
- 各カラースキームにはダークモード用と通常モード用の2種類を用意
データの読み込み
useEffect(() => {
fetch('/data/japan-topojson.json')
.then(response => response.json())
.then(data => {
setTopoData(data);
})
.catch(error => {
console.error('地図データの読み込みに失敗しました:', error);
});
}, []);
- useEffect を使用して、コンポーネントのマウント時に一度だけTopoJSONデータを読み込む
- 読み込んだデータは setTopoData で状態に保存
地図の描画
useEffect(() => {
if (!svgRef.current || !topoData) return;
try {
// TopoJSONからGeoJSONへの変換
const objectKey = Object.keys(topoData?.objects || {})[0];
if (!objectKey) {
throw new Error('地図データの形式が正しくありません');
}
const japanGeo = feature(topoData as Topology, topoData.objects[objectKey]);
const japan = japanGeo as unknown as FeatureCollection<Geometry>;
// データの結合
const prefectureData = new Map<string, number>();
// データを都道府県コードとマッピング
PREFECTURE_DATA.forEach(d => {
const prefCode = PREFECTURE_NAME_TO_CODE[d.prefName];
if (prefCode) {
prefectureData.set(prefCode, d.value);
}
});
// SVGの設定
const width = 600;
const height = 500;
const svg = d3.select(svgRef.current)
.attr('width', width)
.attr('height', height)
.attr('viewBox', [0, 0, width, height])
.attr('style', 'max-width: 100%; height: auto;');
// 既存のSVG要素をクリア
svg.selectAll("*").remove();
// 地図の投影法
const projection = d3.geoMercator()
.center([137, 38])
.scale(1600)
.translate([width / 2, height / 2]);
// パスジェネレータ
const path = d3.geoPath().projection(projection);
// カラースケール
const colorInterpolator = getColorInterpolator(colorScheme, isDarkMode);
const colorScale = d3.scaleSequential()
.domain([0, d3.max(Array.from(prefectureData.values())) || 0])
.interpolator(colorInterpolator);
// ツールチップの設定
const tooltip = d3.select(tooltipRef.current);
// 地図の描画
svg.selectAll('path')
.data(japan.features)
.join('path')
.attr('fill', (d, i) => {
// インデックスから都道府県コードを取得
const prefCode = String(i + 1).padStart(2, '0');
// 都道府県コードから値を取得
return prefectureData.has(prefCode)
? colorScale(prefectureData.get(prefCode) || 0)
: isDarkMode ? '#444' : '#eee'; // データがない場合の色
})
.attr('d', path as GeoPath<GeoJsonProperties, GeoPermissibleObjects>)
.attr('stroke', isDarkMode ? '#555' : 'white')
.attr('stroke-width', 0.5)
.attr('class', 'prefecture')
.on('mouseover', function(event, d) {
// 型アサーションを使用
const feature = d as Feature<Geometry, GeoJsonProperties>;
// インデックスを取得
const index = japan.features.indexOf(feature);
const prefCode = String(index + 1).padStart(2, '0');
const prefName = PREFECTURE_MAP[prefCode] || '不明';
// 値を取得
let value = '該当データなし';
if (prefectureData.has(prefCode)) {
value = (prefectureData.get(prefCode) || 0) + MAP_UNIT;
}
// 要素をハイライト
d3.select(this)
.attr('stroke', '#333')
.attr('stroke-width', 2);
// コンテナの位置を取得
const containerRect = containerRef.current?.getBoundingClientRect();
if (containerRect) {
const mouseX = event.clientX - containerRect.left;
const mouseY = event.clientY - containerRect.top;
// ツールチップを表示
tooltip
.style('display', 'block')
.style('opacity', 1)
.style('left', `${mouseX + 10}px`)
.style('top', `${mouseY - 40}px`)
.html(`
<div class="font-bold">${prefName}</div>
<div>${value}</div>
`);
}
})
.on('mousemove', function(event) {
// マウス移動時の処理
const containerRect = containerRef.current?.getBoundingClientRect();
if (containerRect) {
const mouseX = event.clientX - containerRect.left;
const mouseY = event.clientY - containerRect.top;
tooltip
.style('left', `${mouseX + 10}px`)
.style('top', `${mouseY - 40}px`);
}
})
.on('mouseout', function() {
// マウスアウト時の処理
d3.select(this)
.attr('stroke', isDarkMode ? '#555' : 'white')
.attr('stroke-width', 0.5);
// ツールチップを非表示
tooltip
.style('opacity', 0)
.style('display', 'none');
});
// ツールチップのスタイル調整
tooltip
.style('background-color', isDarkMode ? '#333' : 'white')
.style('color', isDarkMode ? 'white' : 'black')
.style('border-color', isDarkMode ? '#555' : '#ddd');
// データがある場合のみ凡例を表示
if (PREFECTURE_DATA.length > 0) {
// 縦凡例の設定
const legendWidth = 20;
const legendHeight = 120;
const legendX = width - legendWidth - 50;
const legendY = height - legendHeight - 40;
// 凡例用のスケール
const legendScale = d3.scaleLinear()
.domain([d3.max(Array.from(prefectureData.values())) || 0, 0])
.range([0, legendHeight]);
// 凡例の目盛りを固定値に設定
const legendTicks = [0, 500, 1000, 1500];
const maxValue = d3.max(Array.from(prefectureData.values())) || 0;
const usableTicks = legendTicks.filter(tick => tick <= maxValue);
if (usableTicks[usableTicks.length - 1] < maxValue) {
usableTicks.push(maxValue);
}
const legendAxis = d3.axisRight(legendScale)
.tickValues(usableTicks)
.tickSize(2)
.tickFormat(d => {
// 数値を適切なフォーマットで表示
const value = +d;
if (value >= 1000) {
return `${(value / 1000).toFixed(1)}k`;
}
return `${value}`;
});
// グラデーション定義
const defs = svg.append('defs');
const linearGradient = defs.append('linearGradient')
.attr('id', 'legend-gradient')
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '0%')
.attr('y2', '100%');
linearGradient.selectAll('stop')
.data(d3.range(0, 1.01, 0.1))
.join('stop')
.attr('offset', d => d * 100 + '%')
.attr('stop-color', d => colorScale((1 - d) * (d3.max(Array.from(prefectureData.values())) || 0)));
// 凡例の矩形
svg.append('rect')
.attr('x', legendX)
.attr('y', legendY)
.attr('width', legendWidth)
.attr('height', legendHeight)
.style('fill', 'url(#legend-gradient)');
// 凡例の軸
const legendAxisG = svg.append('g')
.attr('transform', `translate(${legendX + legendWidth}, ${legendY})`)
.call(legendAxis as d3.Axis<d3.NumberValue>);
// 凡例の軸のテキストサイズを小さく
legendAxisG.selectAll('text')
.attr('font-size', '8px')
.attr('dx', '1px');
// 凡例のタイトル
svg.append('text')
.attr('x', legendX + legendWidth / 2)
.attr('y', legendY - 6)
.attr('text-anchor', 'middle')
.attr('font-size', '8px')
.attr('fill', isDarkMode ? '#e5e7eb' : '#333')
.text(MAP_UNIT);
}
// タイトルを追加
svg.append('text')
.attr('x', width / 2)
.attr('y', 20)
.attr('text-anchor', 'middle')
.attr('font-size', '14px')
.attr('font-weight', 'bold')
.attr('fill', isDarkMode ? '#e5e7eb' : '#333')
.text(MAP_TITLE);
} catch (error) {
console.error('地図の描画中にエラーが発生しました:', error);
}
}, [topoData, isDarkMode, colorScheme]);
- topoData, isDarkMode, colorScheme が変更されたときに地図を再描画
- TopoJSONデータをGeoJSONに変換
- 都道府県データを都道府県コードとマッピング
- SVGの基本設定と既存要素のクリア
- メルカトル図法を使用して日本地図を投影
- 選択されたカラースキームとダークモードの状態に基づいてカラーインターポレーターを取得
- D3のデータバインディングを使用して、GeoJSONの各フィーチャーにパスを作成
- ホバー、マウス移動、マウスアウト時のイベントハンドラを設定
- データの値に応じた色の凡例を作成
コンポーネントのレンダリング
return (
<div ref={containerRef} className="relative my-8">
<svg ref={svgRef} className="w-full h-auto"></svg>
<div
ref={tooltipRef}
className={`absolute p-2 rounded shadow-lg border text-sm pointer-events-none opacity-0 z-10 ${
isDarkMode ? 'bg-gray-800 text-white border-gray-600' : 'bg-white text-gray-800 border-gray-200'
}`}
style={{
display: 'none',
left: 0,
top: 0,
minWidth: '120px'
}}
></div>
</div>
);
- コンテナ、SVG、ツールチップの要素を配置
- ダークモードに応じたスタイリングを適用
カスタマイズ方法
このコンポーネントは様々な方法でカスタマイズできます:
異なるデータセットの使用
// 別のデータセットを定義
const PREFECTURE_DATA_DENSITY = [
{ prefName: "東京都", value: 6400 }, // 人口密度(人/km²)
// 他の都道府県データ
];
// コンポーネントにデータを渡す
<PrefectureRankTotalPopulationMap data={PREFECTURE_DATA_DENSITY} title="都道府県別人口密度" unit="人/km²" />
カラースキームの変更
// 緑系のカラースキームを使用
<PrefectureRankTotalPopulationMap colorScheme="green" />
// 赤系のカラースキームを使用
<PrefectureRankTotalPopulationMap colorScheme="red" />
地図の投影法のカスタマイズ
// 投影法の設定をカスタマイズ
const projection = d3.geoMercator()
.center([135, 35]) // 中心座標を変更
.scale(2000) // スケールを変更
.translate([width / 2, height / 2]);
インタラクションの拡張
// クリックイベントの追加
.on('click', function(event, d) {
// クリックした都道府県の詳細情報を表示するなどの処理
const feature = d as Feature<Geometry, GeoJsonProperties>;
const index = japan.features.indexOf(feature);
const prefCode = String(index + 1).padStart(2, '0');
const prefName = PREFECTURE_MAP[prefCode] || '不明';
console.log(`${prefName}がクリックされました`);
// 詳細情報を表示する処理など
})
まとめ
Next.jsとD3.jsを組み合わせることで、インタラクティブな日本地図コンポーネントを実装することができました。このコンポーネントは以下の特徴を持っています:
- クライアントサイドでのインタラクティブな可視化
- ダークモード対応
- 複数のカラースキーム
- ホバー時のツールチップ表示
- レスポンシブデザイン
このようなデータ可視化コンポーネントは、地域ごとの統計データを直感的に理解するのに非常に効果的です。Next.jsの最新機能とD3.jsの強力な可視化機能を組み合わせることで、より洗練されたデータビジュアライゼーションを実現できます。
今回紹介したコードをベースに、さまざまなデータセットやカスタマイズを加えることで、あなた独自のデータ可視化コンポーネントを作成してみてください。