Bland-Altman Plot
A four-state method-agreement plot — pair differences against pair means, with a computed bias line, dashed ±1.96 SD limits of agreement, and out-of-limits pairs flagged in the destructive tone.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-bland-altman.jsonPrompt
Build a React + TypeScript + Tailwind "ChartBlandAltman" method-agreement widget
on the shadcn chart primitives (ChartContainer/ChartTooltip over recharts) with zod.
Contract
- A zod schema is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready"; title: string; unit: string;
pairs: { a: number; b: number; label?: string }[] }.
- Component props = z.infer of the schema, plus onRetry?: () => void and the
remaining div attributes spread onto the root (className merged with cn()).
No hand-written parallel interface.
Behavior
- The component computes every statistic itself — callers only hand it raw pairs:
per pair mean = (a + b) / 2 and diff = a − b; bias = mean of the diffs;
sd = sample standard deviation (n − 1); limits of agreement = bias ± 1.96 × sd.
- Non-finite pairs are dropped before anything is measured; "ready" with nothing
plottable left renders the empty branch, not a broken axis.
- Fewer than two usable pairs: draw the dots and the bias line but no limits —
a spread needs at least two differences — and say so in the caption.
- A pair is outside when |diff − bias| > 1.96 × sd; those pairs form their own
scatter series and are counted in a sentence under the plot, so disagreement
is named rather than left to be noticed.
- Four first-class status branches: a skeleton that mirrors the three-line
silhouette, zero-state copy, an error card whose "Try again" button renders
only when onRetry exists, and the ready plot.
Rendering & styling
- ScatterChart: X = pair mean, Y = difference. Solid bias ReferenceLine in
var(--chart-2); dashed ±1.96 SD lines in var(--muted-foreground), each
labelled with its multiplier and value ("+1.96 SD · +11.9 bpm"); in-range
dots var(--chart-1), out-of-limits dots var(--destructive).
- The Y domain is padded ~28% beyond both the extreme differences and the limit
lines so the line labels never clip; ticks snap to a 1/2/5×10^n step so the
padded ends stay blank. Differences always print with a sign.
- The recharts svg is role="img" with a spoken summary (bias, limits, outside
count) and tabIndex −1; the same numbers repeat in an sr-only table (pair, a,
b, mean, difference, within limits). Animations honour prefers-reduced-motion.
Panel: rounded-xl border bg-card p-6, caption in text-muted-foreground.
Customization levers
- Sign convention: diff = a − b; flip it (and the Y-axis label) in analyse()
when the candidate method should read positive.
- Coverage: the 1.96 multiplier is one constant — 2.58 gives 99% limits; the
line labels read the constant, so they follow automatically.
- Density: plot height (default 300px) and dot size (the ZAxis range) are the
two knobs for thumbnail vs full-width embeds; drop the caption row when small.
- Emphasis: out-of-limits pairs use the destructive token by default; soften to
var(--chart-5) when stragglers are expected rather than alarming.
- Extra reference: add a dotted zero line when "no difference at all" is the
claim under test, so the bias line is judged against it visually.Concepts
- Agreement, not correlation — two methods can correlate almost perfectly and still disagree by a constant offset; plotting each pair's difference against its mean is what exposes bias and spread, which is the interchangeability question.
- Component-computed statistics — callers hand raw (a, b) pairs; bias, SD and the ±1.96 SD limits are derived inside the component, so the lines can never drift out of sync with the dots they judge.
- Limits of agreement — bias ± 1.96 × SD brackets roughly 95% of differences when they are near-normal; each dashed line carries its own value label, so the plot reads without a legend lookup.
- Outlier emphasis — a pair whose difference falls beyond the limits is drawn with the destructive token and counted in a sentence under the plot: disagreement is named, not just visible.
- Degenerate-input honesty — non-finite pairs are dropped before measuring, a ready feed with nothing usable renders the empty branch, and a single pair draws its bias without limits and says why.
Fatigue and Freshness
A four-state performance-management chart: fitness (CTL) and fatigue (ATL) as exponential averages of daily training stress, form (TSB) shaded around zero, race days flagged.
Forest Plot
A four-state meta-analysis forest plot — weight-sized squares with CI whiskers per study against a vertical null line, and a pooled summary diamond.