Charts

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.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import {
  CartesianGrid,
  ReferenceLine,
  Scatter,
  ScatterChart,
  XAxis,
  YAxis,
  ZAxis,
} from "recharts"

import { type ChartConfig, ChartContainer, ChartTooltip } from "@/components/ui/chart"

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/chart-bland-altman.json

Prompt

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.

On This Page