Kaplan-Meier Survival Plot in MUI X Charts (JavaScript)

The same plot in 14 other libraries — JavaScript: Chart.js, D3.js, Apache ECharts, Highcharts; Python: Altair, Bokeh, lets-plot, Matplotlib, Plotly, plotnine, Pygal, Seaborn; R: ggplot2; Julia: Makie.jl. Compare all 15 side by side: Kaplan-Meier Survival Plot in Python, R, Julia and JavaScript.

A Kaplan-Meier survival plot visualizes the probability of survival (or event-free time) over a time period using a step function. It is the standard method for estimating survival functions from time-to-event data, handling censored observations where the event has not yet occurred. The plot shows how survival probability decreases over time, with optional confidence intervals and comparison between groups.

Kaplan-Meier Survival Plot rendered with MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// survival-kaplan-meier: Kaplan-Meier Survival Plot
// Library: muix 9.16.0 | JavaScript 22.23.3
// Quality: 87/100 | Updated: 2026-10-23
import { LineChart } from "@mui/x-charts/LineChart";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

const t = window.ANYPLOT_TOKENS;

// Constants
const Z_95 = 1.96; // normal quantile for the 95% confidence interval

// Columns: set these to your data's column names
const TIME = "Time in service (months)"; // role: time
const EVENT = "Failure"; // role: event
const GROUP = "Bearing design"; // role: group

// Data: replace this block with your own records, one object per observation
// Reliability engineering: time-to-failure of a machine bearing, standard vs
// reinforced design. Each unit either fails, is pulled from service early for
// an unrelated reason, or is still running when the 36-month study ends.
let seed = 424242;
const rng = () => {
  seed = (seed * 1664525 + 1013904223) >>> 0; // fixed-seed LCG — the browser has no seeded RNG
  return seed / 4294967296;
};
const data: Record<string, string | number>[] = [];
[
  { design: "Standard design", failureRate: 0.045 },
  { design: "Reinforced design", failureRate: 0.022 },
].forEach(({ design, failureRate }) => {
  for (let i = 0; i < 50; i += 1) {
    const failureTime = -Math.log(1 - rng()) / failureRate;
    const dropoutTime = -Math.log(1 - rng()) / 0.01;
    const time = Math.min(failureTime, dropoutTime, 36);
    const failed = failureTime <= dropoutTime && failureTime <= 36;
    data.push({ [TIME]: time, [EVENT]: failed ? 1 : 0, [GROUP]: design });
  }
});

// Plot: reads only data and the constants above
const groups = [...new Set(data.map((d) => d[GROUP] as string))];
const timeGrid = [...new Set([0, ...data.map((d) => d[TIME] as number)])].sort((a, b) => a - b);
const maxTime = timeGrid[timeGrid.length - 1];

// Kaplan-Meier product-limit estimate with Greenwood's 95% CI, evaluated on the
// shared time grid. Step-function semantics: a value holds from its time onward.
const curves = groups.map((group) => {
  const rows = data.filter((d) => d[GROUP] === group);
  let atRisk = rows.length;
  let survival = 1;
  let varianceSum = 0;
  const points = timeGrid.map((time) => {
    const atThisTime = rows.filter((d) => d[TIME] === time);
    const deaths = atThisTime.filter((d) => d[EVENT] === 1).length;
    if (deaths > 0) {
      survival *= 1 - deaths / atRisk;
      if (atRisk > deaths) varianceSum += deaths / (atRisk * (atRisk - deaths));
    }
    atRisk -= atThisTime.length;
    const standardError = survival * Math.sqrt(varianceSum);
    return {
      survival,
      lower: Math.max(0, survival - Z_95 * standardError),
      upper: Math.min(1, survival + Z_95 * standardError),
      censored: atThisTime.length > deaths,
    };
  });
  return { group, n: rows.length, points };
});

