積み上げ面で「構成変化」を読む
折れ線グラフが時系列の「絶対値の推移」を表すグラフだとしたら、積み上げ面チャート(Stacked Area Chart)は 「構成比の推移」 を表すグラフです。家庭用・産業用・業務用の電力消費が、過去 30 年でどう入れ替わってきたか。総量はだいたい横ばいでも、内訳の比率はまったく違う絵が描けたりします。
ところがこの「積み上げ面」、自分の手で書こうとすると意外に詰まる場面が多い。系列の 並び順(一番下にどれを置くか、上にどれを置くか)、ベースライン(0 から積むのか中央から積むのか)、正規化(絶対値か 100% か)——選択肢が多すぎて、最初の 1 枚を出すまでに半日溶けたりします。
D3.js には d3.stack() という標準モジュールがあって、これらの選択を stackOrder と stackOffset という 2 つのオプションで切り替えられます。本記事の主役はまさにここ。
Claude Code に 「家庭用は変動が大きいから下に置きたくない」 とか 「全体量より構成比を強調したい」 といった 目的を自然言語で伝える だけで、4 種類 × 4 種類 = 16 通りある組み合わせの中から、適切な 1 つを提案してもらうワークフローを紹介します。
シリーズ全 20 本のうち Part 14。前回までで棒グラフ・ヒートマップ・コロプレス・散布図・レーダー・箱ひげ・bar chart race などを扱ってきました。今回扱う積み上げ面とその派生である ストリームグラフ(stream graph) は、時系列データを 1 枚で見せる手法としてはひと味違う表現力を持っています。
この記事のゴールは次の 3 点です。
- e-Stat の電力消費統計から「用途別 × 年次」の時系列データを構築する
- D3 + React で積み上げ面チャートを描く(コード一式公開)
stackOrder/stackOffsetの選択を Claude Code との会話で決める
それでは始めましょう。
使うデータ: 電力消費統計(家庭用 / 産業用 / 業務用)
今回扱うのは「用途別電力消費量」です。総務省統計局や経済産業省・資源エネルギー庁が公表しており、e-Stat にも複数の収載先があります。データの素性として知っておきたいのは次の点。
- 用途区分: 大まかに「家庭用(電灯)」「産業用(高圧・特別高圧の工場向け)」「業務用(オフィス・商業施設)」の 3 つに分かれる
- 単位: GWh(ギガワット時)または億 kWh が主流
- 時系列: 月次データもあるが、本記事では 1990〜2025 の 年次データ を扱う
- 粒度: 都道府県別もあるが、今回は 全国計の用途別時系列 を主軸にする
主な取得元候補は次のとおりです。
| 統計表 | statsDataId(例) | 特徴 |
|---|---|---|
| 電力需給の概要(資源エネルギー庁) | 0003234567 | 用途別の月次・年次。最も網羅性が高い |
| 都道府県別エネルギー消費統計 | 0003345678 | 47 都道府県 × 部門別。時系列はやや短い |
| エネルギー白書(年次集計) | 0003456789 | 確報ベース。1990 以降の長期 |
注記: statsDataId は e-Stat 側の改訂で変わります。Part 2 で紹介した
/search-estatスキルで「電力消費 用途別」と打って最新の ID を確認してください。
時系列の積み上げ面では「系列数が多すぎないこと」が読みやすさの鍵です。家庭用・産業用・業務用の 3 系列にとどめ、必要なら「その他(運輸・農業など)」を 4 つ目として加える程度に抑えるのが定石。10 系列を積み上げたチャートは、ほぼ確実に何が起きているかわからなくなります。
Step 1: e-Stat 取得 → 時系列に整形
stats47 のリポジトリには /fetch-estat-data というスキルがあって、e-Stat API の認証・キャッシュ・retry・年度フィルタを巻き取ってくれます。Part 1〜2 で組んだ道具立てを、Part 14 でも素直に使い回します。
Claude Code に投げるプロンプトはこんな感じ。
/fetch-estat-data を使って、電力需給統計(statsDataId=0003234567)から
全国計 × 用途別(家庭用・産業用・業務用・その他)× 年次(1990-2025)の
電力消費量(GWh)を取得し、JSON で保存してください。
要件:
- 用途分類は「家庭用 / 産業用 / 業務用 / その他」の4区分にまとめる
- 出力フォーマットは { year, household, industry, business, other } の配列
- 単位は GWh に統一(億kWh で配信されていたら ×100 で換算)
- 保存先: /tmp/electricity-by-use.json
- 欠損年度(途中で区分が変わった年など)は null を入れて保持
押さえておきたいポイントが 3 つあります。
- e-Stat 取得は
/fetch-estat-dataに丸投げ。cdTimeFrom/Toを使わずに全年度取得→メモリでフィルタというのが stats47 の規約(.claude/rules/estat-api.md)。R2 キャッシュキーが分散しないためです。 - 用途区分の正規化を最初に決める。e-Stat 側で「家庭用電灯」「業務用電力」のように細かく分かれていることが多いので、4 区分にまとめる粒度を明示しないと、20 列の謎データが返ってきます。
- 欠損は
nullで残す。0を入れてしまうと、後でストリームグラフを描いたときに「その年だけくびれる」異常な絵になります。
実行すると、こんな JSON ができあがります。
[
{
"year": 1990,
"household": 187300,
"industry": 372100,
"business": 198400,
"other": 64200
},
{
"year": 2000,
"household": 251800,
"industry": 384700,
"business": 271600,
"other": 71500
},
{
"year": 2010,
"household": 286400,
"industry": 365200,
"business": 318900,
"other": 78100
},
{
"year": 2020,
"household": 281500,
"industry": 312700,
"business": 295800,
"other": 71300
},
{
"year": 2025,
"household": 278100,
"industry": 305400,
"business": 289200,
"other": 70800
}
]
値はあくまで説明用の例示です。実行時の API 戻り値をそのまま使ってください。
「年 × 4 系列」の wide フォーマット。これが D3 の stack() にそのまま食わせられる形です。
Step 2: d3.stack の stackOrder を比較する
ここから本題。D3 の d3.stack() は次のような関数を返します。
const stack = d3.stack()
.keys(["household", "industry", "business", "other"])
.order(d3.stackOrderNone)
.offset(d3.stackOffsetNone);
const series = stack(data);
// series は [
// [[y0, y1], [y0, y1], ...], // household
// [[y0, y1], [y0, y1], ...], // industry
// ...
// ]
各系列ごとに「各年の [下端, 上端]」のペアが返ってくる。あとは d3.area() を被せれば描けます。
ここで重要なのが .order() の選択。同じデータでも、系列の重ね順を変えると 見える物語がガラッと変わる。
| stackOrder | 動作 | 向いている場面 |
|---|---|---|
stackOrderNone | .keys() で指定した順 | 順序に意味がある(用途を意味のある並びで固定) |
stackOrderAscending | 合計値が小さい系列を下 | 「上に行くほど大きい系列」と読ませたい |
stackOrderDescending | 合計値が大きい系列を下 | 「下に行くほど大きい系列」が安定して見える |
stackOrderInsideOut | ピークが中央の系列を中央に配置 | ストリームグラフ向き。後述 |
stackOrderReverse | keys() の逆順 | レジェンドと積み上げの順序を揃えたいとき |
特に stackOrderInsideOut はストリームグラフのために生まれたような並べ替えで、「ピーク時期が早い系列を端に、遅い系列も端に、中盤がピークの系列を真ん中に」配置してくれます。波打つ形が美しくなるのはこの並べ方が前提です。
実際に同じデータで 4 種類のオーダーを並べてみると、印象がまったく違います。
電力消費の場合、私の感覚では次のように使い分けています。
- 読者が業界関係者(電力会社・行政)→
stackOrderNoneで「家庭用・業務用・産業用・その他」の業界慣習順 - 読者が一般読者 →
stackOrderDescendingで「下に大きい系列」が安定して見える - ストーリーが「構成の変化」自体 →
stackOrderInsideOutでストリームグラフ化
Step 3: stackOffset でベースラインを切り替える
.offset() はベースライン(一番下の位置)の決め方を切り替えます。これも 4 種類。
| stackOffset | 動作 | 向いている場面 |
|---|---|---|
stackOffsetNone | y = 0 から積む(通常の積み上げ) | 絶対値の推移を見せたい |
stackOffsetExpand | 各時点で合計を 1(100%)に正規化 | 構成比の推移を見せたい |
stackOffsetSilhouette | 中央線を上下対称にする | ストリームグラフ(対称型) |
stackOffsetWiggle | 各系列の傾きの 2 乗誤差を最小化 | ストリームグラフ(変動を抑える) |
stackOffsetExpand を使うと、合計が常に 100% に揃った積み上げ面が描けます。電力消費の場合「総量はあまり変わらないけれど、産業用が減って業務用が増えている」という構図がはっきり見える。絶対値の増減を隠して構成比の変化を強調する 表現として有効です。
一方で stackOffsetWiggle は、ストリームグラフ専用のオフセット。各系列の「揺れ幅」が最小になるようにベースラインを調整します。家庭用の急増や産業用の減少が「川の流れ」のように滑らかに見える のが特徴で、データジャーナリズム系の記事でよく見るあの絵です。
stackOffsetSilhouette も似ていますが、こちらは単純に「中央線で上下対称」にするだけ。Wiggle ほど洗練された見た目にはなりませんが、計算が軽くて安定します。
ベースラインの比較を 1 枚に並べると、こうなります。
「同じデータが 4 通りの物語を語る」というのは積み上げ面チャートの面白さでもあり、怖さでもあります。どれを選ぶかは、伝えたいメッセージで決める。それを言語化してから D3 のオプションに翻訳する、というのが Claude Code との会話の出番です。
Step 4: Claude Code に「stream graph に」と頼む
ここからが今回の山場。stackOrder と stackOffset の組み合わせは 4 × 4 = 16 通りあり、機械的に総当たりするのは現実的じゃない。かと言って D3 のドキュメントを読み込んで「Wiggle と InsideOut を組み合わせる」と判断するのも、慣れていないと時間がかかります。
そこで Claude Code に 「データの性質」と「伝えたいこと」 を投げて、適切な組み合わせを提案してもらう。
たとえば、こんなプロンプトを投げます。
電力消費の時系列データ(1990-2025、家庭用・産業用・業務用・その他の4系列)を
積み上げ面チャートで可視化します。次の特徴を踏まえて、d3.stack の order と
offset の組み合わせを提案してください。
データの特徴:
- 総量は1990 → 2010で +15% 増、2010 → 2025で -10% 減
- 産業用は2010以降ずっと減少(製造業の海外移転)
- 家庭用は2020までは横ばい、以降微減
- 業務用は単調増加(オフィス・データセンター需要)
- その他(運輸など)はほぼ横ばい
伝えたいメッセージ:
「総量が頭打ちになる中で、産業用から業務用への重心シフトが進んでいる」
要件:
- d3.stackOrder* と d3.stackOffset* のどれを使うか
- 候補を3つ挙げて、それぞれの強み/弱みを比較
- 色設計の方針もセットで提案
実際に返ってくる回答(要約)。
| 候補 | order | offset | 強み | 弱み |
|---|---|---|---|---|
| 1 | stackOrderInsideOut | stackOffsetWiggle | 構成変化の物語を最も雄弁に語る。stream graph の美観 | 絶対値が読みづらい。報告書向きではない |
| 2 | stackOrderDescending | stackOffsetExpand | 構成比を 100% 正規化で見せる。シェアの変化が一目瞭然 | 総量の頭打ちが見えなくなる |
| 3 | stackOrderNone(業界順固定) | stackOffsetNone(0 ベース) | 絶対値と構成の両方が読める。最も「正統」 | インパクトが弱く、変化の物語性に欠ける |
候補 1 は「インパクト重視」、候補 2 は「構成比を強調」、候補 3 は「報告書としての正確性」。それぞれ別の文脈で使い分けるべき、というのが結論として返ってきました。
私はこの記事のメインビジュアルとして 候補 2(Descending × Expand) を採用しました。「総量より構成」を見せたい記事構成だからです。同時に、Step 3 で並べた 4 種比較画像のように 候補 3 もサブ画像として残す。読者が「絶対値はどう動いているのか」を補足で見られるようにする、という二段構えにしました。
Claude Code に頼むメリットは、自分が無意識に避けていた組み合わせを提示してくれる ところ。私は最初 Wiggle × InsideOut のストリームグラフを「派手すぎる」と思って候補から外していたのですが、提案され「データの動きが大きいからこそ Wiggle がハマる」と言われて見方が変わりました。
実際の React + D3 のコードはこんな感じです(要点を抜粋)。
import * as d3 from "d3";
import { useMemo } from "react";
type Datum = {
year: number;
household: number;
industry: number;
business: number;
other: number;
};
const KEYS = ["industry", "business", "household", "other"] as const;
const COLORS: Record<(typeof KEYS)[number], string> = {
industry: "#1f77b4", // 産業用: 青
business: "#ff7f0e", // 業務用: オレンジ
household: "#2ca02c", // 家庭用: 緑
other: "#9467bd", // その他: 紫
};
export function ElectricityStackedArea({
data,
width = 720,
height = 360,
mode = "stream",
}: {
data: Datum[];
width?: number;
height?: number;
mode?: "normal" | "expand" | "stream";
}) {
const margin = { top: 20, right: 120, bottom: 36, left: 56 };
const innerW = width - margin.left - margin.right;
const innerH = height - margin.top - margin.bottom;
const series = useMemo(() => {
const stack = d3.stack<Datum>().keys([...KEYS]);
if (mode === "expand") {
stack.order(d3.stackOrderDescending).offset(d3.stackOffsetExpand);
} else if (mode === "stream") {
stack.order(d3.stackOrderInsideOut).offset(d3.stackOffsetWiggle);
} else {
stack.order(d3.stackOrderNone).offset(d3.stackOffsetNone);
}
return stack(data);
}, [data, mode]);
const x = useMemo(
() =>
d3
.scaleLinear()
.domain(d3.extent(data, (d) => d.year) as [number, number])
.range([0, innerW]),
[data, innerW]
);
const y = useMemo(() => {
const flat = series.flat(2);
return d3
.scaleLinear()
.domain([d3.min(flat) ?? 0, d3.max(flat) ?? 1])
.nice()
.range([innerH, 0]);
}, [series, innerH]);
const area = d3
.area<[number, number] & { data: Datum }>()
.x((d) => x(d.data.year))
.y0((d) => y(d[0]))
.y1((d) => y(d[1]))
.curve(d3.curveCatmullRom.alpha(0.5));
return (
<svg width={width} height={height} role="img" aria-label="電力消費の用途別積み上げ面">
<g transform={`translate(${margin.left},${margin.top})`}>
{series.map((s) => (
<path
key={s.key}
d={area(s as never) ?? undefined}
fill={COLORS[s.key as (typeof KEYS)[number]]}
opacity={0.85}
/>
))}
<g transform={`translate(0,${innerH})`}>
{x.ticks(8).map((t) => (
<text key={t} x={x(t)} y={20} fontSize={11} textAnchor="middle">
{t}
</text>
))}
</g>
{mode !== "stream" && (
<g>
{y.ticks(5).map((t) => (
<text
key={t}
x={-8}
y={y(t)}
fontSize={11}
textAnchor="end"
dominantBaseline="middle"
>
{mode === "expand" ? `${Math.round(t * 100)}%` : t.toLocaleString()}
</text>
))}
</g>
)}
</g>
</svg>
);
}
mode プロパティを "normal" | "expand" | "stream" で受けて、3 種類の表現を 1 コンポーネントで切り替えられるようにしてあります。コードの差分は .order() と .offset() の 2 行だけ。これが D3 の d3.stack() モジュール設計のうまいところで、表現を切り替えるためにロジック本体を書き換える必要がありません。
curve(d3.curveCatmullRom.alpha(0.5)) で曲線補間を入れているのもポイント。直線補間(curveLinear)だと積み上げ面がカクカクして見栄えが悪く、特に stream graph では滑らかさが命なので Catmull-Rom か Basis を入れます。
Step 5: 色設計 — 用途ごとに「意味色」を与える
積み上げ面の色は、単なる識別ラベルではなく 意味を持たせる ことができます。電力消費の例だと、用途ごとに連想される色があって、それを当てはめると読者が凡例を見ずに区別できるようになります。
私が採用しているマッピングはこちら。
| 用途 | 色(カラーコード) | 連想・理由 |
|---|---|---|
| 産業用 | #1f77b4(青) | 工場・重工業・落ち着いた色合い |
| 業務用 | #ff7f0e(オレンジ) | オフィス照明・温かみ・拡大基調 |
| 家庭用 | #2ca02c(緑) | 生活・電灯・身近さ |
| その他 | #9467bd(紫) | 運輸・農業など多様な内訳 |
これは d3-scale-chromatic の schemeTableau10 を意識した配色で、色覚バリアフリーの観点からもバランスが取れています。schemeCategory10 の古典色も候補ですが、Tableau10 のほうがコントラストが穏やかで、積み上げ面のような 面で見せるグラフ に向いています。
色を決めるときに気をつけたい NG パターンが 3 つあります。
- 似た色相を隣に配置しない。
industry(青)の上にbusiness(水色)を置くと境界がボヤけます。今回はオレンジを挟むことで明確に分離。 - 全部パステルにしない。淡い色だけで構成すると、面が重なる部分が灰色っぽく濁って見える。1 つは彩度の高い色を入れる(今回はオレンジ)。
- 赤を使うときは慎重に。赤は「警告・減少」の連想が強いので、産業用の減少を示したい時くらいに限定する。
色について Claude Code に頼むと、schemeTableau10 ベースの提案がほぼ即座に返ってきます。「e-Stat の電力データで、産業用が減って業務用が増える物語を伝えたい」と一言添えるだけで、ストーリーに合った色順まで提案してくれるのが頼もしいところ。
つまずきポイントと対処
積み上げ面チャートを実装していて、私が実際に踏んだ落とし穴を 3 つ共有します。
1. 負値を含むと積み上げが破綻する
d3.stack() は基本的に 正の値の積み上げ を想定しています。負値が混じると [y0, y1] の関係が逆転して、面が反転したり重なったりします。
電力消費のような統計データでは負値はあまり出ませんが、たとえば「自然エネルギーの自家発電を消費から差し引く」といった処理を入れると、ある時点だけ負値になることがあります。対策は次の 3 通り。
- 負値の系列を別チャート(折れ線など)として独立させる
- ベースラインを 0 ではなく最小値に下げる(
stackOffsetDivergingを使う) - 入力データ側で負値を 0 にクリップ(情報損失と引き換え)
d3.stackOffsetDiverging は 5 種類目の offset で、正値は上に・負値は下にそれぞれ積み上げる挙動になります。
2. 欠損年度をどう扱うか
積み上げ面は 連続した時系列 を前提としています。途中の年度が抜けると、area path のジオメトリが破綻します。
対応パターンは 3 つ。
- 線形補間で埋める: D3 で
d3.curveCatmullRomを指定し、欠損nullを許容しない設計に。欠損があることを隠蔽してしまう のでデータの透明性は落ちる - 欠損年度はチャートから除外: 連続部分のみを表示し、欠損は注釈で「○○年データなし」と表記
- 欠損を別色で塗る: 灰色で「データなし」帯を入れる。視覚的にもっとも誠実だが実装は重い
電力統計の場合、用途分類の改定で「2007 年からその他の定義が変わった」みたいなケースがあるので、chart に注釈を入れる のが基本姿勢です。<text> で「2007 年に区分改定」と書き添えるだけでも、読者は十分察してくれます。
3. 軸スケールを固定するか自動か
stackOffsetExpand を使うと y 軸は常に [0, 1]、stackOffsetWiggle を使うと中央線がデータ依存で揺れます。これらを 同じページで切り替え可能なチャートにする 場合、y 軸の表記が頻繁に変わるので「あれ、何の軸だっけ?」と読者を混乱させがちです。
対策としては次のいずれか。
- モード切替時に y 軸を非表示にする (特に stream graph では y 軸の数字に意味がないので非表示が定石)
- y 軸ラベルをモードごとに切り替える(
%/GWh/(相対値)など) - そもそも切り替えチャートにせず、別チャートとして並べる
上記の React コンポーネント例では、mode !== "stream" の場合のみ y 軸ラベルを表示する形にしてあります。stream モードでは y 軸を消すことで「これは構成の物語であって絶対値ではない」と暗に伝わるようにする、という割り切りです。
まとめ
- 積み上げ面チャートは時系列の 構成変化 を 1 枚で見せる手法
- D3 の
d3.stack()は.order()と.offset()の 2 つで挙動が決まる stackOrder*は系列の重ね順(None / Ascending / Descending / InsideOut / Reverse)stackOffset*はベースラインの決め方(None / Expand / Silhouette / Wiggle / Diverging)- Claude Code に 「データの特徴」と「伝えたいメッセージ」 を伝えると、16 通りの組み合わせから適切な 1 つを提案してくれる
- 色は 用途ごとに意味色 を与えると凡例なしでも区別できる
- 負値・欠損・軸スケールは早めに方針を決めておく
d3.stack() という小さなモジュールに、これだけの表現バリエーションが詰まっているのは改めてすごい。そして「どれを選ぶか」という設計判断こそが、AI ペアプログラミングの醍醐味を一番感じる部分でもあります。
次回予告: Part 15 - Small Multiple
次回は Small Multiple(小さな複数)の手法を扱います。47 都道府県の時系列を「小さなチャートを 47 個並べる」という、Tufte が提唱した古典的だけど強力な可視化手法。1 枚の積み上げ面で見せきれない情報量を、視線を動かすだけで把握できるレイアウトです。
Claude Code との会話では、「47 個のサブチャートを並べたい。レイアウトとサイズを提案して」 という相談から始まる構成になります。次回もお楽しみに。
関連ランキング・記事
- 電力消費量ランキング(都道府県別)
- Part 11: 観光入込客数を積み上げ棒で|カテゴリ別グラデーション
- Part 13: 農業産出額の流れをサンキー|分類軸を Claude Code に提案させる
- Part 1: Claude Code × e-Stat 環境構築
- Part 2: /search-estat スキルで統計表 ID を会話で探す