作品として見る 生成された文書そのもの — 切り替えも開閉もそのまま動きます
作品として見る 生成された文書そのもの — 切り替えも開閉もそのまま動きます
コピーして貼るか、ファイルに保存して渡す。ネットにつながらない環境には「全部入り」を 1 つ渡せば足りる。
このパネルの中身を、URL の先にあるファイルの中身まで含めて 1 つにまとめた Markdown — 大きいのでファイルで渡す(コピーは 32KB まで)
「ストラテジーマップ(視点横断因果)」でHTML資料を作ってください。この型は実行できるので、版面を組み直す必要はありません — 必要なのは中身です。
## この型がすること
視点別の戦略目標を因果edgeで結び、影響度と現在水準から重点目標を導出する
## 渡す必要があるもの
- 戦略視点
- 視点ごとの目標
- 目標間の因果関係と評価尺度
## 入力の形
- `perspectives` — { id, name }[] — 3〜5視点
- `objectives` — { id, label, perspectiveId, impact, current, note? }[] — 4〜12目標
- `links` — { sourceId, targetId, strength }[] — 3〜16因果
入力の機械可読な検証は schema.ts が持ちます。足りない入力は推測せず質問してください。
## 向かない場合
- 固定3階層の配分を示したい場合
- 可変階層を親子へ分解したい場合
## 作り方
入力を JSON にまとめ、次を実行します。
```bash
node scripts/render.mjs strategy-map <入力.json> --theme ink -o out.html
```
出力は 1 ファイルで完結し、外部から何も読み込みません。
## 原本
- https://design-vault.pages.dev/designs/strategy-map/source/template.tsx
- https://design-vault.pages.dev/designs/strategy-map/source/schema.ts
- https://design-vault.pages.dev/designs/strategy-map/source/sample-data.json
- https://design-vault.pages.dev/designs/strategy-map/sample.html テンプレート・入力スキーマ・サンプルデータ・メタ情報。この文書はこの 4 つから生成されています。 生成された文書
/designs/strategy-map/source/template.tsx
import { useId } from "react";
import type { StrategyMapProps, StrategyObjective } from "./schema";
type StrategyPerspective = StrategyMapProps["perspectives"][number];
type StrategyLink = StrategyMapProps["links"][number];
const round = (value: number) => Number(value.toFixed(2));
export function rankStrategyGaps(objectives: StrategyObjective[]) {
return objectives
.map((objective, index) => ({
...objective,
gap: objective.impact * (6 - objective.current),
inputIndex: index,
}))
.sort((a, b) => b.gap - a.gap || a.inputIndex - b.inputIndex);
}
export function strategyGeometry(
perspectives: StrategyPerspective[],
objectives: StrategyObjective[],
links: StrategyLink[],
) {
const nodes = objectives.map((objective) => {
const perspectiveIndex = perspectives.findIndex(
(perspective) => perspective.id === objective.perspectiveId,
);
const peers = objectives.filter(
(item) => item.perspectiveId === objective.perspectiveId,
);
const peerIndex = peers.findIndex((item) => item.id === objective.id);
const x = 22 + ((peerIndex + 1) / (peers.length + 1)) * 56;
const y = perspectives.length === 1
? 50
: 82 - (perspectiveIndex / (perspectives.length - 1)) * 64;
const radius = 2.8 + objective.impact * 0.65;
const gapRadius = radius + 2 + (6 - objective.current) * 0.45;
return {
...objective,
x: round(x),
y: round(y),
radius: round(radius),
gapRadius: round(gapRadius),
gap: objective.impact * (6 - objective.current),
};
});
const position = new Map(nodes.map((node) => [node.id, node]));
const edges = links.map((link, index) => ({
...link,
id: `${link.sourceId}-${link.targetId}-${index}`,
source: position.get(link.sourceId)!,
target: position.get(link.targetId)!,
strokeWidth: round(1 + link.strength * 0.65),
}));
return { nodes, edges };
}
export default function StrategyMap({
title,
subtitle,
perspectives,
objectives,
links,
instanceId,
}: StrategyMapProps) {
// 既定を固定文字列にすると、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)}_`,
) || "strategy-map";
const ranked = rankStrategyGaps(objectives);
const priority = ranked[0];
const notes = objectives.filter((item) => item.note);
const geometry = strategyGeometry(perspectives, objectives, links);
const markerId = `${uid}-causal-arrow`;
const canvasX = (value: number) => value * 10;
const canvasY = (value: number) => value * 4.2;
return (
<section
className="asset group/strategy overflow-hidden rounded-xl border border-outline bg-surface p-5 text-primary shadow-card sm:p-8"
data-layout="strategy-map"
>
<header className="border-b border-outline pb-5">
<p className="font-mono text-label text-tertiary">CAUSE → EFFECT</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="strategy-view-map 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="strategy-view-gap sr-only" type="radio" name={`${uid}-view`} />重点
</label>
</div>
<span className="font-mono text-label text-secondary">RING=GAP · LINE=STRENGTH</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-strategy-default-priority="true"
>
<span className="font-mono text-label text-tertiary">LEVERAGE POINT</span>
<strong>{priority.label}</strong>
<span className="font-mono text-h3">GAP {priority.gap}</span>
</div>
<div data-strategy-view="map" className="mt-6 group-has-[.strategy-view-gap:checked]/strategy:hidden">
<svg
className="hidden h-[26rem] w-full sm:block"
viewBox="0 0 1000 420"
role="img"
aria-label="下位の基盤から上位の成果へ向かう戦略因果マップ"
data-primary-geometry="strategy-map"
data-geometry-channels="perspective position impact gap link-strength"
>
<defs>
<marker id={markerId} viewBox="0 0 10 10" refX="8" refY="5" markerWidth="9" markerHeight="9" markerUnits="userSpaceOnUse" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" className="fill-primary" />
</marker>
</defs>
{perspectives.map((perspective, index) => {
const yPercent = perspectives.length === 1 ? 50 : 82 - (index / (perspectives.length - 1)) * 64;
const y = canvasY(yPercent);
return (
<g key={perspective.id}>
<line x1="112" x2="972" y1={y} y2={y} className="stroke-outline" vectorEffect="non-scaling-stroke" />
<text x="12" y={y + 5} className="fill-secondary font-mono text-label">P{String(index + 1).padStart(2, "0")}</text>
</g>
);
})}
{geometry.edges.map((edge) => {
const sourceX = canvasX(edge.source.x);
const sourceY = canvasY(edge.source.y);
const targetX = canvasX(edge.target.x);
const targetY = canvasY(edge.target.y);
const deltaX = targetX - sourceX;
const deltaY = targetY - sourceY;
const distance = Math.hypot(deltaX, deltaY) || 1;
const unitX = deltaX / distance;
const unitY = deltaY / distance;
const sourceClearance = edge.source.gapRadius * 2.8 + 3;
const targetClearance = edge.target.gapRadius * 2.8 + 9;
const startX = sourceX + unitX * sourceClearance;
const startY = sourceY + unitY * sourceClearance;
const endX = targetX - unitX * targetClearance;
const endY = targetY - unitY * targetClearance;
const middleY = (startY + endY) / 2;
const touchesPriority = edge.sourceId === priority.id || edge.targetId === priority.id;
return (
<path
key={edge.id}
d={`M ${startX} ${startY} C ${startX} ${middleY}, ${endX} ${middleY}, ${endX} ${endY}`}
fill="none"
vectorEffect="non-scaling-stroke"
strokeWidth={edge.strokeWidth}
markerEnd={`url(#${markerId})`}
className={touchesPriority ? "stroke-tertiary" : "stroke-primary"}
data-primary-edge={edge.id}
/>
);
})}
{geometry.nodes.map((node, index) => {
const x = canvasX(node.x);
const y = canvasY(node.y);
const radius = node.radius * 2.8;
const gapRadius = node.gapRadius * 2.8;
const circumference = 2 * Math.PI * gapRadius;
const currentShare = node.current / 5;
const isPriority = node.id === priority.id;
const labelOnLeft = node.x < 50;
const labelX = labelOnLeft ? x - gapRadius - 10 : x + gapRadius + 10;
const anchor = labelOnLeft ? "end" : "start";
return (
<g
key={node.id}
tabIndex={0}
data-strategy-objective={node.id}
data-primary-record={node.id}
data-geometry-a={node.x}
data-geometry-b={node.y}
data-primary-emphasis={isPriority ? "true" : undefined}
>
<title>{`${node.label} · 影響${node.impact} · 現状${node.current} · GAP ${node.gap}`}</title>
{isPriority && <circle cx={x} cy={y} r={gapRadius + 8} className="fill-tertiary/15" />}
<circle cx={x} cy={y} r={gapRadius} fill="none" className={isPriority ? "stroke-tertiary" : "stroke-outline"} strokeWidth="2" />
<circle
cx={x}
cy={y}
r={gapRadius}
fill="none"
className={isPriority ? "stroke-tertiary" : "stroke-primary"}
strokeWidth="3"
strokeDasharray={`${round(circumference * currentShare)} ${round(circumference * (1 - currentShare))}`}
transform={`rotate(-90 ${x} ${y})`}
/>
<circle cx={x} cy={y} r={radius} className={isPriority ? "fill-tertiary" : "fill-primary"} />
<text data-primary-label={node.id} x={labelX} y={y - 4} textAnchor={anchor} className="fill-primary text-label font-semibold">
O{String(index + 1).padStart(2, "0")}
</text>
</g>
);
})}
</svg>
<div className="relative space-y-5 pl-7 sm:hidden">
<span className="absolute bottom-2 left-2 top-2 w-px bg-outline" aria-hidden="true" />
{perspectives.map((perspective, perspectiveIndex) => (
<section key={perspective.id} className="relative">
<h2 className="font-mono text-label text-secondary">{String(perspectiveIndex + 1).padStart(2, "0")} · {perspective.name}</h2>
<div className="mt-2 space-y-3">
{objectives.filter((item) => item.perspectiveId === perspective.id).map((objective) => {
const score = ranked.find((item) => item.id === objective.id)!;
const isPriority = objective.id === priority.id;
return (
<div key={objective.id} data-strategy-objective={objective.id} data-strategy-mobile-objective={objective.id} className="relative grid grid-cols-[1fr_auto] gap-3 py-1 pl-3">
<span className={`absolute -left-[1.65rem] top-2 h-3 w-3 rounded-full ${isPriority ? "bg-tertiary ring-4 ring-tertiary/20" : "bg-primary"}`} aria-hidden="true" />
<strong>{objective.label}</strong>
<span className="font-mono text-label">I{objective.impact} · G{score.gap}</span>
</div>
);
})}
</div>
</section>
))}
</div>
</div>
<div className="strategy-map-key mt-5 hidden gap-x-6 gap-y-2 border-t border-outline pt-4 sm:grid sm:grid-cols-2 xl:grid-cols-3" aria-label="目標一覧">
{objectives.map((objective, index) => {
const perspective = perspectives.find((item) => item.id === objective.perspectiveId)!;
const score = ranked.find((item) => item.id === objective.id)!;
return <div key={objective.id} data-strategy-key={objective.id} className="grid grid-cols-[3rem_1fr] gap-2 py-1">
<strong className="font-mono text-label text-tertiary">O{String(index + 1).padStart(2, "0")}</strong>
<span>{objective.label}<span className="block text-label text-secondary">P{String(perspectives.findIndex((item) => item.id === objective.perspectiveId) + 1).padStart(2, "0")} · {perspective.name} · I{objective.impact} · C{objective.current} · GAP {score.gap}</span></span>
</div>;
})}
</div>
<div data-strategy-view="gap-ledger" className="mt-6 hidden group-has-[.strategy-view-gap:checked]/strategy:block">
<div className="divide-y divide-outline border-y border-outline">
{ranked.map((objective, index) => (
<article key={objective.id} data-strategy-objective={objective.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>{objective.label}</strong><p className="text-label text-secondary">影響{objective.impact} × 現在水準不足{6 - objective.current}</p></div>
<span className="font-mono text-h3">{objective.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/strategy-map/source/schema.ts
import { z } from "zod";
const perspectiveSchema = z.object({ id: z.string().min(1), name: z.string().min(1) }).strict();
export const strategyObjectiveSchema = z.object({ id: z.string().min(1), label: z.string().min(1), perspectiveId: z.string().min(1), impact: z.number().int().min(1).max(5), current: z.number().int().min(1).max(5), note: z.string().optional() }).strict();
const strategyLinkSchema = z.object({ sourceId: z.string().min(1), targetId: z.string().min(1), strength: z.number().int().min(1).max(5) }).strict();
export const schema = z.object({ title: z.string().min(1), subtitle: z.string().optional(), perspectives: z.array(perspectiveSchema).min(3).max(5), objectives: z.array(strategyObjectiveSchema).min(4).max(12), links: z.array(strategyLinkSchema).min(3).max(16), instanceId: z.string().optional() }).strict().superRefine((value, ctx) => {
// id は identity として使う(React の key、data 属性、対応強調の突き合わせ)。
// 重複すると別物どうしが同じ要素を指し、操作が意図しない相手に効く。
{
const ids = value.perspectives.map((item) => item.id);
if (new Set(ids).size !== ids.length) {
ctx.addIssue({ code: "custom", path: ["perspectives"], message: "perspectives ids must be unique" });
}
}
{
const ids = value.objectives.map((item) => item.id);
if (new Set(ids).size !== ids.length) {
ctx.addIssue({ code: "custom", path: ["objectives"], message: "objectives ids must be unique" });
}
}
const perspectives = new Set(value.perspectives.map((item) => item.id)); const ids = new Set(value.objectives.map((item) => item.id));
value.objectives.forEach((item, index) => { if (!perspectives.has(item.perspectiveId)) ctx.addIssue({ code: "custom", path: ["objectives", index, "perspectiveId"], message: "unknown perspective" }); });
value.links.forEach((item, index) => { if (!ids.has(item.sourceId) || !ids.has(item.targetId) || item.sourceId === item.targetId) ctx.addIssue({ code: "custom", path: ["links", index], message: "invalid objective link" }); });
});
export type StrategyObjective = z.infer<typeof strategyObjectiveSchema>;
export type StrategyMapProps = z.infer<typeof schema>;
/designs/strategy-map/source/meta.yaml
id: strategy-map
name:
ja: "ストラテジーマップ(視点横断因果)"
en: "Strategy Map (Cause and Effect Across Perspectives)"
purpose:
ja: "視点別の戦略目標を因果edgeで結び、影響度と現在水準から重点目標を導出する"
en: "Objectives by perspective joined by causal edges, with the ones to focus on following from influence and current level"
# A5 の裁定で draft として移設し、2026-09-04 に型ごとの承認を取って公開した。
status: published
# 索引カードの図版。pnpm layout:render が原本から描く。
still: ./still.png
category: strategy
catalogOrder: 190
jobs:
- explain-structure
- prioritize-diagnose
useCases:
- summary
layoutKinds:
- network
tags: [strategy-map, cause-effect, objective, balanced-scorecard]
whenToUse:
- ja: "先行目標が成果へどうつながるか説明したい場合"
en: "Explaining how a leading objective reaches an outcome"
- ja: "戦略目標の重点化を合意したい場合"
en: "Agreeing which strategic objectives take priority"
avoidWhen:
- ja: "固定3階層の配分を示したい場合"
en: "Showing a split across three fixed tiers"
- ja: "可変階層を親子へ分解したい場合"
en: "Decomposing a variable hierarchy into parent and child"
requiredInputs:
- ja: "戦略視点"
en: "The strategic perspectives"
- ja: "視点ごとの目標"
en: "The objectives under each"
- ja: "目標間の因果関係と評価尺度"
en: "The causal links between them, and how they are measured"
relatedLayouts: [logic-tree, pyramid, business-capability-map]
dataSchema:
perspectives: "{ id, name }[] — 3〜5視点"
objectives: "{ id, label, perspectiveId, impact, current, note? }[] — 4〜12目標"
links: "{ sourceId, targetId, strength }[] — 3〜16因果"
sources: []
/designs/strategy-map/source/sample-data.json
{
"title": "企画リードタイム短縮を重点化し、継続利用の成長へつなげる",
"subtitle": "デジタル事業の戦略仮説(影響度・現在水準は5段階)",
"perspectives": [{"id":"people","name":"人材・基盤"},{"id":"process","name":"業務プロセス"},{"id":"customer","name":"顧客"},{"id":"finance","name":"事業成果"}],
"objectives": [
{"id":"skills","label":"データ活用力を高める","perspectiveId":"people","impact":4,"current":3},
{"id":"platform","label":"共通データ基盤を整える","perspectiveId":"people","impact":5,"current":2},
{"id":"speed","label":"企画リードタイムを短縮","perspectiveId":"process","impact":5,"current":1,"note":"仮説検証に必要なデータ準備と承認が分断されている想定"},
{"id":"quality","label":"判断品質を標準化","perspectiveId":"process","impact":4,"current":2},
{"id":"experience","label":"継続利用を増やす","perspectiveId":"customer","impact":5,"current":3},
{"id":"growth","label":"継続売上を伸ばす","perspectiveId":"finance","impact":5,"current":3}
],
"links": [
{"sourceId":"skills","targetId":"quality","strength":4},
{"sourceId":"platform","targetId":"speed","strength":5},
{"sourceId":"speed","targetId":"experience","strength":5},
{"sourceId":"quality","targetId":"experience","strength":3},
{"sourceId":"experience","targetId":"growth","strength":5}
],
"instanceId": "digital-growth-strategy"
}