BizLayout

型番 pyramid

ピラミッド(戦略階層)

ビジョン・施策・実行タスクを、重み比率から導出した3層の地層断面として示す。ピラミッド帯と階層レコードは同じ上下境界を共有し、hover・focusで対応する1層だけを追える

pyramid · hierarchy · strategy · data-driven · interactive

LIVE SAMPLE

プレビュー

補足の開閉・表示切替を試せます

全社DX戦略ピラミッド — ビジョンから実行タスクまで一気通貫

帯の高さは予算配分比率に比例(2026年度 全社DX戦略・サンプル)

階層の補足を確認3

全社ビジョン

頂点 ・ 配分 10%

「データで意思決定する会社になる」を3年後の到達点とする

  • 経営会議への月次データレポート提出を必須化
  • 全社KPIダッシュボードの経営陣閲覧率100%

重点施策(今期)

重点 ・ 配分 30%

ビジョン実現に向けた今期の重点テーマ3本

  • 基幹データ基盤の統合
  • 部門横断KPIの定義統一
  • 意思決定会議へのデータ活用ルール導入

実行タスク

基盤 ・ 配分 60%

各部門が今期着手する具体タスク群

  • 営業: 案件データの一元化
  • 経理: 月次レポート自動化
  • 人事: 稼働データの可視化
  • IT: データ基盤のセキュリティ強化

こういうときに使う

  • 全社戦略・中期計画のビジョンから実行タスクへの落とし込み
  • 組織階層や意思決定プロセスの整理
  • 優先順位が上位ほど絞り込まれる構造の提示(Maslow型の要件整理等)

ALTERNATIVES

条件を変えて探す →

データスキーマ

title
string — スライドタイトル。結論を書くメッセージラインを推奨
subtitle
string(任意)— 補足・出典など
tiers
[頂点(apex), 中間(mid), 底辺(base)] の固定3要素。{ label, weight, note?, items? }。weight の比率でピラミッド帯と右側レコードの共通高さ・境界を自動計算。note と items は主図下の補足で展開

サンプルコード

template.tsx254 行
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>
  );
}
schema.ts30 行
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>;
sample-data.json36 行
{
  "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: データ基盤のセキュリティ強化"
      ]
    }
  ]
}