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 + TopoJSONで「エンジニア求人14万件の都道府県別ヒートマップ(日本地図)」を作る ── 一極集中データは対数カラースケールで描く

0
Posted at

はじめに

「都道府県ごとの件数」を地図に色で塗る、いわゆる**コロプレス図(choropleth map)**を作る機会は多いのですが、いざ実データでやると 1か所だけ値が突出していて、残りが全部同じ色に潰れる という壁にぶつかります。

この記事では、全国のエンジニア求人 約14.3万件を都道府県別に集計したデータを題材に、D3.js + TopoJSON で日本地図のヒートマップを作ります。そのうえで「一極集中したデータを、対数カラースケールでちゃんと読める地図にする」ところまでをコード付きで解説します。

題材データは求人検索エンジン「Jobmore」の都道府県別ページの掲載件数を集計したものです(2026年6月時点)。

完成イメージ

  • 47都道府県を「エンジニア求人の件数」で色分け
  • マウスオーバーで件数をツールチップ表示
  • 線形スケール版と対数スケール版を比較

題材データ:都道府県別エンジニア求人数

まず集計結果から。47都道府県の合計は 142,789件。上位・下位はこうなっています。

順位 都道府県 件数 全体比
1 東京都 55,033 38.5%
2 神奈川県 12,148 8.5%
3 大阪府 11,982 8.4%
4 愛知県 8,733 6.1%
5 埼玉県 6,223 4.4%
… … … …
45 長崎県 169 0.1%
46 佐賀県 106 0.1%
47 高知県 53 0.04%

ここで効いてくるのが「分布の形」です。

  • 平均:3,038件/県
  • 中央値:974件(岐阜県)
  • 最大/最小:約1,038倍(東京 55,033 ÷ 高知 53)

平均が中央値の3倍以上ある、典型的な**右裾の長い分布(right-skewed)**です。東京都だけで全国の約4割を占めるため、平均値が東京に強く引っ張られています。

このデータをそのまま「件数 → 色の濃さ」に線形で割り当てると、東京だけが濃く塗られ、残り46都道府県はほぼ同じ薄さになってしまいます。後半でこれを対数スケールで解決します。

データはコード内で使うので、JSオブジェクトにしておきます。

// 都道府県名 → エンジニア求人数
const jobData = {
  "北海道": 2902, "青森県": 525, "岩手県": 702, "宮城県": 2731, "秋田県": 486,
  "山形県": 614, "福島県": 976, "茨城県": 1542, "栃木県": 1336, "群馬県": 1306,
  "埼玉県": 6223, "千葉県": 4938, "東京都": 55033, "神奈川県": 12148, "新潟県": 1228,
  "富山県": 786, "石川県": 1039, "福井県": 494, "山梨県": 804, "長野県": 1391,
  "岐阜県": 974, "静岡県": 2224, "愛知県": 8733, "三重県": 1087, "滋賀県": 958,
  "京都府": 2770, "大阪府": 11982, "兵庫県": 3379, "奈良県": 743, "和歌山県": 513,
  "鳥取県": 356, "島根県": 425, "岡山県": 1034, "広島県": 2103, "山口県": 709,
  "徳島県": 405, "香川県": 795, "愛媛県": 261, "高知県": 53, "福岡県": 3500,
  "佐賀県": 106, "長崎県": 169, "熊本県": 601, "大分県": 189, "宮崎県": 175,
  "鹿児島県": 312, "沖縄県": 1029
};

使うもの

  • D3.js v7(d3-geo, d3-scale, d3-scale-chromatic を含むフルバンドル)
  • TopoJSON:日本地図のジオメトリ。ここでは dataofjapan/land の japan.topojson を使います(47都道府県のポリゴンが入っている軽量データ)
  • topojson-client:TopoJSON → GeoJSON 変換用

CDNで読み込む最小構成にします。

<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://cdn.jsdelivr.net/npm/topojson-client@3"></script>
<svg id="map" width="600" height="600"></svg>
<div id="tooltip" style="position:absolute;pointer-events:none;opacity:0;
     background:#333;color:#fff;padding:4px 8px;border-radius:4px;font-size:12px;"></div>

ステップ1:地図データを読み込んでプロパティを確認する

TopoJSON は中身のプロパティ名がデータソースによって違います。最初に必ず1つfeatureを console.log して、都道府県名がどのキーに入っているか確認するのがコツです。

const topo = await d3.json("https://raw.githubusercontent.com/dataofjapan/land/master/japan.topojson");

// topo.objects のキー名を確認(このデータでは "japan")
console.log(Object.keys(topo.objects)); // ["japan"]

const geo = topojson.feature(topo, topo.objects.japan);

// 1件目のプロパティを確認 → 都道府県名がどのキーかを特定する
console.log(geo.features[0].properties);
// → { id: 1, nam: "Hokkai Do", nam_ja: "北海道", ... }

このデータでは日本語名が properties.nam_ja に入っています。以降はこれをキーに jobData と突き合わせます。

