{"spec_id":"bland-altman-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nbland-altman-basic: Bland-Altman Agreement Plot\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-08-11\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens (from prompts/default-style-guide.md)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette (first series = brand green #009E73). The outlier highlight\n# continues the canonical Imprint sequence.\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\")\n\n# Data - Blood pressure readings from two different sphygmomanometers\nnp.random.seed(42)\nn_subjects = 50\n\n# Simulate paired blood pressure measurements (systolic, mmHg)\ntrue_bp = np.random.normal(125, 15, n_subjects)\nmethod1 = true_bp + np.random.normal(0, 5, n_subjects)\nmethod2 = true_bp + np.random.normal(2, 6, n_subjects)\n\n# Bland-Altman calculations\nmean_values = (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\n\n# Point furthest from the bias line - called out separately below\noutlier_idx = int(np.argmax(np.abs(differences - mean_diff)))\noutlier_x = float(mean_values[outlier_idx])\noutlier_y = float(differences[outlier_idx])\noutlier_outside_loa = outlier_y > upper_loa or outlier_y < lower_loa\n\n# Custom style with theme-adaptive colors (canonical pygal sizing, see\n# prompts/library/pygal.md \"Sizing + Theme for 3200x1800 px\")\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    dot_opacity=0.65,  # spec: scatter points at moderate transparency to reveal overlap\n    stroke_width=3,\n)\n\n# Create XY chart for Bland-Altman scatter\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=\"bland-altman-basic · pygal · anyplot.ai\",\n    x_title=\"Mean of Two Methods (mmHg)\",\n    y_title=\"Difference (Method 1 - Method 2) (mmHg)\",\n    show_legend=True,\n    legend_at_bottom=True,\n    dots_size=10,\n    stroke=False,\n    show_x_guides=True,\n    show_y_guides=True,\n    value_formatter=lambda v: f\"{v:.1f} mmHg\",\n)\n\n# Prepare scatter data points with opacity for overlapping observations\nscatter_data = [{\"value\": (float(mean_values[i]), float(differences[i]))} for i in range(n_subjects)]\n\n# Add scatter points (main series in brand green)\nchart.add(\"Measurements\", scatter_data)\n\n# Add horizontal lines for mean and limits of agreement\nx_min, x_max = min(mean_values), max(mean_values)\nmargin = (x_max - x_min) * 0.05\nx_range = [x_min - margin, x_max + margin]\n\n# Mean line (bias)\nchart.add(\n    f\"Mean Bias ({mean_diff:.1f})\",\n    [(x_range[0], mean_diff), (x_range[1], mean_diff)],\n    stroke=True,\n    dots_size=0,\n    stroke_style={\"width\": 3},\n)\n\n# Upper limit of agreement\nchart.add(\n    f\"Upper LoA (+1.96 SD: {upper_loa:.1f})\",\n    [(x_range[0], upper_loa), (x_range[1], upper_loa)],\n    stroke=True,\n    dots_size=0,\n    stroke_style={\"width\": 2, \"dasharray\": \"10, 5\"},\n)\n\n# Lower limit of agreement\nchart.add(\n    f\"Lower LoA (-1.96 SD: {lower_loa:.1f})\",\n    [(x_range[0], lower_loa), (x_range[1], lower_loa)],\n    stroke=True,\n    dots_size=0,\n    stroke_style={\"width\": 2, \"dasharray\": \"10, 5\"},\n)\n\n# Outlier callout - the single observation furthest from the bias line,\n# replotted larger and in a distinct color so it reads as the plot's focal\n# point without altering the underlying \"Measurements\" series.\nchart.add(\n    f\"Outlier ({outlier_y:+.1f}{', outside LoA' if outlier_outside_loa else ''})\",\n    [{\"value\": (outlier_x, outlier_y)}],\n    dots_size=22,\n    stroke=False,\n)\n\n# Save outputs\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}