作品として見る 生成された文書そのもの — 切り替えも開閉もそのまま動きます
作品として見る 生成された文書そのもの — 切り替えも開閉もそのまま動きます
コピーして貼るか、ファイルに保存して渡す。ネットにつながらない環境には「全部入り」を 1 つ渡せば足りる。
このパネルの中身を、URL の先にあるファイルの中身まで含めて 1 つにまとめた Markdown — 大きいのでファイルで渡す(コピーは 32KB まで)
「平行座標プロファイル」で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 つから生成されています。 生成された文書
/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>;
}
/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>;
/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: []
/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"
}