{"spec_id":"calibration-curve","library":"muix","language":"javascript","code":"// anyplot.ai\n// calibration-curve: Calibration Curve\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-02\nimport { LineChart } from \"@mui/x-charts/LineChart\";\nimport { BarChart } from \"@mui/x-charts/BarChart\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n\n// mulberry32 PRNG — small, fast, fixed-seed (the browser has no seeded RNG)\nfunction mulberry32(seed: number) {\n  return function random() {\n    seed = (seed + 0x6d2b79f5) | 0;\n    let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n    x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;\n    return ((x ^ (x >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\n// Standard normal via Box-Muller, driven by the same PRNG stream\nfunction gaussian(random: () => number): number {\n  const u = Math.max(random(), 1e-9);\n  const v = random();\n  return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);\n}\n\nconst clamp01 = (v: number) => Math.min(1, Math.max(0, v));\nconst logit = (p: number) => Math.log(p / (1 - p));\nconst sigmoid = (z: number) => 1 / (1 + Math.exp(-z));\n\nconst N_SAMPLES = 3000;\nconst NUM_BINS = 10;\nconst random = mulberry32(42);\n\n// Latent true disease risk for each patient, and the observed diagnosis\n// drawn from a Bernoulli trial at that risk — this is the ground truth\n// both classifiers below are scored against.\nconst trueRisk = Array.from({ length: N_SAMPLES }, () => random());\nconst yTrue = trueRisk.map((risk) => (random() < risk ? 1 : 0));\n\n// Model A: well-calibrated screening classifier — small prediction noise.\nconst predA = trueRisk.map((risk) => clamp01(risk + 0.05 * gaussian(random)));\n\n// Model B: overconfident classifier — pushes predictions toward 0/1 by\n// sharpening the log-odds, so mid-risk patients get extreme scores.\nconst OVERCONFIDENCE_SCALE = 2.4;\nconst predB = trueRisk.map((risk) =>\n  sigmoid(logit(clamp01(risk) * 0.998 + 0.001) * OVERCONFIDENCE_SCALE),\n);\n\ntype Bin = { center: number; fracPos: number | null; count: number };\n\nfunction calibrationBins(yProb: number[]): Bin[] {\n  const sums = Array.from({ length: NUM_BINS }, () => ({ count: 0, sumTrue: 0 }));\n  yProb.forEach((p, i) => {\n    const idx = Math.min(NUM_BINS - 1, Math.floor(p * NUM_BINS));\n    sums[idx].count += 1;\n    sums[idx].sumTrue += yTrue[i];\n  });\n  return sums.map((b, i) => ({\n    center: (i + 0.5) / NUM_BINS,\n    fracPos: b.count > 0 ? b.sumTrue / b.count : null,\n    count: b.count,\n  }));\n}\n\nfunction brierScore(yProb: number[]): number {\n  const sse = yProb.reduce((acc, p, i) => acc + (p - yTrue[i]) ** 2, 0);\n  return sse / yProb.length;\n}\n\nfunction expectedCalibrationError(bins: Bin[], total: number): number {\n  return bins.reduce((acc, b) => {\n    if (b.fracPos === null) return acc;\n    return acc + (b.count / total) * Math.abs(b.fracPos - b.center);\n  }, 0);\n}\n\nconst binsA = calibrationBins(predA);\nconst binsB = calibrationBins(predB);\nconst eceA = expectedCalibrationError(binsA, N_SAMPLES);\nconst eceB = expectedCalibrationError(binsB, N_SAMPLES);\nconst brierA = brierScore(predA);\nconst brierB = brierScore(predB);\n\n// Shared x-axis: bin centers bracketed by 0 and 1 so the diagonal reaches\n// both corners; the bracket points are `null` on the model series (gaps),\n// so only the 10 real bins draw markers.\nconst xValues = [0, ...binsA.map((b) => b.center), 1];\nconst modelASeries = [null, ...binsA.map((b) => b.fracPos), null];\nconst modelBSeries = [null, ...binsB.map((b) => b.fracPos), null];\nconst diagonalSeries = xValues;\n\nconst binLabels = binsA.map((b) => {\n  const lo = Math.round((b.center - 0.05) * 100);\n  const hi = Math.round((b.center + 0.05) * 100);\n  return `${lo}–${hi}%`;\n});\n\nconst TITLE_HEIGHT = 78;\n\n// --- Chart (default-exported component — the harness mounts it) ------------\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const chartAreaHeight = height - TITLE_HEIGHT;\n  const mainHeight = Math.round(chartAreaHeight * 0.62);\n  const histHeight = chartAreaHeight - mainHeight;\n\n  return (\n    <Box\n      sx={{\n        width,\n        height,\n        display: \"flex\",\n        flexDirection: \"column\",\n        paddingTop: \"16px\",\n      }}\n    >\n      <Typography\n        sx={{\n          color: t.ink,\n          fontSize: 22,\n          fontWeight: 500,\n          textAlign: \"center\",\n          lineHeight: 1.2,\n        }}\n      >\n        calibration-curve · javascript · muix · anyplot.ai\n      </Typography>\n      <Typography\n        sx={{\n          color: t.inkSoft,\n          fontSize: 14,\n          textAlign: \"center\",\n          lineHeight: 1.4,\n          marginTop: \"4px\",\n        }}\n      >\n        {`Model A — ECE ${eceA.toFixed(3)}, Brier ${brierA.toFixed(3)}   ·   Model B — ECE ${eceB.toFixed(3)}, Brier ${brierB.toFixed(3)}`}\n      </Typography>\n\n      <LineChart\n        width={width}\n        height={mainHeight}\n        skipAnimation\n        colors={t.palette}\n        xAxis={[\n          {\n            data: xValues,\n            scaleType: \"linear\",\n            min: 0,\n            max: 1,\n            label: \"Mean Predicted Probability\",\n            valueFormatter: (v: number) => `${Math.round(v * 100)}%`,\n          },\n        ]}\n        yAxis={[\n          {\n            min: 0,\n            max: 1,\n            label: \"Fraction of Positives\",\n            valueFormatter: (v: number) => `${Math.round(v * 100)}%`,\n            // tickFontSize drives the label's reserved offset from the tick\n            // text (MUI X sizes that gap off this prop, not tickLabelStyle),\n            // so it must stay wide enough for a 4-char \"100%\" tick.\n            tickFontSize: 32,\n            labelFontSize: 16,\n            tickLabelStyle: { fontSize: 14 },\n          },\n        ]}\n        series={[\n          {\n            id: \"diagonal\",\n            data: diagonalSeries,\n            label: \"Perfect calibration\",\n            color: t.ink,\n            showMark: false,\n            curve: \"linear\",\n          },\n          {\n            id: \"modelA\",\n            data: modelASeries,\n            label: \"Model A (well-calibrated)\",\n            color: t.palette[0],\n            showMark: true,\n            curve: \"linear\",\n          },\n          {\n            id: \"modelB\",\n            data: modelBSeries,\n            label: \"Model B (overconfident)\",\n            color: t.palette[1],\n            showMark: true,\n            curve: \"linear\",\n          },\n        ]}\n        margin={{ left: 90, right: 40, top: 20, bottom: 70 }}\n        sx={{\n          \"& .MuiChartsAxis-tickLabel\": { fontSize: \"14px\" },\n          \"& .MuiChartsAxis-label\": { fontSize: \"16px\" },\n          \"& .MuiChartsLegend-label\": { fontSize: \"14px\" },\n          \"& .MuiLineElement-series-modelA\": { strokeWidth: 3 },\n          \"& .MuiLineElement-series-modelB\": { strokeWidth: 3 },\n          \"& .MuiLineElement-series-diagonal\": { strokeWidth: 2, strokeDasharray: \"8 5\" },\n          \"& .MuiChartsGrid-line\": { stroke: t.grid },\n        }}\n      >\n        <ChartsGrid horizontal />\n      </LineChart>\n\n      <BarChart\n        width={width}\n        height={histHeight}\n        skipAnimation\n        borderRadius={2}\n        xAxis={[\n          {\n            scaleType: \"band\",\n            data: binLabels,\n            label: \"Predicted Probability Bin\",\n          },\n        ]}\n        yAxis={[\n          {\n            label: \"Count\",\n            tickFontSize: 40,\n            labelFontSize: 14,\n            tickLabelStyle: { fontSize: 12 },\n          },\n        ]}\n        series={[\n          { data: binsA.map((b) => b.count), label: \"Model A\", color: t.palette[0] },\n          { data: binsB.map((b) => b.count), label: \"Model B\", color: t.palette[1] },\n        ]}\n        margin={{ left: 90, right: 40, top: 10, bottom: 60 }}\n        slotProps={{ legend: { hidden: true } }}\n        sx={{\n          \"& .MuiChartsAxis-tickLabel\": { fontSize: \"12px\" },\n          \"& .MuiChartsAxis-label\": { fontSize: \"14px\" },\n        }}\n      />\n    </Box>\n  );\n}\n"}