{"spec_id":"curve-oc","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ncurve-oc: Operating Characteristic (OC) Curve\nLibrary: altair 6.2.1 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-06-20\n\"\"\"\n\n# Remove the script's own directory from sys.path so the installed altair\n# package is found instead of this file (altair.py shadows the package otherwise)\nimport os as _os\nimport sys as _sys\n\n\n_here = _os.path.dirname(_os.path.abspath(__file__))\n_sys.path = [p for p in _sys.path if p and _os.path.abspath(p) != _here]\ndel _here, _os, _sys\n\nimport os\nfrom math import comb\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme-adaptive chrome tokens (Imprint palette)\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 categorical palette — hybrid-v3 sort, first series always #009E73\nIMPRINT_PALETTE = [\n    \"#009E73\",  # 1 brand green — always first series\n    \"#C475FD\",  # 2 lavender\n    \"#4467A3\",  # 3 blue\n    \"#BD8233\",  # 4 ochre\n    \"#AE3030\",  # 5 matte red — semantic: rejection / consumer risk zone\n    \"#2ABCCD\",  # 6 cyan\n    \"#954477\",  # 7 rose\n    \"#99B314\",  # 8 lime\n]\nANYPLOT_AMBER = \"#DDCC77\"  # warning / acceptable quality threshold\n\n# --- Data: binomial OC curves for three acceptance sampling plans ---\nfraction_defective = np.linspace(0, 0.15, 200)\n\nsampling_plans = [\n    {\"n\": 50, \"c\": 1, \"label\": \"n=50, c=1 (lenient)\"},\n    {\"n\": 100, \"c\": 3, \"label\": \"n=100, c=3 (moderate)\"},\n    {\"n\": 200, \"c\": 2, \"label\": \"n=200, c=2 (strict)\"},\n]\n\nrows = []\nfor plan in sampling_plans:\n    n, c = plan[\"n\"], plan[\"c\"]\n    prob_accept = np.zeros_like(fraction_defective, dtype=float)\n    for k in range(c + 1):\n        prob_accept += comb(n, k) * fraction_defective**k * (1 - fraction_defective) ** (n - k)\n    for p, pa in zip(fraction_defective, prob_accept, strict=True):\n        rows.append({\"fraction_defective\": p, \"probability_acceptance\": pa, \"plan\": plan[\"label\"]})\n\ndf = pd.DataFrame(rows)\n\n# AQL / LTPD reference points annotated on the moderate plan (n=100, c=3)\naql = 0.02\nltpd = 0.10\npa_at_aql = sum(comb(100, k) * aql**k * (1 - aql) ** (100 - k) for k in range(4))\nalpha = 1 - pa_at_aql\npa_at_ltpd = sum(comb(100, k) * ltpd**k * (1 - ltpd) ** (100 - k) for k in range(4))\nbeta = pa_at_ltpd\n\nref_data = pd.DataFrame(\n    [\n        {\"x\": aql, \"y\": pa_at_aql, \"risk\": f\"α={alpha:.1%} Producer risk\"},\n        {\"x\": ltpd, \"y\": pa_at_ltpd, \"risk\": f\"β={beta:.1%} Consumer risk\"},\n    ]\n)\n\n# --- Encodings ---\nplan_order = [p[\"label\"] for p in sampling_plans]\ncolor_scale = alt.Scale(domain=plan_order, range=[IMPRINT_PALETTE[0], IMPRINT_PALETTE[1], IMPRINT_PALETTE[2]])\ndash_scale = alt.Scale(domain=plan_order, range=[[1, 0], [8, 4], [2, 2]])\n\nnearest = alt.selection_point(nearest=True, on=\"pointerover\", fields=[\"fraction_defective\"], empty=False)\n\nbase_x = alt.X(\n    \"fraction_defective:Q\",\n    title=\"Fraction Defective (p)\",\n    scale=alt.Scale(domain=[0, 0.15]),\n    axis=alt.Axis(format=\".0%\", values=np.arange(0, 0.16, 0.02).tolist()),\n)\nbase_y = alt.Y(\n    \"probability_acceptance:Q\",\n    title=\"Probability of Acceptance P(a)\",\n    scale=alt.Scale(domain=[0, 1.05]),\n    axis=alt.Axis(values=np.arange(0, 1.1, 0.1).tolist()),\n)\n\n# --- OC curves with Imprint palette + dash redundancy ---\noc_lines = (\n    alt.Chart(df)\n    .mark_line(strokeWidth=2.5)\n    .encode(\n        x=base_x,\n        y=base_y,\n        color=alt.Color(\n            \"plan:N\",\n            scale=color_scale,\n            sort=plan_order,\n            legend=alt.Legend(\n                title=\"Sampling Plan\",\n                titleFontSize=10,\n                titleFontWeight=\"bold\",\n                titleColor=INK,\n                labelFontSize=10,\n                labelColor=INK_SOFT,\n                symbolStrokeWidth=2.5,\n                symbolSize=150,\n                orient=\"top-right\",\n                offset=8,\n                padding=8,\n                cornerRadius=6,\n                fillColor=ELEVATED_BG,\n                strokeColor=INK_SOFT,\n                direction=\"vertical\",\n            ),\n        ),\n        strokeDash=alt.StrokeDash(\"plan:N\", scale=dash_scale, sort=plan_order, legend=None),\n    )\n)\n\n# --- Hover interaction ---\nselect_layer = (\n    alt.Chart(df)\n    .mark_point(size=300, opacity=0)\n    .encode(x=alt.X(\"fraction_defective:Q\"), y=alt.Y(\"probability_acceptance:Q\"))\n    .add_params(nearest)\n)\n\nhover_rule = (\n    alt.Chart(df)\n    .mark_rule(strokeWidth=1, color=INK_SOFT, strokeDash=[3, 3], opacity=0.5)\n    .encode(x=alt.X(\"fraction_defective:Q\"))\n    .transform_filter(nearest)\n)\n\nhover_points = (\n    alt.Chart(df)\n    .mark_point(filled=True, stroke=PAGE_BG, strokeWidth=1.5)\n    .encode(\n        x=alt.X(\"fraction_defective:Q\"),\n        y=alt.Y(\"probability_acceptance:Q\"),\n        color=alt.Color(\"plan:N\", scale=color_scale, legend=None),\n        size=alt.condition(nearest, alt.value(200), alt.value(0)),\n        tooltip=[\n            alt.Tooltip(\"plan:N\", title=\"Plan\"),\n            alt.Tooltip(\"fraction_defective:Q\", title=\"Fraction Defective\", format=\".3f\"),\n            alt.Tooltip(\"probability_acceptance:Q\", title=\"P(Accept)\", format=\".3f\"),\n        ],\n    )\n)\n\n# --- AQL reference line + label (amber = acceptable quality threshold / caution) ---\naql_rule = (\n    alt.Chart(pd.DataFrame([{\"x\": aql}]))\n    .mark_rule(strokeDash=[6, 4], strokeWidth=1.5, color=ANYPLOT_AMBER, opacity=0.9)\n    .encode(x=alt.X(\"x:Q\"))\n)\naql_label = (\n    alt.Chart(pd.DataFrame([{\"x\": aql, \"y\": 1.02, \"text\": \"AQL\"}]))\n    .mark_text(fontSize=10, fontWeight=\"bold\", color=ANYPLOT_AMBER, dy=-4)\n    .encode(x=alt.X(\"x:Q\"), y=alt.Y(\"y:Q\"), text=\"text:N\")\n)\n\n# --- LTPD reference line + label (matte red = rejection boundary / consumer risk zone) ---\nltpd_rule = (\n    alt.Chart(pd.DataFrame([{\"x\": ltpd}]))\n    .mark_rule(strokeDash=[6, 4], strokeWidth=1.5, color=IMPRINT_PALETTE[4], opacity=0.9)\n    .encode(x=alt.X(\"x:Q\"))\n)\nltpd_label = (\n    alt.Chart(pd.DataFrame([{\"x\": ltpd, \"y\": 1.02, \"text\": \"LTPD\"}]))\n    .mark_text(fontSize=10, fontWeight=\"bold\", color=IMPRINT_PALETTE[4], dy=-4)\n    .encode(x=alt.X(\"x:Q\"), y=alt.Y(\"y:Q\"), text=\"text:N\")\n)\n\n# --- Risk annotation points on the n=100, c=3 curve ---\nrisk_points = (\n    alt.Chart(ref_data)\n    .mark_point(filled=True, size=200, stroke=PAGE_BG, strokeWidth=2, color=INK)\n    .encode(\n        x=alt.X(\"x:Q\"),\n        y=alt.Y(\"y:Q\"),\n        tooltip=[alt.Tooltip(\"risk:N\", title=\"Risk\"), alt.Tooltip(\"y:Q\", title=\"P(Accept)\", format=\".3f\")],\n    )\n)\nalpha_label = (\n    alt.Chart(ref_data.iloc[:1])\n    .mark_text(fontSize=10, fontWeight=\"bold\", align=\"left\", dx=8, dy=-8, color=INK)\n    .encode(x=alt.X(\"x:Q\"), y=alt.Y(\"y:Q\"), text=\"risk:N\")\n)\nbeta_label = (\n    alt.Chart(ref_data.iloc[1:])\n    .mark_text(fontSize=10, fontWeight=\"bold\", align=\"left\", dx=10, dy=-14, color=INK)\n    .encode(x=alt.X(\"x:Q\"), y=alt.Y(\"y:Q\"), text=\"risk:N\")\n)\n\n# --- Shaded risk zones (subtle, storytelling only) ---\nalpha_area = (\n    alt.Chart(pd.DataFrame([{\"x\": 0, \"x2\": aql, \"y\": 0, \"y2\": 1.05}]))\n    .mark_rect(fill=ANYPLOT_AMBER, opacity=0.05)\n    .encode(x=alt.X(\"x:Q\"), x2=\"x2:Q\", y=alt.Y(\"y:Q\"), y2=\"y2:Q\")\n)\nbeta_area = (\n    alt.Chart(pd.DataFrame([{\"x\": ltpd, \"x2\": 0.15, \"y\": 0, \"y2\": 1.05}]))\n    .mark_rect(fill=IMPRINT_PALETTE[4], opacity=0.05)\n    .encode(x=alt.X(\"x:Q\"), x2=\"x2:Q\", y=alt.Y(\"y:Q\"), y2=\"y2:Q\")\n)\n\n# --- Compose all layers ---\nchart = (\n    alpha_area\n    + beta_area\n    + aql_rule\n    + ltpd_rule\n    + oc_lines\n    + hover_points\n    + risk_points\n    + alpha_label\n    + beta_label\n    + aql_label\n    + ltpd_label\n    + select_layer\n    + hover_rule\n)\n\nchart = (\n    chart.properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        padding={\"left\": 0, \"right\": 0, \"top\": 0, \"bottom\": 0},\n        title=alt.Title(\n            \"curve-oc · python · altair · anyplot.ai\",\n            subtitle=\"Acceptance Sampling Plans — Producer’s & Consumer’s Risk\",\n            fontSize=16,\n            subtitleFontSize=11,\n            subtitleColor=INK_SOFT,\n            color=INK,\n            anchor=\"start\",\n            offset=12,\n        ),\n    )\n    .configure_view(continuousWidth=620, continuousHeight=320, fill=PAGE_BG, strokeWidth=0)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        titleColor=INK,\n        labelColor=INK_SOFT,\n        gridOpacity=0.15,\n        gridColor=INK,\n        domainWidth=0,\n        tickSize=0,\n    )\n    .configure_legend(titlePadding=6, labelLimit=300)\n    .configure_title(color=INK)\n)\n\n# --- Save PNG and pad to exact 3200 × 1800 ---\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}