{"spec_id":"bland-altman-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nbland-altman-basic: Bland-Altman Agreement Plot\nLibrary: bokeh 3.9.2 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-08-11\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import BoxAnnotation, ColumnDataSource, HoverTool, Label, Span\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\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\"\n\nBRAND = \"#009E73\"\nACCENT = \"#C475FD\"\n\nnp.random.seed(42)\nn = 80\n\ntrue_bp = np.random.normal(125, 15, n)\nmethod1 = true_bp + np.random.normal(0, 5, n)\nmethod2 = true_bp + np.random.normal(2, 6, n)\n\nmean_values = (method1 + method2) / 2\ndiff_values = method1 - method2\nmean_diff = np.mean(diff_values)\nstd_diff = np.std(diff_values, ddof=1)\nupper_loa = mean_diff + 1.96 * std_diff\nlower_loa = mean_diff - 1.96 * std_diff\n\nsource = ColumnDataSource(data={\"mean\": mean_values, \"diff\": diff_values})\n\n# `width` / `height` are the TOTAL canvas; axis labels at the 36-42pt\n# native-pixel sizes need explicit `min_border_*` reservations or they get\n# clipped at the edges of the rendered PNG.\np = figure(\n    width=3200,\n    height=1800,\n    title=\"bland-altman-basic · python · bokeh · anyplot.ai\",\n    x_axis_label=\"Mean of Two Methods (mmHg)\",\n    y_axis_label=\"Difference (Method 1 - Method 2) (mmHg)\",\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Subtle shaded band for the limits-of-agreement zone (design refinement:\n# gives the +-1.96 SD region a visual footprint without competing with data).\nagreement_zone = BoxAnnotation(bottom=lower_loa, top=upper_loa, fill_color=INK, fill_alpha=0.05)\np.add_layout(agreement_zone)\n\nscatter_renderer = p.scatter(\n    x=\"mean\",\n    y=\"diff\",\n    source=source,\n    size=14,\n    color=BRAND,\n    alpha=0.7,\n    line_color=PAGE_BG,\n    line_width=1,\n    legend_label=\"Observations\",\n)\np.add_tools(\n    HoverTool(tooltips=[(\"Mean\", \"@mean{0.2f} mmHg\"), (\"Difference\", \"@diff{0.2f} mmHg\")], renderers=[scatter_renderer])\n)\n\nmean_line = Span(location=mean_diff, dimension=\"width\", line_color=BRAND, line_width=3.5, line_dash=\"solid\")\np.add_layout(mean_line)\n\nupper_line = Span(location=upper_loa, dimension=\"width\", line_color=ACCENT, line_width=2.5, line_dash=\"dashed\")\np.add_layout(upper_line)\n\nlower_line = Span(location=lower_loa, dimension=\"width\", line_color=ACCENT, line_width=2.5, line_dash=\"dashed\")\np.add_layout(lower_line)\n\nx_min = np.min(mean_values)\nx_label_pos = x_min + (np.max(mean_values) - x_min) * 0.02\n\nmean_label = Label(\n    x=x_label_pos,\n    y=mean_diff,\n    text=f\"Mean Bias: {mean_diff:.2f} mmHg\",\n    text_font_size=\"28pt\",\n    text_color=BRAND,\n    text_baseline=\"bottom\",\n    y_offset=5,\n)\np.add_layout(mean_label)\n\nupper_label = Label(\n    x=x_label_pos,\n    y=upper_loa,\n    text=f\"+1.96 SD: {upper_loa:.2f} mmHg\",\n    text_font_size=\"28pt\",\n    text_color=ACCENT,\n    text_baseline=\"bottom\",\n    y_offset=5,\n)\np.add_layout(upper_label)\n\nlower_label = Label(\n    x=x_label_pos,\n    y=lower_loa,\n    text=f\"−1.96 SD: {lower_loa:.2f} mmHg\",\n    text_font_size=\"28pt\",\n    text_color=ACCENT,\n    text_baseline=\"top\",\n    y_offset=-5,\n)\np.add_layout(lower_label)\n\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\n\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.15\np.ygrid.grid_line_alpha = 0.15\n\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\n# No-frame treatment: bokeh's `outline_line_color` draws a single rectangle\n# around the whole plot area (no per-side spine control like matplotlib), so\n# a full box is the only \"outline\" option — skip it in favor of the axis\n# lines alone for a cleaner, less boxed-in look.\np.outline_line_color = None\n\nif p.legend:\n    p.legend.label_text_font_size = \"34pt\"\n    p.legend.label_text_color = INK_SOFT\n    p.legend.location = \"top_left\"\n    p.legend.background_fill_color = ELEVATED_BG\n    p.legend.border_line_color = INK_SOFT\n\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# IMPORTANT: W,H must match the figure's width/height above, otherwise the\n# bokeh canvas paints into the upper-left corner with white space around it.\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\n# IMPORTANT: headless Chrome's --window-size sets the OUTER window, which still\n# reserves a phantom title-bar height even headless — innerHeight (and thus the\n# screenshot) comes out short of H. Pin the viewport exactly via CDP.\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}