高齢化率を47県ヒートマップに|配色を AI に決めさせる Claude Code 流デザイン

ClaudeCode
e-Stat
高齢化率
ヒートマップ
D3

ヒートマップで47都道府県を一望する

「秋田の高齢化が深刻」「沖縄は若い」——なんとなく知ってはいる。でも 47 都道府県を 同じ画面で同じスケールで比べた経験 は、意外と少ないのではないでしょうか。棒グラフは順位を見るには最適ですが、47 本も並ぶと縦に長くなりすぎて、全体傾向が頭に入ってきません。

そこで今回は ヒートマップ の出番です。47 行 × N 列の格子状にデータを並べ、セルを色の濃淡で塗り分ける。1 枚で「どの県が」「どの年代が」「どれくらい高い/低いか」を直感的に把握できるビジュアルが完成します。

本シリーズの過去回では、データ取得(Part 1〜2)と棒グラフ(Part 3)を扱ってきました。今回の Part 4 では、棒グラフ 1 本では扱いきれなかった 「2 軸データ」 をヒートマップで表現します。具体的には、47 都道府県 × 5 年代の高齢化率推移を 1 枚にまとめる、というのがゴールです。

そしてもうひとつのテーマが「配色を Claude Code に任せる」こと。ヒートマップで一番悩むのが色なんですよね。d3-scale-chromatic には 40 種類以上のカラースキームが用意されていますが、「Viridis でいいのか、Oranges でいいのか、それとも RdYlBu の発散系か」——この判断、毎回迷う方は多いはずです。Claude Code に 「何を伝えたいか」を自然言語で投げて、配色を提案してもらう ワークフローを紹介します。

この記事のゴールは次の 3 点です。

  • e-Stat の人口推計データから 47 都道府県 × 年代の行列を構築する
  • D3 + d3-scale-chromatic で連続スケールヒートマップを描く
  • 色覚バリアフリーを考慮した配色を Claude Code に決めさせる

それでは始めましょう。

使うデータ: 高齢化率(65 歳以上人口比率)

今回扱うのは「高齢化率」、つまり総人口に占める 65 歳以上人口の割合(%)です。日本で最もよく引用される統計のひとつで、e-Stat にも複数の収載先があります。

主な取得元候補は次のとおりです(statsDataId は例示なので、実行時は最新のものを e-Stat 検索で確認してください)。

統計表statsDataId(例)特徴
人口推計(年次)0003448237毎年 10 月 1 日時点。47 都道府県 × 年齢 5 歳階級。最も粒度が細かい
国勢調査(5 年ごと)0003410379 系列完全悉皆調査。10 月 1 日時点。最新は 2025 年
住民基本台帳人口別系列1 月 1 日時点。外国人を含む / 含まない選択あり

注記: statsDataId は e-Stat 側の改訂で変わることがあります。Part 2 で紹介した /search-estat スキルで「人口推計 高齢化率」と打って最新の ID を確認するのが確実です。

今回は 人口推計(年次) を使い、2005 / 2010 / 2015 / 2020 / 2025 年の 5 時点を切り出します。「47 県 × 5 年代 = 235 セル」のヒートマップが完成形のイメージです。

なお、e-Stat の人口推計は「65 歳以上人口」と「総人口」が別行で配信されているため、高齢化率は取得後にこちらで (65歳以上 / 総人口) * 100 を計算する必要があります。Claude Code に頼むときは、この計算ステップを明示しておくと事故が減ります。

Step 1: Claude Code に「高齢化率データ取得」を頼む

stats47 のリポジトリには /fetch-estat-data というスキルが入っていて、e-Stat API の認証・キャッシュ・retry・年度フィルタを全部巻き取ってくれます。Part 1〜2 で扱った道具立てを、ここでも素直に使い回します。

Claude Code に投げるプロンプトはこんな感じです。

/fetch-estat-data を使って、人口推計(statsDataId=0003448237)から
47都道府県 × 5年代(2005, 2010, 2015, 2020, 2025)の高齢化率を取得し、
JSON で保存してください。

