{"spec_id":"precision-recall","library":"echarts","language":"javascript","code":"// anyplot.ai\n// precision-recall: Precision-Recall Curve\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 86/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ---------------------------------------\n// Fraud-detection scenario: rare positive class (fraud) among transactions.\n// A tiny fixed-seed LCG stands in for a classifier's predict_proba() scores.\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\nconst nSamples = 2000;\nconst fraudRate = 0.08;\nconst yTrue = [];\nconst yScores = [];\nfor (let i = 0; i < nSamples; i++) {\n  const isFraud = rand() < fraudRate;\n  yTrue.push(isFraud ? 1 : 0);\n  // Fraud scores skew high, legitimate scores skew low, both noisy.\n  const base = isFraud ? 0.72 : 0.28;\n  const noise = (rand() - 0.5) * 0.7;\n  const score = Math.min(1, Math.max(0, base + noise));\n  yScores.push(score);\n}\n\nconst positiveCount = yTrue.reduce((sum, v) => sum + v, 0);\nconst baselinePrecision = positiveCount / nSamples;\n\n// Sort by descending score, then sweep thresholds accumulating precision/recall.\nconst order = yScores\n  .map((score, idx) => idx)\n  .sort((a, b) => yScores[b] - yScores[a]);\n\nconst points = [{ recall: 0, precision: 1 }];\nlet truePositives = 0;\nlet falsePositives = 0;\nfor (const idx of order) {\n  if (yTrue[idx] === 1) {\n    truePositives += 1;\n  } else {\n    falsePositives += 1;\n  }\n  const precision = truePositives / (truePositives + falsePositives);\n  const recall = truePositives / positiveCount;\n  points.push({ recall, precision });\n}\n\n// Average Precision: sum of precision * change in recall (step function).\nlet averagePrecision = 0;\nfor (let i = 1; i < points.length; i++) {\n  const deltaRecall = points[i].recall - points[i - 1].recall;\n  averagePrecision += points[i].precision * deltaRecall;\n}\n\nconst curveData = points.map((p) => [p.recall, p.precision]);\n\n// --- Init -------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option -----------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"precision-recall · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 20,\n    textStyle: { color: t.ink, fontSize: 22 },\n  },\n  grid: { left: 90, right: 60, top: 100, bottom: 80 },\n  legend: {\n    data: [`Fraud classifier (AP = ${averagePrecision.toFixed(2)})`],\n    top: 60,\n    textStyle: { color: t.ink, fontSize: 14 },\n  },\n  xAxis: {\n    type: \"value\",\n    name: \"Recall\",\n    nameLocation: \"middle\",\n    nameGap: 36,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: 0,\n    max: 1,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Precision\",\n    nameLocation: \"middle\",\n    nameGap: 60,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: 0,\n    max: 1,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      name: `Fraud classifier (AP = ${averagePrecision.toFixed(2)})`,\n      type: \"line\",\n      step: \"start\",\n      data: curveData,\n      showSymbol: false,\n      itemStyle: { color: t.palette[0] },\n      lineStyle: { width: 3, color: t.palette[0] },\n      areaStyle: { color: t.palette[0], opacity: 0.12 },\n      // Shade the region up to the AP level so the summary metric reads\n      // directly off the chart, not just from the legend.\n      markArea: {\n        silent: true,\n        itemStyle: { color: t.ink, opacity: 0.05 },\n        label: { position: \"insideTopLeft\", color: t.inkSoft, fontSize: 13 },\n        data: [\n          [\n            { xAxis: 0, yAxis: 0, label: { formatter: `AP = ${averagePrecision.toFixed(2)}` } },\n            { xAxis: 1, yAxis: averagePrecision },\n          ],\n        ],\n      },\n      // Imprint \"neutral\" semantic anchor (random-classifier reference) —\n      // theme-adaptive ink, idiomatic markLine instead of a second series.\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        lineStyle: { color: t.ink, type: \"dashed\", width: 2 },\n        label: { formatter: \"Random baseline\", color: t.inkSoft, fontSize: 12, position: \"insideEndTop\" },\n        data: [{ yAxis: baselinePrecision }],\n      },\n    },\n  ],\n});\n"}