{"spec_id":"precision-recall","library":"muix","language":"javascript","code":"// anyplot.ai\n// precision-recall: Precision-Recall Curve\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-05\n//# anyplot-orientation: landscape\n// anyplot.ai\n// precision-recall: Precision-Recall Curve\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-09-05\n\nimport { LineChart } from \"@mui/x-charts/LineChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG) + Box-Muller normal sampling -----------------\nfunction makeRng(seed: number) {\n  let state = seed >>> 0;\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\n\nfunction randNormal(rng: () => number) {\n  const u1 = Math.max(rng(), 1e-9);\n  const u2 = rng();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst sigmoid = (x: number) => 1 / (1 + Math.exp(-x));\n\n// --- Data: fraud-detection evaluation set (rare positive class) ------------\nconst rng = makeRng(42);\nconst sampleCount = 600;\nconst positiveRate = 0.06;\n\nconst yTrue = Array.from({ length: sampleCount }, () => (rng() < positiveRate ? 1 : 0));\n\n// Two classifiers scored on the same transactions — a stronger gradient-boosted\n// model vs. a weaker logistic-regression baseline, each mapped through a\n// sigmoid to look like predict_proba() output.\nconst scoresGradientBoosting = yTrue.map((label) =>\n  sigmoid(label === 1 ? 2.1 + randNormal(rng) : -1.3 + randNormal(rng)),\n);\nconst scoresLogisticRegression = yTrue.map((label) =>\n  sigmoid(label === 1 ? 0.9 + randNormal(rng) * 1.3 : -0.3 + randNormal(rng) * 1.3),\n);\n\n// --- Precision-recall curve math --------------------------------------------\ntype CurvePoint = { recall: number; precision: number };\n\nfunction precisionRecallCurve(labels: number[], scores: number[]): CurvePoint[] {\n  const order = labels.map((_, i) => i).sort((a, b) => scores[b] - scores[a]);\n  const totalPositives = labels.reduce((sum, v) => sum + v, 0);\n\n  const points: CurvePoint[] = [{ recall: 0, precision: 1 }];\n  let truePositives = 0;\n  let falsePositives = 0;\n  let i = 0;\n  while (i < order.length) {\n    const score = scores[order[i]];\n    let j = i;\n    while (j < order.length && scores[order[j]] === score) {\n      if (labels[order[j]] === 1) truePositives += 1;\n      else falsePositives += 1;\n      j += 1;\n    }\n    points.push({\n      recall: truePositives / totalPositives,\n      precision: truePositives / (truePositives + falsePositives),\n    });\n    i = j;\n  }\n  return points;\n}\n\n// Average Precision: AP = sum_n (R_n - R_{n-1}) * P_n\nfunction averagePrecision(points: CurvePoint[]): number {\n  let ap = 0;\n  for (let i = 1; i < points.length; i += 1) {\n    ap += (points[i].recall - points[i - 1].recall) * points[i].precision;\n  }\n  return ap;\n}\n\n// Right-continuous step lookup: precision held constant until the next\n// (higher) recall breakpoint — matches the \"steps-post\" convention used to\n// draw PR curves.\nfunction precisionAtRecall(points: CurvePoint[], recall: number): number {\n  for (const point of points) {\n    if (point.recall >= recall - 1e-9) return point.precision;\n  }\n  return points[points.length - 1].precision;\n}\n\nconst curveGradientBoosting = precisionRecallCurve(yTrue, scoresGradientBoosting);\nconst curveLogisticRegression = precisionRecallCurve(yTrue, scoresLogisticRegression);\nconst apGradientBoosting = averagePrecision(curveGradientBoosting);\nconst apLogisticRegression = averagePrecision(curveLogisticRegression);\nconst baselinePrecision = yTrue.reduce((sum, v) => sum + v, 0) / sampleCount;\n\n// Resample both curves onto a shared recall grid so they can share one xAxis.\nconst gridSteps = 50;\nconst recallGrid = Array.from({ length: gridSteps + 1 }, (_, i) => Math.round((i / gridSteps) * 100) / 100);\nconst precisionGradientBoosting = recallGrid.map((r) => precisionAtRecall(curveGradientBoosting, r));\nconst precisionLogisticRegression = recallGrid.map((r) => precisionAtRecall(curveLogisticRegression, r));\n\n// Explicit tick positions (0.0, 0.1, …, 1.0) — the default continuous-scale\n// tick generator ignores our 51-point display grid and produces far denser,\n// overlap-prone ticks, so we pin them ourselves.\nconst axisTicks = Array.from({ length: 11 }, (_, i) => Math.round(i * 10) / 100);\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  return (\n    <div style={{ width: \"100%\", height: \"100%\", position: \"relative\" }}>\n      {/* Title rendered in the chart's top margin space */}\n      <div\n        style={{\n          position: \"absolute\",\n          top: 14,\n          left: 0,\n          right: 0,\n          textAlign: \"center\",\n          zIndex: 1,\n          fontSize: 21,\n          fontWeight: 500,\n          color: t.ink,\n          pointerEvents: \"none\",\n          fontFamily: \"'Roboto', 'Helvetica', 'Arial', sans-serif\",\n        }}\n      >\n        Fraud Detection · precision-recall · javascript · muix · anyplot.ai\n      </div>\n\n      <LineChart\n        width={window.ANYPLOT_SIZE.width}\n        height={window.ANYPLOT_SIZE.height}\n        skipAnimation\n        colors={[t.palette[0], t.palette[1]]}\n        xAxis={[\n          {\n            data: recallGrid,\n            scaleType: \"linear\",\n            min: 0,\n            max: 1,\n            label: \"Recall\",\n            valueFormatter: (v: number) => v.toFixed(1),\n            tickInterval: axisTicks,\n          },\n        ]}\n        yAxis={[\n          {\n            min: 0,\n            max: 1,\n            label: \"Precision\",\n            valueFormatter: (v: number) => v.toFixed(1),\n          },\n        ]}\n        series={[\n          {\n            id: \"gradient-boosting\",\n            data: precisionGradientBoosting,\n            label: `Gradient boosting (AP = ${apGradientBoosting.toFixed(2)})`,\n            curve: \"stepAfter\",\n            showMark: false,\n          },\n          {\n            id: \"logistic-regression\",\n            data: precisionLogisticRegression,\n            label: `Logistic regression (AP = ${apLogisticRegression.toFixed(2)})`,\n            curve: \"stepAfter\",\n            showMark: false,\n          },\n        ]}\n        grid={{ horizontal: true, vertical: false }}\n        sx={{\n          \"& .MuiChartsAxis-label\": {\n            fontSize: \"16px !important\",\n          },\n          \"& .MuiChartsAxis-tickLabel\": {\n            fontSize: \"14px !important\",\n          },\n          // Nudge the y-axis label further from its tick labels — the rotated\n          // \"Precision\" title otherwise sits close enough to touch the \"0.5\"\n          // tick label at this font size (x-axis label is untouched: only the\n          // directionY axis root carries this selector).\n          \"& .MuiChartsAxis-directionY .MuiChartsAxis-label\": {\n            transform: \"translateX(-14px)\",\n          },\n          \"& .MuiChartsLegend-label\": {\n            fontSize: \"15px !important\",\n          },\n          \"& .MuiLineElement-root\": {\n            strokeWidth: \"3px\",\n          },\n          \"& .MuiChartsGrid-horizontalLine\": {\n            stroke: t.grid,\n          },\n        }}\n        slotProps={{\n          legend: {\n            direction: \"row\",\n            position: { vertical: \"bottom\", horizontal: \"middle\" },\n          },\n        }}\n        margin={{ top: 70, right: 50, bottom: 120, left: 122 }}\n      >\n        <ChartsReferenceLine\n          y={baselinePrecision}\n          label=\"Random classifier (baseline)\"\n          labelAlign=\"start\"\n          lineStyle={{\n            stroke: t.inkSoft,\n            strokeDasharray: \"6 4\",\n            strokeWidth: 1.5,\n            strokeOpacity: 0.7,\n          }}\n          labelStyle={{\n            fill: t.inkSoft,\n            fontSize: 13,\n          }}\n        />\n      </LineChart>\n    </div>\n  );\n}\n"}