索引に戻る

パレート前線・多目的選定

マトリクス
生成可能
パレート前線・多目的選定

作品として見る

持ち帰る

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

全部入り

pareto-frontier-everything.md

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

依頼文

2.0 KB
「パレート前線・多目的選定」でHTML資料を作ってください。この型は実行できるので、版面を組み直す必要はありません — 必要なのは中身です。

## この型がすること
2つの目的指標を同時に改善できる案が存在しない非劣解を前線として示し、単一の重み付き得点へ潰さずトレードオフを比べる。

## 渡す必要があるもの
- 異なる2目的の名称・単位・望ましい方向(最大化または最小化)を指定する。
- 4〜12案の各目的値を入力する。強調案IDは任意で、非劣解は入力から導出する。

## 入力の形
- `title` — string — 複数目的の選択判断について伝える見出し
- `subtitle` — string? — 対象期間・評価条件・制約
- `xObjective, yObjective` — object (label, unit, direction) — 異なる2目的の評価指標
- `focusOptionId` — string? — 強調する選択肢ID
- `options` — array (id, label, x, y) — 4〜12案。非劣解は最大化・最小化方向を反映して描画時に導出

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

## 向かない場合
- 一つの指標への集中度や累積シェアを読む場合はパレート集中度を使う。
- 3軸以上の各候補プロフィール全体を比べる場合は平行座標を使う。非劣解だけから最終案は自動決定できない。

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

```bash
node scripts/render.mjs pareto-frontier <入力.json> --theme ink -o out.html
```

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

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

ファイル

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

template.tsx

10.4 KB

/designs/pareto-frontier/source/template.tsx

中身を見る
import type { Props } from "./schema";

const CSS = String.raw`
.asset{font-family:var(--font-sans);color:var(--color-on-surface);background:var(--color-surface);max-width:var(--container-slide);margin:auto;padding:2.5rem 3rem;line-height:1.5}.asset *{box-sizing:border-box}.asset h1{font-size:var(--text-h1);line-height:1.3;margin:0}.asset .subtitle{color:var(--color-secondary);margin:.6rem 0 1.25rem}.asset .eyebrow{font:600 var(--text-label) var(--font-mono);letter-spacing:.1em;color:var(--color-tertiary)}.asset .plot{display:block;width:100%;height:auto}.asset .plot text{font-family:var(--font-sans);font-size:13px;fill:var(--color-secondary)}.asset .plot .ink{fill:var(--color-on-surface)}.asset .summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem;margin:1rem 0 1.5rem}.asset .metric{padding:1rem;background:var(--color-surface-subtle);border-top:3px solid var(--color-tertiary);min-width:0}.asset .metric dt{font-size:var(--text-label);color:var(--color-secondary)}.asset .metric dd{margin:.25rem 0 0;font:700 var(--text-h2) var(--font-mono);overflow-wrap:anywhere}.asset .ledger{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums}.asset .ledger th,.asset .ledger td{text-align:left;vertical-align:top;padding:.65rem .75rem;border-bottom:1px solid var(--color-outline);overflow-wrap:anywhere}.asset .ledger th{color:var(--color-secondary)}.asset .ledger tr[data-frontier=true]{background:var(--color-tertiary-container)}.asset .ledger .mono{font-family:var(--font-mono)}.asset details{margin-top:1rem}.asset summary{cursor:pointer;min-height:2.75rem;padding:.5rem 0;color:var(--color-secondary)}.asset .note{border-left:3px solid var(--color-outline);padding:.75rem 1rem;color:var(--color-secondary)}
@media(max-width:640px){.asset{padding:1.5rem 1rem}.asset h1{font-size:var(--text-h2)}.asset .summary{grid-template-columns:1fr}.asset .ledger,.asset .ledger tbody,.asset .ledger tr,.asset .ledger td{display:block}.asset .ledger thead{display:none}.asset .ledger tr{padding:.65rem 0;border-bottom:1px solid var(--color-outline)}.asset .ledger td{display:grid;grid-template-columns:8rem minmax(0,1fr);gap:.75rem;border:0;padding:.2rem 0}.asset .ledger td:before{content:attr(data-label);font-size:var(--text-label);color:var(--color-secondary)}}
`;

type Option = Props["options"][number];
const utility = (value: number, direction: Props["xObjective"]["direction"]) => direction === "maximize" ? value : -value;
const fmt = (value: number) => Number.isInteger(value) ? value.toLocaleString("ja-JP") : value.toLocaleString("ja-JP", { maximumFractionDigits: 2 });
const scoreX = (option: Option, data: Props) => utility(option.x, data.xObjective.direction);
const scoreY = (option: Option, data: Props) => utility(option.y, data.yObjective.direction);
const dominates = (a: Option, b: Option, data: Props) => {
  const x = scoreX(a, data) >= scoreX(b, data);
  const y = scoreY(a, data) >= scoreY(b, data);
  const strict = scoreX(a, data) > scoreX(b, data) || scoreY(a, data) > scoreY(b, data);
  return x && y && strict;
};
const frontier = (options: Option[], data: Props) => options.filter(option => !options.some(other => other.id !== option.id && dominates(other, option, data)));

