{"spec_id":"precision-recall","library":"d3","language":"javascript","code":"// anyplot.ai\n// precision-recall: Precision-Recall Curve\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 140, right: 90, bottom: 110, left: 130 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: fraud-detection classifier scores (in-memory, deterministic) ----\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction gaussian() {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\nfunction sigmoid(z) {\n  return 1 / (1 + Math.exp(-z));\n}\n\nconst nTransactions = 2000;\nconst fraudRate = 0.08;\nconst samples = [];\nfor (let i = 0; i < nTransactions; i++) {\n  const isFraud = rand() < fraudRate ? 1 : 0;\n  const logit = isFraud ? gaussian() * 1.05 + 1.9 : gaussian() * 1.05 - 0.4;\n  samples.push({ label: isFraud, score: sigmoid(logit) });\n}\n\nconst positives = samples.reduce((sum, d) => sum + d.label, 0);\nconst baselinePrecision = positives / nTransactions;\n\n// --- Precision-recall curve (threshold sweep from high score to low) -------\nconst sorted = samples.slice().sort((a, b) => b.score - a.score);\nlet tp = 0;\nlet fp = 0;\nconst curve = [{ recall: 0, precision: 1 }];\nfor (const d of sorted) {\n  if (d.label === 1) tp++;\n  else fp++;\n  curve.push({ recall: tp / positives, precision: tp / (tp + fp) });\n}\n\nlet averagePrecision = 0;\nfor (let i = 1; i < curve.length; i++) {\n  averagePrecision += curve[i].precision * (curve[i].recall - curve[i - 1].recall);\n}\n\n// --- Iso-F1 reference curves (P = F1*R / (2R - F1)) -------------------------\nconst isoF1Levels = [0.4, 0.6, 0.8];\nfunction isoF1Points(f1) {\n  const pts = [];\n  for (let r = f1 / 2 + 0.01; r <= 1; r += 0.01) {\n    const p = (f1 * r) / (2 * r - f1);\n    if (p > 0 && p <= 1) pts.push({ recall: r, precision: p });\n  }\n  return pts;\n}\n\n// --- Scales -------------------------------------------------------------------\nconst x = d3.scaleLinear().domain([0, 1]).range([0, iw]);\nconst y = d3.scaleLinear().domain([0, 1]).range([ih, 0]);\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Y gridlines (line chart: y-axis only) ---------------------------------\ng.selectAll(\".grid-line\")\n  .data(y.ticks(5))\n  .join(\"line\")\n  .attr(\"class\", \"grid-line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid);\n\n// --- Iso-F1 curves (labeled once in the legend, not on the crowded canvas) --\nconst isoLine = d3.line().x((d) => x(d.recall)).y((d) => y(d.precision));\nfor (const f1 of isoF1Levels) {\n  g.append(\"path\")\n    .datum(isoF1Points(f1))\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1.5)\n    .attr(\"stroke-dasharray\", \"4,4\")\n    .attr(\"opacity\", 0.4)\n    .attr(\"d\", isoLine);\n}\n\n// --- Baseline reference line (random / no-skill classifier) -----------------\ng.append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", y(baselinePrecision))\n  .attr(\"y2\", y(baselinePrecision))\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"8,5\")\n  .attr(\"opacity\", 0.55);\n\n// --- Precision-recall curve (stepped, threshold-accurate) -------------------\nconst prLine = d3\n  .line()\n  .x((d) => x(d.recall))\n  .y((d) => y(d.precision))\n  .curve(d3.curveStepAfter);\n\ng.append(\"path\")\n  .datum(curve)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 4.5)\n  .attr(\"stroke-linejoin\", \"round\")\n  .attr(\"d\", prLine);\n\n// --- Axes ---------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(5).tickFormat(d3.format(\".1f\")).tickSize(0).tickPadding(14));\nconst yAxis = g\n  .append(\"g\")\n  .call(d3.axisLeft(y).ticks(5).tickFormat(d3.format(\".1f\")).tickSize(0).tickPadding(14));\n\nfor (const axis of [xAxis, yAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\n  axis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Axis labels ----------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Recall\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -88)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Precision\");\n\n// --- Legend (top-right — empty region once a classifier's curve decays) -----\nconst legend = g.append(\"g\").attr(\"transform\", `translate(${iw - 470},14)`);\nlegend\n  .append(\"rect\")\n  .attr(\"width\", 470)\n  .attr(\"height\", 130)\n  .attr(\"fill\", t.elevatedBg)\n  .attr(\"opacity\", 0.92)\n  .attr(\"rx\", 6);\n\nlegend\n  .append(\"line\")\n  .attr(\"x1\", 18)\n  .attr(\"x2\", 52)\n  .attr(\"y1\", 28)\n  .attr(\"y2\", 28)\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 4.5);\nlegend\n  .append(\"text\")\n  .attr(\"x\", 62)\n  .attr(\"y\", 33)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(`Precision-Recall (AP = ${averagePrecision.toFixed(2)})`);\n\nlegend\n  .append(\"line\")\n  .attr(\"x1\", 18)\n  .attr(\"x2\", 52)\n  .attr(\"y1\", 66)\n  .attr(\"y2\", 66)\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"8,5\")\n  .attr(\"opacity\", 0.55);\nlegend\n  .append(\"text\")\n  .attr(\"x\", 62)\n  .attr(\"y\", 71)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(`No-skill baseline (fraud rate = ${(baselinePrecision * 100).toFixed(1)}%)`);\n\nlegend\n  .append(\"line\")\n  .attr(\"x1\", 18)\n  .attr(\"x2\", 52)\n  .attr(\"y1\", 100)\n  .attr(\"y2\", 100)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"4,4\")\n  .attr(\"opacity\", 0.6);\nlegend\n  .append(\"text\")\n  .attr(\"x\", 62)\n  .attr(\"y\", 105)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(`Iso-F1 curves (F1 = ${isoF1Levels.join(\" / \")})`);\n\n// --- Title --------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"precision-recall · javascript · d3 · anyplot.ai\");\n"}