{"spec_id":"curve-oc","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ncurve-oc: Operating Characteristic (OC) Curve\nLibrary: bokeh 3.9.1 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-06-20\n\"\"\"\n\nimport base64\nimport os\nimport time\nfrom math import comb\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import BoxAnnotation, ColumnDataSource, Label, Legend, LegendItem, Span\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — canonical order, theme-independent\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nANYPLOT_AMBER = \"#DDCC77\"  # warning / caution anchor (outside categorical pool)\n\n# Data — binomial CDF: P(accept) = sum C(n,k)*p^k*(1-p)^(n-k), k=0..c\nfraction_defective = np.linspace(0, 0.12, 200)\n\nplans = [(50, 1), (100, 2), (150, 3)]\noc_curves = []\nfor n, c in plans:\n    pa = np.ones(len(fraction_defective))\n    for i, p_val in enumerate(fraction_defective):\n        if p_val > 0:\n            pa[i] = sum(comb(n, k) * p_val**k * (1 - p_val) ** (n - k) for k in range(c + 1))\n    oc_curves.append(pa)\n\n# AQL and LTPD reference points\naql = 0.01\nltpd = 0.08\n\n# Risk values at AQL and LTPD for plan 2 (n=100, c=2)\nn2, c2 = plans[1]\npa_at_aql = sum(comb(n2, k) * aql**k * (1 - aql) ** (n2 - k) for k in range(c2 + 1))\nalpha = 1 - pa_at_aql\npa_at_ltpd = sum(comb(n2, k) * ltpd**k * (1 - ltpd) ** (n2 - k) for k in range(c2 + 1))\n\n# Title font scaling — shrink only when title exceeds 67-char baseline\ntitle_str = \"curve-oc · python · bokeh · anyplot.ai\"\nn_chars = len(title_str)\ntitle_pt = round(50 * 67 / n_chars) if n_chars > 67 else 50\ntitle_fontsize = f\"{title_pt}pt\"\n\n# Figure — 3200×1800 landscape canvas (hard contract, no deviation)\np = figure(\n    width=3200,\n    height=1800,\n    title=title_str,\n    x_axis_label=\"Fraction Defective (p)\",\n    y_axis_label=\"Probability of Acceptance P(a)\",\n    x_range=(-0.003, 0.125),\n    y_range=(-0.03, 1.06),\n    toolbar_location=None,  # REQUIRED: default toolbar adds ~30-50px, shrinking the PNG\n    min_border_bottom=160,  # room for 34pt tick labels + 42pt axis label\n    min_border_left=180,\n    min_border_top=110,  # room for 50pt title\n    min_border_right=50,\n)\n\n# Shaded risk regions\nproducer_risk_zone = BoxAnnotation(left=0, right=aql, fill_color=IMPRINT_PALETTE[0], fill_alpha=0.07)\np.add_layout(producer_risk_zone)\n\nconsumer_risk_zone = BoxAnnotation(left=ltpd, right=0.125, fill_color=IMPRINT_PALETTE[4], fill_alpha=0.07)\np.add_layout(consumer_risk_zone)\n\n# Zone label — increased alpha for visibility (was 0.35, now 0.65)\nzone_reject = Label(\n    x=0.086,\n    y=0.50,\n    text=\"Rejectable\\nQuality\",\n    text_font_size=\"22pt\",\n    text_color=IMPRINT_PALETTE[4],\n    text_alpha=0.65,\n    text_font_style=\"italic\",\n)\np.add_layout(zone_reject)\n\n# OC curves with distinct line dashes for redundant encoding (3 series)\nline_dashes = [\"solid\", [12, 6], [6, 4, 2, 4]]\nlines = []\nfor i, pa in enumerate(oc_curves):\n    source = ColumnDataSource(data={\"p\": fraction_defective, \"pa\": pa})\n    line = p.line(\"p\", \"pa\", source=source, line_width=4, line_color=IMPRINT_PALETTE[i], line_dash=line_dashes[i])\n    lines.append(line)\n\n# AQL vertical reference line\naql_line = Span(location=aql, dimension=\"height\", line_color=INK_SOFT, line_width=2, line_dash=\"dashed\", line_alpha=0.6)\np.add_layout(aql_line)\n\naql_label = Label(\n    x=aql + 0.002, y=0.90, text=\"AQL = 1%\", text_font_size=\"22pt\", text_color=INK_SOFT, text_font_style=\"italic\"\n)\np.add_layout(aql_label)\n\n# LTPD vertical reference line\nltpd_line = Span(\n    location=ltpd, dimension=\"height\", line_color=INK_SOFT, line_width=2, line_dash=\"dashed\", line_alpha=0.6\n)\np.add_layout(ltpd_line)\n\nltpd_label = Label(\n    x=ltpd + 0.002, y=0.90, text=\"LTPD = 8%\", text_font_size=\"22pt\", text_color=INK_SOFT, text_font_style=\"italic\"\n)\np.add_layout(ltpd_label)\n\n# Producer's risk (alpha) marker — repositioned above the curve cluster to avoid congestion\nrisk_source_alpha = ColumnDataSource(data={\"x\": [aql], \"y\": [pa_at_aql]})\np.scatter(\n    \"x\", \"y\", source=risk_source_alpha, size=20, color=ANYPLOT_AMBER, line_color=INK, line_width=2, marker=\"diamond\"\n)\n\nalpha_label = Label(\n    x=aql + 0.005,\n    y=pa_at_aql + 0.04,\n    text=f\"α (producer) = {alpha:.3f}\",\n    text_font_size=\"20pt\",\n    text_color=INK_SOFT,\n    text_font_style=\"bold\",\n)\np.add_layout(alpha_label)\n\n# Consumer's risk (beta) marker\nrisk_source_beta = ColumnDataSource(data={\"x\": [ltpd], \"y\": [pa_at_ltpd]})\np.scatter(\n    \"x\", \"y\", source=risk_source_beta, size=20, color=IMPRINT_PALETTE[4], line_color=INK, line_width=2, marker=\"diamond\"\n)\n\nbeta_label = Label(\n    x=ltpd - 0.030,\n    y=pa_at_ltpd + 0.06,\n    text=f\"β (consumer) = {pa_at_ltpd:.3f}\",\n    text_font_size=\"20pt\",\n    text_color=INK_SOFT,\n    text_font_style=\"bold\",\n)\np.add_layout(beta_label)\n\n# Legend\nplan_labels = [\"n=50, c=1\", \"n=100, c=2\", \"n=150, c=3\"]\nlegend = Legend(\n    items=[LegendItem(label=lbl, renderers=[ln]) for lbl, ln in zip(plan_labels, lines, strict=True)],\n    location=\"top_right\",\n)\nlegend.label_text_font_size = \"34pt\"\nlegend.background_fill_color = ELEVATED_BG\nlegend.border_line_color = INK_SOFT\nlegend.border_line_width = 1\nlegend.spacing = 12\nlegend.padding = 20\nlegend.glyph_width = 60\nlegend.glyph_height = 34\nlegend.label_text_color = INK_SOFT\np.add_layout(legend)\n\n# Style — theme-adaptive chrome\np.title.text_font_size = title_fontsize\np.title.text_font_style = \"bold\"\np.title.text_color = INK\n\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.axis_label_text_font_style = \"bold\"\np.yaxis.axis_label_text_font_style = \"bold\"\n\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.xgrid.grid_line_alpha = 0.15\np.ygrid.grid_line_alpha = 0.15\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\n\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_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\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\n# Save — write HTML then screenshot with headless Chrome (export_png unavailable in CI)\noutput_file(f\"plot-{THEME}.html\", title=\"Operating Characteristic (OC) Curve\")\nsave(p)\n\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()}\")\ntime.sleep(3)\n# Use CDP captureScreenshot with explicit clip to guarantee exact W×H output\n# (driver.save_screenshot captures viewport which can be cropped by browser chrome)\nresult = driver.execute_cdp_cmd(\n    \"Page.captureScreenshot\",\n    {\"format\": \"png\", \"clip\": {\"x\": 0, \"y\": 0, \"width\": W, \"height\": H, \"scale\": 1}, \"captureBeyondViewport\": True},\n)\nwith open(f\"plot-{THEME}.png\", \"wb\") as _f:\n    _f.write(base64.b64decode(result[\"data\"]))\ndriver.quit()\n"}