作品として見る 生成された文書そのもの — 切り替えも開閉もそのまま動きます
作品として見る 生成された文書そのもの — 切り替えも開閉もそのまま動きます
コピーして貼るか、ファイルに保存して渡す。ネットにつながらない環境には「全部入り」を 1 つ渡せば足りる。
このパネルの中身を、URL の先にあるファイルの中身まで含めて 1 つにまとめた Markdown — 大きいのでファイルで渡す(コピーは 32KB まで)
「工程管理図」でHTML資料を作ってください。この型は実行できるので、版面を組み直す必要はありません — 必要なのは中身です。 ## この型がすること 時系列の測定値を中心線と上下の管理限界に照らし、通常変動から外れた時点を見つける。 ## 渡す必要があるもの - 測定値を発生順に並べる。測定方法・頻度・対象条件を揃える。 - 中心線と上下の管理限界を指定する。規格限界と混同しない。 ## 入力の形 - `title` — string — 工程の状態を伝える見出し - `subtitle` — string? — 対象工程・測定条件・期間 - `unit` — string — 測定値の単位 - `centerLine` — number — 工程平均または管理中心線 - `lowerControlLimit` — number — 下方管理限界 - `upperControlLimit` — number — 上方管理限界 - `points` — array (period, value) — 時系列順の測定点。5〜12点、時点ラベルは6文字以内で一意 入力の機械可読な検証は schema.ts が持ちます。足りない入力は推測せず質問してください。 ## 向かない場合 - 規格値への適合や目標達成を判定する場合。管理限界は規格限界・目標値ではない。 - 時点順がなく、独立したカテゴリ同士だけを比較する場合。 ## 作り方 入力を JSON にまとめ、次を実行します。 ```bash node scripts/render.mjs control-chart <入力.json> --theme ink -o out.html ``` 出力は 1 ファイルで完結し、外部から何も読み込みません。 ## 原本 - https://design-vault.pages.dev/designs/control-chart/source/template.tsx - https://design-vault.pages.dev/designs/control-chart/source/schema.ts - https://design-vault.pages.dev/designs/control-chart/source/sample-data.json - https://design-vault.pages.dev/designs/control-chart/sample.html
テンプレート・入力スキーマ・サンプルデータ・メタ情報。この文書はこの 4 つから生成されています。 生成された文書
/designs/control-chart/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);margin:0;line-height:1.5}
.asset .desktop-chart{display:block;width:1280px;height:720px;max-width:100%;margin:0 auto}
.asset .mobile-chart{display:none}
.asset details{border-top:1px solid var(--color-outline);margin-top:1rem;padding-top:.75rem}
.asset summary{cursor:pointer;min-height:2.75rem;padding:.5rem 0;color:var(--color-secondary)}
.asset ol{list-style:none;padding:0;margin:1rem 0}
.asset li{border-top:1px solid var(--color-outline);padding:.9rem 0;overflow-wrap:anywhere}
.asset .mobile-period{font:600 .8rem var(--font-mono);color:var(--color-secondary)}
.asset .mobile-value{font:700 1.1rem var(--font-mono);margin:.25rem 0}
.asset .mobile-status{font-size:.85rem;color:var(--color-secondary)}
.asset .mobile-status[data-outside="true"]{font-weight:700;color:var(--color-tertiary)}
.asset .limits{display:flex;flex-wrap:wrap;gap:.5rem}
.asset .limit{border:1px solid var(--color-outline);border-radius:999px;padding:.25rem .65rem;font-size:.8rem}
.asset .desktop-notes{max-width:1080px;margin:0 auto;padding:0 1rem}
@media(max-width:640px){.asset .desktop-chart{display:none!important}.asset .mobile-chart{display:block;padding:1.25rem 1rem}.asset .desktop-notes{display:block}.asset h1{font-size:1.5rem;line-height:1.35;overflow-wrap:anywhere}.asset .mobile-header{margin-bottom:1.25rem}.asset .mobile-kicker{font:600 .75rem var(--font-mono);letter-spacing:.08em;color:var(--color-secondary)}}
@media(min-width:641px){.asset .mobile-chart{display:none!important}}
`;
const formatValue = (value: number, unit: string) => `${value.toLocaleString("ja-JP", { maximumFractionDigits: 2 })} ${unit}`;
const wrap = (value: string, max: number) => {
const chars = [...value];
const lines: string[] = [];
for (let i = 0; i < chars.length; i += max) lines.push(chars.slice(i, i + max).join(""));
return lines.length ? lines : [""];
};
export default function Template(d: Props) {
const domainMin = Math.min(d.lowerControlLimit, ...d.points.map((point) => point.value));
const domainMax = Math.max(d.upperControlLimit, ...d.points.map((point) => point.value));
const padding = (domainMax - domainMin) * 0.12 || 1;
const min = domainMin - padding;
const max = domainMax + padding;
const titleLines = wrap(d.title, 46);
const subtitleLines = wrap(d.subtitle ?? "", 82);
const metadataY = 132 + titleLines.length * 31 + subtitleLines.length * 20;
const plot = { left: 150, right: 1145, top: Math.max(250, metadataY + 45), bottom: 520 };
const x = (index: number) => plot.left + index * (plot.right - plot.left) / Math.max(1, d.points.length - 1);
const y = (value: number) => plot.bottom - (value - min) / (max - min) * (plot.bottom - plot.top);
const outOfControl = d.points.filter((point) => point.value < d.lowerControlLimit || point.value > d.upperControlLimit).length;
const line = d.points.map((point, index) => `${index === 0 ? "M" : "L"} ${x(index).toFixed(2)} ${y(point.value).toFixed(2)}`).join(" ");
const guideLines = [
{ label: "上方管理限界", value: d.upperControlLimit, kind: "limit" },
{ label: "中心線", value: d.centerLine, kind: "center" },
{ label: "下方管理限界", value: d.lowerControlLimit, kind: "limit" },
];
return <section className="asset asset-control-chart">
<style>{CSS}</style>
<svg className="desktop-chart" data-layout-still="true" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="control-title control-desc">
<title id="control-title">{d.title}</title>
<desc id="control-desc">{d.points.length}件の測定値を中心線と管理限界に対して示す工程管理図。管理限界外は{outOfControl}件。</desc>
<rect x="0" y="0" width="1280" height="720" fill="var(--color-surface)" />
<text x="72" y="39" fontFamily="sans-serif" fontSize="14" fontWeight="700" letterSpacing="2" fill="var(--color-tertiary)">PROCESS CONTROL</text>
{titleLines.map((part, index) => <text key={`title-${index}`} x="72" y={82 + index * 31} fontFamily="sans-serif" fontSize="25" fontWeight="700" fill="var(--color-on-surface)">{part}</text>)}
{subtitleLines.map((part, index) => <text key={`subtitle-${index}`} x="72" y={98 + titleLines.length * 31 + index * 20} fontFamily="sans-serif" fontSize="14" fill="var(--color-secondary)">{part}</text>)}
<text x="72" y={metadataY} fontFamily="sans-serif" fontSize="13" fill="var(--color-secondary)">対象 {d.points.length}点 · 単位 {d.unit} · 管理限界外 {outOfControl}点</text>
<text x={plot.left} y={plot.top - 14} fontFamily="sans-serif" fontSize="13" fill="var(--color-secondary)">測定順</text>
{guideLines.map((guide) => <g key={guide.label}>
<line x1={plot.left} x2={plot.right} y1={y(guide.value)} y2={y(guide.value)} stroke={guide.kind === "center" ? "var(--color-on-surface)" : "var(--color-tertiary)"} strokeWidth={guide.kind === "center" ? 2 : 1.5} strokeDasharray={guide.kind === "limit" ? "7 6" : undefined} />
<text x="140" y={y(guide.value) + 5} textAnchor="end" fontFamily="sans-serif" fontSize="13" fill="var(--color-secondary)">{guide.label} {formatValue(guide.value, d.unit)}</text>
</g>)}
<line x1={plot.left} x2={plot.right} y1={plot.bottom} y2={plot.bottom} stroke="var(--color-outline)" strokeWidth="1" />
<path d={line} fill="none" stroke="var(--color-on-surface)" strokeWidth="3" strokeLinejoin="round" strokeLinecap="round" />
{d.points.map((point, index) => {
const outside = point.value < d.lowerControlLimit || point.value > d.upperControlLimit;
return <g key={point.period}>
<circle cx={x(index)} cy={y(point.value)} r={outside ? 8 : 6} fill={outside ? "var(--color-tertiary)" : "var(--color-on-surface)"} stroke="var(--color-surface)" strokeWidth="2" />
<text data-point-period={point.period} x={x(index)} y="548" textAnchor="middle" fontFamily="sans-serif" fontSize="12" fill="var(--color-secondary)">{point.period}</text>
{outside && <text x={x(index)} y={y(point.value) - 13} textAnchor="middle" fontFamily="sans-serif" fontSize="12" fontWeight="700" fill="var(--color-tertiary)">限界外</text>}
</g>;
})}
<line x1="72" x2="1208" y1="592" y2="592" stroke="var(--color-outline)" strokeWidth="1" />
<line x1="78" x2="110" y1="623" y2="623" stroke="var(--color-tertiary)" strokeWidth="2" strokeDasharray="7 6" />
<text x="120" y="628" fontFamily="sans-serif" fontSize="13" fill="var(--color-secondary)">上下の管理限界</text>
<line x1="310" x2="342" y1="623" y2="623" stroke="var(--color-on-surface)" strokeWidth="2" />
<text x="352" y="628" fontFamily="sans-serif" fontSize="13" fill="var(--color-secondary)">中心線</text>
<circle cx="500" cy="623" r="6" fill="var(--color-tertiary)" />
<text x="516" y="628" fontFamily="sans-serif" fontSize="13" fill="var(--color-secondary)">管理限界外の測定値</text>
<rect x="72" y="656" width="1136" height="1" fill="var(--color-outline)" />
<text x="72" y="684" fontFamily="sans-serif" fontSize="13" fill="var(--color-secondary)">管理限界は工程の通常変動を示します。規格値の合否や目標値とは区別します。</text>
</svg>
<div className="mobile-chart">
<header className="mobile-header">
<p className="mobile-kicker">PROCESS CONTROL</p>
<h1>{d.title}</h1>
{d.subtitle && <p>{d.subtitle}</p>}
<p className="limits">
<span className="limit">上限 {formatValue(d.upperControlLimit, d.unit)}</span>
<span className="limit">中心 {formatValue(d.centerLine, d.unit)}</span>
<span className="limit">下限 {formatValue(d.lowerControlLimit, d.unit)}</span>
</p>
<p>管理限界外 {outOfControl} / {d.points.length}点</p>
</header>
<ol>{d.points.map((point) => {
const outside = point.value < d.lowerControlLimit || point.value > d.upperControlLimit;
return <li key={point.period} data-point-period={point.period}>
<span className="mobile-period">{point.period}</span>
<p className="mobile-value">{formatValue(point.value, d.unit)}</p>
<p className="mobile-status" data-outside={outside ? "true" : "false"}>{outside ? "管理限界外・要調査" : "管理限界内"}</p>
</li>;
})}</ol>
</div>
<details className="desktop-notes"><summary>管理図の読み方</summary><p>管理限界は工程の通常変動から計算します。規格値の合否や目標値とは別に扱い、逸脱点は測定条件と記録を確認します。</p></details>
</section>;
}
/designs/control-chart/source/schema.ts
import { z } from "zod";
const text = z.string().trim().min(1).max(160);
const short = z.string().trim().min(1).max(6);
const value = z.number();
const unique = (values: string[]) => new Set(values).size === values.length;
export const schema = z.object({
title: text.describe("工程の状態について伝える見出し"),
subtitle: text.optional().describe("対象工程・測定条件・期間"),
unit: short.describe("測定値の単位"),
centerLine: value.describe("工程平均または管理中心線"),
lowerControlLimit: value.describe("下方管理限界"),
upperControlLimit: value.describe("上方管理限界"),
points: z.array(z.object({
period: short.describe("測定順を示す時点ラベル"),
value: value.describe("各時点の測定値"),
}).strict()).min(5).max(12).describe("時系列順の測定点"),
}).strict().refine(d =>
d.lowerControlLimit < d.centerLine && d.centerLine < d.upperControlLimit,
"下方管理限界 < 中心線 < 上方管理限界であること",
).refine(d => unique(d.points.map(point => point.period)), "時点ラベルは重複しない");
export type Props = z.infer<typeof schema>;
/designs/control-chart/source/meta.yaml
id: control-chart
name:
ja: 工程管理図
en: Process Control Chart
purpose:
ja: 時系列の測定値を中心線と上下の管理限界に照らし、通常変動から外れた時点を見つける。
en: Compare sequential measurements with a center line and control limits to identify points outside routine variation.
status: published
still: ./still.png
category: analysis
catalogOrder: 720
jobs:
- measure-change
- prioritize-diagnose
useCases:
- process
- kpi
layoutKinds:
- timeline
tags:
- control-chart
- statistical-process-control
- process-quality
- offline
- responsive
whenToUse:
- ja: 製造・運用工程の測定値が、工程固有の通常変動に収まっているかを時系列で判断する。
en: Use when deciding whether sequential process measurements remain within routine process variation.
- ja: 上下の管理限界を越えた時点を特定し、調査対象を絞りたい。
en: Use to identify measurements beyond upper or lower control limits and focus investigation.
avoidWhen:
- ja: 規格値への適合や目標達成を判定する場合。管理限界は規格限界・目標値ではない。
en: Do not use control limits to judge specification compliance or target attainment.
- ja: 時点順がなく、独立したカテゴリ同士だけを比較する場合。
en: Do not use for unrelated categories without a meaningful observation order.
requiredInputs:
- ja: 測定値を発生順に並べる。測定方法・頻度・対象条件を揃える。
en: Order measurements by occurrence and keep method, frequency, and population conditions consistent.
- ja: 中心線と上下の管理限界を指定する。規格限界と混同しない。
en: Supply a center line and upper/lower control limits; keep them distinct from specification limits.
relatedLayouts:
- line-trend
- bullet-target
dataSchema:
title: string — 工程の状態を伝える見出し
subtitle: string? — 対象工程・測定条件・期間
unit: string — 測定値の単位
centerLine: number — 工程平均または管理中心線
lowerControlLimit: number — 下方管理限界
upperControlLimit: number — 上方管理限界
points: array (period, value) — 時系列順の測定点。5〜12点、時点ラベルは6文字以内で一意
sources: []
/designs/control-chart/source/sample-data.json
{
"title": "充填量は管理限界内で推移、一時的な逸脱を確認",
"subtitle": "架空サンプル|飲料ラインA・週次の抜取検査",
"unit": "mL",
"centerLine": 500,
"lowerControlLimit": 494,
"upperControlLimit": 506,
"points": [
{ "period": "週01", "value": 499 },
{ "period": "週02", "value": 501 },
{ "period": "週03", "value": 498 },
{ "period": "週04", "value": 503 },
{ "period": "週05", "value": 500 },
{ "period": "週06", "value": 502 },
{ "period": "週07", "value": 504 },
{ "period": "週08", "value": 501 },
{ "period": "週09", "value": 499 },
{ "period": "週10", "value": 507 },
{ "period": "週11", "value": 503 },
{ "period": "週12", "value": 500 }
]
}