索引に戻る

平行座標プロファイル

一軸
生成可能
平行座標プロファイル

作品として見る

持ち帰る

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

全部入り

parallel-coordinates-profile-everything.md

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

依頼文

1.8 KB
「平行座標プロファイル」でHTML資料を作ってください。この型は実行できるので、版面を組み直す必要はありません — 必要なのは中身です。

## この型がすること
値域の異なる複数の評価軸を並べ、各対象のプロフィールとトレードオフを比較する。

## 渡す必要があるもの
- 3〜6軸の名称・単位・最小値・最大値と、各対象の軸順の値を指定する。
- 各値は指定範囲内に収め、強調する対象を一つ選ぶ。

## 入力の形
- `title` — string — 比較結果を示す見出し
- `subtitle` — string? — 対象・測定条件・時点
- `dimensions` — array (label, unit, min, max) — 3〜6軸
- `profiles` — array (label, values) — 4〜10対象
- `focusProfile` — string — 強調する対象名

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

## 向かない場合
- 二つの指標間の正確な相関や因果を判定する場合。軸の順番が交差の見え方に影響する。
- 対象や評価軸が多く線の重なりが強い場合。対象数や軸数を絞る。

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

```bash
node scripts/render.mjs parallel-coordinates-profile <入力.json> --theme ink -o out.html
```

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

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

ファイル

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

template.tsx

7.5 KB

/designs/parallel-coordinates-profile/source/template.tsx

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

const joinIds = (...parts: (string | number)[]) =>
  parts.map((part) => String(part).replace(/[_-]/g, (c) => `_${c.charCodeAt(0).toString(16)}_`)).join("-");

const fmt = (n: number) => Number.isInteger(n) ? n.toLocaleString("ja-JP") : n.toLocaleString("ja-JP", { maximumFractionDigits: 2 });
const wrap = (text: string, width: number) => Array.from({ length: Math.ceil([...text].length / width) }, (_, i) => [...text].slice(i * width, (i + 1) * width).join(""));

