{"spec_id":"precision-recall","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// precision-recall: Precision-Recall Curve\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: synthetic diagnostic-test scores for a rare condition -----------\nfunction makeLcg(seed) {\n  let state = seed;\n  return function next() {\n    state = (state * 9301 + 49297) % 233280;\n    return state / 233280;\n  };\n}\nconst uniform = makeLcg(42);\nfunction gaussian(mean, std) {\n  const u1 = Math.max(uniform(), 1e-9);\n  const u2 = uniform();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + std * z;\n}\n\nconst N_PATIENTS = 400;\nconst PREVALENCE = 0.12;\nconst nPositive = Math.round(N_PATIENTS * PREVALENCE);\nconst nNegative = N_PATIENTS - nPositive;\n\nconst yTrue = [];\nconst yScores = [];\nfor (let i = 0; i < nPositive; i++) {\n  yTrue.push(1);\n  yScores.push(Math.min(1, Math.max(0, gaussian(0.72, 0.16))));\n}\nfor (let i = 0; i < nNegative; i++) {\n  yTrue.push(0);\n  yScores.push(Math.min(1, Math.max(0, gaussian(0.28, 0.18))));\n}\n\n// --- Precision-recall curve (descending-score thresholds, sklearn-style) ---\nconst order = yScores.map((_, i) => i).sort((a, b) => yScores[b] - yScores[a]);\n\nlet truePositives = 0;\nlet falsePositives = 0;\nconst precisionPoints = [1];\nconst recallPoints = [0];\nfor (const i of order) {\n  if (yTrue[i] === 1) truePositives += 1;\n  else falsePositives += 1;\n  precisionPoints.push(truePositives / (truePositives + falsePositives));\n  recallPoints.push(truePositives / nPositive);\n}\n\nlet averagePrecision = 0;\nfor (let i = 1; i < recallPoints.length; i++) {\n  averagePrecision += (recallPoints[i] - recallPoints[i - 1]) * precisionPoints[i];\n}\n\nconst prCurve = recallPoints.map((r, i) => ({ x: r, y: precisionPoints[i] }));\nconst baselinePrecision = nPositive / N_PATIENTS;\n\n// --- Knee point: the threshold with the highest F1 score, used for the callout\nlet kneeIndex = 1;\nlet bestF1 = -1;\nfor (let i = 1; i < prCurve.length; i++) {\n  const { x: r, y: p } = prCurve[i];\n  const f1 = p + r > 0 ? (2 * p * r) / (p + r) : 0;\n  if (f1 > bestF1) {\n    bestF1 = f1;\n    kneeIndex = i;\n  }\n}\nconst kneePoint = prCurve[kneeIndex];\n\n// --- Iso-F1 reference curves: precision as a function of recall for a fixed F1\nfunction isoF1Curve(f1, steps = 100) {\n  const points = [];\n  for (let i = 0; i <= steps; i++) {\n    const r = i / steps;\n    if (2 * r - f1 <= 1e-6) continue;\n    const p = (f1 * r) / (2 * r - f1);\n    if (p > 0 && p <= 1) points.push({ x: r, y: p });\n  }\n  return points;\n}\nconst isoF1Levels = [0.3, 0.5, 0.7];\nconst isoF1Datasets = isoF1Levels.map((f1) => ({\n  label: `F1 = ${f1.toFixed(1)}`,\n  data: isoF1Curve(f1),\n  isoF1: true,\n  borderColor: t.inkSoft,\n  borderWidth: 1,\n  borderDash: [2, 4],\n  pointRadius: 0,\n  fill: false,\n}));\n\nfunction hexToRgba(hex, alpha) {\n  const clean = hex.replace(\"#\", \"\");\n  const r = parseInt(clean.substring(0, 2), 16);\n  const g = parseInt(clean.substring(2, 4), 16);\n  const b = parseInt(clean.substring(4, 6), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\n// --- Mount -------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Custom plugin: callout marking the best-F1 point on the curve ---------\nconst apCalloutPlugin = {\n  id: \"apCallout\",\n  afterDatasetsDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    const x = scales.x.getPixelForValue(kneePoint.x);\n    const y = scales.y.getPixelForValue(kneePoint.y);\n    const onLeftHalf = kneePoint.x <= 0.5;\n    const labelX = onLeftHalf ? x + 50 : x - 50;\n    const labelY = Math.min(Math.max(y - 50, chartArea.top + 24), chartArea.bottom - 20);\n\n    ctx.save();\n    ctx.beginPath();\n    ctx.arc(x, y, 6, 0, 2 * Math.PI);\n    ctx.fillStyle = t.palette[0];\n    ctx.fill();\n    ctx.lineWidth = 2;\n    ctx.strokeStyle = t.pageBg;\n    ctx.stroke();\n\n    ctx.beginPath();\n    ctx.moveTo(x, y);\n    ctx.lineTo(labelX, labelY + 6);\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1;\n    ctx.stroke();\n\n    ctx.font = \"600 15px sans-serif\";\n    ctx.fillStyle = t.ink;\n    ctx.textAlign = onLeftHalf ? \"left\" : \"right\";\n    ctx.textBaseline = \"bottom\";\n    ctx.fillText(`Best F1 = ${bestF1.toFixed(2)} (AP = ${averagePrecision.toFixed(2)})`, labelX, labelY);\n    ctx.restore();\n  },\n};\n\n// --- Chart -----------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"line\",\n  data: {\n    datasets: [\n      {\n        label: `Diagnostic test (AP = ${averagePrecision.toFixed(2)})`,\n        data: prCurve,\n        borderColor: t.palette[0],\n        backgroundColor: hexToRgba(t.palette[0], 0.14),\n        stepped: \"after\",\n        borderWidth: 3.5,\n        pointRadius: 0,\n        fill: \"origin\",\n      },\n      ...isoF1Datasets,\n      {\n        label: `Baseline (prevalence = ${baselinePrecision.toFixed(2)})`,\n        data: [\n          { x: 0, y: baselinePrecision },\n          { x: 1, y: baselinePrecision },\n        ],\n        borderColor: t.inkSoft,\n        borderDash: [8, 6],\n        borderWidth: 2,\n        pointRadius: 0,\n        fill: false,\n      },\n    ],\n  },\n  plugins: [apCalloutPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"precision-recall · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { bottom: 24 },\n      },\n      legend: {\n        position: \"top\",\n        align: \"end\",\n        labels: {\n          color: t.ink,\n          font: { size: 15 },\n          boxWidth: 24,\n          boxHeight: 3,\n          filter: (item, data) => !data.datasets[item.datasetIndex].isoF1,\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0,\n        max: 1,\n        title: { display: true, text: \"Recall\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 }, stepSize: 0.2 },\n        grid: { color: t.grid },\n      },\n      y: {\n        min: 0,\n        max: 1,\n        title: { display: true, text: \"Precision\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 }, stepSize: 0.2 },\n        grid: { color: t.grid },\n      },\n    },\n  },\n});\n"}