索引に戻る

バリューチェーンマップ(改善余地)

フロー
生成可能
バリューチェーンマップ(改善余地)

作品として見る

持ち帰る

コピーして貼るか、ファイルに保存して渡す。ネットにつながらない環境には「全部入り」を 1 つ渡せば足りる。

全部入り

value-chain-map-everything.md

このパネルの中身を、URL の先にあるファイルの中身まで含めて 1 つにまとめた Markdown

依頼文

1.4 KB
「バリューチェーンマップ(改善余地)」でHTML資料を作ってください。この型は実行できるので、版面を組み直す必要はありません — 必要なのは中身です。

## この型がすること
主活動の幅をコスト比率、高さを価値寄与へ写し、最大の価値改善余地を導出する

## 渡す必要があるもの
- 主活動と支援活動
- 活動ごとのコスト
- 活動ごとの価値寄与

## 入力の形
- `primaryActivities` — { id, label, cost, value, note? }[] — 4〜8主活動
- `supportActivities` — string[] — 1〜4支援活動

入力の機械可読な検証は schema.ts が持ちます。足りない入力は推測せず質問してください。

## 向かない場合
- 起点から終点への増減要因を示したい場合
- 能力を事業ドメインへ包含したい場合

## 作り方
入力を JSON にまとめ、次を実行します。

```bash
node scripts/render.mjs value-chain-map <入力.json> --theme ink -o out.html
```

出力は 1 ファイルで完結し、外部から何も読み込みません。

## 原本
- https://design-vault.pages.dev/designs/value-chain-map/source/template.tsx
- https://design-vault.pages.dev/designs/value-chain-map/source/schema.ts
- https://design-vault.pages.dev/designs/value-chain-map/source/sample-data.json
- https://design-vault.pages.dev/designs/value-chain-map/sample.html

ファイル

テンプレート・入力スキーマ・サンプルデータ・メタ情報。この文書はこの 4 つから生成されています。 生成された文書

template.tsx

11.2 KB

/designs/value-chain-map/source/template.tsx

中身を見る
import { useId } from "react";
import type { ValueActivity, ValueChainMapProps } from "./schema";

const round = (value: number) => Number(value.toFixed(2));

// 2 つの識別子を `-` で繋ぐと単射にならない —— ("a-b","c") と ("a","b-c") が
// 同じ鍵になり、React の key が重複する。区切りに使う文字を各辺で逃がしてから繋ぐ。
const joinIds = (...parts: (string | number)[]) =>
  parts.map((p) => String(p).replace(/[_-]/g, (c) => `_${c.charCodeAt(0).toString(16)}_`)).join("-");

export function activityGeometry(activities: ValueActivity[]) {
  const total = activities.reduce((sum, item) => sum + item.cost, 0);
  let cumulative = 0;
  return activities.map((item) => {
    const startPercent = round((cumulative / total) * 100);
    cumulative += item.cost;
    const endPercent = round((cumulative / total) * 100);
    return {
      ...item,
      startPercent,
      widthPercent: round((item.cost / total) * 100),
      endPercent,
      heightPercent: (item.value / 5) * 100,
    };
  });
}

export function rankValueGaps(activities: ValueActivity[]) {
  return activities
    .map((item, index) => ({
      ...item,
      gap: item.cost * (6 - item.value),
      inputIndex: index,
    }))
    .sort((a, b) => b.gap - a.gap || a.inputIndex - b.inputIndex);
}

