全社ビジョン
頂点 ・ 配分 10%
「データで意思決定する会社になる」を3年後の到達点とする
- 経営会議への月次データレポート提出を必須化
- 全社KPIダッシュボードの経営陣閲覧率100%
型番 pyramid
ビジョン・施策・実行タスクを、重み比率から導出した3層の地層断面として示す。ピラミッド帯と階層レコードは同じ上下境界を共有し、hover・focusで対応する1層だけを追える
pyramid · hierarchy · strategy · data-driven · interactive
LIVE SAMPLE
補足の開閉・表示切替を試せます
ALTERNATIVES
型番 logic-tree
親IDで結んだ可変階層をD3のtidy treeで幹・枝・葉へ配置し、葉の寄与値を親へ自動集計して細い枝の線幅へ反映する。同じnode identityを構造から葉ドライバーの寄与度へ切り替え、構造では曲線・直線・直角の枝を閲覧者が選び、最大寄与の祖先経路を1件強調する
型番 process-flow
業務・システム導入の手順を一本の連続process laneとして描き、各工程の区間長を所要日数からデータ駆動で計算する。hand-off境界には開始からの累計日数を置き、hover・focusでは同じ工程の区間とレコードだけを穏やかに強調する。最も時間のかかる工程をlane上で自動強調し、補足は図の下で展開する
型番 matrix-2x2
項目を2軸のスコアで座標平面に自動配置し、優先順位やポジショニングをデータ駆動で示す。補足は図の下で明示的に展開し、マップ⇄リストを切り替えられる
import type { PyramidProps } from "./schema";
/** 重みを高さ比率(%)に正規化。全重み0以下の退化ケースは均等割りにする */
export const normalizeWeights = (tiers: { weight: number }[]): number[] => {
const total = tiers.reduce((s, t) => s + Math.max(0, t.weight), 0);
if (total <= 0) return tiers.map(() => 100 / tiers.length);
return tiers.map((t) => (Math.max(0, t.weight) / total) * 100);
};
/**
* 各帯の頂点からの累積位置(%)。真の三角形では「頂点からの垂直位置」と
* 「その高さでのテーパー幅」が同じ比率になるため、topWidthPct/bottomWidthPct
* は帯の縦位置(top/height)と横のテーパー(clip-path)の両方に使い回せる。
*/
export const pyramidBands = (
tiers: { weight: number }[],
): { heightPct: number; topWidthPct: number; bottomWidthPct: number }[] => {
const heights = normalizeWeights(tiers);
let acc = 0;
return heights.map((h) => {
const topWidthPct = acc;
acc += h;
return { heightPct: h, topWidthPct, bottomWidthPct: acc };
});
};
/** 頂点→中間→底辺の固定indexを、帯・ガイド・ledgerの共有identityへ使う */
const TIER_STYLE = [
{
band: "bg-tertiary",
record: "text-tertiary",
guide: "border-tertiary/55",
role: "頂点",
},
{
band: "bg-tertiary-container",
record: "text-secondary",
guide: "border-outline",
role: "重点",
},
{
band: "bg-neutral",
record: "text-secondary",
guide: "border-outline",
role: "基盤",
},
];
/** エントランスの時差。間隔は global.css の --stagger-step で統一 */
const stagger = (i: number, baseMs = 0) =>
`calc(${baseMs}ms + ${i} * var(--stagger-step))`;
export default function Pyramid({ title, subtitle, tiers }: PyramidProps) {
const bands = pyramidBands(tiers);
const notes = tiers.filter(
(tier) => tier.note || (tier.items && tier.items.length > 0),
);
return (
<section className="mx-auto max-w-slide bg-surface p-6 font-sans text-on-surface sm:p-12">
<header className="mb-8">
<h1 className="text-h1">{title}</h1>
{subtitle && (
<p className="mt-2 text-body-sm text-secondary">{subtitle}</p>
)}
</header>
<style>{`
[data-pyramid-map] [data-pyramid-band],
[data-pyramid-map] [data-pyramid-record],
[data-pyramid-map] [data-pyramid-guide] {
transition:
color 140ms ease,
background-color 140ms ease,
border-color 140ms ease,
opacity 140ms ease;
}
[data-pyramid-map]:has([data-pyramid-hotspot="0"]:is(:hover, :focus))
[data-pyramid-band="0"],
[data-pyramid-map]:has([data-pyramid-hotspot="1"]:is(:hover, :focus))
[data-pyramid-band="1"],
[data-pyramid-map]:has([data-pyramid-hotspot="2"]:is(:hover, :focus))
[data-pyramid-band="2"] {
background-color: var(--color-on-surface);
opacity: 1;
}
[data-pyramid-map]:has([data-pyramid-hotspot="0"]:is(:hover, :focus))
[data-pyramid-record="0"],
[data-pyramid-map]:has([data-pyramid-hotspot="1"]:is(:hover, :focus))
[data-pyramid-record="1"],
[data-pyramid-map]:has([data-pyramid-hotspot="2"]:is(:hover, :focus))
[data-pyramid-record="2"] {
color: var(--color-on-surface);
opacity: 1;
}
[data-pyramid-map]:has([data-pyramid-hotspot="0"]:is(:hover, :focus))
[data-pyramid-guide="0"],
[data-pyramid-map]:has([data-pyramid-hotspot="1"]:is(:hover, :focus))
[data-pyramid-guide="1"],
[data-pyramid-map]:has([data-pyramid-hotspot="2"]:is(:hover, :focus))
[data-pyramid-guide="2"] {
border-color: var(--color-on-surface);
opacity: 1;
}
[data-pyramid-map]:has([data-pyramid-hotspot="1"]:is(:hover, :focus))
:is([data-pyramid-band="0"], [data-pyramid-record="0"], [data-pyramid-guide="0"]),
[data-pyramid-map]:has([data-pyramid-hotspot="2"]:is(:hover, :focus))
:is([data-pyramid-band="0"], [data-pyramid-record="0"], [data-pyramid-guide="0"]) {
opacity: 0.34;
}
`}</style>
{/*
* 一つの重み付き座標系を左右で共有する「地層断面」。
* 左の三角帯と右の階層ledgerは同じ top/height を使い、
* mobileでも上下へ分離せず対応関係を保つ。
*/}
<div
data-pyramid-map="true"
className="grid h-80 grid-cols-[4.5rem_minmax(0,1fr)] gap-x-4 sm:h-96 sm:grid-cols-[16rem_minmax(0,1fr)] sm:gap-x-8"
>
<div className="relative h-full">
{tiers.map((_, i) => {
const band = bands[i];
const clipPath = `polygon(${50 - band.topWidthPct / 2}% 0%, ${50 + band.topWidthPct / 2}% 0%, ${50 + band.bottomWidthPct / 2}% 100%, ${50 - band.bottomWidthPct / 2}% 100%)`;
return (
<div key={i}>
<button
type="button"
data-pyramid-band={i}
data-pyramid-hotspot={i}
data-top-pct={band.topWidthPct}
data-height-pct={band.heightPct}
aria-label={`${tiers[i].label}、配分${Math.round(band.heightPct)}%`}
className={`absolute inset-x-0 cursor-default focus-visible:outline-none ${TIER_STYLE[i].band}`}
style={{
top: `${band.topWidthPct}%`,
height: `${band.heightPct}%`,
clipPath,
}}
/>
<span
data-pyramid-guide={i}
aria-hidden="true"
className={`pointer-events-none absolute right-0 border-t ${TIER_STYLE[i].guide}`}
style={{
top: `${band.topWidthPct}%`,
left: `${50 + band.topWidthPct / 2}%`,
}}
/>
</div>
);
})}
</div>
<div className="relative h-full min-w-0">
{tiers.map((tier, i) => (
<button
type="button"
key={i}
data-pyramid-record={i}
data-pyramid-hotspot={i}
data-top-pct={bands[i].topWidthPct}
data-height-pct={bands[i].heightPct}
className={`absolute inset-x-0 grid min-h-0 cursor-default grid-cols-[minmax(0,1fr)_auto] items-center gap-2 border-t pl-2 pr-1 text-left focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-on-surface sm:gap-4 sm:pl-4 sm:pr-3 ${i === tiers.length - 1 ? "border-b" : ""} ${TIER_STYLE[i].record} ${TIER_STYLE[i].guide}`}
style={{
top: `${bands[i].topWidthPct}%`,
height: `${bands[i].heightPct}%`,
}}
>
<span
aria-hidden="true"
className={`pointer-events-none absolute right-full top-0 w-4 border-t sm:w-8 ${TIER_STYLE[i].guide}`}
/>
<span
className="animate-rise flex min-w-0 items-baseline gap-3"
style={{ animationDelay: stagger(i, 150) }}
>
<span className="hidden shrink-0 font-mono text-label opacity-60 sm:inline">
{TIER_STYLE[i].role}
</span>
<span className="min-w-0 truncate text-label sm:text-h3">
{tier.label}
</span>
</span>
<span className="font-mono text-label tabular-nums">
{Math.round(bands[i].heightPct)}%
</span>
</button>
))}
</div>
</div>
{notes.length > 0 && (
<details
data-inline-disclosure="true"
className="group mt-6 border-y border-outline bg-surface-subtle"
>
<summary className="flex cursor-pointer list-none items-center px-4 py-3 text-label text-secondary focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tertiary">
<span>階層の補足を確認</span>
<span className="ml-auto font-mono">{notes.length}層</span>
<span
aria-hidden="true"
className="ml-2 text-tertiary transition-transform group-open:rotate-45"
>
+
</span>
</summary>
<div className="border-t border-outline px-4">
{notes.map((tier) => {
const index = tiers.indexOf(tier);
return (
<article
key={index}
data-note-item="true"
className="grid gap-2 border-b border-outline py-4 last:border-b-0 sm:grid-cols-[11rem_minmax(0,1fr)] sm:gap-6"
>
<div>
<h2 className="text-label">{tier.label}</h2>
<p className="mt-1 font-mono text-label text-secondary">
{TIER_STYLE[index].role} ・ 配分{" "}
{Math.round(bands[index].heightPct)}%
</p>
</div>
<div className="min-w-0">
{tier.note && (
<p className="text-body-sm">{tier.note}</p>
)}
{tier.items && tier.items.length > 0 && (
<ul className="mt-2 grid gap-x-6 gap-y-1 text-body-sm sm:grid-cols-2">
{tier.items.map((item, itemIndex) => (
<li key={itemIndex} className="flex gap-2">
<span aria-hidden="true" className="text-tertiary">
—
</span>
<span>{item}</span>
</li>
))}
</ul>
)}
</div>
</article>
);
})}
</div>
</details>
)}
</section>
);
}
import { z } from "zod";
export const pyramidTierSchema = z
.object({
label: z.string(),
weight: z
.number()
.describe(
"この階層の重み(予算配分比率・工数比率など)。帯の高さ配分に使う",
),
note: z.string().optional().describe("階層の補足説明"),
items: z
.array(z.string())
.optional()
.describe("details で展開する明細(施策・タスク等)"),
})
.strict();
export const schema = z
.object({
title: z.string().describe("スライドタイトル。結論を書くメッセージラインを推奨"),
subtitle: z.string().optional().describe("補足・出典など"),
tiers: z
.tuple([pyramidTierSchema, pyramidTierSchema, pyramidTierSchema])
.describe("頂点(apex)→中間(mid)→底辺(base) の固定3階層"),
})
.strict();
export type PyramidTier = z.infer<typeof pyramidTierSchema>;
export type PyramidProps = z.infer<typeof schema>;
{
"title": "全社DX戦略ピラミッド — ビジョンから実行タスクまで一気通貫",
"subtitle": "帯の高さは予算配分比率に比例(2026年度 全社DX戦略・サンプル)",
"tiers": [
{
"label": "全社ビジョン",
"weight": 10,
"note": "「データで意思決定する会社になる」を3年後の到達点とする",
"items": [
"経営会議への月次データレポート提出を必須化",
"全社KPIダッシュボードの経営陣閲覧率100%"
]
},
{
"label": "重点施策(今期)",
"weight": 30,
"note": "ビジョン実現に向けた今期の重点テーマ3本",
"items": [
"基幹データ基盤の統合",
"部門横断KPIの定義統一",
"意思決定会議へのデータ活用ルール導入"
]
},
{
"label": "実行タスク",
"weight": 60,
"note": "各部門が今期着手する具体タスク群",
"items": [
"営業: 案件データの一元化",
"経理: 月次レポート自動化",
"人事: 稼働データの可視化",
"IT: データ基盤のセキュリティ強化"
]
}
]
}