D3.jsで日本地図ヒートマップを作ろうとして4回やり直した話
日本全国のAI教育機関を都道府県別に色分けした地図チャートを作った。
シンプルそうに見えて、完成まで4回やり直した。この記事はその記録。「こうすれば上手くいく」というよりも、「何をやって、なぜダメで、どう直したか」を書く。
同じことでつまずく人が1人でも減ればいいなと思っている。
完成したもの: AI教育・リスキリング大学マップ
やり直し1: Leaflet.jsがWordPressで動かない
最初はLeaflet.jsでやろうとした。OpenStreetMapのタイルを背景に、都道府県ごとに色を塗る方法。ローカルではきれいに動いた。
WordPressに貼ったら何も表示されなかった。
原因が2つ重なっていた。
1つ目: WordPressのカスタムHTMLブロックは外部の <script src> を無視する。Leaflet.jsが読み込まれない。
2つ目: LeafletのCSSがWordPressのテーマと衝突して、マップコンテナの高さが0になる。
両方直そうとしたが、CSSの競合がひどくてキリがなかった。全部D3.jsに書き直した。
学び: WordPressで地図チャートを出すならLeafletよりD3.js + TopoJSON。最初からD3.jsにしておけば良かった。
やり直し2: TopoJSONが読めなかった
D3.jsで日本地図を描くために、都道府県の境界線データ(TopoJSON形式)を使う。
読み込んで topojson.feature() を呼んだらエラーが出た。
TypeError: topojson.feature is not a function
原因: TopoJSONのライブラリ読み込みを <script src> で書いていた。WordPressはこれを無視するので、topojson が undefined になっていた。
D3.jsと同じ方法で動的ロードに変えた。
function loadLibs(cb) {
// まずD3.jsを読む
if (typeof d3 === 'undefined') {
var s1 = document.createElement('script');
s1.src = 'https://cdn.jsdelivr.net/npm/d3@7';
s1.onload = function() {
// 次にTopoJSONを読む
var s2 = document.createElement('script');
s2.src = 'https://cdn.jsdelivr.net/npm/topojson@3';
s2.onload = cb;
document.head.appendChild(s2);
};
document.head.appendChild(s1);
return;
}
if (typeof topojson === 'undefined') {
var s2 = document.createElement('script');
s2.src = 'https://cdn.jsdelivr.net/npm/topojson@3';
s2.onload = cb;
document.head.appendChild(s2);
return;
}
cb();
}
D3.jsが読み込まれてから、TopoJSONを読む。順番が大事。
学び: WordPressで使うライブラリは全て createElement('script') で動的ロード。<script src> は使わない。
やり直し3: && でスクリプトが全滅した
地図は表示されるようになった。ところが一部の機能が動かない。
デバッグしたら、JavaScriptの構文エラーが出ていた。
SyntaxError: Unexpected token '&'
自分のコードを見ても全部普通に見える。WordPressの管理画面でHTMLソースを確認したら、書いたはずの && が全部 && になっていた。
WordPressのコンテンツフィルター(wpautop)が && をHTMLエンティティに変換する仕様がある。ブラウザのDevToolsで見るとちゃんと && に見えるから、原因を特定するのにかなり時間がかかった。
直し方は簡単で、&& を使わないこと。
// NG(WordPressが壊す)
if (isMobile && data.length > 0) {
drawChart();
}
// OK(ネストif)
if (isMobile) {
if (data.length > 0) {
drawChart();
}
}
学び: WordPressではJavaScriptの中も書き換えが起きる。&& は最初からネストifか三項演算子で書く。
やり直し4: スマホで地図のラベルが全部切れた
スマホで開いたら、都道府県名のラベルが全部途中で切れていた。
SVGの <text> は自動改行ができない。画面幅が狭くなっても文字の長さは変わらないので、そのままはみ出す。word-wrap はSVGに効かない。
「文字を小さくすれば入るか」と試したが、日本語の都道府県名は漢字3〜5文字あって小さくしても読めなくなるだけだった。
最終的にスマホではラベルを全部消して、タップすると下部にパネルが出る方式にした。
var isMobile = window.innerWidth < 768;
// スマホではラベルを描画しない
if (!isMobile) {
svg.selectAll('.pref-label')
.data(prefData)
.enter()
.append('text')
.attr('x', function(d) { return path.centroid(d)[0]; })
.attr('y', function(d) { return path.centroid(d)[1]; })
.text(function(d) { return d.properties.name_short; });
}
タップイベントの実装でもう1回ハマった。パネルが開いた瞬間に閉じる現象が起きた。
原因は stopPropagation() の書き忘れ。タッチイベントがdocumentまで伝わって「パネル外タップ」と判定されていた。
svg.selectAll('.pref-path').on('touchstart', function(e, d) {
e.preventDefault();
e.stopPropagation(); // これが必須
showMobilePanel({
name: d.properties.name,
count: getValue(d.properties.name)
});
});
preventDefault() と stopPropagation() はセットで書く。
学び: SVGの中に日本語テキストを入れるのはやめた方がいい。スマホでは必ず切れる。HTMLのdivで作るかタップ表示に切り替える。
4回の失敗から作ったチェックリスト
次回は最初にこれだけ確認してからコードを書く。
- 外部ライブラリは全て
createElement('script')で動的ロード -
&&は使わない。ネストifか三項演算子で書く - 複数ライブラリを読む順番がある場合、ネストコールバックで確実に順番通りに読む
- 日本語のラベルはSVGに入れない。HTMLのdivかタップ表示に切り替える
- タッチイベントには
preventDefault()とstopPropagation()をセットで書く
どれも「先に知っていれば」で済む話だが、先に知る機会がなかった。
完成したダッシュボード: AI教育・リスキリング大学マップ
FAQ
Q. TopoJSONとGeoJSONの違いは何ですか?
GeoJSONは境界線を単純にすべて持つ形式で、隣の都道府県の境界線が重複して格納される。TopoJSONはその重複をなくしてファイルサイズを小さくした形式。日本全都道府県の地図データでGeoJSON(約2MB)をTopoJSON(約200KB)に圧縮できる。読み込みは topojson.feature() で一度GeoJSONに戻してから使う。
Q. D3.jsv7でイベントハンドラの書き方が変わったと聞きました。
v6以降、イベントハンドラの第1引数にeventオブジェクトが直接渡されるようになった。v5以前の d3.event は廃止された。v5以前のコードサンプルをそのまま使うと d3.event is not defined エラーになる。第1引数が event、第2引数がdata(d)という順番で受け取るように直す。
Q. fitSize と fitExtent の違いは何ですか?
fitSize([w, h], geojson) はSVG全体にちょうど収まるように縮尺と位置を自動計算する。fitExtent([[x1, y1], [x2, y2]], geojson) はパディングを指定できる。地図の周囲に余白を持たせたい場合は fitExtent の方が使いやすい。
引用・出典
- D3.js v7 公式: https://d3js.org/
- TopoJSON v3 公式: https://github.com/topojson/topojson
- WordPress wpautop: https://developer.wordpress.org/reference/functions/wpautop/
- SWELLテーマ: https://swell-theme.com/
- AI教育・リスキリング大学マップ: https://ai-japan-index.com/ai-education-map/