要件:
- 65歳以上人口 / 総人口 × 100 を都道府県・年代ごとに計算する
- 出力フォーマットは { areaCode, areaName, year, agingRate } の配列
- 保存先: /tmp/aging-rate-47x5.json
- 都道府県は areaCode 01000〜47000 の5桁固定

ポイントは 3 つあります。

  1. e-Stat 取得は /fetch-estat-data に任せる。年度フィルタや 47 都道府県の取得はスキル側でやってくれるので、こちらは「最終形の JSON 構造」だけ指示すればよい。
  2. 計算ステップを明示する。「分子と分母の API レスポンス行は別」という前提を共有しておかないと、間違えて 65 歳以上人口の絶対値を率と勘違いされることがある。
  3. areaCode は 5 桁固定.claude/rules/estat-api.md にも書かれているプロジェクト規約。01(2 桁)と 01000(5 桁)が混在するとマージ時に詰む。

実行すると、こんな JSON ができあがります。

[
  { "areaCode": "01000", "areaName": "北海道", "year": 2005, "agingRate": 21.5 },
  { "areaCode": "01000", "areaName": "北海道", "year": 2010, "agingRate": 24.7 },
  { "areaCode": "01000", "areaName": "北海道", "year": 2015, "agingRate": 29.1 },
  { "areaCode": "01000", "areaName": "北海道", "year": 2020, "agingRate": 32.1 },
  { "areaCode": "01000", "areaName": "北海道", "year": 2025, "agingRate": 33.8 },
  { "areaCode": "02000", "areaName": "青森県", "year": 2005, "agingRate": 22.7 }
]

「47 行 × 5 年代 = 235 件」のフラットな配列。これがヒートマップの素材になります。

値は説明用の例示で、最新の確定値ではありません。実行時は API の戻り値をそのまま使ってください。

Step 2: データを 47 行 × N 列の行列に整形

ヒートマップは内部的には 2 次元配列 で扱うのが定石です。フラット配列のままでも D3 で描けますが、「行 = 県」「列 = 年代」と明示的に整理しておくと、後段のスケール設定と凡例実装が格段に楽になります。

整形コードはこんな感じ。Node.js で書いていますが、Claude Code に投げる場合も同じロジックを書いてもらえばよいです。

import fs from "node:fs";

const raw = JSON.parse(
  fs.readFileSync("/tmp/aging-rate-47x5.json", "utf-8")
);

const years = [2005, 2010, 2015, 2020, 2025];

// 都道府県ごとにグループ化
const byArea = new Map();
for (const row of raw) {
  if (!byArea.has(row.areaCode)) {
    byArea.set(row.areaCode, {
      areaCode: row.areaCode,
      areaName: row.areaName,
      values: new Array(years.length).fill(null),
    });
  }
  const idx = years.indexOf(row.year);
  if (idx >= 0) {
    byArea.get(row.areaCode).values[idx] = row.agingRate;
  }
}

// 行列に変換(areaCode 昇順 = 北海道 → 沖縄)
const matrix = Array.from(byArea.values()).sort((a, b) =>
  a.areaCode.localeCompare(b.areaCode)
);

console.log(JSON.stringify({ years, matrix }, null, 2).slice(0, 500));

出力構造はこうなります。

{
  "years": [2005, 2010, 2015, 2020, 2025],
  "matrix": [
    {
      "areaCode": "01000",
      "areaName": "北海道",
      "values": [21.5, 24.7, 29.1, 32.1, 33.8]
    },
    {
      "areaCode": "02000",
      "areaName": "青森県",
      "values": [22.7, 25.8, 30.1, 33.7, 35.2]
    }
  ]
}

行(県)の並び順は 議論の余地あり です。

  • 北→南順(areaCode 昇順): 地理感覚と一致して直感的。ただし「どこが高いか」は色で読み取る必要がある
  • 値で降順ソート: 「秋田・島根・高知が一番濃い」が一発で分かる。ただし地理感覚は失われる
  • クラスタリング: 似た推移パターンの県をまとめる。高度だが解釈は最高

今回は素直に areaCode 昇順を採用します。読み手の頭に「秋田は東北だな」というメンタルマップがあるので、地理順のほうがストーリーを語りやすいのです。

Step 3: D3 でヒートマップ — rect グリッド + カラースケール

