Back to index

Parallel Coordinates Profile

Axis
Generates documents
Parallel Coordinates Profile

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

parallel-coordinates-profile-everything.md

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

Prompt

1.7 KB
Build an HTML document with "Parallel Coordinates Profile". This layout runs, so there is no composition to rebuild — what it needs is content.

## What this layout does
Compare profiles and tradeoffs across multiple dimensions with different value ranges.

## What you have to supply
- Supply 3 to 6 dimensions with labels, units, bounds, and one ordered value per dimension for each profile.
- Keep values within each axis range and select one profile to highlight.

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

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

## When not to use it
- Do not infer exact correlation or causality; axis order changes the apparent crossings.
- Reduce the number of profiles or dimensions when line overlap obscures individual patterns.

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

```bash
node scripts/render.mjs parallel-coordinates-profile <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/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

Files

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

template.tsx

7.5 KB

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

Show contents
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

Show contents
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

Show contents
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

Show contents
{
  "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"
}