{"spec_id":"bland-altman-basic","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nbland-altman-basic: Bland-Altman Agreement Plot\nLibrary: plotly 6.9.0 | Python 3.13.14\nQuality: 94/100 | Updated: 2026-08-11\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\nfrom plotly.subplots import make_subplots\n\n\n# Theme tokens (Imprint palette — theme-adaptive chrome)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nGRID = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\nBRAND = \"#009E73\"  # Imprint palette position 1 — observations + bias line\nACCENT = \"#C475FD\"  # Imprint palette position 2 — limits of agreement\n\n# Data - Glucose meter readings comparison (medical device validation)\nnp.random.seed(73)\nn_samples = 95\n\n# Lab reference method (plasma glucose in mg/dL)\nmethod1 = np.random.normal(120, 35, n_samples)\n\n# New handheld glucose meter with slight systematic bias\nbias = -3.2\nmethod2 = method1 + bias + np.random.normal(0, 8, n_samples)\n\n# Calculate Bland-Altman statistics\nmeans = (method1 + method2) / 2\ndifferences = method1 - method2\n\nmean_diff = np.mean(differences)\nstd_diff = np.std(differences, ddof=1)\nupper_loa = mean_diff + 1.96 * std_diff\nlower_loa = mean_diff - 1.96 * std_diff\nwithin_limits = np.sum((differences >= lower_loa) & (differences <= upper_loa))\npct_within = 100 * within_limits / n_samples\n\n# Figure: scatter panel + marginal histogram of the differences, so the\n# distribution shape (and its symmetry around the bias line) is visible at a\n# glance instead of only implied by the LoA math.\nfig = make_subplots(rows=1, cols=2, column_widths=[0.8, 0.2], shared_yaxes=True, horizontal_spacing=0.015)\n\n# Shaded band for the 95% limits of agreement, sitting behind the data\nfig.add_hrect(y0=lower_loa, y1=upper_loa, fillcolor=ACCENT, opacity=0.08, line_width=0, layer=\"below\", row=1, col=1)\n\n# Scatter of differences vs means\nfig.add_trace(\n    go.Scatter(\n        x=means,\n        y=differences,\n        mode=\"markers\",\n        marker={\"size\": 14, \"color\": BRAND, \"opacity\": 0.65, \"line\": {\"width\": 1.5, \"color\": PAGE_BG}},\n        name=\"Observations\",\n        hovertemplate=\"Mean: %{x:.1f} mg/dL<br>Difference: %{y:.1f} mg/dL<extra></extra>\",\n    ),\n    row=1,\n    col=1,\n)\n\n# Mean difference line (bias)\nfig.add_hline(y=mean_diff, line={\"color\": BRAND, \"width\": 3}, layer=\"below\", row=1, col=1)\n\n# Upper limit of agreement\nfig.add_hline(y=upper_loa, line={\"color\": ACCENT, \"width\": 2.5, \"dash\": \"dash\"}, layer=\"below\", row=1, col=1)\n\n# Lower limit of agreement\nfig.add_hline(y=lower_loa, line={\"color\": ACCENT, \"width\": 2.5, \"dash\": \"dash\"}, layer=\"below\", row=1, col=1)\n\n# Line-end labels, anchored well inside the row=1,col=1 domain (x domain=0.95)\n# so they never cross into the marginal-histogram column and cover its bars.\nfor label_text, label_y, label_color in (\n    (\"Mean\", mean_diff, BRAND),\n    (\"+1.96 SD\", upper_loa, ACCENT),\n    (\"−1.96 SD\", lower_loa, ACCENT),\n):\n    fig.add_annotation(\n        xref=\"x domain\",\n        yref=\"y\",\n        x=0.95,\n        y=label_y,\n        xanchor=\"right\",\n        yanchor=\"middle\",\n        text=label_text,\n        showarrow=False,\n        font={\"size\": 12, \"color\": INK if label_color == BRAND else label_color},\n        bgcolor=ELEVATED_BG,\n        bordercolor=label_color,\n        borderwidth=1.5,\n        borderpad=4,\n        row=1,\n        col=1,\n    )\n\n# Boxed summary of the agreement statistics, in one place instead of\n# scattered across three separate line labels\nfig.add_annotation(\n    xref=\"x domain\",\n    yref=\"y domain\",\n    x=0.02,\n    y=0.98,\n    xanchor=\"left\",\n    yanchor=\"top\",\n    align=\"left\",\n    showarrow=False,\n    text=(\n        f\"Bias (mean Δ): {mean_diff:+.2f} mg/dL<br>\"\n        f\"+1.96 SD: {upper_loa:+.2f}<br>\"\n        f\"−1.96 SD: {lower_loa:+.2f}<br>\"\n        f\"{pct_within:.1f}% within limits\"\n    ),\n    font={\"size\": 12, \"color\": INK},\n    bgcolor=ELEVATED_BG,\n    bordercolor=INK_SOFT,\n    borderwidth=1,\n    borderpad=10,\n    row=1,\n    col=1,\n)\n\n# Marginal histogram — shows the shape of the difference distribution,\n# which is what the ±1.96 SD limits of agreement assume is approximately normal\nfig.add_trace(\n    go.Histogram(\n        y=differences,\n        orientation=\"h\",\n        nbinsy=18,\n        marker={\"color\": BRAND, \"opacity\": 0.55, \"line\": {\"width\": 0}},\n        showlegend=False,\n        hovertemplate=\"Count: %{x}<extra></extra>\",\n    ),\n    row=1,\n    col=2,\n)\nfig.add_hline(y=mean_diff, line={\"color\": BRAND, \"width\": 2, \"dash\": \"dot\"}, opacity=0.6, row=1, col=2)\n\n# Axes\nfig.update_xaxes(\n    title={\"text\": \"Mean Glucose (mg/dL)\", \"font\": {\"size\": 13, \"color\": INK}},\n    tickfont={\"size\": 11, \"color\": INK_SOFT},\n    gridcolor=GRID,\n    showgrid=True,\n    zeroline=False,\n    linecolor=INK_SOFT,\n    row=1,\n    col=1,\n)\nfig.update_yaxes(\n    title={\"text\": \"Difference (Lab − Handheld) (mg/dL)\", \"font\": {\"size\": 13, \"color\": INK}},\n    tickfont={\"size\": 11, \"color\": INK_SOFT},\n    gridcolor=GRID,\n    showgrid=True,\n    zeroline=False,\n    linecolor=INK_SOFT,\n    row=1,\n    col=1,\n)\nfig.update_xaxes(\n    title={\"text\": \"Count\", \"font\": {\"size\": 11, \"color\": INK_SOFT}},\n    tickfont={\"size\": 9, \"color\": INK_SOFT},\n    showgrid=False,\n    zeroline=False,\n    linecolor=INK_SOFT,\n    row=1,\n    col=2,\n)\nfig.update_yaxes(showgrid=False, zeroline=False, linecolor=INK_SOFT, showticklabels=False, row=1, col=2)\n\n# Layout\nfig.update_layout(\n    autosize=False,\n    title={\n        \"text\": \"bland-altman-basic · python · plotly · anyplot.ai\",\n        \"font\": {\"size\": 18, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    showlegend=False,\n    margin={\"l\": 90, \"r\": 40, \"t\": 90, \"b\": 80},\n    hovermode=\"closest\",\n)\n\n# Save outputs\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}