データができたら、いよいよ描画です。React のコンポーネントとして書いていますが、ロジック自体は素の D3 で完結します。

"use client";

import { useMemo } from "react";
import * as d3 from "d3";
import {
  interpolateOranges,
  interpolateRdYlBu,
  interpolateViridis,
} from "d3-scale-chromatic";

type CellDatum = {
  areaCode: string;
  areaName: string;
  year: number;
  value: number;
};

type Props = {
  data: { areaCode: string; areaName: string; values: number[] }[];
  years: number[];
  scheme?: "oranges" | "rdylbu" | "viridis";
};

const INTERPOLATORS = {
  oranges: interpolateOranges,
  rdylbu: interpolateRdYlBu,
  viridis: interpolateViridis,
};

export function AgingHeatmap({ data, years, scheme = "oranges" }: Props) {
  const cells = useMemo<CellDatum[]>(() => {
    const out: CellDatum[] = [];
    for (const row of data) {
      row.values.forEach((value, i) => {
        if (value == null) return;
        out.push({
          areaCode: row.areaCode,
          areaName: row.areaName,
          year: years[i],
          value,
        });
      });
    }
    return out;
  }, [data, years]);

  const [vMin, vMax] = useMemo(() => {
    const arr = cells.map((c) => c.value);
    return [d3.min(arr) ?? 0, d3.max(arr) ?? 1];
  }, [cells]);

  const color = useMemo(
    () => d3.scaleSequential(INTERPOLATORS[scheme]).domain([vMin, vMax]),
    [scheme, vMin, vMax]
  );

  const cellW = 56;
  const cellH = 18;
  const padL = 80;
  const padT = 32;
  const width = padL + cellW * years.length + 24;
  const height = padT + cellH * data.length + 8;

  return (
    <svg viewBox={`0 0 ${width} ${height}`} role="img" aria-label="47県×5年代の高齢化率ヒートマップ">
      <g transform={`translate(${padL}, ${padT - 8})`}>
        {years.map((y, i) => (
          <text key={y} x={cellW * i + cellW / 2} y={0} textAnchor="middle" fontSize={11}>
            {y}
          </text>
        ))}
      </g>

      <g transform={`translate(${padL - 6}, ${padT})`}>
        {data.map((row, i) => (
          <text
            key={row.areaCode}
            x={0}
            y={cellH * i + cellH * 0.7}
            textAnchor="end"
            fontSize={10}
          >
            {row.areaName}
          </text>
        ))}
      </g>

      <g transform={`translate(${padL}, ${padT})`}>
        {cells.map((c) => {
          const row = data.findIndex((r) => r.areaCode === c.areaCode);
          const col = years.indexOf(c.year);
          return (
            <rect
              key={`${c.areaCode}-${c.year}`}
              x={col * cellW}
              y={row * cellH}
              width={cellW - 1}
              height={cellH - 1}
              fill={color(c.value)}
            >
              <title>{`${c.areaName} ${c.year}: ${c.value.toFixed(1)}%`}</title>
            </rect>
          );
        })}
      </g>
    </svg>
  );
}

ポイントは 3 つです。

  1. d3.scaleSequential を使う。連続値を 1 つのカラーランプにマッピングするときの定番。scaleLinear<string> + interpolateRgb を手書きするより安全
  2. <title> で簡易ツールチップ。SVG ネイティブの <title> 要素は、マウスホバーで OS のツールチップを出す。実装ゼロでアクセシブル
  3. role="img" + aria-label。ヒートマップ全体の意味をスクリーンリーダーに伝える

ライブラリの追加はこれだけ。

npm install d3 d3-scale-chromatic
npm install --save-dev @types/d3 @types/d3-scale-chromatic

d3-scale-chromatic は D3 本体とは独立した別パッケージなので、忘れず明示インストールしてください。

Step 4: 配色を AI に決めさせる

ここからが本記事の本題です。「配色を Claude Code に任せる」というのは、雰囲気で良さげな色を選んでもらうということではありません。目的と制約を言語化してプロンプトに含め、複数候補の比較を出力させる という具体的なワークフローです。

たとえば、こんなプロンプトを投げてみます。