export default function ValueChainMap({
  title,
  subtitle,
  costUnit,
  supportActivities,
  primaryActivities,
  instanceId,
}: ValueChainMapProps) {
  // 既定を固定文字列にすると、id を指定せず 2 つ置いたときに radio の name と
  // SVG marker の id を共有してしまう。他の型と同じく useId() に寄せる。
  const autoId = useId();
  const uid = (instanceId ?? autoId).replace(
      // 逃がし文字(`_`)自身も逃がすので単射。捨てる形にすると `a:b` と `a/b` が
      // 同じ id になる。
      /[^a-zA-Z0-9-]/g,
      (c: string) => `_${c.charCodeAt(0).toString(16)}_`,
    ) || "value-chain";
  const geometry = activityGeometry(primaryActivities).map((activity) => ({
    ...activity,
    startX: round(80 + activity.startPercent * 8.6),
    endX: round(80 + activity.endPercent * 8.6),
    halfHeight: 14 + activity.value * 13,
  }));
  const ranked = rankValueGaps(primaryActivities);
  const priority = ranked[0];
  const notes = primaryActivities.filter((item) => item.note);
  const markerId = `${uid}-value-arrow`;
  const centerY = 232;

  return (
    <section
      className="asset group/value overflow-hidden rounded-xl border border-outline bg-surface p-5 text-primary shadow-card sm:p-8"
      data-layout="value-chain-map"
    >
      <header className="border-b border-outline pb-5">
        <p className="font-mono text-label text-tertiary">VALUE RIVERBED</p>
        <h1 className="mt-2 text-h1 text-balance">{title}</h1>
        {subtitle && <p className="mt-2 text-body text-secondary">{subtitle}</p>}
      </header>

      <div className="mt-5 flex flex-wrap justify-between gap-3">
        <div className="inline-flex rounded-full border border-outline bg-surface-subtle p-1 text-label">
          <label className="rounded-full px-3 py-1.5 has-[:checked]:bg-primary has-[:checked]:text-on-primary">
            <input className="value-view-chain sr-only" type="radio" name={`${uid}-view`} defaultChecked />価値連鎖
          </label>
          <label className="rounded-full px-3 py-1.5 has-[:checked]:bg-primary has-[:checked]:text-on-primary">
            <input className="value-view-gap sr-only" type="radio" name={`${uid}-view`} />改善余地
          </label>
        </div>
        <span className="font-mono text-label text-secondary">LENGTH=COST · THICKNESS=VALUE · RAIL=SUPPORT</span>
      </div>

      <div
        className="mt-5 grid gap-2 border-l-4 border-tertiary bg-gradient-to-r from-tertiary/20 to-transparent px-4 py-3 sm:grid-cols-[auto_1fr_auto] sm:items-center"
        data-value-chain-default-priority="true"
      >
        <span className="font-mono text-label text-tertiary">IMPROVE FIRST</span>
        <strong>{priority.label}</strong>
        <span className="font-mono text-h3">GAP {priority.gap}</span>
      </div>

      <div data-value-chain-view="chain" className="mt-6 group-has-[.value-view-gap:checked]/value:hidden">
        <svg
          className="hidden h-80 w-full sm:block"
          viewBox="0 0 1000 360"
          role="img"
          aria-label="コストを長さ、価値を太さとして連続表示するバリューチェーン"
          data-primary-geometry="value-chain-map"
          data-geometry-channels="cost-length value-thickness sequence-position support-rail gap-emphasis"
        >
          <defs>
            <marker id={markerId} viewBox="0 0 10 10" refX="8" refY="5" markerWidth="10" markerHeight="10" markerUnits="userSpaceOnUse" orient="auto">
              <path d="M 0 0 L 10 5 L 0 10 z" className="fill-surface" />
            </marker>
          </defs>

          {supportActivities.map((item, index) => {
            const y = 35 + index * 28;
            return (
              <g key={item}>
                <line x1="80" x2="940" y1={y} y2={y} className="stroke-outline" strokeWidth="6" strokeLinecap="round" />
                <circle cx="80" cy={y} r="6" className="fill-primary" />
                <text x="94" y={y + 4} className="fill-secondary font-mono text-label">S{index + 1} · {item}</text>
              </g>
            );
          })}

          <text x="80" y="116" className="fill-secondary font-mono text-label">PRIMARY VALUE STREAM</text>
          {geometry.map((activity, index) => {
            const isPriority = activity.id === priority.id;
            const centerX = (activity.startX + activity.endX) / 2;
            const previous = geometry[index - 1];
            const next = geometry[index + 1];
            const startHalf = previous
              ? (previous.halfHeight + activity.halfHeight) / 2
              : activity.halfHeight;
            const endHalf = next
              ? (activity.halfHeight + next.halfHeight) / 2
              : activity.halfHeight;
            const labelAbove = index % 2 === 0;
            const labelY = labelAbove ? centerY - activity.halfHeight - 44 : 335;
            const edgeY = labelAbove ? centerY - activity.halfHeight : centerY + activity.halfHeight;
            const ribbonPoints = `${activity.startX},${centerY - startHalf} ${centerX},${centerY - activity.halfHeight} ${activity.endX},${centerY - endHalf} ${activity.endX},${centerY + endHalf} ${centerX},${centerY + activity.halfHeight} ${activity.startX},${centerY + startHalf}`;
            const haloPoints = `${activity.startX - 6},${centerY - startHalf - 9} ${centerX},${centerY - activity.halfHeight - 9} ${activity.endX + 6},${centerY - endHalf - 9} ${activity.endX + 6},${centerY + endHalf + 9} ${centerX},${centerY + activity.halfHeight + 9} ${activity.startX - 6},${centerY + startHalf + 9}`;
            return (
              <g
                key={activity.id}
                tabIndex={0}
                data-value-chain-activity={activity.id}
                data-primary-record={activity.id}
                data-geometry-a={activity.cost}
                data-geometry-b={activity.value}
                data-primary-emphasis={isPriority ? "true" : undefined}
              >
                {isPriority && (
                  <polygon
                    points={haloPoints}
                    className="fill-tertiary/15"
                  />
                )}
                <polygon
                  points={ribbonPoints}
                  className={isPriority ? "fill-tertiary" : "fill-primary"}
                />
                <line x1={centerX} x2={centerX} y1={edgeY} y2={labelAbove ? labelY + 8 : labelY - 20} className="stroke-outline" vectorEffect="non-scaling-stroke" />
                <text data-primary-label={activity.id} x={centerX} y={labelY} textAnchor="middle" className="fill-primary text-label font-semibold">{index + 1} · {activity.label}</text>
                <text x={centerX} y={labelY + 17} textAnchor="middle" className="fill-secondary font-mono text-label">{activity.cost}{costUnit} · V{activity.value}</text>
              </g>
            );
          })}

          {geometry.slice(0, -1).map((activity, index) => {
            const next = geometry[index + 1];
            const startX = (activity.startX + activity.endX) / 2 + 12;
            const endX = (next.startX + next.endX) / 2 - 12;
            return (
              <path
                key={joinIds(activity.id, next.id)}
                d={`M ${startX} ${centerY} H ${endX}`}
                fill="none"
                className="stroke-surface"
                strokeWidth="3"
                vectorEffect="non-scaling-stroke"
                markerEnd={`url(#${markerId})`}
                data-primary-edge={joinIds(activity.id, next.id)}
              />
            );
          })}
        </svg>

        <div className="relative space-y-3 pl-7 sm:hidden">
          <span className="absolute bottom-2 left-2 top-2 w-px bg-outline" aria-hidden="true" />
          {primaryActivities.map((activity, index) => (
            <article key={activity.id} data-value-chain-activity={activity.id} data-value-chain-mobile-activity={activity.id} className="relative grid grid-cols-[3rem_1fr_auto] gap-3 border-b border-outline py-3 pl-3">
              <span className={`absolute -left-6 top-4 h-3 w-3 rounded-full ${activity.id === priority.id ? "bg-tertiary ring-4 ring-tertiary/20" : "bg-primary"}`} aria-hidden="true" />
              <span className="font-mono text-label text-tertiary">P{index + 1}</span>
              <div><strong>{activity.label}</strong><div className="mt-2 h-1.5 bg-outline"><span className="block h-full bg-primary" style={{ width: `${geometry[index].widthPercent}%` }} /></div></div>
              <span className="font-mono text-label">{activity.cost}{costUnit} · V{activity.value}</span>
            </article>
          ))}
        </div>
      </div>

      <div data-value-chain-view="gap-ledger" className="mt-6 hidden group-has-[.value-view-gap:checked]/value:block">
        <div className="divide-y divide-outline border-y border-outline">
          {ranked.map((activity, index) => (
            <article key={activity.id} data-value-chain-activity={activity.id} className={`grid grid-cols-[3rem_1fr_auto] items-center gap-3 py-3 ${index === 0 ? "border-l-4 border-tertiary pl-3" : "pl-4"}`}>
              <span className="font-mono text-label text-secondary">{String(index + 1).padStart(2, "0")}</span>
              <div><strong>{activity.label}</strong><p className="text-label text-secondary">{activity.cost}{costUnit} × 価値不足{6 - activity.value}</p></div>
              <span className="font-mono text-h3">{activity.gap}</span>
            </article>
          ))}
        </div>
      </div>

      {notes.length > 0 && (
        <details data-inline-disclosure className="mt-6 rounded-lg border border-outline bg-surface-subtle p-4">
          <summary className="cursor-pointer font-semibold">補足を確認({notes.length}件)</summary>
          {notes.map((item) => <p key={item.id} data-note-item="true" className="mt-3 text-body text-secondary"><strong className="text-primary">{item.label}:</strong> {item.note}</p>)}
        </details>
      )}
    </section>
  );
}

