0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

D3.jsで日本地図ヒートマップを作ろうとして4回やり直した話

0
Posted at

ai-education-map.jpg

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ソースを確認したら、書いたはずの && が全部 &#038;&#038; になっていた。

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回の失敗から作ったチェックリスト

次回は最初にこれだけ確認してからコードを書く。

  1. 外部ライブラリは全て createElement('script') で動的ロード
  2. && は使わない。ネストifか三項演算子で書く
  3. 複数ライブラリを読む順番がある場合、ネストコールバックで確実に順番通りに読む
  4. 日本語のラベルはSVGに入れない。HTMLのdivかタップ表示に切り替える
  5. タッチイベントには 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. fitSizefitExtent の違いは何ですか?

fitSize([w, h], geojson) はSVG全体にちょうど収まるように縮尺と位置を自動計算する。fitExtent([[x1, y1], [x2, y2]], geojson) はパディングを指定できる。地図の周囲に余白を持たせたい場合は fitExtent の方が使いやすい。


引用・出典

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?