{"spec_id":"curve-oc","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ncurve-oc: Operating Characteristic (OC) Curve\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 88/100 | Updated: 2026-06-20\n\"\"\"\n\nimport os\nimport sys\n\n\n# This file is named pygal.py — remove its own directory from sys.path so that\n# `import pygal` resolves to the installed package, not this script itself.\n_thisdir = os.path.dirname(os.path.abspath(__file__))\nif _thisdir in sys.path:\n    sys.path.remove(_thisdir)\n\nfrom math import comb\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\n# Theme-adaptive chrome tokens\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 categorical palette — first series always #009E73\nIMPRINT_PALETTE = (\n    \"#009E73\",  # green  — n=50, c=1\n    \"#C475FD\",  # lavender — n=50, c=2\n    \"#4467A3\",  # blue   — n=100, c=2 (reference plan)\n    \"#BD8233\",  # ochre  — n=100, c=3\n    INK_MUTED,  # muted neutral — AQL reference line\n    INK_MUTED,  # muted neutral — LTPD reference line\n    \"#AE3030\",  # matte red — producer's risk (alpha)\n    \"#AE3030\",  # matte red — consumer's risk (beta)\n)\n\n# Data — tighter x-range for practical readability\nfraction_defective = np.linspace(0, 0.15, 200)\n\n# Sampling plans: (sample_size, acceptance_number, label)\nplans = [(50, 1, \"n=50, c=1\"), (50, 2, \"n=50, c=2\"), (100, 2, \"n=100, c=2\"), (100, 3, \"n=100, c=3\")]\n\n# Compute OC curves — P(accept) = sum C(n,k) * p^k * (1-p)^(n-k) for k=0..c\noc_curves = {}\nfor n, c, label in plans:\n    p = fraction_defective\n    oc_curves[label] = sum(comb(n, k) * p**k * (1 - p) ** (n - k) for k in range(c + 1))\n\n# Quality levels\naql = 0.01  # Acceptable Quality Level (1%)\nltpd = 0.08  # Lot Tolerance Percent Defective (8%)\n\n# Risks for reference plan n=100, c=2\npa_at_aql = float(sum(comb(100, k) * aql**k * (1 - aql) ** (100 - k) for k in range(3)))\nalpha = 1 - pa_at_aql\nbeta = float(sum(comb(100, k) * ltpd**k * (1 - ltpd) ** (100 - k) for k in range(3)))\n\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_PALETTE,\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    stroke_width=2.5,\n    font_family=\"sans-serif\",\n    tooltip_font_size=36,\n    opacity=0.9,\n    opacity_hover=1.0,\n)\n\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=\"curve-oc · python · pygal · anyplot.ai\",\n    x_title=\"Fraction Defective (p)\",\n    y_title=\"P(accept)\",\n    show_dots=False,\n    stroke=True,\n    fill=False,\n    show_x_guides=False,\n    show_y_guides=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=4,\n    legend_box_size=28,\n    truncate_legend=-1,\n    interpolate=\"hermite\",\n    interpolation_parameters={\"type\": \"cardinal\", \"c\": 0.75},\n    range=(0, 1.05),\n    xrange=(0, 0.15),\n    x_value_formatter=lambda x: f\"{x:.0%}\",\n    value_formatter=lambda y: f\"{y:.2f}\",\n    allow_interruptions=True,\n    x_labels=[0, 0.01, 0.02, 0.04, 0.06, 0.08, 0.10, 0.12, 0.15],\n    y_labels=[0, 0.2, 0.4, 0.6, 0.8, 1.0],\n    show_minor_y_labels=True,\n    js=[],\n    print_values=False,\n    margin_top=40,\n    margin_bottom=80,\n    margin_left=60,\n    spacing=20,\n)\n\n# Linestyle map: solid for n=100 plans, dashed for n=50 to aid distinction near p=0\nlinestyles = {\n    \"n=50, c=1\": {\"width\": 6, \"dasharray\": \"16, 10\", \"linecap\": \"round\"},\n    \"n=50, c=2\": {\"width\": 6, \"dasharray\": \"4, 8\", \"linecap\": \"round\"},\n    \"n=100, c=2\": {\"width\": 12, \"linecap\": \"round\", \"linejoin\": \"round\"},\n    \"n=100, c=3\": {\"width\": 6, \"linecap\": \"round\", \"linejoin\": \"round\"},\n}\n\nfor _n, _c, label in plans:\n    curve_data = list(zip(fraction_defective.tolist(), oc_curves[label].tolist(), strict=True))\n    chart.add(\n        label,\n        curve_data,\n        show_dots=False,\n        stroke_style=linestyles[label],\n        formatter=lambda v: f\"P(accept)={v[1]:.3f}\" if isinstance(v, (list, tuple)) else f\"{v:.3f}\",\n    )\n\n# AQL vertical reference line — thin dashed, subordinate to curves\nchart.add(\n    f\"AQL ({aql:.0%})\",\n    [(aql, 0), (aql, 1.05)],\n    show_dots=False,\n    stroke_style={\"width\": 3, \"dasharray\": \"14, 8\", \"linecap\": \"round\"},\n)\n\n# LTPD vertical reference line\nchart.add(\n    f\"LTPD ({ltpd:.0%})\",\n    [(ltpd, 0), (ltpd, 1.05)],\n    show_dots=False,\n    stroke_style={\"width\": 3, \"dasharray\": \"14, 8\", \"linecap\": \"round\"},\n)\n\n# Producer's risk point (alpha at AQL for n=100, c=2)\nchart.add(\n    f\"α={alpha:.1%} (producer risk)\",\n    [(aql, pa_at_aql)],\n    stroke=False,\n    show_dots=True,\n    dots_size=22,\n    formatter=lambda v: f\"α={1 - v[1]:.1%}\" if isinstance(v, (list, tuple)) else f\"{v:.3f}\",\n)\n\n# Consumer's risk point (beta at LTPD for n=100, c=2)\nchart.add(\n    f\"β={beta:.1%} (consumer risk)\",\n    [(ltpd, beta)],\n    stroke=False,\n    show_dots=True,\n    dots_size=22,\n    formatter=lambda v: f\"β={v[1]:.1%}\" if isinstance(v, (list, tuple)) else f\"{v:.3f}\",\n)\n\n# Post-process SVG: inject CSS to remove the 4-sided box frame (top/right spines).\n# Pygal draws a full border rect around the plot area; there is no built-in option\n# to suppress individual sides, so we hide the rect stroke via CSS.\n_svg = chart.render()\n_frame_css = (\n    b'<style type=\"text/css\">'\n    b\".graph .plot .background{stroke:none!important}\"\n    b\".graph .plot rect.background{stroke:none!important}\"\n    b\"</style>\"\n)\n_svg_patched = (\n    _svg.replace(b\"</defs>\", _frame_css + b\"</defs>\", 1)\n    if b\"</defs>\" in _svg\n    else _svg.replace(b\"</svg>\", _frame_css + b\"</svg>\", 1)\n)\n\ncairosvg.svg2png(bytestring=_svg_patched, write_to=f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(_svg_patched)\n"}