schema.ts

1.1 KB

/designs/value-chain-map/source/schema.ts

中身を見る
import { z } from "zod";
export const valueActivitySchema = z.object({ id: z.string().min(1), label: z.string().min(1), cost: z.number().positive(), value: z.number().int().min(1).max(5), note: z.string().optional() }).strict();
export const schema = z.object({ title: z.string().min(1), subtitle: z.string().optional(), costUnit: z.string().min(1), supportActivities: z.array(z.string().min(1)).min(1).max(4), primaryActivities: z.array(valueActivitySchema).min(4).max(8), instanceId: z.string().optional() }).strict().superRefine((value, ctx) => {
  // id は identity として使う(React の key、data 属性、対応強調の突き合わせ)。
  // 重複すると別物どうしが同じ要素を指し、操作が意図しない相手に効く。
  {
    const ids = value.primaryActivities.map((item) => item.id);
    if (new Set(ids).size !== ids.length) {
      ctx.addIssue({ code: "custom", path: ["primaryActivities"], message: "primaryActivities ids must be unique" });
    }
  }
});
export type ValueActivity = z.infer<typeof valueActivitySchema>;
export type ValueChainMapProps = z.infer<typeof schema>;

meta.yaml

1.8 KB

/designs/value-chain-map/source/meta.yaml

