作品として見る 生成された文書そのもの — 切り替えも開閉もそのまま動きます
作品として見る 生成された文書そのもの — 切り替えも開閉もそのまま動きます
コピーして貼るか、ファイルに保存して渡す。ネットにつながらない環境には「全部入り」を 1 つ渡せば足りる。
このパネルの中身を、URL の先にあるファイルの中身まで含めて 1 つにまとめた Markdown — 大きいのでファイルで渡す(コピーは 32KB まで)
「ステークホルダー・ネットワーク(合意形成リスク)」でHTML資料を作ってください。この型は実行できるので、版面を組み直す必要はありません — 必要なのは中身です。
## この型がすること
支持度・関心度・影響力と関係線を一つのnetworkへ写し、最大の合意形成リスクを導出する
## 渡す必要があるもの
- 利害関係者と役割
- 影響力・関心度・支持度
- 関係者間の影響関係
## 入力の形
- `stakeholders` — { id, name, role, influence, interest, support, note? }[] — 4〜10件
- `relations` — { sourceId, targetId, strength, label? }[] — 1〜16関係
入力の機械可読な検証は schema.ts が持ちます。足りない入力は推測せず質問してください。
## 向かない場合
- 責任記号を活動別に監査したい場合
- 2軸だけで関係者を位置づけたい場合
## 作り方
入力を JSON にまとめ、次を実行します。
```bash
node scripts/render.mjs stakeholder-network <入力.json> --theme ink -o out.html
```
出力は 1 ファイルで完結し、外部から何も読み込みません。
## 原本
- https://design-vault.pages.dev/designs/stakeholder-network/source/template.tsx
- https://design-vault.pages.dev/designs/stakeholder-network/source/schema.ts
- https://design-vault.pages.dev/designs/stakeholder-network/source/sample-data.json
- https://design-vault.pages.dev/designs/stakeholder-network/sample.html テンプレート・入力スキーマ・サンプルデータ・メタ情報。この文書はこの 4 つから生成されています。 生成された文書
/designs/stakeholder-network/source/template.tsx
import { useId } from "react";
import type {
Stakeholder,
StakeholderNetworkProps,
} from "./schema";
type Relation = StakeholderNetworkProps["relations"][number];
const round = (value: number) => Number(value.toFixed(2));
export function rankStakeholderRisks(stakeholders: Stakeholder[]) {
return stakeholders
.map((item, index) => ({
...item,
risk: item.influence * item.interest * (6 - item.support),
inputIndex: index,
}))
.sort((a, b) => b.risk - a.risk || a.inputIndex - b.inputIndex);
}
/**
* 図の中に置くラベルの縁取り。`stroke-surface` と組で使う。
*
* 塗りの前に地の色で縁を引くので、線はラベルの手前で途切れて見え、線の上にラベルが来ても
* 字が残る。背後に何も無いところでは地と同じ色なので見えない。線を避けて置き場を解き直す
* 道は取らない —— 密な図では解が無い入力が必ず残るが、縁取りは入力に依らず効く。
*/
const HALO = { paintOrder: "stroke", strokeWidth: 4, strokeLinejoin: "round" } as const;
export function stakeholderGeometry(stakeholders: Stakeholder[]) {
return stakeholders.map((item) => {
const peers = stakeholders.filter((peer) => peer.support === item.support);
const peerIndex = peers.findIndex((peer) => peer.id === item.id);
const peerSpread = (peerIndex - (peers.length - 1) / 2) * 18;
const degrees = 200 - ((item.support - 1) / 4) * 220 + peerSpread;
const angle = (degrees * Math.PI) / 180;
const distance = 60 + item.interest * 24;
return {
...item,
x: round(500 + Math.cos(angle) * distance),
y: round(200 + Math.sin(angle) * distance),
radius: 3 + item.influence,
degrees: round(degrees),
distance,
};
});
}
/**
* 描く前に文字の幅を見積もる。全角は 1em、半角は 0.58em(timeline と同じ見積もり)。
* 置き場を決めるのが描画より前なので、実測ではなく見積もりで決める。
*/
const estimateTextWidth = (text: string, fontPx: number): number => {
let em = 0;
for (const ch of text) em += (ch.codePointAt(0) ?? 0) >= 0x1100 ? 1 : 0.58;
return em * fontPx;
};
/** 二次ベジエ `M p0 Q p1 p2` 上の点 */
const bezierPoint = (
p0: { x: number; y: number },
p1: { x: number; y: number },
p2: { x: number; y: number },
t: number,
) => ({
x: (1 - t) ** 2 * p0.x + 2 * (1 - t) * t * p1.x + t ** 2 * p2.x,
y: (1 - t) ** 2 * p0.y + 2 * (1 - t) * t * p1.y + t ** 2 * p2.y,
});
/** ラベルは点の 13 上に baseline を置く。矩形は baseline から上へ 12・下へ 3 */
const LABEL_BASELINE = 13;
const LABEL_TOP = LABEL_BASELINE + 12;
const LABEL_BOTTOM = LABEL_BASELINE - 3;
/** 丸との間に空ける距離。この型が丸とラベルの間に使っている値 */
const LABEL_CLEARANCE = 4;
/** 矩形の中心から baseline までの距離(下向きが正) */
const BASELINE_FROM_CENTER = (LABEL_TOP + LABEL_BOTTOM) / 2 - LABEL_BASELINE;
/**
* 関係線のラベルを、線に沿ったどこへ置くか。
*
* 中点に固定すると、関係者を極座標に並べるこの型では**長い線の中点が無関係な
* 第三の丸の上に乗る**。実測(1280px)で「実行報告」が事業責任者の丸へ 21.4px
* 入り、ほとんど読めなかった。線の両端を避けるだけでは足りない —— 邪魔をするのは
* その線が結んでいない丸のほうだった。
*
* 線に沿った位置(t)と、線に対して直角の逃がし量の組を、決め打ちの順で試して
* 最初に空いたものを採る。中点・逃がし 0 から見るので、空いている図では従来どおり
* 中点に置かれる。
*
* 2 つの形を先に捨てている。
*
* 「いちばん食い込んでいる丸から押し出す」反復は、近い 2 つの丸の間で往復して
* 重なったまま返る(総当たり 116000 通りで 1080 件・最悪 -18.9px)。
*
* 有限の候補リストから選んで、どれも空かなければ「いちばんまし」を返す形も足りない。
* リストの外に答えがある入力が必ず残る(無作為 276682 本で 150 件・最悪 -5.4px)。
* どちらも外部レビューが指摘して、総当たりで裏を取った。
*
* だから最後の一手は探索ではなく計算にする。どの丸からも離れていることが式で
* 分かる距離まで逃がせば、候補が尽きても後条件は必ず満たされる。近い順に試すので、
* 空いている図では従来どおり中点に置かれ、この最後の一手は使われない。
*/
export const relationLabelPoint = (
start: { x: number; y: number },
control: { x: number; y: number },
end: { x: number; y: number },
label: string,
obstacles: { x: number; y: number; radius: number }[],
) => {
const halfWidth = estimateTextWidth(label, 13) / 2;
const halfHeight = (LABEL_TOP - LABEL_BOTTOM) / 2;
/** 矩形の中心から見て、いちばん食い込んでいる丸との余白 */
const clearanceAt = (cx: number, cy: number) =>
obstacles.reduce((least, o) => {
const nx = Math.max(cx - halfWidth, Math.min(o.x, cx + halfWidth));
const ny = Math.max(cy - halfHeight, Math.min(o.y, cy + halfHeight));
// Math.hypot は仕様上「実装依存の近似」で、Math.sqrt だけが IEEE-754 の
// 正しい丸めを要求される。ここの値は「どこに置くか」の分岐に入るので、
// 最下位ビットの差が位置の差になって機械ごとに別の絵が出る
// (5225ecc が sankey-flow で踏んだのと同じ種類の食い違い)。
return Math.min(least, Math.sqrt((nx - o.x) ** 2 + (ny - o.y) ** 2) - o.radius);
}, Number.POSITIVE_INFINITY);
const T_CANDIDATES = [0.5, 0.44, 0.56, 0.38, 0.62, 0.32, 0.68, 0.26, 0.74, 0.2, 0.8, 0.14, 0.86];
const OFFSET_STEP = 16;
const OFFSET_STEPS = 8;
/** t での接線から、線に直角な単位ベクトル */
const normalAt = (t: number) => {
const tangentX = 2 * (1 - t) * (control.x - start.x) + 2 * t * (end.x - control.x);
const tangentY = 2 * (1 - t) * (control.y - start.y) + 2 * t * (end.y - control.y);
const length = Math.sqrt(tangentX ** 2 + tangentY ** 2) || 1;
return { x: -tangentY / length, y: tangentX / length };
};
/** 返す値そのもので測る —— 丸めた後に条件が崩れていないことを見る */
const place = (point: { x: number; y: number }, normal: { x: number; y: number }, offset: number) => {
const x = round(point.x + normal.x * offset);
const y = round(point.y + normal.y * offset - LABEL_BASELINE);
return { x, y, clearance: clearanceAt(x, y - BASELINE_FROM_CENTER) };
};
for (let step = 0; step <= OFFSET_STEPS; step += 1) {
for (const t of T_CANDIDATES) {
const point = bezierPoint(start, control, end, t);
const normal = normalAt(t);
// 逃がし 0 は 1 度だけ。近いほうから見るので、空いていれば中点に落ち着く
for (const offset of step === 0 ? [0] : [step * OFFSET_STEP, -step * OFFSET_STEP]) {
const placed = place(point, normal, offset);
if (placed.clearance >= LABEL_CLEARANCE) return { x: placed.x, y: placed.y };
}
}
}
// 候補が尽きた。中点から、どの丸からも離れることが式で分かる距離まで逃がす。
//
// 矩形の中心が丸の中心から「丸の半径 + 矩形の対角の半分 + 余白」より遠ければ、
// 矩形上のどの点も丸の外にある。全部の丸についてその最大を取る。
//
// 矩形の中心は逃がした点そのものではなく、baseline 変換のぶん
// (LABEL_BASELINE + BASELINE_FROM_CENTER) だけ上にある。この分を式に入れ忘れると
// 距離が最大 17.5px 足りない(外部レビューが 10 人の配置で 1.85px しか空かない
// 例を出した)。式で出した距離も丸めるので、最後に必ず測ってから返す。
const point = bezierPoint(start, control, end, 0.5);
const normal = normalAt(0.5);
const reach = obstacles.reduce((far, o) => {
const toObstacle = Math.sqrt((o.x - point.x) ** 2 + (o.y - point.y) ** 2);
return Math.max(far, toObstacle + o.radius);
}, 0);
let offset =
reach +
Math.sqrt(halfWidth ** 2 + halfHeight ** 2) +
LABEL_BASELINE +
BASELINE_FROM_CENTER +
LABEL_CLEARANCE +
1; // 丸めのぶん
let safe = place(point, normal, offset);
// 式は上限のはずだが、それを信じずに測る。足りなければ外へ伸ばす —— 外へ動かす
// ほど余白は増えるので、この繰り返しは往復しない。
for (let i = 0; i < 8 && safe.clearance < LABEL_CLEARANCE; i += 1) {
offset += LABEL_CLEARANCE - safe.clearance + 1;
safe = place(point, normal, offset);
}
return { x: safe.x, y: safe.y };
};
export function stakeholderNetworkGeometry(
stakeholders: Stakeholder[],
relations: Relation[],
) {
const nodes = stakeholderGeometry(stakeholders);
const positions = new Map(nodes.map((node) => [node.id, node]));
const edges = relations.map((relation, index) => ({
...relation,
id: `${relation.sourceId}-${relation.targetId}-${index}`,
source: positions.get(relation.sourceId)!,
target: positions.get(relation.targetId)!,
strokeWidth: round(1 + relation.strength * 0.65),
}));
return { nodes, edges };
}
export default function StakeholderNetwork({
title,
subtitle,
stakeholders,
relations,
instanceId,
}: StakeholderNetworkProps) {
// 既定を固定文字列にすると、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)}_`,
) || "stakeholder-network";
const ranked = rankStakeholderRisks(stakeholders);
const priority = ranked[0];
const geometry = stakeholderNetworkGeometry(stakeholders, relations);
const notes = stakeholders.filter((item) => item.note);
const markerId = `${uid}-influence-arrow`;
const drawnEdges = geometry.edges.map((edge) => {
const dx = edge.target.x - edge.source.x;
const dy = edge.target.y - edge.source.y;
// この値は startX/startY を通ってラベルの置き場の分岐に入るので、実装依存の近似である
// Math.hypot ではなく、正しい丸めが保証される Math.sqrt を使う。
const distance = Math.sqrt(dx ** 2 + dy ** 2) || 1;
const ux = dx / distance;
const uy = dy / distance;
const sourceRadius = edge.source.radius * 3;
const targetRadius = edge.target.radius * 3;
const startX = edge.source.x + ux * (sourceRadius + 4);
const startY = edge.source.y + uy * (sourceRadius + 4);
const endX = edge.target.x - ux * (targetRadius + 10);
const endY = edge.target.y - uy * (targetRadius + 10);
const midX = (startX + endX) / 2;
const midY = (startY + endY) / 2;
return {
edge,
path: `M ${startX} ${startY} Q ${midX} ${midY - 16} ${endX} ${endY}`,
spot: edge.label
? relationLabelPoint(
{ x: startX, y: startY },
{ x: midX, y: midY - 16 },
{ x: endX, y: endY },
edge.label,
geometry.nodes.map((n) => ({ x: n.x, y: n.y, radius: n.radius * 3 })),
)
: null,
touchesPriority:
edge.sourceId === priority.id || edge.targetId === priority.id,
};
});
return (
<section
className="asset group/stakeholder overflow-hidden rounded-xl border border-outline bg-surface p-5 text-primary shadow-card sm:p-8"
data-layout="stakeholder-network"
>
<header className="border-b border-outline pb-5">
<p className="font-mono text-label text-tertiary">INFLUENCE ORBIT</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="stakeholder-view-network 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="stakeholder-view-risk sr-only" type="radio" name={`${uid}-view`} />合意リスク
</label>
</div>
<span className="font-mono text-label text-secondary">ANGLE=SUPPORT · ORBIT=INTEREST · SIZE=INFLUENCE</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-stakeholder-default-priority="true"
>
<span className="font-mono text-label text-tertiary">ALIGN FIRST</span>
<strong>{priority.name}</strong>
<span className="font-mono text-h3">RISK {priority.risk}</span>
</div>
<div data-stakeholder-view="network" className="mt-6 group-has-[.stakeholder-view-risk:checked]/stakeholder:hidden">
<svg
className="hidden h-96 w-full sm:block"
viewBox="0 0 1000 440"
role="img"
aria-label="支持度の角度、関心度の距離、影響度の大きさで示す関係者ネットワーク"
data-primary-geometry="stakeholder-network"
data-geometry-channels="support-angle interest-orbit influence-radius relation-width"
>
<defs>
<marker id={markerId} viewBox="0 0 10 10" refX="8" refY="5" markerWidth="9" markerHeight="9" markerUnits="userSpaceOnUse" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" className="fill-primary" />
</marker>
</defs>
{[84, 132, 180].map((radius, index) => (
<path
key={radius}
d={`M ${500 - radius} 200 A ${radius} ${radius} 0 1 1 ${500 + radius} 200`}
fill="none"
className={index === 2 ? "stroke-outline" : "stroke-outline/70"}
strokeDasharray={index === 2 ? undefined : "5 7"}
vectorEffect="non-scaling-stroke"
/>
))}
<line x1="300" x2="700" y1="200" y2="200" className="stroke-outline" vectorEffect="non-scaling-stroke" />
<line x1="500" x2="500" y1="200" y2="400" className="stroke-outline" strokeDasharray="5 7" vectorEffect="non-scaling-stroke" />
<text x="292" y="196" textAnchor="end" className="fill-secondary font-mono text-label">OPPOSED</text>
<text x="500" y="424" textAnchor="middle" className="fill-secondary font-mono text-label">NEUTRAL</text>
<text x="708" y="196" className="fill-secondary font-mono text-label">ALIGNED</text>
{/*
線を全部描いてから、ラベルを全部描く。1 本ずつ「線・ラベル」で並べると、後の線が
前のラベルの上に来る —— SVG は文書の順に描くので、縁取りは自分より前に描かれた
ものしか止められない(system-landscape-map で同じ並びが実測の事故になった)。
*/}
{drawnEdges.map(({ edge, path, touchesPriority }) => (
<path
key={edge.id}
d={path}
fill="none"
className={touchesPriority ? "stroke-tertiary" : "stroke-primary"}
strokeWidth={edge.strokeWidth}
vectorEffect="non-scaling-stroke"
markerEnd={`url(#${markerId})`}
data-primary-edge={edge.id}
/>
))}
{geometry.nodes.map((node) => {
const radius = node.radius * 3;
const isPriority = node.id === priority.id;
const nodeNumber = geometry.nodes.indexOf(node) + 1;
return (
<g
key={node.id}
tabIndex={0}
data-stakeholder-record={node.id}
data-primary-record={node.id}
data-geometry-a={node.support}
data-geometry-b={node.interest}
data-primary-emphasis={isPriority ? "true" : undefined}
>
<title>{`${node.name} · ${node.role} · INF ${node.influence} · INT ${node.interest} · SUP ${node.support}`}</title>
{isPriority && <circle cx={node.x} cy={node.y} r={radius + 12} className="fill-tertiary/15" />}
<circle cx={node.x} cy={node.y} r={radius} className={isPriority ? "fill-tertiary" : "fill-primary"} />
<circle cx={node.x} cy={node.y} r={Math.max(4, radius * 0.28)} className="fill-surface" />
{/* 2 行の間隔は 23px 取る。18px(-4 と +14)では実測で 3.7px しか空かず、
判定の下限 3px をかろうじて超えているだけで、1 つの塊に見えていた。 */}
<text data-primary-label={node.id} x={node.x} y={node.y + 3.5} textAnchor="middle" className="fill-on-primary font-mono font-semibold" fontSize="10">S{String(nodeNumber).padStart(2, "0")}</text>
</g>
);
})}
</svg>
<div className="space-y-3 sm:hidden">
{stakeholders.map((item) => (
<article key={item.id} data-stakeholder-record={item.id} data-stakeholder-mobile-record={item.id} className={`grid grid-cols-[1fr_auto] gap-3 border-b py-3 ${item.id === priority.id ? "border-tertiary" : "border-outline"}`}>
<div><strong>{item.name}</strong><p className="text-label text-secondary">{item.role}</p></div>
<span className="font-mono text-label">INF {item.influence} · INT {item.interest} · SUP {item.support}</span>
</article>
))}
<div className="space-y-2 border-t border-outline pt-4" aria-label="関係一覧">
{relations.map((relation, index) => {
const source = stakeholders.find((item) => item.id === relation.sourceId)!;
const target = stakeholders.find((item) => item.id === relation.targetId)!;
return <p key={`${relation.sourceId}-${relation.targetId}-${index}`} className="break-words text-label text-secondary"><strong className="font-mono text-primary">R{String(index + 1).padStart(2, "0")}</strong> · {source.name} → {target.name} · {relation.label} · 強度{relation.strength}</p>;
})}
</div>
</div>
</div>
<div className="stakeholder-network-key mt-5 hidden border-t border-outline pt-4 sm:block" aria-label="関係者と関係の一覧">
<h2 className="mb-3 text-h3">関係者</h2>
<div className="grid gap-x-6 gap-y-2 sm:grid-cols-2 xl:grid-cols-3">
{stakeholders.map((item, index) => <article key={item.id} data-stakeholder-key={item.id} className="min-w-0 break-words border-b border-outline/60 py-2 text-label"><strong className="font-mono">S{String(index + 1).padStart(2, "0")} · {item.name}</strong><p className="text-secondary">{item.role} · INF {item.influence} · INT {item.interest} · SUP {item.support}</p></article>)}
</div>
<h2 className="mb-3 mt-5 text-h3">関係</h2>
<div className="grid gap-x-6 gap-y-2 sm:grid-cols-2 xl:grid-cols-3">
{relations.map((relation, index) => {
const source = stakeholders.find((item) => item.id === relation.sourceId)!;
const target = stakeholders.find((item) => item.id === relation.targetId)!;
return <article key={`${relation.sourceId}-${relation.targetId}-${index}`} data-relation-key className="min-w-0 break-words border-b border-outline/60 py-2 text-label"><strong className="font-mono">R{String(index + 1).padStart(2, "0")} · {relation.label}</strong><p className="text-secondary">{source.name} → {target.name} · 強度{relation.strength}</p></article>;
})}
</div>
</div>
<div data-stakeholder-view="risk-ledger" className="mt-6 hidden group-has-[.stakeholder-view-risk:checked]/stakeholder:block">
<div className="divide-y divide-outline border-y border-outline">
{ranked.map((item, index) => (
<article key={item.id} data-stakeholder-record={item.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>{item.name}</strong><p className="text-label text-secondary">影響{item.influence} × 関心{item.interest} × 支持不足{6 - item.support}</p></div>
<span className="font-mono text-h3">{item.risk}</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.name}:</strong> {item.note}</p>)}
</details>
)}
</section>
);
}
/designs/stakeholder-network/source/schema.ts
import { z } from "zod";
export const stakeholderSchema = z.object({ id: z.string().min(1), name: z.string().min(1), role: z.string().min(1), influence: z.number().int().min(1).max(5), interest: z.number().int().min(1).max(5), support: z.number().int().min(1).max(5), note: z.string().optional() }).strict();
const relationSchema = z.object({ sourceId: z.string().min(1), targetId: z.string().min(1), strength: z.number().int().min(1).max(5), label: z.string().optional() }).strict();
export const schema = z.object({ title: z.string().min(1), subtitle: z.string().optional(), stakeholders: z.array(stakeholderSchema).min(4).max(10), relations: z.array(relationSchema).min(1).max(16), instanceId: z.string().optional() }).strict().superRefine((value, ctx) => {
// id は identity として使う(React の key、data 属性、対応強調の突き合わせ)。
// 重複すると別物どうしが同じ要素を指し、操作が意図しない相手に効く。
{
const ids = value.stakeholders.map((item) => item.id);
if (new Set(ids).size !== ids.length) {
ctx.addIssue({ code: "custom", path: ["stakeholders"], message: "stakeholders ids must be unique" });
}
}
const ids = new Set(value.stakeholders.map((item) => item.id));
value.relations.forEach((item, index) => { if (!ids.has(item.sourceId) || !ids.has(item.targetId) || item.sourceId === item.targetId) ctx.addIssue({ code: "custom", path: ["relations", index], message: "invalid stakeholder relation" }); });
});
export type Stakeholder = z.infer<typeof stakeholderSchema>;
export type StakeholderNetworkProps = z.infer<typeof schema>;
/designs/stakeholder-network/source/meta.yaml
id: stakeholder-network
name:
ja: "ステークホルダー・ネットワーク(合意形成リスク)"
en: "Stakeholder Network (Alignment Risk)"
purpose:
ja: "支持度・関心度・影響力と関係線を一つのnetworkへ写し、最大の合意形成リスクを導出する"
en: "Support, interest, influence and the relationships between people in one network, with the greatest risk to alignment read off it"
# A5 の裁定で draft として移設し、2026-09-04 に型ごとの承認を取って公開した。
status: published
# 索引カードの図版。pnpm layout:render が原本から描く。
still: ./still.png
category: governance
catalogOrder: 210
jobs:
- assign-governance
- prioritize-diagnose
- explain-structure
useCases:
- org
layoutKinds:
- network
tags: [stakeholder, influence, network, alignment, governance]
whenToUse:
- ja: "変革案件の合意形成順を決めたい場合"
en: "Deciding the order to build agreement on a change"
- ja: "公式組織図では見えない影響経路を説明したい場合"
en: "Explaining influence that the formal org chart does not show"
avoidWhen:
- ja: "責任記号を活動別に監査したい場合"
en: "Auditing responsibility letters activity by activity"
- ja: "2軸だけで関係者を位置づけたい場合"
en: "Placing people on two axes alone"
requiredInputs:
- ja: "利害関係者と役割"
en: "The stakeholders and their roles"
- ja: "影響力・関心度・支持度"
en: "Their influence, interest and support"
- ja: "関係者間の影響関係"
en: "How they influence one another"
relatedLayouts: [raci-matrix, matrix-2x2, logic-tree]
dataSchema:
stakeholders: "{ id, name, role, influence, interest, support, note? }[] — 4〜10件"
relations: "{ sourceId, targetId, strength, label? }[] — 1〜16関係"
sources: []
/designs/stakeholder-network/source/sample-data.json
{
"title": "財務部長との合意形成を最優先 — 高影響・高関心・支持度1",
"subtitle": "基幹刷新プログラムの関係者仮説(各尺度は5段階)",
"stakeholders": [
{"id":"sponsor","name":"事業責任者","role":"スポンサー","influence":5,"interest":5,"support":4},
{"id":"finance","name":"財務部長","role":"予算承認","influence":5,"interest":4,"support":1,"note":"投資回収と段階移行の根拠が不足し、全面刷新への懸念が強い想定"},
{"id":"it","name":"IT部長","role":"実行責任","influence":5,"interest":5,"support":4},
{"id":"users","name":"現場リーダー","role":"利用部門","influence":3,"interest":5,"support":3},
{"id":"audit","name":"内部監査","role":"統制確認","influence":4,"interest":3,"support":2},
{"id":"vendor","name":"導入ベンダー","role":"実行支援","influence":2,"interest":4,"support":5}
],
"relations": [
{"sourceId":"sponsor","targetId":"finance","strength":5,"label":"投資判断"},
{"sourceId":"it","targetId":"sponsor","strength":4,"label":"実行報告"},
{"sourceId":"users","targetId":"it","strength":4,"label":"業務要件"},
{"sourceId":"audit","targetId":"finance","strength":3,"label":"統制意見"},
{"sourceId":"vendor","targetId":"it","strength":3,"label":"技術支援"}
],
"instanceId": "core-renewal-stakeholders"
}