Seen as a design The generated document itself — its toggles and disclosures work here
Seen as a design The generated document itself — its toggles and disclosures work here
Copy and paste, or save a file and hand it over. For a machine with no network, one “Everything” file is enough.
Everything in this panel, including the contents behind each URL, in one Markdown file — Large, so it goes as a file (copy is for 32 KB and under)
Build an HTML document with "Meta-analysis Forest Plot". This layout runs, so there is no composition to rebuild — what it needs is content. ## What this layout does Compare study-level effect estimates and intervals against a null reference, alongside a weighted pooled estimate. ## What you have to supply - Supply each study's effect, bounds, estimate, relative weight totaling 100%, null value, and interval definition. - Supply 4 to 12 studies and a pooled estimate using the same scale and interval definition. ## The shape of the input - `title` — string — 統合結果を要約する結論 - `subtitle` — string? — 分析対象・出典・時点・対象集団 - `effectMeasure` — string — 効果量の種類 - `unit` — string — 効果量の単位 - `nullValue` — number — 効果なしの基準値 - `intervalLabel` — string — 区間の定義 - `studies` — array (label, lower, estimate, upper, weight) — 4〜12件。重みの合計100% - `pooled` — object (lower, estimate, upper) — 重み付き統合推定 schema.ts holds the machine-checkable validation. Ask rather than guess when an input is missing. ## When not to use it - Avoid when studies use incompatible effect measures, units, or populations. - Do not use as a substitute for a valid pooled analysis or causal evidence; the chart only visualizes supplied estimates. ## How to build it Put the input in a JSON file and run: ```bash node scripts/render.mjs meta-analysis-forest-plot <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/meta-analysis-forest-plot/source/template.tsx - https://design-vault.pages.dev/designs/meta-analysis-forest-plot/source/schema.ts - https://design-vault.pages.dev/designs/meta-analysis-forest-plot/source/sample-data.json - https://design-vault.pages.dev/designs/meta-analysis-forest-plot/sample.html
Template, input schema, sample data and metadata. This document was generated from those four. The generated document
/designs/meta-analysis-forest-plot/source/template.tsx
import type { Props } from "./schema";
const fmt = (n: number) => Number.isInteger(n) ? n.toLocaleString("ja-JP") : n.toLocaleString("ja-JP", { maximumFractionDigits: 2 });
function wrap(text: string, width: number) {
return 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, 54);
const subtitleLines = d.subtitle ? wrap(d.subtitle, 80) : [];
const titleY = 48;
const subtitleY = titleY + titleLines.length * 26 + 5;
const infoY = subtitleY + subtitleLines.length * 18 + 18;
const chartY = infoY + 30;
const rowH = 25;
const axisY = chartY + d.studies.length * rowH + 18;
const pooledY = axisY + 62;
const plot = { x: 350, w: 820 };
const bounds = [d.nullValue, ...d.studies.flatMap(row => [row.lower, row.upper]), d.pooled.lower, d.pooled.upper];
let min = Math.min(...bounds);
let max = Math.max(...bounds);
if (max === min) { const pad = Math.max(1, Math.abs(min) * 0.1); min -= pad; max += pad; }
const pad = (max - min) * 0.06;
min -= pad;
max += pad;
const x = (v: number) => plot.x + (v - min) / (max - min) * plot.w;
const maxWeight = Math.max(...d.studies.map(study => study.weight));
const pooledRadius = 12;
const ticks = Array.from({ length: 5 }, (_, i) => min + (max - min) * i / 4);
return <section className="forest">
<style>{`.forest{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)}.forest *{box-sizing:border-box}.forest 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}.forest-table{width:100%;border-collapse:collapse;margin-top:1.5rem;font-variant-numeric:tabular-nums}.forest-table th,.forest-table td{padding:.65rem .55rem;border-bottom:1px solid var(--color-outline);text-align:right;overflow-wrap:anywhere}.forest-table th{color:var(--color-secondary)}.forest-table th:first-child,.forest-table td:first-child{text-align:left}.pooled{font-weight:700;background:var(--color-surface-subtle)}.notes{margin-top:1rem;padding:1rem;border-left:.25rem solid var(--color-tertiary);background:var(--color-surface-subtle)}@media(max-width:640px){.forest{padding:1rem}.forest h1{font-size:var(--text-h3)}.plot{display:none}.forest-table,.forest-table tbody,.forest-table tr,.forest-table td{display:block}.forest-table thead{display:none}.forest-table tr{padding:.75rem 0;border-bottom:1px solid var(--color-outline)}.forest-table td{border:0;padding:.2rem 0;text-align:left!important}.forest-table td:not(:first-child){display:inline-block;margin-right:.7rem}.forest-table td:not(:first-child)::before{content:attr(data-label) ": ";color:var(--color-secondary)}}`}</style>
<header><p className="muted">META-ANALYSIS · {d.intervalLabel}</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 * 26} fontSize="21" 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={infoY} fontSize="14" fill="var(--color-secondary)">{d.effectMeasure}({d.unit}) · ● 研究推定値(面積は重み) · ◆ 統合推定 · 破線=効果なし</text>
<line x1={x(d.nullValue)} y1={chartY - 12} x2={x(d.nullValue)} y2={axisY} stroke="var(--color-outline)" strokeWidth="2" strokeDasharray="7 6" />
{d.studies.map((study, i) => {
const cy = chartY + i * rowH + rowH / 2;
const radius = Math.sqrt(study.weight / maxWeight) * 10;
return <g key={study.label}>
<text x="64" y={cy + 5} fontSize="15" fill="var(--color-on-surface)">{study.label}</text>
<line x1={x(study.lower)} y1={cy} x2={x(study.upper)} y2={cy} stroke="var(--color-tertiary)" strokeWidth="3" />
<line x1={x(study.lower)} y1={cy - 5} x2={x(study.lower)} y2={cy + 5} stroke="var(--color-tertiary)" strokeWidth="2" />
<line x1={x(study.upper)} y1={cy - 5} x2={x(study.upper)} y2={cy + 5} stroke="var(--color-tertiary)" strokeWidth="2" />
<circle cx={x(study.estimate)} cy={cy} r={radius} fill="var(--color-tertiary)" />
</g>;
})}
{ticks.map((tick, i) => <g key={i}><line x1={x(tick)} y1={axisY - 4} x2={x(tick)} y2={axisY + 4} stroke="var(--color-outline)" /><text x={x(tick)} y={axisY + 22} textAnchor="middle" fontSize="13" fill="var(--color-secondary)">{fmt(tick)}</text></g>)}
<line x1={plot.x} y1={axisY} x2={plot.x + plot.w} y2={axisY} stroke="var(--color-outline)" />
<text x="64" y={pooledY + 5} fontSize="16" fontWeight="700" fill="var(--color-on-surface)">統合推定</text>
<line x1={x(d.pooled.lower)} y1={pooledY} x2={x(d.pooled.upper)} y2={pooledY} stroke="var(--color-on-surface)" strokeWidth="4" />
<path d={`M ${x(d.pooled.estimate)} ${pooledY - pooledRadius} L ${x(d.pooled.estimate) + pooledRadius} ${pooledY} L ${x(d.pooled.estimate)} ${pooledY + pooledRadius} L ${x(d.pooled.estimate) - pooledRadius} ${pooledY} Z`} fill="var(--color-on-surface)" />
<text x="64" y="690" fontSize="13" fill="var(--color-secondary)">区間が破線(効果なし)をまたぐ場合は、その研究だけで方向を確定できません。</text>
</svg>
<table className="forest-table"><thead><tr><th>研究</th><th>推定値</th><th>{d.intervalLabel}</th><th>重み</th></tr></thead><tbody>
{d.studies.map(study => <tr key={study.label} data-study-row><td>{study.label}</td><td data-label="推定値">{fmt(study.estimate)} {d.unit}</td><td data-label={d.intervalLabel}>{fmt(study.lower)}〜{fmt(study.upper)} {d.unit}</td><td data-label="重み">{fmt(study.weight)}%</td></tr>)}
<tr className="pooled"><td>統合推定</td><td data-label="推定値">{fmt(d.pooled.estimate)} {d.unit}</td><td data-label={d.intervalLabel}>{fmt(d.pooled.lower)}〜{fmt(d.pooled.upper)} {d.unit}</td><td data-label="重み">—</td></tr>
</tbody></table>
<p className="notes">この図は入力された推定値・区間・重みを描画します。統合方法の妥当性や因果関係を検証するものではありません。無効果の基準値は {fmt(d.nullValue)} {d.unit} です。</p>
</section>;
}
/designs/meta-analysis-forest-plot/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 intervalFields = z.object({ lower: finite, estimate: finite, upper: finite }).strict();
const unique = (values: string[]) => new Set(values).size === values.length;
const orderedInterval = <T extends { lower: number; estimate: number; upper: number }>(value: T) =>
value.lower <= value.estimate && value.estimate <= value.upper;
export const schema = z.object({
title: text.describe("統合結果を要約する結論"),
subtitle: text.optional().describe("分析対象・出典・時点・対象集団"),
effectMeasure: z.string().trim().min(1).max(24).describe("効果量の種類。例:得点差、リスク比"),
unit: z.string().trim().min(1).max(12).describe("効果量の単位"),
nullValue: finite.describe("差なし・効果なしを表す基準値"),
intervalLabel: z.string().trim().min(1).max(32).describe("区間の定義。例:95%信頼区間"),
studies: z.array(z.object({
label: label.describe("研究・比較対象の識別名"),
...intervalFields.shape,
weight: z.number().finite().min(1).max(100).describe("全研究に対する相対重み(%)"),
}).strict().refine(orderedInterval, "下限≦推定値≦上限")).min(4).max(12).describe("各研究の推定値、区間、相対重み"),
pooled: z.object({ ...intervalFields.shape })
.strict().refine(orderedInterval, "下限≦推定値≦上限").describe("統合推定とその区間"),
}).strict()
.refine(d => unique(d.studies.map(study => study.label)), "研究ラベルは重複しない")
.refine(d => Math.abs(d.studies.reduce((sum, study) => sum + study.weight, 0) - 100) <= 0.5,
"研究の相対重みの合計は100%(丸め誤差0.5%以内)");
export type Props = z.infer<typeof schema>;
/designs/meta-analysis-forest-plot/source/meta.yaml
id: meta-analysis-forest-plot
name:
ja: メタ分析フォレストプロット
en: Meta-analysis Forest Plot
purpose:
ja: 個別研究の効果推定と区間を無効果の基準線に対して並べ、重み付き統合推定と比較する。
en: Compare study-level effect estimates and intervals against a null reference, alongside a weighted pooled estimate.
status: published
still: ./still.png
category: analysis
catalogOrder: 740
jobs:
- measure-change
- explain-structure
useCases:
- comparison
- kpi
layoutKinds:
- axis
tags:
- forest-plot
- meta-analysis
- confidence-interval
- offline
- responsive
whenToUse:
- ja: 複数の独立研究の効果推定・信頼区間と、重み付き統合推定を同じ尺度で比べる。
en: Use to compare independent study estimates and confidence intervals with a weighted pooled estimate on one scale.
- ja: 各研究が無効果の基準値をまたぐか、統合結果がどちら側にあるかを確認する。
en: Use to inspect whether study intervals cross the null and where the pooled result lies.
avoidWhen:
- ja: 研究間で効果量・単位・対象条件が異なり、同じ軸で比較できない場合。
en: Avoid when studies use incompatible effect measures, units, or populations.
- ja: 原データを統合した推定や因果関係の証明が必要な場合。この図は入力された推定値を表示する。
en: Do not use as a substitute for a valid pooled analysis or causal evidence; the chart only visualizes supplied estimates.
requiredInputs:
- ja: 各研究の効果量、下限・推定値・上限、合計100%となる相対重み、無効果の基準値、区間定義を入力する。
en: Supply each study's effect, bounds, estimate, relative weight totaling 100%, null value, and interval definition.
- ja: 個別研究は4〜12件。統合推定も同じ尺度と区間定義を使う。
en: Supply 4 to 12 studies and a pooled estimate using the same scale and interval definition.
relatedLayouts:
- interval-estimates
- research-evidence-table
dataSchema:
title: string — 統合結果を要約する結論
subtitle: string? — 分析対象・出典・時点・対象集団
effectMeasure: string — 効果量の種類
unit: string — 効果量の単位
nullValue: number — 効果なしの基準値
intervalLabel: string — 区間の定義
studies: array (label, lower, estimate, upper, weight) — 4〜12件。重みの合計100%
pooled: object (lower, estimate, upper) — 重み付き統合推定
sources: []
/designs/meta-analysis-forest-plot/source/sample-data.json
{
"title": "6件を統合した効果は改善方向で、区間は基準値を上回る",
"subtitle": "架空の品質改善プログラムを評価した独立研究|同じ指標・対象条件",
"effectMeasure": "品質指標の差",
"unit": "点",
"nullValue": 0,
"intervalLabel": "95%信頼区間",
"studies": [
{ "label": "北部工場", "lower": 1.2, "estimate": 2.8, "upper": 4.4, "weight": 18 },
{ "label": "南部工場", "lower": -0.8, "estimate": 1.1, "upper": 3.0, "weight": 12 },
{ "label": "第一ライン", "lower": 0.4, "estimate": 2.2, "upper": 4.0, "weight": 20 },
{ "label": "第二ライン", "lower": -0.2, "estimate": 1.5, "upper": 3.2, "weight": 15 },
{ "label": "試験工程", "lower": 1.0, "estimate": 3.1, "upper": 5.2, "weight": 22 },
{ "label": "標準工程", "lower": -1.4, "estimate": 0.3, "upper": 2.0, "weight": 13 }
],
"pooled": { "lower": 0.8, "estimate": 1.9, "upper": 3.0 }
}