export default function Template(d: Props) {
  const titleLines = wrap(d.title, 56);
  const subtitleLines = d.subtitle ? wrap(d.subtitle, 80) : [];
  const titleY = 48;
  const subtitleY = titleY + titleLines.length * 25 + 5;
  const noteY = subtitleY + subtitleLines.length * 18 + 14;
  const top = noteY + 80;
  const bottom = Math.min(555, top + 310);
  const left = 150;
  const right = 990;
  const step = (right - left) / (d.dimensions.length - 1);
  const xs = d.dimensions.map((_, i) => left + step * i);
  const y = (value: number, dimension: number) => bottom -
    (value - d.dimensions[dimension].min) / (d.dimensions[dimension].max - d.dimensions[dimension].min) * (bottom - top);
  const focus = d.profiles.find(profile => profile.label === d.focusProfile)!;
  const endpointLabels = d.profiles.map((profile, index) => ({
    profile,
    index,
    actualY: y(profile.values[d.dimensions.length - 1], d.dimensions.length - 1),
  })).sort((a, b) => a.actualY - b.actualY);
  const labelGap = endpointLabels.length > 1 ? (bottom - top) / (endpointLabels.length - 1) : 0;

  return <section className="asset parallel">
    <style>{`.parallel{font-family:var(--font-sans);color:var(--color-on-surface);background:var(--color-surface);max-width:var(--container-slide);margin:auto;padding:2rem;line-height:1.5;font-size:var(--text-body-sm)}.parallel *{box-sizing:border-box}.parallel h1{font-size:var(--text-h2);line-height:1.3;margin:0}.muted{color:var(--color-secondary)}.plot{display:block;width:100%;height:auto}.plot .svg-title,.plot .svg-subtitle{display:none}.profile-table{width:100%;border-collapse:collapse;margin-top:1.5rem;font-variant-numeric:tabular-nums}.profile-table th,.profile-table td{padding:.65rem .55rem;border-bottom:1px solid var(--color-outline);text-align:right;overflow-wrap:anywhere}.profile-table th{color:var(--color-secondary)}.profile-table th:first-child,.profile-table td:first-child{text-align:left}.focus-row{font-weight:700;background:var(--color-surface-subtle)}.note{margin-top:1rem;color:var(--color-secondary)}@media(max-width:640px){.parallel{padding:1rem}.parallel h1{font-size:var(--text-h3)}.plot{display:none}.profile-table,.profile-table tbody,.profile-table tr,.profile-table td{display:block}.profile-table thead{display:none}.profile-table tr{padding:.75rem 0;border-bottom:1px solid var(--color-outline)}.profile-table td{border:0;padding:.2rem 0;text-align:left!important}.profile-table td:not(:first-child){display:inline-block;margin-right:.75rem}.profile-table td:not(:first-child)::before{content:attr(data-label) ": ";color:var(--color-secondary)}}`}</style>
    <header><p className="muted">PARALLEL COORDINATES · 値域ごとに正規化して配置</p>
      <h1>{titleLines.map((line, i) => <span key={i}>{line}{i + 1 < titleLines.length && <br />}</span>)}</h1>
      {d.subtitle && <p className="muted">{d.subtitle}</p>}
    </header>
    <svg className="plot" data-layout-still="true" xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-label="複数の対象を値域ごとに正規化した平行座標図">
      <rect width="1280" height="720" fill="var(--color-surface)" />
      {titleLines.map((line, i) => <text key={`title-${i}`} className="svg-title" x="64" y={titleY + i * 25} fontSize="20" fontWeight="700" fill="var(--color-on-surface)">{line}</text>)}
      {subtitleLines.map((line, i) => <text key={`subtitle-${i}`} className="svg-subtitle" x="64" y={subtitleY + i * 18} fontSize="14" fill="var(--color-secondary)">{line}</text>)}
      <text x="64" y={noteY} fontSize="14" fill="var(--color-secondary)">強調:{d.focusProfile} / 各軸を指定した最小〜最大で正規化</text>
      {[0, 0.5, 1].map((fraction, i) => {
        const gy = bottom - (bottom - top) * fraction;
        return <line key={i} x1={left} y1={gy} x2={right} y2={gy} stroke="var(--color-outline)" strokeWidth="1" strokeDasharray={i === 1 ? "3 5" : undefined} />;
      })}
      {d.dimensions.map((dimension, i) => <g key={dimension.label}>
        <line x1={xs[i]} y1={top} x2={xs[i]} y2={bottom} stroke="var(--color-outline)" strokeWidth="2" />
        <text x={xs[i]} y={top - 56} textAnchor="middle" fontSize="15" fontWeight="700" fill="var(--color-on-surface)">{dimension.label}</text>
        <text x={xs[i]} y={top - 38} textAnchor="middle" fontSize="12" fill="var(--color-secondary)">{dimension.unit}</text>
        <text x={xs[i]} y={top - 18} textAnchor="middle" fontSize="11" fill="var(--color-secondary)">{fmt(dimension.max)}</text>
        <text x={xs[i]} y={bottom + 37} textAnchor="middle" fontSize="11" fill="var(--color-secondary)">{fmt(dimension.min)}</text>
      </g>)}
      {d.profiles.filter(profile => profile.label !== d.focusProfile).map(profile => <polyline key={profile.label} data-profile={profile.label} points={profile.values.map((value, i) => `${xs[i]},${y(value, i)}`).join(" ")} fill="none" stroke="var(--color-secondary)" strokeWidth="2" strokeOpacity="0.48" />)}
      {focus && <polyline data-profile={focus.label} points={focus.values.map((value, i) => `${xs[i]},${y(value, i)}`).join(" ")} fill="none" stroke="var(--color-tertiary)" strokeWidth="4" />}
      {d.profiles.map(profile => profile.values.map((value, i) => <circle key={joinIds(profile.label, i)} cx={xs[i]} cy={y(value, i)} r={profile.label === d.focusProfile ? 6 : 3.5} fill={profile.label === d.focusProfile ? "var(--color-tertiary)" : "var(--color-secondary)"} />))}
      {endpointLabels.map(({ profile, index, actualY }, rank) => {
        const labelY = top + rank * labelGap;
        const id = String(index + 1).padStart(2, "0");
        return <g key={`endpoint-${profile.label}`}>
          <line x1={right + 3} y1={actualY} x2={right + 14} y2={labelY} stroke="var(--color-secondary)" strokeWidth="1" />
          <line x1={right + 14} y1={labelY} x2={right + 21} y2={labelY} stroke="var(--color-secondary)" strokeWidth="1" />
          <text x={right + 26} y={labelY + 4} fontSize="12" fontWeight="700" fill={profile.label === d.focusProfile ? "var(--color-tertiary)" : "var(--color-secondary)"}>{id}</text>
        </g>;
      })}
      <text x="64" y="690" fontSize="13" fill="var(--color-secondary)">軸の順序を変えると交差の見え方も変わります。相関や因果の証拠としては使わないでください。</text>
    </svg>
    <table className="profile-table"><thead><tr><th>対象</th>{d.dimensions.map(axis => <th key={axis.label}>{axis.label}({axis.unit})</th>)}</tr></thead><tbody>
      {d.profiles.map((profile, index) => <tr key={profile.label} className={profile.label === d.focusProfile ? "focus-row" : ""} data-profile-row><td><strong>{String(index + 1).padStart(2, "0")}</strong> {profile.label}</td>{profile.values.map((value, i) => <td key={d.dimensions[i].label} data-label={`${d.dimensions[i].label} (${d.dimensions[i].unit})`}>{fmt(value)}</td>)}</tr>)}
    </tbody></table>
    <p className="note">各指標は設定した範囲の中で位置を正規化しています。実数値は表で確認できます。</p>
  </section>;
}

schema.ts

1.8 KB

/designs/parallel-coordinates-profile/source/schema.ts

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