export default function Template(d: Props) {
  const best = frontier(d.options, d);
  const selected = d.options.find(option => option.id === d.focusOptionId);
  const xScores = d.options.map(option => scoreX(option, d));
  const yScores = d.options.map(option => scoreY(option, d));
  const xMin = Math.min(...xScores), xMax = Math.max(...xScores), yMin = Math.min(...yScores), yMax = Math.max(...yScores);
  const xAt = (option: Option) => 110 + (scoreX(option, d) - xMin) / (xMax - xMin || 1) * 700;
  const yAt = (option: Option) => 500 - (scoreY(option, d) - yMin) / (yMax - yMin || 1) * 360;
  const orderedFrontier = [...best].sort((a, b) => scoreX(a, d) - scoreX(b, d) || scoreY(b, d) - scoreY(a, d));
  const frontPath = orderedFrontier.map((option, index) => `${index ? "L" : "M"} ${xAt(option)} ${yAt(option)}`).join(" ");
  const xLabel = `${d.xObjective.label}(${d.xObjective.unit}・${d.xObjective.direction === "maximize" ? "大きいほど良い" : "小さいほど良い"})`;
  const yLabel = `${d.yObjective.label}(${d.yObjective.unit}・${d.yObjective.direction === "maximize" ? "大きいほど良い" : "小さいほど良い"})`;
  return <section className="asset asset-pareto-frontier">
    <style>{CSS}</style>
    <header>
      <p className="eyebrow">PARETO FRONTIER / MULTI-OBJECTIVE CHOICE</p>
      <h1>{d.title}</h1>
      {d.subtitle && <p className="subtitle">{d.subtitle}</p>}
    </header>
    <svg className="plot" data-layout-still="true" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="pareto-title pareto-desc" fontFamily="sans-serif" fontSize="13" fill="var(--color-secondary)">
      <title id="pareto-title">{d.title}</title>
      <desc id="pareto-desc">{d.options.length}案のうち{best.length}案が非劣解。両目的を同時に改善できる案がない選択肢を前線として示す。{selected ? `強調案は${selected.label}。` : ""}</desc>
      <rect x="0" y="0" width="1280" height="720" fill="var(--color-surface)" />
      <text x="76" y="40" fontSize="13" fontWeight="700" letterSpacing="2" fill="var(--color-tertiary)">PARETO FRONTIER</text>
      <text x="76" y="76" fontSize="22" fontWeight="700" fill="var(--color-on-surface)">{d.title}</text>
      {[0, .25, .5, .75, 1].map((t, index) => {
        const px = 110 + t * 700, py = 500 - t * 360;
        const xScore = xMin + t * (xMax - xMin), yScore = yMin + t * (yMax - yMin);
        const xValue = d.xObjective.direction === "maximize" ? xScore : -xScore;
        const yValue = d.yObjective.direction === "maximize" ? yScore : -yScore;
        return <g key={index}>
          <line x1="110" y1={py} x2="810" y2={py} stroke="var(--color-outline)" strokeWidth="1" opacity=".5" />
          <line x1={px} y1="140" x2={px} y2="500" stroke="var(--color-outline)" strokeWidth="1" opacity=".5" />
          {index > 0 && <text x={px} y="526" textAnchor="middle">{fmt(xValue)}</text>}
          <text x="94" y={py + 5} textAnchor="end">{fmt(yValue)}</text>
        </g>;
      })}
      <line x1="110" y1="500" x2="810" y2="500" stroke="var(--color-outline)" strokeWidth="1.5" />
      <line x1="110" y1="140" x2="110" y2="500" stroke="var(--color-outline)" strokeWidth="1.5" />
      {orderedFrontier.length > 1 && <path d={frontPath} fill="none" stroke="var(--color-tertiary)" strokeWidth="3" strokeDasharray="8 5" />}
      {d.options.map((option, index) => {
        const efficient = best.some(item => item.id === option.id);
        const focused = selected?.id === option.id;
        return <g key={option.id} data-pareto-option={option.id}>
          <circle cx={xAt(option)} cy={yAt(option)} r={focused ? 10 : efficient ? 8 : 6} fill={efficient ? "var(--color-tertiary)" : "var(--color-secondary)"} stroke={focused ? "var(--color-on-surface)" : "var(--color-surface)"} strokeWidth={focused ? 3 : 2} />
          <text x={xAt(option) + 10} y={yAt(option) - 9} fontWeight={focused ? 700 : 400} fill={focused ? "var(--color-on-surface)" : "var(--color-secondary)"}>{index + 1}</text>
        </g>;
      })}
      <text x="460" y="568" textAnchor="middle" fill="var(--color-on-surface)">{xLabel}</text>
      <text x="36" y="320" textAnchor="middle" transform="rotate(-90 36 320)" fill="var(--color-on-surface)">{yLabel}</text>
      <text x="866" y="150" fontWeight="700" fill="var(--color-on-surface)">選択肢 {d.options.length}案</text>
      <circle cx="878" cy="190" r="7" fill="var(--color-tertiary)" />
      <text x="896" y="195">非劣解 {best.length}案</text>
      <circle cx="878" cy="224" r="6" fill="var(--color-secondary)" />
      <text x="896" y="229">他案に劣後 {d.options.length - best.length}案</text>
      <line x1="866" y1="258" x2="916" y2="258" stroke="var(--color-tertiary)" strokeWidth="3" strokeDasharray="8 5" />
      <text x="930" y="263">非劣解を結んだ前線</text>
      <rect x="866" y="300" width="340" height="150" fill="var(--color-surface)" stroke="var(--color-outline)" />
      <text x="888" y="336" fontWeight="700" fill="var(--color-on-surface)">{selected ? `強調案|${selected.label}` : "前線の読み方"}</text>
      <text x="888" y="373">両方の目的を同時に上回る案が</text>
      <text x="888" y="398">存在しない選択肢が非劣解。</text>
      <text x="888" y="423">最終選定には制約や優先度を加える。</text>
      <text x="110" y="620" fontWeight="700" fill="var(--color-on-surface)">読み方</text>
      <text x="110" y="650">両軸は望ましい方向が右上。破線が非劣解の境界で、単一の重み付け得点は作らない。</text>
      <text x="110" y="680">番号と値の対応、全選択肢、優劣判定は下の一覧で確認する。</text>
    </svg>
    <dl className="summary">
      <div className="metric"><dt>非劣解</dt><dd>{best.length} / {d.options.length}案</dd></div>
      <div className="metric"><dt>横軸|{xLabel}</dt><dd>{fmt(Math.min(...d.options.map(x => x.x)))} – {fmt(Math.max(...d.options.map(x => x.x)))}</dd></div>
      <div className="metric"><dt>縦軸|{yLabel}</dt><dd>{fmt(Math.min(...d.options.map(x => x.y)))} – {fmt(Math.max(...d.options.map(x => x.y)))}</dd></div>
    </dl>
    <p className="note">非劣解は2目的だけの数学的比較です。予算・規制・実装可能性などの制約や、どちらを優先するかは別途判断してください。</p>
    <details open>
      <summary>全選択肢と非劣解の判定を確認({d.options.length}案)</summary>
      <table className="ledger"><thead><tr><th>番号 / 選択肢</th><th>{xLabel}</th><th>{yLabel}</th><th>判定</th></tr></thead>
        <tbody>{d.options.map((option, index) => {
          const efficient = best.some(item => item.id === option.id);
          return <tr key={option.id} data-pareto-option={option.id} data-frontier={efficient} data-focus={option.id === selected?.id}>
            <td data-label="選択肢">{index + 1} / {option.label}{option.id === selected?.id ? "(強調)" : ""}</td>
            <td data-label={xLabel} className="mono">{fmt(option.x)}</td>
            <td data-label={yLabel} className="mono">{fmt(option.y)}</td>
            <td data-label="判定">{efficient ? "非劣解" : "他案に劣後"}</td>
          </tr>;
        })}</tbody>
      </table>
    </details>
  </section>;
}

