作品として見る 生成された文書そのもの — 切り替えも開閉もそのまま動きます
作品として見る 生成された文書そのもの — 切り替えも開閉もそのまま動きます
コピーして貼るか、ファイルに保存して渡す。ネットにつながらない環境には「全部入り」を 1 つ渡せば足りる。
このパネルの中身を、URL の先にあるファイルの中身まで含めて 1 つにまとめた Markdown — 大きいのでファイルで渡す(コピーは 32KB まで)
「計画と実績の累積推移」でHTML資料を作ってください。この型は実行できるので、版面を組み直す必要はありません — 必要なのは中身です。 ## この型がすること 累積計画と実績を重ね、最新観測時点での差を読む。 ## 渡す必要があるもの - 当期の増分ではなく累積値を入力。未観測期間との比較は行わない。 ## 入力の形 - `title` — string — 結論を伝える見出し - `subtitle` — string? — 出典・時点・母集団や仮定 - `unit` — string — 累積量の単位 - `periods` — array (0, 1) — 等間隔の期間 - `planned` — array () — 単調増加する累積計画 - `actual` — array () — 累積実績。未来は末尾のnull 入力の機械可読な検証は schema.ts が持ちます。足りない入力は推測せず質問してください。 ## 向かない場合 - 同じ条件・単位で比較できないデータには使わない ## 作り方 入力を JSON にまとめ、次を実行します。 ```bash node scripts/render.mjs cumulative-progress <入力.json> --theme ink -o out.html ``` 出力は 1 ファイルで完結し、外部から何も読み込みません。 ## 原本 - https://design-vault.pages.dev/designs/cumulative-progress/source/template.tsx - https://design-vault.pages.dev/designs/cumulative-progress/source/schema.ts - https://design-vault.pages.dev/designs/cumulative-progress/source/sample-data.json - https://design-vault.pages.dev/designs/cumulative-progress/sample.html
テンプレート・入力スキーマ・サンプルデータ・メタ情報。この文書はこの 4 つから生成されています。 生成された文書
/designs/cumulative-progress/source/template.tsx
import type { ReactNode } from "react";
import type { Props } from "./schema";
const fmt = (n: number) => Number.isInteger(n) ? n.toLocaleString("ja-JP") : n.toLocaleString("ja-JP", {maximumFractionDigits: 2});
const periodIndex = (index: number) => `#${String(index + 1).padStart(2, "0")}`;
const colors = ["var(--color-tertiary)", "var(--color-primary)", "var(--color-secondary)", "var(--color-accent-violet)"];
function Ledger({heads,rows,caption}:{heads:string[];rows:ReactNode[][];caption?:string}) { return <table className="ledger">{caption && <caption>{caption}</caption>}<thead><tr>{heads.map((h,i)=><th key={i} scope="col">{h}</th>)}</tr></thead><tbody>{rows.map((r,i)=><tr key={i}>{r.map((v,j)=><td key={j} data-label={heads[j]}>{v}</td>)}</tr>)}</tbody></table>; }
function Notes({children}:{children:ReactNode}) { return <details><summary>データ・判断条件を確認</summary>{children}</details>; }
const points = (values:number[],max:number,min=0,width=820,height=230) => values.map((v,i)=>(70+i*width/Math.max(values.length-1,1))+","+(30+height-(v-min)/(max-min||1)*height)).join(" ");
const CSS = String.raw`
.asset{font-family:var(--font-sans);color:var(--color-primary);background:var(--color-surface);max-width:var(--container-slide);margin:auto;padding:3rem 2rem;line-height:1.6;font-size:var(--text-body-sm)}
.asset *{box-sizing:border-box}.asset header{margin-bottom:2rem}.asset .eyebrow{font-family:var(--font-mono);font-size:var(--text-label);letter-spacing:.08em;color:var(--color-secondary);margin-bottom:.75rem}.asset h1{font-size:var(--text-h1);font-weight:700;line-height:1.35;letter-spacing:-.025em;max-width:32em}.asset h2{font-size:var(--text-h3);font-weight:600;margin-bottom:.75rem}.asset h3{font-size:var(--text-body-md);font-weight:600}.asset .subtitle,.asset .muted{color:var(--color-secondary)}.asset .subtitle{margin-top:.75rem}.asset .number{font-variant-numeric:tabular-nums;font-family:var(--font-mono)}.asset .hero-number{font-size:var(--text-display);line-height:1.2;font-weight:700;letter-spacing:-.04em}.asset .callout{border-left:.25rem solid var(--color-tertiary);padding:1rem 1.5rem;background:var(--color-surface-subtle);margin-bottom:1.5rem}.asset .grid2{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem}.asset .grid3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1.5rem}.asset .panel{padding:1.5rem;background:var(--color-surface-subtle);border-radius:var(--radius-md);min-width:0}.asset .rule{border-top:1px solid var(--color-outline);padding-top:1rem;margin-top:1rem}.asset .pill{display:inline-block;padding:.25rem .75rem;border:1px solid var(--color-outline);border-radius:var(--radius-full);font-size:var(--text-label)}.asset .focus{background:var(--color-tertiary-container);color:var(--color-on-tertiary-container)}.asset .dark{background:var(--color-primary);color:var(--color-on-primary)}.asset .dark .muted{color:var(--color-on-primary-container)}.asset .row{display:grid;grid-template-columns:12rem minmax(0,1fr) 6rem;gap:1rem;align-items:center;padding:.75rem 0;border-bottom:1px solid var(--color-outline)}.asset .row>*{min-width:0}.asset .track{height:1rem;position:relative;background:var(--color-neutral);border-radius:var(--radius-sm)}.asset .bar{height:100%;background:var(--color-tertiary);border-radius:var(--radius-sm)}.asset .track.tall{height:2rem}.asset .flex{display:flex;gap:1rem;align-items:center;justify-content:space-between;flex-wrap:wrap}.asset .stack>*+*{margin-top:1rem}.asset ul{list-style:disc;padding-left:1.25rem}.asset li+li{margin-top:.5rem}.asset .plot{display:block;width:100%;height:auto;overflow:visible}.asset .plot text{font-family:var(--font-sans);font-size:14px;fill:var(--color-secondary)}.asset .plot .ink{fill:var(--color-primary)}.asset .plot .axis{stroke:var(--color-outline);stroke-width:1}.asset .plot .accent{stroke:var(--color-tertiary);fill:none;stroke-width:3}.asset .legend{display:flex;gap:1.5rem;flex-wrap:wrap;font-size:var(--text-label);margin-bottom:1rem}.asset .legend b{display:inline-block;width:.75rem;height:.75rem;margin-right:.5rem}.asset .ledger{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums}.asset .ledger caption{text-align:left;font-weight:600;margin:1rem 0}.asset .ledger th{font-weight:600;color:var(--color-secondary);text-align:left}.asset .ledger th,.asset .ledger td{padding:.75rem 1rem;border-bottom:1px solid var(--color-outline);vertical-align:top;overflow-wrap:anywhere}.asset .ledger th:first-child,.asset .ledger td:first-child{padding-left:0}.asset .ledger th:last-child,.asset .ledger td:last-child{padding-right:0}.asset .ledger tbody tr:hover{background:var(--color-surface-subtle)}.asset details{border-top:1px solid var(--color-outline);margin-top:1.5rem;padding-top:1rem}.asset summary{cursor:pointer;color:var(--color-secondary);padding:.5rem 0;min-height:2.75rem}.asset summary:focus-visible{outline:2px solid var(--color-tertiary);outline-offset:.25rem}.asset .small{font-size:var(--text-label)}.asset .mobile-only{display:none}.asset .horizontal-list{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem}.asset .board-column{border-top:.25rem solid var(--color-primary);padding-top:1rem;min-width:0}.asset .board-item{padding:1rem 0;border-bottom:1px solid var(--color-outline)}.asset .board-item p{margin-top:.5rem}.asset .statement{font-size:var(--text-h2);line-height:1.5;font-weight:600}.asset .meta-line{margin-top:.75rem;font-size:var(--text-label);color:var(--color-secondary)}.asset pre{white-space:pre-wrap;overflow-wrap:anywhere}.asset footer{margin-top:2rem;font-size:var(--text-label);color:var(--color-secondary)}
@media(max-width:640px){.asset{padding:1.5rem 1rem}.asset h1{font-size:var(--text-h2)}.asset header{margin-bottom:1.5rem}.asset .grid2,.asset .grid3,.asset .horizontal-list{grid-template-columns:1fr}.asset .row{grid-template-columns:minmax(0,1fr) 5rem;gap:.5rem}.asset .row>:first-child{grid-column:1/-1}.asset .desktop-only{display:none!important}.asset .mobile-only{display:block}.asset .panel{padding:1rem}.asset .ledger,.asset .ledger tbody,.asset .ledger tr,.asset .ledger td{display:block}.asset .ledger thead{display:none}.asset .ledger tr{padding:.75rem 0;border-bottom:1px solid var(--color-outline)}.asset .ledger td{display:grid;grid-template-columns:7rem minmax(0,1fr);gap:.75rem;border:0;padding:.25rem 0!important}.asset .ledger td:before{content:attr(data-label);color:var(--color-secondary);font-size:var(--text-label)}.asset .hero-number{font-size:var(--text-h1)}}
@media print{.asset{max-width:none;padding:1rem}.asset details>*{display:block}.asset summary{display:none}}
`;
export default function Template(d: Props) {
const actual=d.actual.filter((v):v is number=>v!==null);const max=Math.max(...d.planned,...actual,1);const last=actual.length-1;const xx=(i:number)=>70+i*820/(d.periods.length-1), yy=(v:number)=>260-v/max*230;
return <section className="asset asset-cumulative-progress"><style>{CSS}</style><header><p className="eyebrow">CUMULATIVE PROGRESS</p><h1>{d.title}</h1>{d.subtitle && <p className="subtitle">{d.subtitle}</p>}</header>
<div className="callout">{d.periods[last]}の差 <strong className="hero-number">{fmt(actual[last]-d.planned[last])}</strong> {d.unit}</div><div className="legend"><span>実線:実績</span><span>破線:計画</span><span>期間順:左から{periodIndex(0)}〜{periodIndex(d.periods.length-1)}</span></div><svg className="plot desktop-only" viewBox="0 0 960 310" role="img" aria-label={`累積計画と累積実績。横軸は期間順で${periodIndex(0)}から${periodIndex(d.periods.length-1)}。詳細期間名は下の表を参照。`}><polyline points={points(d.planned,max)} fill="none" stroke={colors[2]} strokeWidth="2" strokeDasharray="8 5"/><polyline points={actual.map((v,i)=>xx(i)+","+yy(v)).join(" ")} fill="none" stroke={colors[0]} strokeWidth="3"/>{[0,max].map((v,i)=><text key={i} x="55" y={yy(v)+5} textAnchor="end">{fmt(v)}</text>)}{d.periods.map((_,i)=><text key={i} x={xx(i)} y="295" textAnchor="middle">{periodIndex(i)}</text>)}</svg><div className="mobile-only"><Ledger heads={["期間番号","期間","計画","実績"]} rows={d.periods.map((p,i)=>[periodIndex(i),p,d.planned[i],d.actual[i]??"未観測"])}/></div><Notes><Ledger heads={["期間","計画","実績"]} rows={d.periods.map((p,i)=>[p,d.planned[i],d.actual[i]??"未観測"])}/></Notes>
<footer>当期の増分ではなく累積値を入力。未観測期間との比較は行わない。</footer></section>;
}
/designs/cumulative-progress/source/schema.ts
import { z } from "zod";
const text = z.string().trim().min(1).max(160);
const short = z.string().trim().min(1).max(48);
const value = z.number().finite();
const nonneg = value.nonnegative();
const positive = value.positive();
export const schema = z.object({
title: text.describe("結論を伝える見出し"),
subtitle: text.optional().describe("出典・時点・母集団や仮定"),
unit:short.describe("累積量の単位"),periods:z.array(short).min(3).max(12).describe("等間隔の期間"),
planned:z.array(nonneg).min(3).max(12).describe("単調増加する累積計画"),actual:z.array(nonneg.nullable()).min(3).max(12).describe("累積実績。未来は末尾のnull"),
}).strict().refine(d=>d.periods.length===d.planned.length&&d.actual.length===d.periods.length&&d.actual[0]!==null&&d.planned.every((v,i)=>i===0||v>=d.planned[i-1])&&d.actual.every((v,i)=>v===null||(!d.actual.slice(0,i).includes(null)&&(i===0||v>=d.actual[i-1]!))),"累積値は非減少、未観測は末尾");
export type Props = z.infer<typeof schema>;
/designs/cumulative-progress/source/meta.yaml
id: cumulative-progress
name:
ja: 計画と実績の累積推移
en: Cumulative Progress
purpose:
ja: 累積計画と実績を重ね、最新観測時点での差を読む。
en: Cumulative Progress with explicit source values and reading conditions.
status: published
still: ./still.png
category: analysis
catalogOrder: 390
jobs:
- measure-change
useCases:
- kpi
layoutKinds:
- timeline
tags:
- cumulative-progress
- analysis
- offline
- responsive
whenToUse:
- ja: 累積計画と実績を重ね、最新観測時点での差を読む。
en: Use to communicate cumulative progress.
avoidWhen:
- ja: 同じ条件・単位で比較できないデータには使わない
en: Do not compare values with incompatible units, populations, or assumptions.
requiredInputs:
- ja: 当期の増分ではなく累積値を入力。未観測期間との比較は行わない。
en: Provide the source values, labels and decision conditions described by the schema.
relatedLayouts:
- line-trend
- critical-path-gantt
dataSchema:
title: string — 結論を伝える見出し
subtitle: string? — 出典・時点・母集団や仮定
unit: string — 累積量の単位
periods: array (0, 1) — 等間隔の期間
planned: array () — 単調増加する累積計画
actual: array () — 累積実績。未来は末尾のnull
sources: []
/designs/cumulative-progress/source/sample-data.json
{
"title": "累積実績は8月時点で計画を15件下回る",
"subtitle": "架空サンプル|レイアウトの使用例",
"unit": "件",
"periods": [
"4月",
"5月",
"6月",
"7月",
"8月",
"9月"
],
"planned": [
20,
50,
90,
140,
200,
260
],
"actual": [
18,
48,
86,
125,
185,
null
]
}