プロパティ名が違うデータを使う場合(name、N03_001 など)も、この console.log で確認して読み替えてください。


ステップ2:投影法とパスを設定する

日本地図には geoMercator(メルカトル)で十分です。fitSize を使うと、SVGのサイズにジオメトリを自動でフィットしてくれるので、緯度経度の手計算が不要になります。

const width = 600, height = 600;
const svg = d3.select("#map");

const projection = d3.geoMercator().fitSize([width, height], geo);
const path = d3.geoPath(projection);

ステップ3:まずは「線形スケール」で塗ってみる(失敗例)

ここが本題です。素直に scaleSequential を線形で作ると、どうなるか見てみます。

const maxJobs = d3.max(Object.values(jobData)); // 55033(東京)

const colorLinear = d3.scaleSequential()
  .domain([0, maxJobs])
  .interpolator(d3.interpolateBlues);

svg.selectAll("path")
  .data(geo.features)
  .join("path")
    .attr("d", path)
    .attr("fill", d => colorLinear(jobData[d.properties.nam_ja] ?? 0))
    .attr("stroke", "#fff")
    .attr("stroke-width", 0.5);

結果:東京だけが濃紺で、神奈川(12,148件)ですら薄い水色、それ以外の40県以上はほぼ真っ白で見分けがつきません。

理由は単純で、東京の 55,033 に対して中央値が 974。スケール上では中央値の県は 974 / 55033 ≒ 0.018、つまり色の濃さ2%弱にしかマッピングされません。これでは「東京が多い」以外の情報がゼロです。


ステップ4:対数カラースケールで描き直す(解決)

こういう右裾の長いデータは、**対数スケール(scaleSequentialLog)**を使うと各県の差が見えるようになります。対数は0を扱えないので domain の下限は最小値(か1)にします。

const values = Object.values(jobData);
const colorLog = d3.scaleSequentialLog()
  .domain([d3.min(values), d3.max(values)]) // [53, 55033]
  .interpolator(d3.interpolateYlOrRd);

svg.selectAll("path")
  .data(geo.features)
  .join("path")
    .attr("d", path)
    .attr("fill", d => {
      const v = jobData[d.properties.nam_ja];
      return v ? colorLog(v) : "#eee"; // データなしはグレー
    })
    .attr("stroke", "#fff")
    .attr("stroke-width", 0.5);

これで、東京(最濃)→ 大阪・愛知・神奈川(濃いめ)→ 地方中核県(中間)→ 過疎県(淡色)と、全国のグラデーションが読める地図になります。

対数にすると「件数が10倍違うと色が1段階濃くなる」という見え方になるので、55,033 / 53 ≒ 1,000倍 ≒ 3桁 の差が、3段階ぶんの色差としてきれいに表現されます。


ステップ5:ツールチップを足す

最後に件数を表示するツールチップを付けて仕上げます。

const tooltip = d3.select("#tooltip");

svg.selectAll("path")
  .on("mousemove", (event, d) => {
    const name = d.properties.nam_ja;
    const v = jobData[name];
    tooltip
      .style("opacity", 1)
      .style("left", (event.pageX + 12) + "px")
      .style("top", (event.pageY + 12) + "px")
      .html(`<b>${name}</b><br>${v ? v.toLocaleString() : "-"} 件`);
  })
  .on("mouseleave", () => tooltip.style("opacity", 0));

これで「色で全体傾向、ホバーで正確な件数」が両立した地図の完成です。


データから分かること

可視化すると、数字の羅列では見えなかった構造が一目で分かります。

  • 東京一極集中:東京都だけで全国の 38.5%。上位5都府県(東京・神奈川・大阪・愛知・埼玉)で 65.9%、首都圏1都3県だけで 54.9% を占めます。
  • 第2極は関西・中京:大阪・愛知が1万件前後で続き、三大都市圏で大半が説明できます。
  • 地方中核都市の存在感:一方で、福岡(九州)・宮城(東北)・北海道・広島(中国)など、各地方ブロックの中核県は数千件規模を確保しています。たとえば九州のIT集積が進む福岡では、福岡県のエンジニア求人が3,500件にのぼり、地方でも都市単位では一定の母数があることが分かります。

「平均3,038件」だけ見ると地方にもそれなりに求人がありそうに見えますが、中央値は974件。平均は東京に引っ張られた値であり、可視化と合わせて見ると実態(都市圏集中)が正しく掴めます。集計や平均値の扱いでも、分布の形を意識することの大切さが分かる例だと思います。


まとめ

  • D3.js + TopoJSON で日本地図のコロプレス図は数十行で作れる
  • TopoJSONは最初にプロパティを console.log で確認してから結合キーを決める
  • 一極集中したデータは線形カラースケールだと潰れる。scaleSequentialLog(対数)を使うと各地域の差が読めるようになる
  • 地図にする前に「平均と中央値の乖離」を見ておくと、スケール選択の判断がつけやすい

件数系のデータを地図にする機会があれば、ぜひカラースケールの線形/対数を切り替えて比べてみてください。

関連記事

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?