schema.ts

1.9 KB

/designs/pareto-frontier/source/schema.ts

中身を見る
import { z } from "zod";

const text = z.string().trim().min(1).max(160);
const short = z.string().trim().min(1).max(64);
const id = z.string().trim().min(1).max(24);
const objective = z.object({
  label: short.describe("評価する目的指標の名前"),
  unit: z.string().trim().min(1).max(24).describe("目的指標の単位"),
  direction: z.enum(["maximize", "minimize"]).describe("大きい値と小さい値のどちらを望ましいとするか"),
}).strict();

export const schema = z.object({
  title: text.describe("複数目的の選択判断について伝える見出し"),
  subtitle: text.optional().describe("対象期間・評価条件・制約"),
  xObjective: objective.describe("横軸の目的指標・単位・望ましい方向"),
  yObjective: objective.describe("縦軸の目的指標・単位・望ましい方向"),
  focusOptionId: id.optional().describe("議論の対象として強調する選択肢ID"),
  options: z.array(z.object({
    id: id.describe("選択肢の一意なID"),
    label: short.describe("比較する選択肢名"),
    x: z.number().finite().describe("横軸の生値"),
    y: z.number().finite().describe("縦軸の生値"),
  }).strict()).min(4).max(12).describe("2目的で比較する選択肢。非劣解は描画時に導出する"),
}).strict().superRefine((data, ctx) => {
  const ids = data.options.map(option => option.id);
  if (new Set(ids).size !== ids.length) {
    ctx.addIssue({ code: "custom", path: ["options"], message: "選択肢IDは一意" });
  }
  if (data.focusOptionId && !ids.includes(data.focusOptionId)) {
    ctx.addIssue({ code: "custom", path: ["focusOptionId"], message: "強調する選択肢はoptionsに存在する" });
  }
  if (data.xObjective.label === data.yObjective.label) {
    ctx.addIssue({ code: "custom", path: ["yObjective", "label"], message: "横軸と縦軸に異なる目的指標を指定する" });
  }
});

