Seen as a design The generated document itself — its toggles and disclosures work here
Seen as a design The generated document itself — its toggles and disclosures work here
Copy and paste, or save a file and hand it over. For a machine with no network, one “Everything” file is enough.
Everything in this panel, including the contents behind each URL, in one Markdown file — Large, so it goes as a file (copy is for 32 KB and under)
Build an HTML document with "Value Chain Map (Room to Improve)". This layout runs, so there is no composition to rebuild — what it needs is content.
## What this layout does
Primary activities with cost share as width and value contribution as height, so the largest room to improve value follows from the shape
## What you have to supply
- The primary and supporting activities
- The cost of each
- The value each contributes
## The shape of the input
- `primaryActivities` — { id, label, cost, value, note? }[] — 4〜8主活動
- `supportActivities` — string[] — 1〜4支援活動
schema.ts holds the machine-checkable validation. Ask rather than guess when an input is missing.
## When not to use it
- Showing what moved a value from a start to an end
- Nesting capabilities inside business domains
## How to build it
Put the input in a JSON file and run:
```bash
node scripts/render.mjs value-chain-map <input.json> --theme ink -o out.html
```
The output is one self-contained file and fetches nothing at render time.
## The source
- 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 Template, input schema, sample data and metadata. This document was generated from those four. The generated document
/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>
);
}
/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>;
/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: []
/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"
}