const text = z.string().trim().min(1).max(160);
const label = z.string().trim().min(1).max(16);
const finite = z.number().finite();
const unique = (values: string[]) => new Set(values).size === values.length;

export const schema = z.object({
  title: text.describe("比較結果を示す見出し"),
  subtitle: text.optional().describe("対象・測定条件・時点"),
  dimensions: z.array(z.object({
    label: z.string().trim().min(1).max(12).describe("評価軸名"),
    unit: z.string().trim().min(1).max(10).describe("評価軸の単位"),
    min: finite.describe("軸の下限"),
    max: finite.describe("軸の上限"),
  }).strict().refine(axis => axis.min < axis.max, "軸の下限は上限より小さい")).min(3).max(6)
    .describe("値域が異なる複数の評価軸"),
  profiles: z.array(z.object({
    label: label.describe("対象・候補名"),
    values: z.array(finite).describe("評価軸順の実測値"),
  }).strict()).min(4).max(10).describe("評価軸に沿って結ぶ対象別プロフィール"),
  focusProfile: label.describe("強調して比較するプロフィール"),
}).strict()
  .refine(d => unique(d.dimensions.map(axis => axis.label)), "評価軸名は重複しない")
  .refine(d => unique(d.profiles.map(profile => profile.label)), "プロフィール名は重複しない")
  .refine(d => d.profiles.every(profile => profile.values.length === d.dimensions.length &&
    profile.values.every((value, i) => value >= d.dimensions[i].min && value <= d.dimensions[i].max)),
  "各プロフィールの値の個数と順序を評価軸に合わせ、値を軸範囲に収める")
  .refine(d => d.profiles.some(profile => profile.label === d.focusProfile), "強調対象はプロフィール一覧に含める");

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

meta.yaml

2.2 KB

/designs/parallel-coordinates-profile/source/meta.yaml

中身を見る
id: parallel-coordinates-profile
name:
  ja: 平行座標プロファイル
  en: Parallel Coordinates Profile
purpose:
  ja: 値域の異なる複数の評価軸を並べ、各対象のプロフィールとトレードオフを比較する。
  en: Compare profiles and tradeoffs across multiple dimensions with different value ranges.
status: published
still: ./still.png
category: analysis
catalogOrder: 750
jobs:
  - compare-choose
  - prioritize-diagnose
useCases:
  - comparison
  - requirements
layoutKinds:
  - axis
tags:
  - parallel-coordinates
  - multivariate
  - tradeoff
  - offline
  - responsive
whenToUse:
  - ja: 3〜6個の指標を持つ複数候補の組み合わせや、軸ごとのトレードオフを見たい。
    en: Use to compare combinations and tradeoffs across 3 to 6 measures for multiple candidates.
  - ja: 指標ごとに単位や値域が異なるが、各対象のプロフィールを一望したい。
    en: Use when measures have different units or ranges but should be inspected together by profile.
avoidWhen:
  - ja: 二つの指標間の正確な相関や因果を判定する場合。軸の順番が交差の見え方に影響する。
    en: Do not infer exact correlation or causality; axis order changes the apparent crossings.
  - ja: 対象や評価軸が多く線の重なりが強い場合。対象数や軸数を絞る。
    en: Reduce the number of profiles or dimensions when line overlap obscures individual patterns.
requiredInputs:
  - ja: 3〜6軸の名称・単位・最小値・最大値と、各対象の軸順の値を指定する。
    en: Supply 3 to 6 dimensions with labels, units, bounds, and one ordered value per dimension for each profile.
  - ja: 各値は指定範囲内に収め、強調する対象を一つ選ぶ。
    en: Keep values within each axis range and select one profile to highlight.
relatedLayouts:
  - scatter-relationship
  - bubble-portfolio
dataSchema:
  title: string — 比較結果を示す見出し
  subtitle: string? — 対象・測定条件・時点
  dimensions: array (label, unit, min, max) — 3〜6軸
  profiles: array (label, values) — 4〜10対象
  focusProfile: string — 強調する対象名
sources: []

sample-data.json

804 B

/designs/parallel-coordinates-profile/source/sample-data.json

中身を見る
{
  "title": "候補Bは納期とコストで優位、信頼性は候補Aが上回る",
  "subtitle": "架空の設備更新候補|過去12か月の同一条件で比較",
  "dimensions": [
    { "label": "納期", "unit": "日", "min": 20, "max": 80 },
    { "label": "稼働率", "unit": "%", "min": 80, "max": 100 },
    { "label": "保守費", "unit": "万円/年", "min": 100, "max": 700 },
    { "label": "省電力", "unit": "点", "min": 0, "max": 100 }
  ],
  "profiles": [
    { "label": "候補A", "values": [56, 98, 590, 44] },
    { "label": "候補B", "values": [34, 95, 360, 72] },
    { "label": "候補C", "values": [48, 92, 420, 65] },
    { "label": "候補D", "values": [41, 89, 250, 81] },
    { "label": "候補E", "values": [68, 96, 310, 55] }
  ],
  "focusProfile": "候補B"
}