export type Props = z.infer<typeof schema>;

meta.yaml

2.6 KB

/designs/pareto-frontier/source/meta.yaml

中身を見る
id: pareto-frontier
name:
  ja: パレート前線・多目的選定
  en: Pareto Frontier for Multi-objective Choice
purpose:
  ja: 2つの目的指標を同時に改善できる案が存在しない非劣解を前線として示し、単一の重み付き得点へ潰さずトレードオフを比べる。
  en: Show options that cannot be improved on both objectives at once, preserving the tradeoff instead of collapsing it into one weighted score.
status: published
still: ./still.png
category: analysis
catalogOrder: 810
jobs:
  - compare-choose
  - prioritize-diagnose
useCases:
  - comparison
  - prioritization
layoutKinds:
  - matrix
tags:
  - pareto-frontier
  - multi-objective
  - tradeoff
  - offline
  - responsive
whenToUse:
  - ja: 費用・品質・速度など2目的の望ましい方向を決め、同時に他案へ劣る案と非劣解群を分ける。
    en: Use to separate dominated options from the nondominated set across two objectives such as cost, quality, or speed.
  - ja: 評価軸に恣意的な重みを付ける前に、残す価値がある候補の境界を見せる。
    en: Use to show the boundary of worthwhile options before assigning subjective weights.
avoidWhen:
  - ja: 一つの指標への集中度や累積シェアを読む場合はパレート集中度を使う。
    en: Use Pareto concentration to read ranked contribution and cumulative share on one measure.
  - ja: 3軸以上の各候補プロフィール全体を比べる場合は平行座標を使う。非劣解だけから最終案は自動決定できない。
    en: Use parallel coordinates for profiles with three or more measures; a frontier alone cannot select the final option.
requiredInputs:
  - ja: 異なる2目的の名称・単位・望ましい方向(最大化または最小化)を指定する。
    en: Supply two distinct objectives, units, and whether each should be maximized or minimized.
  - ja: 4〜12案の各目的値を入力する。強調案IDは任意で、非劣解は入力から導出する。
    en: Provide values for four to twelve options; a focus ID is optional and the nondominated set is derived.
relatedLayouts:
  - pareto-concentration
  - weighted-decision-scorecard
  - parallel-coordinates-profile
dataSchema:
  title: string — 複数目的の選択判断について伝える見出し
  subtitle: string? — 対象期間・評価条件・制約
  xObjective, yObjective: object (label, unit, direction) — 異なる2目的の評価指標
  focusOptionId: string? — 強調する選択肢ID
  options: array (id, label, x, y) — 4〜12案。非劣解は最大化・最小化方向を反映して描画時に導出
sources: []

sample-data.json

789 B

/designs/pareto-frontier/source/sample-data.json

中身を見る
{
  "title": "費用と可用性の両立案を絞り込む",
  "subtitle": "架空サンプル|業務基盤の運用委託先候補",
  "xObjective": { "label": "年間費用", "unit": "百万円", "direction": "minimize" },
  "yObjective": { "label": "月間可用性", "unit": "%", "direction": "maximize" },
  "focusOptionId": "s2",
  "options": [
    { "id": "s1", "label": "標準サポート", "x": 2.4, "y": 99.1 },
    { "id": "s2", "label": "東西二拠点", "x": 3.1, "y": 99.6 },
    { "id": "s3", "label": "専任運用付き", "x": 4.4, "y": 99.85 },
    { "id": "s4", "label": "高可用性構成", "x": 5.7, "y": 99.9 },
    { "id": "s5", "label": "追加監視のみ", "x": 3.8, "y": 99.3 },
    { "id": "s6", "label": "初期費用重視", "x": 2.8, "y": 98.7 }
  ]
}