Back to index

Slope Change

Two Column
Generates documents
Slope Change

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

slope-change-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 "Slope Change". This layout runs, so there is no composition to rebuild — what it needs is content.

## What this layout does
Slope Change 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 — 共通単位
- `beforeLabel` — string — 基準時点
- `afterLabel` — string — 比較時点
- `items` — array (label, before, after) — 各対象の比較可能な2時点の値

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 slope-change <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/slope-change/source/template.tsx
- https://design-vault.pages.dev/designs/slope-change/source/schema.ts
- https://design-vault.pages.dev/designs/slope-change/source/sample-data.json
- https://design-vault.pages.dev/designs/slope-change/sample.html

Files

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

template.tsx

8.5 KB

/designs/slope-change/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 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 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}}
`;

function spreadLabels(positions:number[]) {
  const sorted=positions.map((y,index)=>({y,index})).sort((a,b)=>a.y-b.y);
  for(let i=1;i<sorted.length;i++) sorted[i].y=Math.max(sorted[i].y,sorted[i-1].y+24);
  sorted[sorted.length-1].y=Math.min(sorted[sorted.length-1].y,280);
  for(let i=sorted.length-2;i>=0;i--) sorted[i].y=Math.min(sorted[i].y,sorted[i+1].y-24);
  const result:number[]=[]; for(const label of sorted) result[label.index]=label.y; return result;
}
export default function Template(d: Props) {
const vals=d.items.flatMap(r=>[r.before,r.after]); const min=Math.min(...vals),max=Math.max(...vals);const y=(v:number)=>280-(v-min)/(max-min||1)*220;
const beforeY=spreadLabels(d.items.map(r=>y(r.before))),afterY=spreadLabels(d.items.map(r=>y(r.after)));
const deltaUnit=["%","%"].includes(d.unit)?"pt":d.unit;
const rows=d.items.map((r,i)=>[`${String(i+1).padStart(2,"0")} · ${r.label}`,fmt(r.before)+d.unit,fmt(r.after)+d.unit,fmt(r.after-r.before)+deltaUnit]);
return <section className="asset asset-slope-change"><style>{CSS}</style><header><p className="eyebrow">SLOPE CHANGE</p><h1>{d.title}</h1>{d.subtitle && <p className="subtitle">{d.subtitle}</p>}</header>
<svg className="plot desktop-only" viewBox="0 0 960 330" role="img" aria-label="2時点のスロープ比較"><text x="220" y="20">基準時点</text><text x="650" y="20">比較時点</text>{d.items.map((r,i)=><g key={i}><line x1="290" x2="670" y1={y(r.before)} y2={y(r.after)} stroke={colors[i%4]} strokeWidth="3"/><circle cx="290" cy={y(r.before)} r="5" fill={colors[i%4]}/><circle cx="670" cy={y(r.after)} r="5" fill={colors[i%4]}/><line x1="250" x2="290" y1={beforeY[i]} y2={y(r.before)} stroke={colors[i%4]}/><line x1="670" x2="710" y1={y(r.after)} y2={afterY[i]} stroke={colors[i%4]}/><text x="238" y={beforeY[i]+5} textAnchor="end">{String(i+1).padStart(2,"0")} · {fmt(r.before)}</text><text x="722" y={afterY[i]+5}>{String(i+1).padStart(2,"0")} · {fmt(r.after)}</text></g>)}</svg><Ledger heads={["対象",d.beforeLabel,d.afterLabel,"差分"]} rows={rows}/><Notes><p>線番号は表の入力順です。途中の推移はこの図からは分かりません。</p></Notes>
<footer>2時点の定義を揃える。割合の差はパーセントポイントとして読む。</footer></section>;
}

schema.ts

684 B

/designs/slope-change/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 positive = value.positive();

export const schema = z.object({
  title: text.describe("結論を伝える見出し"),
  subtitle: text.optional().describe("出典・時点・母集団や仮定"),
  unit:short.describe("共通単位"), beforeLabel:short.describe("基準時点"), afterLabel:short.describe("比較時点"),
  items:z.array(z.object({label:short,before:value,after:value}).strict()).min(2).max(6).describe("各対象の比較可能な2時点の値"),
}).strict();
export type Props = z.infer<typeof schema>;

meta.yaml

1.3 KB

/designs/slope-change/source/meta.yaml

Show contents
id: slope-change
name:
  ja: 2時点のスロープ比較
  en: Slope Change
purpose:
  ja: 複数対象の2時点の変化を、対応する線と差分で読む。
  en: Slope Change with explicit source values and reading conditions.
status: published
still: ./still.png
category: analysis
catalogOrder: 260
jobs:
  - measure-change
useCases:
  - kpi
layoutKinds:
  - 2-column
tags:
  - slope-change
  - analysis
  - offline
  - responsive
whenToUse:
  - ja: 複数対象の2時点の変化を、対応する線と差分で読む。
    en: Use to communicate slope change.
avoidWhen:
  - ja: 同じ条件・単位で比較できないデータには使わない
    en: Do not compare values with incompatible units, populations, or assumptions.
requiredInputs:
  - ja: 2時点の定義を揃える。割合の差はパーセントポイントとして読む。
    en: Provide the source values, labels and decision conditions described by the schema.
relatedLayouts:
  - dumbbell-gap
  - line-trend
dataSchema:
  title: string — 結論を伝える見出し
  subtitle: string? — 出典・時点・母集団や仮定
  unit: string — 共通単位
  beforeLabel: string — 基準時点
  afterLabel: string — 比較時点
  items: array (label, before, after) — 各対象の比較可能な2時点の値
sources: []

sample-data.json

448 B

/designs/slope-change/source/sample-data.json

Show contents
{
  "title": "改善幅はWeb受付が最大",
  "subtitle": "架空サンプル|レイアウトの使用例",
  "unit": "%",
  "beforeLabel": "導入前",
  "afterLabel": "導入後",
  "items": [
    {
      "label": "Web受付",
      "before": 45,
      "after": 78
    },
    {
      "label": "電話受付",
      "before": 60,
      "after": 70
    },
    {
      "label": "窓口受付",
      "before": 72,
      "after": 80
    }
  ]
}