このヒートマップは「47都道府県の高齢化率 (2005-2025)」を表示します。
配色を提案してください。要件:

1. 連続スケール(順序のある値)であること
2. 色覚バリアフリー(P型・D型)に配慮
3. 高齢化率が高い県を「目立たせたい」(暖色寄り or 暗色寄り)
4. d3-scale-chromatic の interpolator 名で答えてください
5. 候補を3つ挙げて、それぞれのメリット・デメリットを比較

なお、この記事は印刷される可能性もあるので、グレースケール印刷時にも順序が
残るスキームを優先してください。

実際に返ってくる回答(要約)。

候補スキーム名強み弱み
1interpolateViridis色覚バリアフリーで世界標準。グレースケールでも順序が残る「高齢化=オレンジ」の文化的連想が薄い。やや学術的
2interpolateOranges高齢化を「暖色=注意喚起」で表現でき、直感的単色グラデのため値の差が分かりにくい。低値が薄すぎる
3interpolatePlasma暗→明のコントラストが強く、最大値の県が刺さる紫から黄への変化が派手で、報告書には向かない場面も

実は d3-scale-chromatic には 40 種類以上のスキームがあり、用途別に整理されています。代表的なものを並べると以下のとおり。

種別スキーム例使いどころ
Sequential (single hue)Blues, Oranges, Greens, Purples「0 から最大値」の単方向データ。直感的だが幅が狭い
Sequential (multi hue)Viridis, Plasma, Magma, Inferno, Cividis色覚バリアフリーの本命。研究論文での標準
DivergingRdYlBu, RdBu, BrBG, PiYG中央値(例: 全国平均)から上下に発散させたいとき
CyclicalRainbow, Sinebow時刻・角度など循環するデータ専用

高齢化率は「単方向データ(高いほど課題が深刻)」なので Sequential が適切。色覚バリアフリーを最優先するなら Viridis、ストーリーを「警告色」で語りたいなら Oranges、というのが定石です。

私の結論はこうでした。

  • 記事のメイン画像: interpolateOranges(読者の直感に合わせる)
  • 論文・レポート用の代替版: interpolateViridis(同じデータで色だけ差し替えた画像を別に書き出す)
  • 「全国平均からの偏差」を見せる派生版: interpolateRdYlBu(reverse=true で「赤=高齢化高い」に)

Claude Code に頼むメリットは、自分が無意識に避けていた候補を提示してくれる こと。私は Cividis を知らなかったのですが、提案されてから常に Viridis と一緒に比較するようになりました。

Step 5: 凡例(legend)を追加

ヒートマップは凡例なしでは読めません。「この色が何 % か」が分からないと、相対比較しかできないからです。

D3 の連続スケール用 legend は、実は標準ヘルパーがありません。自分で linearGradient を仕込んで横長の矩形に塗る のが定石です。

function HeatmapLegend({
  color,
  vMin,
  vMax,
  width = 240,
  height = 12,
}: {
  color: d3.ScaleSequential<string>;
  vMin: number;
  vMax: number;
  width?: number;
  height?: number;
}) {
  const stops = d3.range(0, 1.0001, 0.1).map((t) => ({
    offset: `${t * 100}%`,
    color: color(vMin + (vMax - vMin) * t),
  }));

  return (
    <svg width={width} height={height + 18} role="img" aria-label="凡例">
      <defs>
        <linearGradient id="heatmap-legend">
          {stops.map((s, i) => (
            <stop key={i} offset={s.offset} stopColor={s.color} />
          ))}
        </linearGradient>
      </defs>
      <rect x={0} y={0} width={width} height={height} fill="url(#heatmap-legend)" />
      <text x={0} y={height + 12} fontSize={10} textAnchor="start">
        {vMin.toFixed(1)}%
      </text>
      <text x={width} y={height + 12} fontSize={10} textAnchor="end">
        {vMax.toFixed(1)}%
      </text>
    </svg>
  );
}

<defs><linearGradient> の組み合わせがコツです。stops を 10 等分に切って color() でサンプリングし、SVG ネイティブの線形グラデーションを生成しています。カラースケール本体と完全に同期する ので、scheme を切り替えても凡例側を書き直す必要がありません。

