{"spec_id":"precision-recall","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// precision-recall: Precision-Recall Curve\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst muted = window.ANYPLOT_THEME === \"dark\" ? \"#A8A79F\" : \"#6B6A63\";\n\n// --- Data: synthetic fraud-detection classifier scores (in-memory, deterministic) ---\nlet seed = 42;\nfunction lcgRandom() {\n  seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;\n  return seed / 4294967296;\n}\nfunction gaussian() {\n  const u1 = Math.max(lcgRandom(), 1e-9);\n  const u2 = lcgRandom();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst nSamples = 6000;\nconst positiveRate = 0.12;\nconst yTrue = [];\nconst yScores = [];\nfor (let i = 0; i < nSamples; i += 1) {\n  const isPositive = lcgRandom() < positiveRate;\n  yTrue.push(isPositive ? 1 : 0);\n  const score = isPositive ? 0.62 + 0.2 * gaussian() : 0.32 + 0.2 * gaussian();\n  yScores.push(Math.min(Math.max(score, 0), 1));\n}\n\n// --- Precision-recall curve (mirrors sklearn's threshold-sweep algorithm) --\nconst positives = yTrue.reduce((sum, label) => sum + label, 0);\nconst order = yScores\n  .map((_, i) => i)\n  .sort((a, b) => yScores[b] - yScores[a]);\n\nconst prPoints = [{ recall: 0, precision: 1 }];\nlet truePositives = 0;\nlet falsePositives = 0;\nfor (let i = 0; i < order.length; i += 1) {\n  const idx = order[i];\n  if (yTrue[idx] === 1) truePositives += 1;\n  else falsePositives += 1;\n  const isLastAtThreshold =\n    i === order.length - 1 || yScores[order[i + 1]] !== yScores[idx];\n  if (isLastAtThreshold) {\n    prPoints.push({\n      recall: truePositives / positives,\n      precision: truePositives / (truePositives + falsePositives),\n    });\n  }\n}\n\nlet averagePrecision = 0;\nfor (let i = 1; i < prPoints.length; i += 1) {\n  averagePrecision +=\n    (prPoints[i].recall - prPoints[i - 1].recall) * prPoints[i].precision;\n}\n\n// --- Best-F1 operating point (callout on the curve itself) -------------------\nlet bestF1Idx = 1;\nlet bestF1 = 0;\nfor (let i = 1; i < prPoints.length; i += 1) {\n  const { recall, precision } = prPoints[i];\n  const f1 = recall + precision > 0 ? (2 * recall * precision) / (recall + precision) : 0;\n  if (f1 > bestF1) {\n    bestF1 = f1;\n    bestF1Idx = i;\n  }\n}\n\n// --- Iso-F1 reference curves (spec note: contour lines for F1 reference) ---\nconst isoF1Values = [0.3, 0.5, 0.7, 0.9];\nconst isoF1Series = isoF1Values.map((f1) => {\n  const points = [];\n  for (let i = 1; i <= 100; i += 1) {\n    const recall = i / 100;\n    const denom = 2 * recall - f1;\n    if (denom <= 0) continue;\n    const precision = (f1 * recall) / denom;\n    if (precision > 0 && precision <= 1) points.push([recall, precision]);\n  }\n  return {\n    type: \"line\",\n    name: `F1 = ${f1}`,\n    data: points,\n    color: muted,\n    dashStyle: \"ShortDot\",\n    lineWidth: 1.5,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: false,\n    dataLabels: {\n      enabled: true,\n      allowOverlap: true,\n      align: \"left\",\n      x: 6,\n      y: -8,\n      style: { color: muted, fontSize: \"13px\", fontWeight: \"normal\", textOutline: \"none\" },\n      formatter() {\n        return this.point.index === this.series.data.length - 1 ? `F1=${f1}` : null;\n      },\n    },\n  };\n});\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"line\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"precision-recall · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    title: { text: \"Recall\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    min: 0,\n    max: 1.02,\n    tickInterval: 0.2,\n    gridLineWidth: 0,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    title: { text: \"Precision\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    min: 0,\n    max: 1.0,\n    tickInterval: 0.2,\n    endOnTick: false,\n    gridLineColor: t.grid,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  plotOptions: {\n    series: { animation: false },\n  },\n  series: [\n    {\n      type: \"area\",\n      name: `Precision-Recall (AP = ${averagePrecision.toFixed(2)})`,\n      data: prPoints.map((p, i) =>\n        i === bestF1Idx\n          ? {\n              x: p.recall,\n              y: p.precision,\n              marker: { enabled: true, radius: 5, fillColor: t.palette[0] },\n              dataLabels: {\n                enabled: true,\n                format: `Best F1 = ${bestF1.toFixed(2)}`,\n                align: \"left\",\n                x: 8,\n                y: -10,\n                style: { color: t.ink, fontSize: \"13px\", fontWeight: \"600\", textOutline: \"none\" },\n              },\n            }\n          : { x: p.recall, y: p.precision },\n      ),\n      step: \"left\",\n      color: t.palette[0],\n      lineWidth: 3,\n      fillOpacity: 0.15,\n      marker: { enabled: false },\n      dataLabels: { enabled: false },\n    },\n    {\n      type: \"line\",\n      name: `Baseline (prevalence = ${positiveRate.toFixed(2)})`,\n      data: [\n        [0, positiveRate],\n        [1, positiveRate],\n      ],\n      color: t.ink,\n      dashStyle: \"Dash\",\n      lineWidth: 2,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n    },\n    ...isoF1Series,\n  ],\n});\n"}