{"spec_id":"calibration-curve","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// calibration-curve: Calibration Curve\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// 2,000 simulated diagnostic screenings: a latent true disease risk drives the\n// binary outcome, and two classifiers predict probabilities from it — one\n// well-calibrated (logistic regression), one overconfident (random forest).\nfunction lcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (1664525 * state + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(20260225);\n\nfunction randNormal() {\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}\n\nconst patientCount = 2000;\nconst actualOutcome = [];\nconst probLogReg = [];\nconst probRandForest = [];\n\nfor (let i = 0; i < patientCount; i++) {\n  const trueRisk = rand();\n  actualOutcome.push(rand() < trueRisk ? 1 : 0);\n  const calibratedNoise = randNormal() * 0.05;\n  probLogReg.push(Math.min(1, Math.max(0, trueRisk + calibratedNoise)));\n  const sharpenedRisk = 0.5 + (trueRisk - 0.5) * 1.7;\n  const overconfidentNoise = randNormal() * 0.04;\n  probRandForest.push(Math.min(1, Math.max(0, sharpenedRisk + overconfidentNoise)));\n}\n\nfunction binCalibration(yTrue, yProb, binCount) {\n  const bins = Array.from({ length: binCount }, () => ({ sumProb: 0, sumOutcome: 0, count: 0 }));\n  for (let i = 0; i < yProb.length; i++) {\n    const idx = Math.min(binCount - 1, Math.floor(yProb[i] * binCount));\n    bins[idx].sumProb += yProb[i];\n    bins[idx].sumOutcome += yTrue[i];\n    bins[idx].count += 1;\n  }\n  return bins.filter((b) => b.count > 0).map((b) => [b.sumProb / b.count, b.sumOutcome / b.count]);\n}\n\nfunction brierScore(yTrue, yProb) {\n  let sum = 0;\n  for (let i = 0; i < yProb.length; i++) sum += (yProb[i] - yTrue[i]) ** 2;\n  return sum / yProb.length;\n}\n\nconst calibrationLogReg = binCalibration(actualOutcome, probLogReg, 10);\nconst calibrationRandForest = binCalibration(actualOutcome, probRandForest, 10);\nconst brierLogReg = brierScore(actualOutcome, probLogReg);\nconst brierRandForest = brierScore(actualOutcome, probRandForest);\n\n// Locate the Random Forest bin with the largest predicted-vs-observed gap so\n// the chart can call out exactly where the overconfidence is worst.\nlet maxGapIndex = 0;\nlet maxGap = 0;\ncalibrationRandForest.forEach(([predicted, observed], i) => {\n  const gap = Math.abs(predicted - observed);\n  if (gap > maxGap) {\n    maxGap = gap;\n    maxGapIndex = i;\n  }\n});\nconst [maxGapX, maxGapY] = calibrationRandForest[maxGapIndex];\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"line\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      // Highcharts-specific: draw a native SVG callout (renderer.label with the\n      // built-in \"callout\" symbol) anchored to a data point via axis-pixel\n      // conversion — not a portable Chart.js/ECharts pattern.\n      load() {\n        const chart = this;\n        const xAxis = chart.xAxis[0];\n        const yAxis = chart.yAxis[0];\n        const anchorX = xAxis.toPixels(maxGapX);\n        const anchorY = yAxis.toPixels(maxGapY);\n        const labelX = anchorX + (maxGapX < 0.5 ? 16 : -176);\n        const labelY = anchorY + (maxGapY > 0.5 ? -56 : 24);\n        chart.renderer\n          .label(`Largest gap: ${maxGap.toFixed(2)}`, labelX, labelY, \"callout\", anchorX, anchorY)\n          .attr({\n            fill: t.elevatedBg,\n            stroke: t.inkSoft,\n            \"stroke-width\": 1,\n            r: 4,\n            padding: 6,\n            zIndex: 8,\n          })\n          .css({ color: t.ink, fontSize: \"13px\" })\n          .add();\n      },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"calibration-curve · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: `Brier score — Logistic Regression: ${brierLogReg.toFixed(3)} · Random Forest: ${brierRandForest.toFixed(3)}`,\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    title: { text: \"Mean Predicted Probability\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    min: 0,\n    max: 1,\n    tickInterval: 0.1,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    title: { text: \"Observed Frequency (Fraction Positive)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    min: 0,\n    max: 1,\n    tickInterval: 0.2,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\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  tooltip: {\n    enabled: true,\n    backgroundColor: t.elevatedBg,\n    borderColor: t.inkSoft,\n    style: { color: t.ink, fontSize: \"13px\" },\n    valueDecimals: 3,\n  },\n  plotOptions: {\n    series: { animation: false },\n    line: { lineWidth: 3, marker: { enabled: true, radius: 6, lineWidth: 1.5, lineColor: t.pageBg } },\n  },\n  series: [\n    {\n      name: `Logistic Regression (Brier ${brierLogReg.toFixed(3)})`,\n      data: calibrationLogReg,\n    },\n    {\n      name: `Random Forest (Brier ${brierRandForest.toFixed(3)})`,\n      data: calibrationRandForest,\n    },\n    {\n      name: \"Perfect calibration\",\n      data: [\n        [0, 0],\n        [1, 1],\n      ],\n      color: t.ink,\n      dashStyle: \"Dash\",\n      lineWidth: 2,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n    },\n  ],\n});\n"}