{"spec_id":"curve-oc","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// curve-oc: Operating Characteristic (OC) Curve\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-20\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Binomial CDF: P(X ≤ c | n, p) via log-space for numerical stability\nfunction probAccept(n, c, p) {\n  if (p <= 0) return 1.0;\n  if (p >= 1) return 0.0;\n  const logP = Math.log(p);\n  const logQ = Math.log(1 - p);\n  let prob = 0;\n  for (let k = 0; k <= c; k++) {\n    let logBinom = 0;\n    for (let i = 0; i < k; i++) {\n      logBinom += Math.log(n - i) - Math.log(i + 1);\n    }\n    prob += Math.exp(logBinom + k * logP + (n - k) * logQ);\n  }\n  return Math.min(1, Math.max(0, prob));\n}\n\n// Quality thresholds\nconst AQL   = 0.02;  // Acceptable Quality Level (2%)\nconst LTPD  = 0.08;  // Lot Tolerance Percent Defective (8%)\nconst X_MAX = 0.15;  // x-axis upper bound\n\n// Three sampling plans — vary n and c to show discrimination power\nconst plans = [\n  { n: 50,  c: 2, label: \"n=50, c=2\"  },\n  { n: 100, c: 3, label: \"n=100, c=3\" },\n  { n: 200, c: 5, label: \"n=200, c=5\" },\n];\n\n// Build 151 (p, Pa) pairs per sampling plan\nfunction buildOCCurve(n, c) {\n  const pts = [];\n  for (let i = 0; i <= 150; i++) {\n    const p = (i / 150) * X_MAX;\n    pts.push({ x: p, y: probAccept(n, c, p) });\n  }\n  return pts;\n}\n\n// OC curve datasets — Imprint palette positions 0, 1, 2\nconst datasets = plans.map((plan, i) => ({\n  label: plan.label,\n  data: buildOCCurve(plan.n, plan.c),\n  showLine: true,\n  fill: false,\n  borderColor: t.palette[i],\n  backgroundColor: \"transparent\",\n  borderWidth: 3,\n  pointRadius: 0,\n  tension: 0,\n}));\n\n// AQL vertical reference line — amber (warning/acceptable threshold)\ndatasets.push({\n  label: `AQL (${(AQL * 100).toFixed(0)}%)`,\n  data: [{ x: AQL, y: 0 }, { x: AQL, y: 1 }],\n  showLine: true,\n  fill: false,\n  borderColor: t.amber,\n  backgroundColor: \"transparent\",\n  borderWidth: 2,\n  borderDash: [10, 5],\n  pointRadius: 0,\n  tension: 0,\n});\n\n// LTPD vertical reference line — matte red (rejection threshold)\ndatasets.push({\n  label: `LTPD (${(LTPD * 100).toFixed(0)}%)`,\n  data: [{ x: LTPD, y: 0 }, { x: LTPD, y: 1 }],\n  showLine: true,\n  fill: false,\n  borderColor: t.palette[4],\n  backgroundColor: \"transparent\",\n  borderWidth: 2,\n  borderDash: [10, 5],\n  pointRadius: 0,\n  tension: 0,\n});\n\n// Mount\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// Inline plugin: annotate α (producer's risk) and β (consumer's risk) from plan[0]\nconst riskLabels = {\n  id: \"riskLabels\",\n  afterDraw(chart) {\n    const ctx = chart.ctx;\n    const xsc = chart.scales.x;\n    const ysc = chart.scales.y;\n    const ref = plans[0];\n\n    ctx.save();\n    ctx.textBaseline = \"middle\";\n    ctx.font = \"bold 16px sans-serif\";\n\n    // α = producer's risk (probability of rejecting a good lot) at AQL\n    const paAql = probAccept(ref.n, ref.c, AQL);\n    ctx.fillStyle = t.amber;\n    ctx.fillText(\n      `α = ${(1 - paAql).toFixed(2)}`,\n      xsc.getPixelForValue(AQL) + 10,\n      ysc.getPixelForValue(paAql) - 14\n    );\n\n    // β = consumer's risk (probability of accepting a bad lot) at LTPD\n    const paLtpd = probAccept(ref.n, ref.c, LTPD);\n    ctx.fillStyle = t.palette[4];\n    ctx.fillText(\n      `β = ${paLtpd.toFixed(2)}`,\n      xsc.getPixelForValue(LTPD) + 10,\n      ysc.getPixelForValue(paLtpd) + 14\n    );\n\n    ctx.restore();\n  },\n};\n\n// Chart\nnew Chart(canvas, {\n  type: \"scatter\",\n  plugins: [riskLabels],\n  data: { datasets },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"curve-oc · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { top: 12, bottom: 8 },\n      },\n      legend: {\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          boxWidth: 30,\n          padding: 20,\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0,\n        max: X_MAX,\n        title: {\n          display: true,\n          text: \"Fraction Defective (p)\",\n          color: t.ink,\n          font: { size: 18 },\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (v) => (v * 100).toFixed(0) + \"%\",\n          maxTicksLimit: 9,\n        },\n        grid: { color: t.grid },\n      },\n      y: {\n        type: \"linear\",\n        min: 0,\n        max: 1,\n        title: {\n          display: true,\n          text: \"Probability of Acceptance\",\n          color: t.ink,\n          font: { size: 18 },\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (v) => (v * 100).toFixed(0) + \"%\",\n          maxTicksLimit: 6,\n        },\n        grid: { color: t.grid },\n      },\n    },\n  },\n});\n"}