Back to index

System Landscape (Modernization Impact)

Network
Generates documents
System Landscape (Modernization Impact)

Seen as a design

Take it with you

Copy and paste, or save a file and hand it over. For a machine with no network, one “Everything” file is enough.

Everything

system-landscape-map-everything.md

Everything in this panel, including the contents behind each URL, in one Markdown file

Prompt

1.4 KB
Build an HTML document with "System Landscape (Modernization Impact)". This layout runs, so there is no composition to rebuild — what it needs is content.

## What this layout does
Systems placed by business domain and architecture layer, with the order to modernize following from size, health, criticality and how they interconnect

## What you have to supply
- The business domains and layers
- Each system's size, health and criticality
- The interfaces between them

## The shape of the input
- `systems` — { id, name, domainId, layerId, userCount, health, criticality, note? }[] — 4〜12件
- `interfaces` — { sourceId, targetId, criticality, label? }[] — 1〜20件

schema.ts holds the machine-checkable validation. Ask rather than guess when an input is missing.

## When not to use it
- Showing volume as the width of many-to-many bands
- Showing capability maturity and how investment is split

## How to build it
Put the input in a JSON file and run:

```bash
node scripts/render.mjs system-landscape-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/system-landscape-map/source/template.tsx
- https://design-vault.pages.dev/designs/system-landscape-map/source/schema.ts
- https://design-vault.pages.dev/designs/system-landscape-map/source/sample-data.json
- https://design-vault.pages.dev/designs/system-landscape-map/sample.html

Files

Template, input schema, sample data and metadata. This document was generated from those four. The generated document

template.tsx

20.7 KB

/designs/system-landscape-map/source/template.tsx

Show contents
import { useId } from "react";
import type {
  LandscapeSystem,
  SystemInterface,
  SystemLandscapeMapProps,
} from "./schema";

type Axis = SystemLandscapeMapProps["domains"][number];

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

/**
 * 図の中に置くラベルの縁取り。`stroke-surface` と組で使う。
 *
 * 塗りの前に地の色で縁を引くので、連携線はラベルの手前で途切れて見え、線の上にラベルが
 * 来ても字が残る。背後に何も無いところでは地と同じ色なので見えない。
 *
 * 線を避けて置き場を解き直す道は取らない。この型の連携線はレイヤの間を縦横に走っていて、
 * 丸だけを避ける求解ですら 43 万本の総当たりを要した(45d736b)。そこへ線を足すと、解が
 * 無い入力が必ず残る。縁取りは入力に依らず効く。
 */
const HALO = { paintOrder: "stroke", strokeWidth: 4, strokeLinejoin: "round" } as const;

export function rankSystemRisks(
  systems: LandscapeSystem[],
  interfaces: SystemInterface[],
) {
  return systems
    .map((system, index) => {
      const connectionCount = interfaces.filter(
        (item) => item.sourceId === system.id || item.targetId === system.id,
      ).length;
      return {
        ...system,
        connectionCount,
        risk: system.criticality * (5 - system.health) + connectionCount,
        inputIndex: index,
      };
    })
    .sort((a, b) => b.risk - a.risk || a.inputIndex - b.inputIndex);
}

/**
 * 連携線のラベルをどこに置くか。
 *
 * 中点の真上(`textAnchor="middle"`)に置くと、縦向きの線ではラベルが行き先の丸と
 * 同じ x に来る。丸のラベルは枠に 1 つしか無いとき丸の真上か真下へ 2 行で置くので、
 * 線のラベルとぶつかる。レイヤ 3 段の行間は 118px で、両端の丸の半径と丸のラベル
 * 2 行がその大半を使うため、縦の線に残る隙間は元から 20px も無い。
 *
 * 実測(1280px・`svg text` の外接矩形): 直す前は「顧客情報」と「CRM」が 0.7px、
 * 「経費・購買申請」と「ERP」が 0.1px しか離れておらず、上下が接して 1 つの塊に
 * 見えていた。丸のラベル 2 行の意図した行間が 4px なので、そこと区別が付かない。
 *
 * 縦に近い線ではラベルを線の脇へ逃がす。右寄りの線を右へ逃がすと軸の外へ出るので、
 * その場合は左へ倒す。横に近い線は従来どおり中点の上に置く —— 横の線では行き先の
 * 丸のラベルと x が揃わないので、ぶつかる理由が無い。
 */
/**
 * 線のラベルを線から持ち上げる量。
 *
 * 横へ逃がすだけでは足りなかった。逃がしたあとも「顧客情報」と「CRM」は横に 3.1px、
 * 「経費・購買申請」と「ERP」は 5.2px しか空いておらず、ラベル衝突の下限 3px を
 * かろうじて超えているだけだった(1200px で実測)。行き先の名前の幅は入力で変わるので、
 * 横の逃げは入力に依らない余裕を作れない。縦へ持ち上げるほうは作れる。
 *
 * 11 がこの図の天井で、12 にすると上のレイヤの健全性の輪へ字が乗る(`layout:browser` が
 * 「顧客情報」と「経費・購買申請」の 2 件で落ちる)。輪は丸より後に描かれるので縁取りが
 * 効かない。11 で最小の隙間は 4.03px —— 下限に対する余裕は 1px しかないが、これは図の
 * 骨格が決めている限界で、これ以上は横の逃げか丸の配置を解き直すしかない。
 */
const LIFT = 11;

export const edgeLabelPlacement = (
  middleX: number,
  middleY: number,
  dx: number,
  dy: number,
) => {
  // 縦横比 2:1 より急なら「縦の線」。真横に近い線を巻き込まない角度。
  if (Math.abs(dy) <= Math.abs(dx) * 2) {
    return { x: middleX, y: middleY - LIFT, anchor: "middle" as const };
  }
  // 逃がす量は丸のラベル 1 行目の半幅を超えている必要がある。10 では足りず
  // (実測で横の重なりが 23px → 3px に減るだけだった)、システム名の半幅は
  // サンプル最長の「顧客ポータル」で 33 なので 20 を取る。縦は動かさない ——
  // 下げると丸のラベルへ寄ってしまう。
  // 描画領域は viewBox の 0-1000。右端に寄った線は左へ逃がす。
  const toLeft = middleX > 700;
  return {
    x: round(middleX + (toLeft ? -20 : 20)),
    y: round(middleY - LIFT),
    anchor: toLeft ? ("end" as const) : ("start" as const),
  };
};

export function systemLandscapeGeometry(
  domains: Axis[],
  layers: Axis[],
  systems: LandscapeSystem[],
  interfaces: SystemInterface[],
) {
  const maxUsers = Math.max(...systems.map((system) => system.userCount));
  const nodes = systems.map((system) => {
    const domainIndex = domains.findIndex((item) => item.id === system.domainId);
    const layerIndex = layers.findIndex((item) => item.id === system.layerId);
    const peers = systems.filter(
      (item) =>
        item.domainId === system.domainId && item.layerId === system.layerId,
    );
    const peerIndex = peers.findIndex((item) => item.id === system.id);
    const peerOffset = peerIndex - (peers.length - 1) / 2;
    return {
      ...system,
      x: round(
        120 + ((domainIndex + 0.5) / domains.length) * 850 + peerOffset * 62,
      ),
      y: round(
        66 + ((layerIndex + 0.5) / layers.length) * 354 + peerOffset * 18,
      ),
      radius: round(18 + Math.sqrt(system.userCount / maxUsers) * 22),
      domainIndex,
      peerCount: peers.length,
      labelSide:
        peers.length > 1 ? (peerIndex % 2 === 0 ? "left" : "right") : "right",
    };
  });
  const positions = new Map(nodes.map((node) => [node.id, node]));
  const edges = interfaces.map((item, index) => ({
    ...item,
    id: `${item.sourceId}-${item.targetId}-${index}`,
    source: positions.get(item.sourceId)!,
    target: positions.get(item.targetId)!,
    strokeWidth: round(1 + item.criticality * 0.65),
  }));
  return { nodes, edges };
}

export default function SystemLandscapeMap({
  title,
  subtitle,
  domains,
  layers,
  systems,
  interfaces,
  instanceId,
}: SystemLandscapeMapProps) {
  // 既定を固定文字列にすると、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)}_`,
    ) || "system-landscape";
  const ranked = rankSystemRisks(systems, interfaces);
  const priority = ranked[0];
  const notes = systems.filter((item) => item.note);
  const geometry = systemLandscapeGeometry(
    domains,
    layers,
    systems,
    interfaces,
  );
  const markerId = `${uid}-interface-arrow`;
  const drawnEdges = geometry.edges.map((edge) => {
    const dx = edge.target.x - edge.source.x;
    const dy = edge.target.y - edge.source.y;
    // この値は middleX を通ってラベルの置き場の分岐に入るので、実装依存の近似である
    // Math.hypot ではなく、正しい丸めが保証される Math.sqrt を使う。
    const distance = Math.sqrt(dx ** 2 + dy ** 2) || 1;
    const ux = dx / distance;
    const uy = dy / distance;
    const startX = edge.source.x + ux * (edge.source.radius + 4);
    const startY = edge.source.y + uy * (edge.source.radius + 4);
    const endX = edge.target.x - ux * (edge.target.radius + 10);
    const endY = edge.target.y - uy * (edge.target.radius + 10);
    const middleX = (startX + endX) / 2;
    const middleY = (startY + endY) / 2;
    return {
      edge,
      path: `M ${startX} ${startY} Q ${middleX} ${startY} ${endX} ${endY}`,
      label: edgeLabelPlacement(middleX, middleY, dx, dy),
      touchesPriority:
        edge.sourceId === priority.id || edge.targetId === priority.id,
    };
  });

  return (
    <section
      className="asset group/system overflow-hidden rounded-xl border border-outline bg-surface p-5 text-primary shadow-card sm:p-8"
      data-layout="system-landscape-map"
    >
      <header className="border-b border-outline pb-5">
        <p className="font-mono text-label text-tertiary">APPLICATION TOPOLOGY</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="system-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="system-view-risk sr-only" type="radio" name={`${uid}-view`} />刷新優先
          </label>
        </div>
        <span className="font-mono text-label text-secondary">RADIUS=USERS · RING=HEALTH · LINE=CRITICALITY</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-system-default-priority="true"
      >
        <span className="font-mono text-label text-tertiary">MODERNIZE FIRST</span>
        <strong>{priority.name}</strong>
        <span className="font-mono text-h3">RISK {priority.risk}</span>
      </div>

      <div data-system-view="landscape" className="mt-6 group-has-[.system-view-risk:checked]/system:hidden">
        <svg
          className="hidden h-96 w-full sm:block"
          viewBox="0 0 1000 460"
          role="img"
          aria-label="業務ドメインとシステム層に配置したアプリケーション連携図"
          data-primary-geometry="system-landscape-map"
          data-geometry-channels="domain layer user-radius health-ring interface-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>

          {domains.map((domain, index) => {
            const startX = 120 + (index / domains.length) * 850;
            const centerX = 120 + ((index + 0.5) / domains.length) * 850;
            return (
              <g key={domain.id}>
                <line x1={startX} x2={startX} y1="46" y2="438" className="stroke-outline" vectorEffect="non-scaling-stroke" />
                <title>{domain.name}</title>
                <text x={centerX} y="26" textAnchor="middle" className="fill-primary font-mono text-label font-semibold">D{String(index + 1).padStart(2, "0")}</text>
              </g>
            );
          })}
          <line x1="970" x2="970" y1="46" y2="438" className="stroke-outline" vectorEffect="non-scaling-stroke" />
          {layers.map((layer, index) => {
            const y = 66 + ((index + 0.5) / layers.length) * 354;
            const boundaryY = 66 + (index / layers.length) * 354;
            return (
              <g key={layer.id}>
                <line x1="116" x2="970" y1={boundaryY} y2={boundaryY} className="stroke-outline" vectorEffect="non-scaling-stroke" />
                <title>{layer.name}</title>
                <text x="106" y={y + 4} textAnchor="end" className="fill-secondary font-mono text-label">L{String(index + 1).padStart(2, "0")}</text>
              </g>
            );
          })}
          <line x1="116" x2="970" y1="420" y2="420" className="stroke-outline" vectorEffect="non-scaling-stroke" />

          {/*
            線を全部描いてから、ラベルを全部描く。1 本ずつ「線・ラベル」で並べると、後の線が
            前のラベルの上に来る —— SVG は文書の順に描くので、縁取りは自分より前に描かれた
            ものしか止められない。実測で「受注・売上」が 2 本あとの線に切られていた。
          */}
          {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 circumference = 2 * Math.PI * (node.radius + 5);
            const share = node.health / 5;
            const isPriority = node.id === priority.id;
            const nodeNumber = geometry.nodes.indexOf(node) + 1;
            return (
              <g
                key={node.id}
                tabIndex={0}
                data-system-record={node.id}
                data-primary-record={node.id}
                data-geometry-a={node.radius}
                data-geometry-b={node.health}
                data-primary-emphasis={isPriority ? "true" : undefined}
              >
                <title>{`${node.name} · ${domains[node.domainIndex]?.name} · ${layers.find((item) => item.id === node.layerId)?.name} · ${node.userCount}人 · 健全性${node.health} · 重要度${node.criticality}`}</title>
                {isPriority && <circle cx={node.x} cy={node.y} r={node.radius + 13} className="fill-tertiary/15" />}
                <circle cx={node.x} cy={node.y} r={node.radius + 5} fill="none" className="stroke-outline" strokeWidth="2" />
                <circle
                  cx={node.x}
                  cy={node.y}
                  r={node.radius + 5}
                  fill="none"
                  className={isPriority ? "stroke-tertiary" : "stroke-primary"}
                  strokeWidth="4"
                  strokeDasharray={`${round(circumference * share)} ${round(circumference * (1 - share))}`}
                  transform={`rotate(-90 ${node.x} ${node.y})`}
                />
                <circle cx={node.x} cy={node.y} r={node.radius} className={isPriority ? "fill-tertiary" : "fill-primary"} />
                {/* 図が運ぶのは名前だけ。数値は「刷新優先」の台帳と携帯のリストが持つ。
                    2 行だと丸のまわりに 31px 要り、レイヤ 3 段の行間 118px では
                    両端の丸と線のラベルを引いた残りに収まらない —— 実測で統計行が
                    健全性の輪を 8px、優先の丸では 13.4px 横切って H1 が読めなかった。 */}
                <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="relative space-y-4 pl-7 sm:hidden">
          <span className="absolute bottom-2 left-2 top-2 w-px bg-outline" aria-hidden="true" />
          {systems.map((system) => {
            const isPriority = system.id === priority.id;
            return (
              <article key={system.id} data-system-record={system.id} data-system-mobile-record={system.id} className="relative border-b border-outline py-3 pl-3">
                <span className={`absolute -left-6 top-4 h-3 w-3 rounded-full ${isPriority ? "bg-tertiary ring-4 ring-tertiary/20" : "bg-primary"}`} aria-hidden="true" />
                <div className="flex justify-between gap-3"><strong>{system.name}</strong><span className="font-mono text-label">H{system.health} · C{system.criticality}</span></div>
                <p className="text-label text-secondary">{domains.find((item) => item.id === system.domainId)?.name} / {layers.find((item) => item.id === system.layerId)?.name} · {system.userCount}人</p>
              </article>
            );
          })}
          <div className="space-y-2 border-t border-outline pt-4" aria-label="連携一覧">
            {interfaces.map((item, index) => {
              const source = systems.find((system) => system.id === item.sourceId)!;
              const target = systems.find((system) => system.id === item.targetId)!;
              return <p key={`${item.sourceId}-${item.targetId}-${index}`} className="break-words text-label text-secondary"><strong className="font-mono text-primary">I{String(index + 1).padStart(2, "0")}</strong> · {source.name} → {target.name} · {item.label ?? "連携"} · 重要度{item.criticality}</p>;
            })}
          </div>
        </div>
      </div>

      <div className="system-landscape-key mt-5 hidden border-t border-outline pt-4 sm:block" aria-label="領域・層・システム・連携の一覧">
        <div className="grid gap-5 xl:grid-cols-2">
          <section><h2 className="mb-3 text-h3">業務領域</h2><ul className="grid gap-2 sm:grid-cols-2">{domains.map((item, index) => <li key={item.id} className="break-words border-b border-outline/60 py-2 text-label"><strong className="font-mono">D{String(index + 1).padStart(2, "0")}</strong> · {item.name}</li>)}</ul></section>
          <section><h2 className="mb-3 text-h3">システム層</h2><ul className="grid gap-2 sm:grid-cols-2">{layers.map((item, index) => <li key={item.id} className="break-words border-b border-outline/60 py-2 text-label"><strong className="font-mono">L{String(index + 1).padStart(2, "0")}</strong> · {item.name}</li>)}</ul></section>
        </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">{systems.map((system, index) => <article key={system.id} data-system-key={system.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")} · {system.name}</strong><p className="text-secondary">D{domains.findIndex((item) => item.id === system.domainId) + 1} / L{layers.findIndex((item) => item.id === system.layerId) + 1} · {system.userCount}人 · H{system.health} · C{system.criticality}</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">{interfaces.map((item, index) => { const source = systems.find((system) => system.id === item.sourceId)!; const target = systems.find((system) => system.id === item.targetId)!; return <article key={`${item.sourceId}-${item.targetId}-${index}`} className="min-w-0 break-words border-b border-outline/60 py-2 text-label"><strong className="font-mono">I{String(index + 1).padStart(2, "0")} · {item.label ?? "連携"}</strong><p className="text-secondary">S{systems.indexOf(source) + 1} {source.name} → S{systems.indexOf(target) + 1} {target.name} · 重要度{item.criticality}</p></article>; })}</div>
      </div>

      <div data-system-view="risk-ledger" className="mt-6 hidden group-has-[.system-view-risk:checked]/system:block">
        <div className="divide-y divide-outline border-y border-outline">
          {ranked.map((system, index) => (
            <article key={system.id} data-system-record={system.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>
              {/* 規模は図から降ろしたので、卓上で数字として読めるのはここだけになる。
                  risk の式には入らない値なので、式とは中黒で分ける。 */}
              <div><strong>{system.name}</strong><p className="text-label text-secondary">{system.userCount}人 ・ 重要度{system.criticality} × 健全性不足{5 - system.health} + 接続{system.connectionCount}</p></div>
              <span className="font-mono text-h3">{system.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>
  );
}

schema.ts

2.7 KB

/designs/system-landscape-map/source/schema.ts

Show contents
import { z } from "zod";

const axisSchema = z.object({ id: z.string().min(1), name: z.string().min(1) }).strict();
export const systemSchema = z.object({ id: z.string().min(1), name: z.string().min(1), domainId: z.string().min(1), layerId: z.string().min(1), userCount: z.number().positive(), health: z.number().int().min(1).max(5), criticality: z.number().int().min(1).max(5), note: z.string().optional() }).strict();
export const interfaceSchema = z.object({ sourceId: z.string().min(1), targetId: z.string().min(1), criticality: 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(), domains: z.array(axisSchema).min(2).max(4), layers: z.array(axisSchema).min(2).max(4), systems: z.array(systemSchema).min(4).max(12), interfaces: z.array(interfaceSchema).min(1).max(20), instanceId: z.string().optional() }).strict().superRefine((value, ctx) => {
  // id は identity として使う(React の key、data 属性、対応強調の突き合わせ)。
  // 重複すると別物どうしが同じ要素を指し、操作が意図しない相手に効く。
  {
    const ids = value.domains.map((item) => item.id);
    if (new Set(ids).size !== ids.length) {
      ctx.addIssue({ code: "custom", path: ["domains"], message: "domains ids must be unique" });
    }
  }
  {
    const ids = value.layers.map((item) => item.id);
    if (new Set(ids).size !== ids.length) {
      ctx.addIssue({ code: "custom", path: ["layers"], message: "layers ids must be unique" });
    }
  }
  {
    const ids = value.systems.map((item) => item.id);
    if (new Set(ids).size !== ids.length) {
      ctx.addIssue({ code: "custom", path: ["systems"], message: "systems ids must be unique" });
    }
  }
  const domains = new Set(value.domains.map((item) => item.id));
  const layers = new Set(value.layers.map((item) => item.id));
  const ids = new Set(value.systems.map((item) => item.id));
  value.systems.forEach((item, index) => {
    if (!domains.has(item.domainId)) ctx.addIssue({ code: "custom", path: ["systems", index, "domainId"], message: "unknown domain" });
    if (!layers.has(item.layerId)) ctx.addIssue({ code: "custom", path: ["systems", index, "layerId"], message: "unknown layer" });
  });
  value.interfaces.forEach((item, index) => {
    if (!ids.has(item.sourceId) || !ids.has(item.targetId) || item.sourceId === item.targetId) ctx.addIssue({ code: "custom", path: ["interfaces", index], message: "interface must reference different systems" });
  });
});
export type LandscapeSystem = z.infer<typeof systemSchema>;
export type SystemInterface = z.infer<typeof interfaceSchema>;
export type SystemLandscapeMapProps = z.infer<typeof schema>;

meta.yaml

2.0 KB

/designs/system-landscape-map/source/meta.yaml

Show contents
id: system-landscape-map
name:
  ja: "システムランドスケープ(刷新影響)"
  en: "System Landscape (Modernization Impact)"
purpose:
  ja: "業務ドメインとアーキテクチャ層へシステムを配置し、規模・健全性・重要度・連携から刷新優先度を導出する"
  en: "Systems placed by business domain and architecture layer, with the order to modernize following from size, health, criticality and how they interconnect"
# A5 の裁定で draft として移設し、2026-09-04 に型ごとの承認を取って公開した。
status: published
# 索引カードの図版。pnpm layout:render が原本から描く。
still: ./still.png
category: architecture
catalogOrder: 160
jobs:
  - explain-structure
  - prioritize-diagnose
useCases:
  - org
layoutKinds:
  - network
tags: [system-landscape, enterprise-architecture, interface, modernization]
whenToUse:
  - ja: "現行システム構成と刷新順を一画面で議論したい場合"
    en: "Discussing the current estate and the order to replace it, on one screen"
  - ja: "変更影響の大きい連携集中点を説明したい場合"
    en: "Explaining where interfaces concentrate and change hurts most"
avoidWhen:
  - ja: "流量の大きさを多対多の帯幅で示したい場合"
    en: "Showing volume as the width of many-to-many bands"
  - ja: "業務能力の成熟度と投資配分を示したい場合"
    en: "Showing capability maturity and how investment is split"
requiredInputs:
  - ja: "業務ドメインと層"
    en: "The business domains and layers"
  - ja: "システムの規模・健全性・重要度"
    en: "Each system's size, health and criticality"
  - ja: "システム間interface"
    en: "The interfaces between them"
relatedLayouts: [business-capability-map, sankey-flow, heatmap]
dataSchema:
  systems: "{ id, name, domainId, layerId, userCount, health, criticality, note? }[] — 4〜12件"
  interfaces: "{ sourceId, targetId, criticality, label? }[] — 1〜20件"
sources: []

sample-data.json

1.9 KB

/designs/system-landscape-map/source/sample-data.json

Show contents
{
  "title": "刷新優先はERP — 低健全性と4本の連携が変更影響を広げる",
  "subtitle": "顧客・販売・経営基盤の現行システム仮説評価",
  "domains": [{"id":"customer","name":"顧客接点"},{"id":"sales","name":"販売・提供"},{"id":"corp","name":"経営基盤"}],
  "layers": [{"id":"experience","name":"接点"},{"id":"application","name":"業務アプリ"},{"id":"data","name":"データ"}],
  "systems": [
    {"id":"portal","name":"顧客ポータル","domainId":"customer","layerId":"experience","userCount":2400,"health":4,"criticality":4},
    {"id":"crm","name":"CRM","domainId":"customer","layerId":"application","userCount":420,"health":3,"criticality":4},
    {"id":"order","name":"受注管理","domainId":"sales","layerId":"application","userCount":380,"health":2,"criticality":5},
    {"id":"intra","name":"社内ポータル","domainId":"corp","layerId":"experience","userCount":1800,"health":4,"criticality":3},
    {"id":"erp","name":"ERP","domainId":"corp","layerId":"application","userCount":900,"health":1,"criticality":5,"note":"保守期限と個別改修の集中により変更リードタイムが長期化している想定"},
    {"id":"dwh","name":"統合DWH","domainId":"corp","layerId":"data","userCount":120,"health":4,"criticality":3}
  ],
  "interfaces": [
    {"sourceId":"portal","targetId":"crm","criticality":3,"label":"顧客情報"},
    {"sourceId":"crm","targetId":"order","criticality":4,"label":"商談・見積"},
    {"sourceId":"order","targetId":"erp","criticality":5,"label":"受注・売上"},
    {"sourceId":"intra","targetId":"erp","criticality":3,"label":"経費・購買申請"},
    {"sourceId":"erp","targetId":"dwh","criticality":4,"label":"会計実績"},
    {"sourceId":"erp","targetId":"portal","criticality":3,"label":"請求明細"},
    {"sourceId":"order","targetId":"dwh","criticality":3,"label":"販売実績"}
  ],
  "instanceId": "core-landscape"
}