// Each group contributes 3 series: an invisible base (stacked to the CI lower
// bound) + a filled delta on top of it (renders as the CI band from lower to
// upper), then the visible KM step line. Only the step line gets a `label`,
// so the band helpers are automatically excluded from the legend. The base's
// fill and both helpers' outlines are knocked out via the
// `.MuiLineChart-area/line[data-series=…]` sx rules below (MUI X always
// paints an area fill opaque, so a transparent `color` is not enough).
const series = curves.flatMap(({ group, n, points }, i) => [
  {
    id: `ci-base-${i}`,
    data: points.map((p) => p.lower),
    stack: `ci-${i}`,
    area: true,
    curve: "stepAfter" as const,
    color: t.palette[i],
    showMark: false,
    disableHighlight: true,
  },
  {
    id: `ci-band-${i}`,
    data: points.map((p) => Math.max(0, p.upper - p.lower)),
    stack: `ci-${i}`,
    area: true,
    curve: "stepAfter" as const,
    color: t.palette[i],
    showMark: false,
    disableHighlight: true,
  },
  {
    id: `survival-${i}`,
    data: points.map((p) => p.survival),
    curve: "stepAfter" as const,
    color: t.palette[i],
    label: `${group} (n=${n})`,
    shape: "circle" as const,
    showMark: ({ index }: { index: number }) => points[index].censored,
  },
]);

const bandStyles = Object.fromEntries(
  curves.flatMap((_, i) => [
    [`& .MuiLineChart-area[data-series="ci-base-${i}"]`, { fill: "none" }],
    [`& .MuiLineChart-area[data-series="ci-band-${i}"]`, { fill: t.palette[i], fillOpacity: 0.16 }],
    [`& .MuiLineChart-line[data-series="ci-base-${i}"]`, { display: "none" }],
    [`& .MuiLineChart-line[data-series="ci-band-${i}"]`, { display: "none" }],
  ]),
);

const TITLE = "survival-kaplan-meier · javascript · muix · anyplot.ai";

export default function Chart() {
  const size = window.ANYPLOT_SIZE;
  const paddingX = 40;
  const paddingY = 28;
  const headerHeight = 76;

  return (
    <Box
      sx={{
        width: size.width,
        height: size.height,
        boxSizing: "border-box",
        padding: `${paddingY}px ${paddingX}px`,
        display: "flex",
        flexDirection: "column",
      }}
    >
      <Box sx={{ height: headerHeight, flexShrink: 0 }}>
        <Typography sx={{ fontSize: 22, fontWeight: 600, color: "text.primary", lineHeight: 1.3 }}>
          {TITLE}
        </Typography>
        <Typography sx={{ fontSize: 14, color: "text.secondary", mt: "4px" }}>
          Shaded bands are 95% confidence intervals · open circles mark units censored while still in service
        </Typography>
      </Box>
      <LineChart
        width={size.width - paddingX * 2}
        height={size.height - paddingY * 2 - headerHeight - 40}
        series={series}
        xAxis={[
          {
            data: timeGrid,
            scaleType: "linear",
            min: 0,
            max: maxTime,
            label: TIME,
            labelStyle: { fontSize: 16 },
            tickLabelStyle: { fontSize: 14 },
            height: 52,
          },
        ]}
        yAxis={[
          {
            min: 0,
            max: 1,
            label: "Survival probability",
            labelStyle: { fontSize: 16 },
            tickLabelStyle: { fontSize: 14 },
            width: 84,
            valueFormatter: (v: number) => `${Math.round(v * 100)}%`,
          },
        ]}
        grid={{ horizontal: true }}
        margin={{ top: 8, right: 24, bottom: 8, left: 16 }}
        skipAnimation
        slotProps={{
          legend: {
            position: { vertical: "top", horizontal: "end" },
            sx: { fontSize: 15 },
          },
        }}
        sx={{
          "& .MuiLineChart-line": { strokeWidth: 3 },
          "& .MuiLineChart-mark": { strokeWidth: 2.5 },
          ...bandStyles,
        }}
      />
    </Box>
  );
}

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/survival-kaplan-meier/muix/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "survival-kaplan-meier",
  "language": "javascript",
  "library": "muix",
  "page": "https://anyplot.ai/survival-kaplan-meier/javascript/muix",
  "hub": "https://anyplot.ai/survival-kaplan-meier",
  "code_json": "https://api.anyplot.ai/specs/survival-kaplan-meier/muix/code",
  "spec_json": "https://api.anyplot.ai/specs/survival-kaplan-meier",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/survival-kaplan-meier/javascript/muix/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/survival-kaplan-meier/javascript/muix/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/survival-kaplan-meier/javascript/muix/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/survival-kaplan-meier/javascript/muix/plot-dark.html",
  "quality_score": 87.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Kaplan-Meier Survival Plot on anyplot.ai.

Other implementations