Back to index

Cumulative Progress

Timeline
Generates documents
Cumulative Progress

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

cumulative-progress-everything.md

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

Prompt

1.4 KB
Build an HTML document with "Cumulative Progress". This layout runs, so there is no composition to rebuild — what it needs is content.

## What this layout does
Cumulative Progress with explicit source values and reading conditions.

## What you have to supply
- Provide the source values, labels and decision conditions described by the schema.

## The shape of the input
- `title` — string — 結論を伝える見出し
- `subtitle` — string? — 出典・時点・母集団や仮定
- `unit` — string — 累積量の単位
- `periods` — array (0, 1) — 等間隔の期間
- `planned` — array () — 単調増加する累積計画
- `actual` — array () — 累積実績。未来は末尾のnull

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

## When not to use it
- Do not compare values with incompatible units, populations, or assumptions.

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

```bash
node scripts/render.mjs cumulative-progress <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/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

Files

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

template.tsx

8.3 KB

/designs/cumulative-progress/source/template.tsx

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

schema.ts

1.0 KB

/designs/cumulative-progress/source/schema.ts

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

meta.yaml

1.4 KB

/designs/cumulative-progress/source/meta.yaml

Show contents
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: []

sample-data.json

383 B

/designs/cumulative-progress/source/sample-data.json

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