凡例の位置は、ヒートマップ本体の右側に縦長で配置するパターンと、上部に横長で配置するパターンの 2 通りがあります。47 県が縦に並ぶ今回のレイアウトでは、上部に横長 のほうがバランスがよいです。

Step 6: アクセシビリティ — ARIA とツールチップ

ヒートマップは「色だけで情報を伝える」ビジュアルなので、色が見えないユーザーへの代替手段 が必須です。

最低限こなしたいのは次の 3 点。

  1. role="img"aria-label: チャート全体の意味を 1 文で説明する
  2. <title> 要素: 各セルにホバーで詳細値を表示
  3. タブ可能なフォールバック: スクリーンリーダーで全データを順に読める表を提供

3 番目が手抜きされがちです。<details> でデータテーブルを折りたたんで併置するのが、実装コストとアクセシビリティのバランスが良い解です。

<details>
  <summary>テキストでデータを見る</summary>
  <table>
    <thead>
      <tr>
        <th>都道府県</th>
        {years.map((y) => (
          <th key={y}>{y}</th>
        ))}
      </tr>
    </thead>
    <tbody>
      {data.map((row) => (
        <tr key={row.areaCode}>
          <th scope="row">{row.areaName}</th>
          {row.values.map((v, i) => (
            <td key={i}>{v?.toFixed(1) ?? "—"}</td>
          ))}
        </tr>
      ))}
    </tbody>
  </table>
</details>

「テキストで見る」というラベルを付けておけば、晴眼者でも「正確な値を引用したい」ときに開いて使えるので、二度おいしい実装です。

つまずきポイントまとめ

実装してみると、「あれ?」となる場面がいくつかあります。代表的なものを列挙しておきます。

カラーランプの向きが逆になる

interpolateRdYlBu は「赤 → 黄 → 青」の順なので、そのまま使うと「赤=低値・青=高値」 になります。高齢化率を表したいときは「赤=高齢化進行」のほうが直感的なので、ドメインを反転させましょう。

const color = d3
  .scaleSequential(d3.interpolateRdYlBu)
  .domain([vMax, vMin]); // ← 反転

color.range().reverse() で interpolator 側を反転させる方法もありますが、d3.scaleSequential の場合はドメイン反転のほうが意図が明確です。

ゼロ値・null セルの扱い

データが欠損している都道府県・年代があると、color(null)"#000000" になってしまいます。これは「最低値」と誤読されかねないので、欠損は別色(薄いグレー)で塗るのが安全。

fill={c.value == null ? "#f1f5f9" : color(c.value)}

印刷時にグレースケールになる

社内資料が白黒コピーされる前提なら、グレースケール印刷でも順序が保たれるか を確認しておきましょう。Viridis 系は明度の単調変化が設計されているので問題ありませんが、RdYlBu のような発散系は中央が灰色になって順序が読めなくなります。

Chrome の DevTools には Rendering タブに「Emulate vision deficiencies」と「Print preview」があるので、完成後に必ず両方で目視確認 することをおすすめします。

セル幅と県名ラベルのバランス

47 県の県名ラベルは縦に 47 行並びます。フォントを小さくしすぎると老眼の読者がつらいですが、大きくしすぎると 1 画面に収まりません。12px 程度、line-height 1.4 が経験的な妥協点でした。スマホ向けには横スクロール許容で書き出すのが現実的です。

次回予告

Part 5 では、いよいよ コロプレス地図 に進みます。ヒートマップは「県の並び順」を恣意的に決める必要がありましたが、コロプレス地図なら 地理空間そのものを軸にできる ので、地域クラスタの可視化に強い武器になります。

GeoJSON の取得、d3-geogeoPathgeoMercator の使い分け、トポロジーの簡略化(topojson-simplify)、配色の流用、ホバーインタラクション——テーマは盛りだくさんです。本記事で作ったカラースケール設計の知見が、ほぼそのままコロプレス地図に転用できます。お楽しみに。

関連ランキング・記事

実データで遊んでみたい方は、stats47.jp の以下のランキングを参考にどうぞ。

シリーズの他記事もぜひ。

それでは、よい AI コーディングライフを。