1. 導入: なぜ Small Multiple は「47 都道府県」と相性がいいのか
Claude Code × e-Stat API 実例集の Part 15 へようこそ。今回のテーマは Small Multiple(スモールマルチプル) です。
Small Multiple とは、同じ軸・同じスケールの小さなチャートを格子状に並べて、「全体傾向」と「個別の異常値」を一目で見せる手法のこと。Edward Tufte が著書『Envisioning Information』で広めた古典的なテクニックですが、47 都道府県のような 中規模カーディナリティのカテゴリ に対しては、いまだに敵なしの可視化手法です。
「47 本の折れ線を 1 枚のチャートに重ねる」とどうなるか、一度試した人なら分かります。色が足りない。凡例が画面の半分を占める。読者は 3 秒で離脱する。GA4 のセッション時間が落ちる。GSC の CTR が落ちる。負のサイクルです。
一方、Small Multiple なら 47 県を 7×7 の格子に並べ、各 mini chart は「線 1 本+全国平均の薄い線」だけ。読者は次の 3 ステップで情報を吸収できます。
- 全体をぼんやり眺めて、外れ値(線の角度が異常な県)を探す
- 気になる県の mini chart を凝視する
- その県の見出しと数値を確認する
この記事では、警察庁の犯罪統計(人口10万人当たり認知件数)を題材に、Claude Code で 47 枚の SVG を一括生成 するレシピを解説します。コード量は約 60 行。プロンプトを 3 回投げるだけで、out/sm-aichi.svg から out/sm-okinawa.svg まで一式が出力されます。
ついでに、地理風レイアウト(北海道は左上、沖縄は右下) で 47 枚を並べる工夫、軸のスケール統一の重要性、SVG ファイルサイズの最適化、ブログ埋め込み時の Lazy Load 戦略まで、つまずきポイントをまとめて潰します。
この記事のゴール
- 警察庁の犯罪統計から「都道府県別 × 10 年分」の時系列を取得する
- D3.js(または素の SVG)で 1 県 1 mini chart を関数化する
- 47 県を 7 列 × 7 行のグリッドに配置する(共通スケール)
- 全国平均ラインを薄く重ねて「相対比較」を可能にする
- ブログ用に SVG 1 枚に集約する版と、47 枚個別 SVG 版の 2 系統を出力する
2. 使うデータ: 警察庁の犯罪統計(罪種別認知件数 / 人口10万人当たり)
e-Stat に登録されている 警察庁「犯罪統計」 は、毎月更新される一次データの宝庫です。罪種(刑法犯総数、凶悪犯、粗暴犯、窃盗犯、知能犯、風俗犯、その他)× 都道府県 × 月次 という細かい軸で公開されており、Small Multiple の素材として理想的。
ただし生の認知件数のままだと「東京は多い、鳥取は少ない」という当たり前の結論しか出ません。可視化するときは必ず 人口10万人当たり に正規化します。
データ仕様まとめ
| 項目 | 内容 |
|---|---|
| 提供元 | 警察庁刑事局 |
| 統計表 ID 例 | 0003456789(罪種別 認知件数 都道府県別) |
| 集計頻度 | 月次(年計あり) |
| 期間 | 2002 年以降(10 年分の利用が現実的) |
| 単位 | 件 / 人口10万人当たり件数 |
| 注意点 | 2017 年に「強盗」「強制性交等」の区分変更あり。比較は罪種別ではなく総数で行うのが無難 |
正規化に使う人口データは、総務省統計局「人口推計」または「国勢調査」の中間補正値を使います。注意:人口の分母を国勢調査年度だけで取ると、人口減少が激しい県では「分母が古い → 分子だけ減る → 見かけ上の改善」が起こります。毎年の推計値で割るのが正解です。
罪種の選択
Small Multiple は 1 mini chart に 1〜2 系列が見やすさの限界です。今回は「刑法犯総数」だけを線で描き、参考に「全国平均」を薄く重ねる構成にします。
罪種別の対比を見せたい場合は、ファセット軸を罪種にし、47 県を線で重ねる逆構成も検討に値しますが、47 線が重なって読めなくなるのは前述のとおり。素直に 47 県 × 1 線 × 全国平均 が最適解です。
3. Step 1: e-Stat 取得(時系列 10 年程度)
stats47 リポジトリには /fetch-estat-data スキルが整備されており、年次・都道府県別のデータは 1 行で取得できます。Claude Code 上で次のプロンプトを投げます。
/fetch-estat-data 警察庁 犯罪統計 認知件数 都道府県別 年計 2015-2024
スキルは内部で次の順序で動きます。
search-estatで統計表 ID を解決(0003****)inspect-estat-metaでcat01(罪種コード)とarea(都道府県コード)を確認fetch-estat-data本体で全年度・全都道府県を一括取得(年度フィルタはメモリ側)- R2 キャッシュに保存(同じ statsDataId のヒット率を上げる)
.claude/rules/estat-api.md に書いてあるとおり、cdTimeFrom / cdArea を使わないのが鉄則。全件取って後でフィルタする方が、結果的にキャッシュヒット率が上がります。
取得結果の JSON 例
{
"stats_table_id": "0003456789",
"title": "罪種別 認知件数 都道府県別",
"unit": "件",
"period": "2015-2024",
"records": [
{ "area_code": "01000", "area_name": "北海道", "year": 2024, "crime": "刑法犯総数", "value": 38214 },
{ "area_code": "01000", "area_name": "北海道", "year": 2023, "crime": "刑法犯総数", "value": 36502 },
{ "area_code": "13000", "area_name": "東京都", "year": 2024, "crime": "刑法犯総数", "value": 92103 },
{ "area_code": "47000", "area_name": "沖縄県", "year": 2024, "crime": "刑法犯総数", "value": 4321 }
]
}
人口で正規化する
人口推計は別 API 呼び出しが必要です。Claude Code に次のように指示します。
人口推計(総務省統計局 0003448237)から、2015-2024 の都道府県別総人口を取得して、
さきほどの犯罪認知件数と (year, area_code) で join。
人口10万人当たり件数 = value / population * 100000 を計算して、
out/crime-per-100k.json に書き出して。
Claude Code は次のような Python スクリプトを書いて実行してくれます(手で直すのは 2 行程度)。
import json
from pathlib import Path
crime = json.loads(Path("data/crime-raw.json").read_text())["records"]
pop = json.loads(Path("data/population-raw.json").read_text())["records"]
# (area_code, year) -> population
pop_map = {(r["area_code"], r["year"]): r["value"] for r in pop}
out = []
for r in crime:
if r["crime"] != "刑法犯総数":
continue
key = (r["area_code"], r["year"])
if key not in pop_map:
continue
per100k = round(r["value"] / pop_map[key] * 100_000, 2)
out.append({
"area_code": r["area_code"],
"area_name": r["area_name"],
"year": r["year"],
"per100k": per100k,
})
Path("out/crime-per-100k.json").write_text(
json.dumps(out, ensure_ascii=False, indent=2)
)
print(f"wrote {len(out)} records")
出力サンプル:
[
{ "area_code": "01000", "area_name": "北海道", "year": 2024, "per100k": 743.5 },
{ "area_code": "13000", "area_name": "東京都", "year": 2024, "per100k": 658.2 },
{ "area_code": "47000", "area_name": "沖縄県", "year": 2024, "per100k": 295.4 }
]
これで「47 県 × 10 年 = 470 行」のフラットな配列ができました。Small Multiple のソースデータはこれで十分です。
4. Step 2: 47 県を地域順 or 地理レイアウト順にソート
Small Multiple の善し悪しは 並び順 で 8 割決まります。アルファベット順(あるいは五十音順)は、最後の選択肢にしてください。読者の認知負荷が無駄に上がります。
選択肢は 3 つ。
選択肢 A: 地域ブロック順(推奨デフォルト)
北海道 → 東北 → 関東 → 中部 → 近畿 → 中国 → 四国 → 九州・沖縄。
これは日本人の頭の中の地理感と一致するので、迷ったらこれ。
選択肢 B: 地理風レイアウト(タイルマップ)
「北海道は左上、沖縄は右下」と、実際の地図に近い格子配置をする方法。Financial Times がよく使うアレです。コードは少し増えますが、視覚的インパクトは抜群です。
選択肢 C: 指標値順(例: 2024 年の犯罪率降順)
「ランキングを兼ねた Small Multiple」として機能します。ただし、地理感が失われるので、ランキング目的でなければ非推奨。
今回は B の地理風タイルマップ を採用します。Claude Code に次のプロンプトを投げます。
47 都道府県の地理風タイルマップ(8 列 × 12 行 or 任意)の座標表を JSON で出してほしい。
キー: area_code(5桁、"01000" 形式)、value: { row, col, area_name }
北海道 = (0, 7) みたいに、ざっくり地理的な位置で。
返ってくる JSON はこんな感じになります(一部抜粋)。
{
"01000": { "row": 0, "col": 7, "area_name": "北海道" },
"02000": { "row": 1, "col": 6, "area_name": "青森県" },
"03000": { "row": 2, "col": 7, "area_name": "岩手県" },
"13000": { "row": 4, "col": 5, "area_name": "東京都" },
"23000": { "row": 5, "col": 4, "area_name": "愛知県" },
"27000": { "row": 6, "col": 3, "area_name": "大阪府" },
"40000": { "row": 7, "col": 2, "area_name": "福岡県" },
"47000": { "row": 8, "col": 0, "area_name": "沖縄県" }
}
このレイアウト表は再利用性が高いので、data/tile-layout-jp.json として保存しておきます。今後の Small Multiple、コロプレス代替、SNS 静止画 47 枚など、いろんな場面で流用できます。
レイアウト方式の比較
| 方式 | 学習コスト | 視認性 | 制作コスト | 推奨度 |
|---|---|---|---|---|
| 五十音順 | 低 | 低 | 低 | ✕ |
| 地域ブロック順(7×7) | 中 | 中 | 低 | ◯ |
| 地理風タイル(8×12) | 高 | 高 | 中 | ◎ |
| 指標値降順 | 中 | 中 | 低 | △(ランキング目的のみ) |
5. Step 3: D3 で 1 県 1 mini chart を生成(共通スケール)
ここからが Claude Code の真骨頂です。「1 県 1 mini chart を返す関数」を 1 回書いてもらい、それを 47 回 map で回します。
Claude Code への指示
out/crime-per-100k.json を読み込んで、
1 都道府県について 1 つの SVG mini chart を返す関数 buildMini(areaCode) を作って。
仕様:
- サイズ 160 × 100 px
- 横軸: year(2015-2024、共通スケール)
- 縦軸: per100k(全国共通スケール、最大値は全県・全年の最大に統一)
- 折れ線: その県の per100k 推移(stroke #2563eb, width 1.8)
- 全国平均(年ごと算術平均)を薄く重ねる(stroke #94a3b8, width 1, dasharray 2 2)
- 左上に県名(font-size 11, fill #0f172a)
- 右上に 2024 年の値(font-size 10, fill #475569)
D3.js(v7)を使う。Node.js で jsdom 経由で document.createElement('svg') できるように。
出力は文字列(SVG 全体)。後段で組み合わせるので、buildMini 単体ではファイル書き出しはしない。
返ってくるコードはおよそ次のとおりです(軽く整形しています)。
// build-mini.mjs
import * as d3 from "d3";
import { JSDOM } from "jsdom";
import fs from "node:fs";
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
const document = dom.window.document;
const W = 160, H = 100, MARGIN = { t: 18, r: 28, b: 16, l: 8 };
const innerW = W - MARGIN.l - MARGIN.r;
const innerH = H - MARGIN.t - MARGIN.b;
const raw = JSON.parse(fs.readFileSync("out/crime-per-100k.json", "utf-8"));
// グルーピング
const byArea = d3.group(raw, (d) => d.area_code);
const years = [...new Set(raw.map((d) => d.year))].sort();
// 共通スケール(最大値を全県統一)
const maxPer100k = d3.max(raw, (d) => d.per100k);
const x = d3.scaleLinear().domain(d3.extent(years)).range([0, innerW]);
const y = d3.scaleLinear().domain([0, maxPer100k]).nice().range([innerH, 0]);
// 全国平均
const nationalAvg = years.map((yr) => {
const ys = raw.filter((d) => d.year === yr).map((d) => d.per100k);
return { year: yr, per100k: d3.mean(ys) };
});
const line = d3.line()
.x((d) => x(d.year))
.y((d) => y(d.per100k))
.curve(d3.curveMonotoneX);
export function buildMini(areaCode) {
const series = byArea.get(areaCode);
if (!series) throw new Error(`no data for ${areaCode}`);
const sorted = [...series].sort((a, b) => a.year - b.year);
const latest = sorted[sorted.length - 1];
const svg = d3.select(document.body).append("svg")
.attr("xmlns", "http://www.w3.org/2000/svg")
.attr("viewBox", `0 0 ${W} ${H}`)
.attr("width", W).attr("height", H);
const g = svg.append("g")
.attr("transform", `translate(${MARGIN.l},${MARGIN.t})`);
// 全国平均(背景)
g.append("path")
.datum(nationalAvg)
.attr("d", line)
.attr("stroke", "#94a3b8").attr("stroke-width", 1)
.attr("stroke-dasharray", "2 2").attr("fill", "none");
// 都道府県の線
g.append("path")
.datum(sorted)
.attr("d", line)
.attr("stroke", "#2563eb").attr("stroke-width", 1.8).attr("fill", "none");
// 県名(左上)
svg.append("text")
.attr("x", 4).attr("y", 13)
.attr("font-size", 11).attr("fill", "#0f172a")
.text(latest.area_name);
// 2024 年の値(右上)
svg.append("text")
.attr("x", W - 4).attr("y", 13)
.attr("text-anchor", "end")
.attr("font-size", 10).attr("fill", "#475569")
.text(`${latest.per100k.toFixed(0)}`);
const out = svg.node().outerHTML;
svg.remove();
return out;
}
ここで一番重要なのは y スケールが 47 県すべて共通 であることです。Small Multiple は「同じ軸でしか比較できない」という鉄の掟があります。県ごとに最大値を自動算出すると、見た目は綺麗になりますが、もはや Small Multiple ではなくただの 47 個のチャートです。
6. Step 4: グリッド配置(7 列 × 7 行)or 地理風配置
buildMini ができたら、あとは並べるだけ。Claude Code に「47 枚を 1 枚の親 SVG に集約してくれ」と投げます。
Claude Code への指示
buildMini を使って、47 都道府県を data/tile-layout-jp.json の (row, col) に従って配置した
1 枚の親 SVG を out/sm-jp.svg に書き出すスクリプトを書いて。
- 各 mini は 160 × 100 px
- セル間ギャップは 8 px
- 親 SVG のサイズは layout から自動算出
- 全国平均ラインの凡例を右下に小さく描く("--- 全国平均")
- 親 SVG の上部に title と subtitle を 1 行ずつ
生成されるコード(要点):
// build-grid.mjs
import fs from "node:fs";
import { buildMini } from "./build-mini.mjs";
const layout = JSON.parse(fs.readFileSync("data/tile-layout-jp.json", "utf-8"));
const CELL_W = 160, CELL_H = 100, GAP = 8, HEADER = 60;
const maxCol = Math.max(...Object.values(layout).map((l) => l.col));
const maxRow = Math.max(...Object.values(layout).map((l) => l.row));
const totalW = (maxCol + 1) * (CELL_W + GAP) - GAP;
const totalH = HEADER + (maxRow + 1) * (CELL_H + GAP) - GAP + 40;
let body = "";
for (const [areaCode, { row, col }] of Object.entries(layout)) {
const x = col * (CELL_W + GAP);
const yPx = HEADER + row * (CELL_H + GAP);
const mini = buildMini(areaCode).replace(/^<svg /, `<svg x="${x}" y="${yPx}" `);
body += mini;
}
const svg = `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${totalW} ${totalH}"
width="${totalW}" height="${totalH}" font-family="sans-serif">
<text x="0" y="22" font-size="20" font-weight="bold" fill="#0f172a">
都道府県別 刑法犯認知件数(人口10万人当たり)
</text>
<text x="0" y="44" font-size="13" fill="#475569">
2015 - 2024 / 警察庁・総務省統計局より作成 / 共通スケール
</text>
${body}
<g transform="translate(${totalW - 160},${totalH - 24})">
<line x1="0" y1="6" x2="20" y2="6" stroke="#94a3b8"
stroke-width="1" stroke-dasharray="2 2"/>
<text x="26" y="10" font-size="11" fill="#475569">全国平均</text>
</g>
</svg>`;
fs.writeFileSync("out/sm-jp.svg", svg);
console.log(`wrote out/sm-jp.svg (${(svg.length / 1024).toFixed(1)} KB)`);
個別 SVG 47 枚版
ブログ本文では 1 枚版を埋め込み、SNS 用には 1 県 1 枚で出力したいケースが多いです。buildMini を流用して 47 ファイルに書き出す版を追加します。
// build-each.mjs
import fs from "node:fs";
import { buildMini } from "./build-mini.mjs";
const layout = JSON.parse(fs.readFileSync("data/tile-layout-jp.json", "utf-8"));
const outDir = "out/each";
fs.mkdirSync(outDir, { recursive: true });
for (const [areaCode, { area_name }] of Object.entries(layout)) {
const svg = buildMini(areaCode);
const safe = area_name.replace(/[都道府県]/g, "");
const path = `${outDir}/sm-${areaCode}-${safe}.svg`;
fs.writeFileSync(path, svg);
}
console.log(`wrote 47 svg files into ${outDir}/`);
実行:
node build-each.mjs
# wrote 47 svg files into out/each/
ls out/each/ | head -5
# sm-01000-北海道.svg
# sm-02000-青森.svg
# sm-03000-岩手.svg
# sm-04000-宮城.svg
# sm-05000-秋田.svg
これで Small Multiple 1 枚版 と 47 枚個別 SVG が同じ buildMini を共通基盤として手に入りました。
7. Step 5: 強調ハイライト(全国平均ライン)
Small Multiple は「ぼんやり眺める」ためのものですが、それだけでは記憶に残りません。読者の視線を 3 秒間ロックする仕掛けが必要です。
代表的なテクニックは 3 つ。
テクニック 1: 全国平均ラインを薄く重ねる(実装済み)
これは事実上必須。各 mini chart に「比較対象」がないと相対的な良し悪しが分かりません。今回のコードでは既に実装済み(#94a3b8 ダッシュ)。
テクニック 2: 上位・下位の県だけ枠線を色付け
全国平均より明確に高い・低い県は、mini chart の枠線を赤・青にしておくと、Small Multiple 全体を見たときに「赤いセルが集中している地域」が浮き出ます。
const nationalLatest = d3.mean(
raw.filter((d) => d.year === 2024).map((d) => d.per100k)
);
const myLatest = latest.per100k;
const diff = (myLatest - nationalLatest) / nationalLatest;
const borderColor =
diff > 0.2 ? "#dc2626" : // 全国比 +20% 以上
diff < -0.2 ? "#2563eb" : // 全国比 -20% 以上
"#e2e8f0";
svg.append("rect")
.attr("x", 0.5).attr("y", 0.5)
.attr("width", W - 1).attr("height", H - 1)
.attr("fill", "none")
.attr("stroke", borderColor)
.attr("stroke-width", 1);
テクニック 3: 線の傾向で色分け(trend coloring)
10 年間で増加トレンドの県は線を赤、減少トレンドの県は線を青にする方法。線形回帰の傾きで判定します。
function slope(series) {
const n = series.length;
const meanX = d3.mean(series, (d) => d.year);
const meanY = d3.mean(series, (d) => d.per100k);
const num = d3.sum(series, (d) => (d.year - meanX) * (d.per100k - meanY));
const den = d3.sum(series, (d) => (d.year - meanX) ** 2);
return num / den;
}
const s = slope(sorted);
const strokeColor = s > 5 ? "#dc2626" : s < -5 ? "#2563eb" : "#475569";
3 つすべて適用すると情報過多になります。1 つだけ選ぶ のが鉄則。今回は記事の主題が「全国平均比較」なので、テクニック 1 + 2 の組み合わせを推奨します。
強調テクニックの使い分け表
| テクニック | 主張 | 視覚的負荷 | 実装行数 |
|---|---|---|---|
| 全国平均ライン重ね | 「平均と比べて高い/低い」 | 低 | 5 |
| 枠線色分け | 「目立つ県はどこか」 | 中 | 8 |
| 線色分け(trend) | 「増減トレンド」 | 中 | 12 |
| 上位 N 県だけ塗りつぶし | 「上位の集中」 | 高 | 6 |
8. つまずきポイント
実際にこのレシピを動かしてみると、いくつか必ず引っかかる罠があります。先回りで潰しておきます。
つまずき 1: 軸を統一し忘れる
buildMini の中で d3.extent(series, (d) => d.per100k) のように その県だけのデータから y スケールを作る と、見た目は綺麗ですがデータの嘘が始まります。沖縄(200 件台)と東京(600 件台)の mini chart が同じ縦幅で描かれてしまい、「沖縄も似たような傾向」と誤読されます。
正解: スケールは外で 1 回だけ作り、buildMini の中ではそれを参照するだけ。コード再掲:
const maxPer100k = d3.max(raw, (d) => d.per100k);
const y = d3.scaleLinear().domain([0, maxPer100k]).nice().range([innerH, 0]);
つまずき 2: 47 枚のラベルが読めない
160 × 100 px に「県名 + 値 + ライン」を詰め込むと、フォントサイズの調整が苦しくなります。経験則:
- 県名: 11 px(10 px だと iPhone で潰れる、12 px だと「東京都」が窮屈)
- 数値: 10 px(補助情報なので小さくて OK)
- 余白: 左 4 px / 上 2 px を絶対確保
実機(iPhone 15 Pro / Pixel 8)で 1 度確認すると安心。Chrome DevTools の Mobile Emulator では満足してはいけません。実機で見ると、Retina ディスプレイで line が消える現象に気づきます。stroke-width: 1 ではなく 1.5 以上にすべきです。
つまずき 3: 出力 SVG サイズが想定外に巨大
buildMini を 47 回呼ぶと、SVG 1 枚あたり 5-8 KB として、合計 300 KB 前後になります。生のままブログに <img> で埋め込むと LCP に響きます。
対策:
- D3 が出力する小数を
path内で丸める(.toFixed(1)まで) - 不要な
font-family属性を親 SVG 1 箇所に集約 - 改行を全削除して 1 行 SVG にする
- 最終的に SVGO で minify
npx svgo --multipass --pretty=false out/sm-jp.svg -o out/sm-jp.min.svg
# wrote out/sm-jp.min.svg (saving 38%)
stats47.jp 本番では <img> ではなく <object data="...svg" /> で埋め込み、SVG を gzip 配信させます。text/svg+xml の gzip 圧縮率は 70-80% が出るので、転送量は 60-90 KB に収まります。
つまずき 4: 47 県の地理配置が「らしく」見えない
タイルマップは奥が深く、北海道の幅・沖縄の位置・四国の浮き具合などで違和感が出ます。Claude Code に「data/tile-layout-jp.json の sanity check をして」と投げると、距離行列を取って隣接県が遠すぎる箇所を指摘してくれます。
data/tile-layout-jp.json を読んで、
実際の地理的に隣接している都道府県(陸続き or フェリー直通)が
グリッド上で 2 セル以上離れている組み合わせを列挙して。
例: 北海道(01000)と 青森県(02000)は地理的に隣接だが、
グリッドが (0,7) と (1,6) なので距離 1.41。OK。
返ってくる「離れすぎ警告」を 3-5 回反映すると、見た目が一気に「らしく」なります。
つまずき 5: D3 の SSR 環境構築でハマる
jsdom を使う Node スクリプトで D3 を動かすとき、d3.select(document.body) がエラーになる場合があります。原因はだいたい document の参照が global ではなく dom.window 経由。
// NG(global document が undefined)
import * as d3 from "d3";
d3.select(document.body); // ReferenceError
// OK
import { JSDOM } from "jsdom";
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
const document = dom.window.document;
d3.select(document.body);
または global.document = dom.window.document してから D3 を読む方法もあります。stats47 の apps/web 内で動かすときは next.config.ts の SSR とぶつかるので、この種のスクリプトは apps/web の外(packages/visualization/scripts/ 等)に置く のが定石。
つまずきポイント早見表
| # | 症状 | 原因 | 対策 |
|---|---|---|---|
| 1 | 全県のラインが同じ高さに見える | y スケールが県ごと | 共通スケールに統一 |
| 2 | iPhone でラベルが潰れる | font-size 10 以下 | 県名 11 px / 数値 10 px |
| 3 | LCP が悪化する | SVG 300 KB | SVGO + gzip 配信 |
| 4 | 地理配置に違和感 | 手動配置の精度不足 | tile-layout の sanity check |
| 5 | D3 が SSR で動かない | global document なし | jsdom + 明示参照 |
9. 次回予告(Part 16: バブルチャート)
Part 15 はここまで。次回 Part 16 は「バブルチャート」を扱います。
バブルチャートは Small Multiple と対極の存在です。47 県を 1 枚のチャートに「散布図 + 円のサイズ = 第 3 軸」で詰め込みます。情報密度は最強。ただし、円が重なって読めなくなる、サイズ知覚バイアス、外れ値の処理など、Small Multiple とは別系統の罠が山ほどあります。
予定している題材:
- 県内総生産(GDP)× 県民所得 × 人口
- 高齢化率 × 出生率 × 人口
- 平均寿命 × 医療費 × 人口
Claude Code 視点では「Force-directed で重なりを回避する」「ラベル衝突回避」「ツールチップ実装」あたりが見どころです。
10. 関連ランキング・記事
同シリーズ
- Part 14: 電力消費の積み上げ面|stack 系列の順序を会話で決める Claude Code
- Part 13: 農業産出額の流れをサンキー|分類軸を Claude Code に提案させる
- Part 12: 都道府県別住宅着工をツリーマップ|階層データ整形を Claude Code に頼む
- Part 11: 観光客の Stacked Bar|Claude Code
関連ランキング(stats47.jp 内)
参考資料
- 警察庁 犯罪統計
- 総務省統計局 人口推計
- Edward Tufte "Envisioning Information"
- D3.js v7 公式ドキュメント
- stats47 e-Stat API 利用規約
Small Multiple は「47 都道府県」というカーディナリティと完璧に噛み合います。Claude Code を使えば、データ取得 → 正規化 → mini chart 関数 → 47 枚一括出力までを 1 セッション 30 分で組み上げられます。手作業で 47 枚を Illustrator で並べていた時代と比べると、生産性は 30 倍くらい違うはず。
Part 16 のバブルチャートでも、また同じ流れ(/fetch-estat-data → 関数化 → 一括レンダリング)で攻めます。お楽しみに。