中身を見る
id: value-chain-map
name:
  ja: "バリューチェーンマップ(改善余地)"
  en: "Value Chain Map (Room to Improve)"
purpose:
  ja: "主活動の幅をコスト比率、高さを価値寄与へ写し、最大の価値改善余地を導出する"
  en: "Primary activities with cost share as width and value contribution as height, so the largest room to improve value follows from the shape"
# A5 の裁定で draft として移設し、2026-09-04 に型ごとの承認を取って公開した。
status: published
# 索引カードの図版。pnpm layout:render が原本から描く。
still: ./still.png
category: operating-model
catalogOrder: 200
jobs:
  - explain-structure
  - prioritize-diagnose
  - measure-change
useCases:
  - process
layoutKinds:
  - flow
tags: [value-chain, operating-model, cost, value, improvement]
whenToUse:
  - ja: "業務活動のコスト配分と価値を同時に見直したい場合"
    en: "Revisiting how cost is spread across activities alongside the value they add"
  - ja: "オペレーティングモデルの改善順を説明したい場合"
    en: "Explaining the order to improve an operating model"
avoidWhen:
  - ja: "起点から終点への増減要因を示したい場合"
    en: "Showing what moved a value from a start to an end"
  - ja: "能力を事業ドメインへ包含したい場合"
    en: "Nesting capabilities inside business domains"
requiredInputs:
  - ja: "主活動と支援活動"
    en: "The primary and supporting activities"
  - ja: "活動ごとのコスト"
    en: "The cost of each"
  - ja: "活動ごとの価値寄与"
    en: "The value each contributes"
relatedLayouts: [business-capability-map, waterfall, process-flow]
dataSchema:
  primaryActivities: "{ id, label, cost, value, note? }[] — 4〜8主活動"
  supportActivities: "string[] — 1〜4支援活動"
sources: []

sample-data.json

834 B

/designs/value-chain-map/source/sample-data.json

中身を見る
{
  "title": "設計・開発の改善余地が最大 — 30人月を価値2から引き上げる",
  "subtitle": "デジタルサービス提供活動の仮説評価",
  "costUnit": "人月",
  "supportActivities": ["共通データ基盤", "人材・標準", "調達・契約"],
  "primaryActivities": [
    {"id":"insight","label":"顧客理解","cost":12,"value":4},
    {"id":"plan","label":"企画","cost":18,"value":3},
    {"id":"build","label":"設計・開発","cost":30,"value":2,"note":"個別要件と手戻りが集中し、再利用可能な標準部品が少ない想定"},
    {"id":"launch","label":"提供開始","cost":14,"value":4},
    {"id":"operate","label":"運用・支援","cost":20,"value":3},
    {"id":"improve","label":"分析・改善","cost":10,"value":5}
  ],
  "instanceId": "service-value-chain"
}