Bland-Altman Agreement Plot — MUI X Charts

A Bland-Altman plot (also known as a difference plot or Tukey mean-difference plot) visualizes the agreement between two measurement methods by plotting the difference against the average of paired observations. It displays the mean difference (bias) as a horizontal line and limits of agreement (mean ± 1.96 SD) to assess whether the methods are interchangeable within acceptable tolerances.

Bland-Altman Agreement Plot rendered with MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// bland-altman-basic: Bland-Altman Agreement Plot
// Library: muix 7.29.1 | JavaScript 22.23.1
// Quality: 94/100 | Created: 2026-08-11
import { ScatterChart } from "@mui/x-charts/ScatterChart";
import { ChartsReferenceLine } from "@mui/x-charts/ChartsReferenceLine";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

const t = window.ANYPLOT_TOKENS;

// --- Data (in-memory, deterministic LCG) ------------------------------------
// Systolic blood pressure (mmHg) for 70 patients, each measured once with a
// new oscillometric cuff and once with the mercury-column reference standard.
let seed = 42;
function nextRandom() {
  seed = (seed * 1103515245 + 12345) & 0x7fffffff;
  return seed / 0x7fffffff;
}
function gaussian() {
  const u1 = Math.max(nextRandom(), 1e-9);
  const u2 = nextRandom();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

const PATIENT_COUNT = 70;
const reference = Array.from({ length: PATIENT_COUNT }, () => 122 + gaussian() * 15);
// The cuff reads slightly high on average, and its scatter widens a little
// for higher pressures — a realistic proportional-error pattern.
const cuff = reference.map((value) => value + 3.4 + gaussian() * (4 + value * 0.02));

const meanValues = reference.map((value, i) => (value + cuff[i]) / 2);
const diffValues = reference.map((value, i) => cuff[i] - value);

const bias = diffValues.reduce((sum, d) => sum + d, 0) / PATIENT_COUNT;
const variance =
  diffValues.reduce((sum, d) => sum + (d - bias) ** 2, 0) / (PATIENT_COUNT - 1);
const stdDev = Math.sqrt(variance);
const upperLimit = bias + 1.96 * stdDev;
const lowerLimit = bias - 1.96 * stdDev;

// Points outside the ±1.96 SD limits of agreement get an accent treatment —
// they're the story the reviewer of a Bland-Altman plot looks for first.
const withinLimits = [];
const outsideLimits = [];
meanValues.forEach((mean, i) => {
  const diff = diffValues[i];
  const point = { x: mean, y: diff, id: i };
  if (diff > upperLimit || diff < lowerLimit) {
    outsideLimits.push(point);
  } else {
    withinLimits.push(point);
  }
});

const TITLE_HEIGHT = 66;

// --- Chart (default-exported component — the harness mounts it) ------------
export default function Chart() {
  const { width, height } = window.ANYPLOT_SIZE;

  return (
    <Box
      sx={{
        width,
        height,
        display: "flex",
        flexDirection: "column",
        paddingTop: "20px",
      }}
    >
      <Typography
        sx={{
          color: t.ink,
          fontSize: 26,
          fontWeight: 600,
          textAlign: "center",
          lineHeight: 1.2,
        }}
      >
        bland-altman-basic · javascript · muix · anyplot.ai
      </Typography>
      <ScatterChart
        width={width}
        height={height - TITLE_HEIGHT}
        skipAnimation
        series={[
          {
            id: "within",
            data: withinLimits,
            label: "Within limits of agreement",
            markerSize: 7,
            color: "rgba(0, 158, 115, 0.6)",
          },
          {
            id: "outside",
            data: outsideLimits,
            label: "Outside limits of agreement",
            markerSize: 11,
            color: "rgba(174, 48, 48, 0.85)",
          },
        ]}
        xAxis={[
          {
            label: "Mean of Cuff & Reference (mmHg)",
            labelStyle: { fontSize: 16, fill: t.ink },
            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },
          },
        ]}
        yAxis={[
          {
            label: "Difference: Cuff − Reference (mmHg)",
            labelStyle: { fontSize: 16, fill: t.ink },
            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },
          },
        ]}
        margin={{ left: 100, right: 140, top: 20, bottom: 90 }}
        grid={{ horizontal: true, vertical: true }}
        slotProps={{
          legend: {
            position: { vertical: "top", horizontal: "middle" },
            direction: "row",
            labelStyle: { fontSize: 13, fill: t.inkSoft },
          },
        }}
        sx={{
          "& .MuiChartsGrid-line": { stroke: t.grid, strokeWidth: 1 },
          "& circle": { stroke: t.pageBg, strokeWidth: 1 },
        }}
      >
        <ChartsReferenceLine
          y={bias}
          label={`Bias: ${bias.toFixed(1)} mmHg`}
          labelAlign="end"
          lineStyle={{ stroke: t.ink, strokeWidth: 2.5 }}
          labelStyle={{ fill: t.ink, fontSize: 14, fontWeight: 600 }}
        />
        <ChartsReferenceLine
          y={upperLimit}
          label={`+1.96 SD: ${upperLimit.toFixed(1)}`}
          labelAlign="end"
          lineStyle={{ stroke: t.inkSoft, strokeDasharray: "8 6", strokeWidth: 1.75 }}
          labelStyle={{ fill: t.inkSoft, fontSize: 14 }}
        />
        <ChartsReferenceLine
          y={lowerLimit}
          label={`−1.96 SD: ${lowerLimit.toFixed(1)}`}
          labelAlign="end"
          lineStyle={{ stroke: t.inkSoft, strokeDasharray: "8 6", strokeWidth: 1.75 }}
          labelStyle={{ fill: t.inkSoft, fontSize: 14 }}
        />
      </ScatterChart>
    </Box>
  );
}

Part of Bland-Altman Agreement Plot on anyplot.ai.

Other implementations