{"spec_id":"line-win-probability","library":"muix","language":"javascript","code":"// anyplot.ai\n// line-win-probability: Win Probability Chart\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 92/100 | Created: 2026-06-21\n//# anyplot-orientation: landscape\n// anyplot.ai\n// line-win-probability: Win Probability Chart\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-06-21\n\nimport { LineChart } from \"@mui/x-charts/LineChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Deterministic LCG — no Math.random(), fully reproducible\nfunction lcg(seed: number): () => number {\n  let s = seed | 0;\n  return () => {\n    s = (Math.imul(1664525, s) + 1013904223) | 0;\n    return (s >>> 0) / 0x100000000;\n  };\n}\n\nfunction clamp(v: number, lo: number, hi: number): number {\n  return Math.max(lo, Math.min(hi, v));\n}\n\nconst rand = lcg(42);\nconst TOTAL_MINUTES = 90;\n\nconst AWAY_GOAL = 19;\nconst HALF_TIME = 45;\nconst HOME_GOAL_EQ = 58;\nconst HOME_GOAL_WIN = 84;\n\n// Forest Green FC (home) vs City Blue FC (away) — away scores early, home fights back\nconst minutes: number[] = Array.from({ length: TOTAL_MINUTES + 1 }, (_, i) => i);\n\nlet wp = 0.50;\nconst winProb: number[] = [0.50];\n\nfor (let i = 1; i <= TOTAL_MINUTES; i++) {\n  if (i === AWAY_GOAL) {\n    wp = 0.26;\n  } else if (i === HOME_GOAL_EQ) {\n    wp = 0.54;\n  } else if (i === HOME_GOAL_WIN) {\n    wp = 0.88;\n  } else {\n    const noise = (rand() - 0.5) * 0.055;\n    const revert = (0.5 - wp) * 0.018;\n    wp = clamp(wp + noise + revert, 0.05, 0.95);\n    if (i > AWAY_GOAL && i < HOME_GOAL_EQ) {\n      wp = clamp(wp, 0.11, 0.50);\n    } else if (i > HOME_GOAL_EQ && i < HOME_GOAL_WIN) {\n      wp = clamp(wp, 0.42, 0.67);\n    } else if (i > HOME_GOAL_WIN) {\n      wp = clamp(wp, 0.81, 0.97);\n    }\n  }\n  winProb.push(Math.round(wp * 1000) / 1000);\n}\n\n// Spec fill semantics: fill area above 50% with home color, below 50% with away color.\n// baseline=0.5 makes each series fill only the band between the probability line and\n// the 50% baseline — home green fills upward from 50% when home leads, away blue\n// fills downward from 50% when away leads.\nconst homeArea: number[] = winProb.map(v => Math.max(v, 0.5));\nconst awayArea: number[] = winProb.map(v => Math.min(v, 0.5));\n\nconst HOME_COLOR = t.palette[0]; // #009E73 brand green\nconst AWAY_COLOR = t.palette[2]; // #4467A3 Imprint blue — semantic match for \"City Blue FC\"\n\nconst TITLE = \"line-win-probability · javascript · muix · anyplot.ai\";\nconst SUBTITLE = \"Forest Green FC 2 – 1 City Blue FC · 90 minutes\";\nconst TITLE_HEIGHT = 70;\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const chartHeight = height - TITLE_HEIGHT;\n\n  return (\n    <Box\n      sx={{\n        width,\n        height,\n        display: \"flex\",\n        flexDirection: \"column\",\n        bgcolor: t.pageBg,\n      }}\n    >\n      <Box sx={{ textAlign: \"center\", pt: 1.5 }}>\n        <Typography\n          sx={{ fontSize: 22, fontWeight: 600, color: t.ink, lineHeight: 1.3 }}\n        >\n          {TITLE}\n        </Typography>\n        <Typography sx={{ fontSize: 13, color: t.inkSoft, mt: 0.25 }}>\n          {SUBTITLE}\n        </Typography>\n      </Box>\n\n      <LineChart\n        width={width}\n        height={chartHeight}\n        skipAnimation\n        xAxis={[\n          {\n            data: minutes,\n            label: \"Match Time (minutes)\",\n            min: 0,\n            max: 90,\n            tickNumber: 7,\n            valueFormatter: (v: number) => `${v}'`,\n            labelStyle: { fontSize: 14, fill: t.inkSoft },\n            tickLabelStyle: { fontSize: 12, fill: t.inkSoft },\n            disableTicks: true,\n          },\n        ]}\n        yAxis={[\n          {\n            min: 0,\n            max: 1,\n            tickNumber: 5,\n            valueFormatter: (v: number) => `${Math.round(v * 100)}%`,\n            label: \"Home Win Probability\",\n            labelStyle: { fontSize: 14, fill: t.inkSoft },\n            tickLabelStyle: { fontSize: 12, fill: t.inkSoft },\n            disableTicks: true,\n          },\n        ]}\n        series={[\n          {\n            id: \"home\",\n            data: homeArea,\n            label: \"Home Win — Forest Green FC\",\n            area: true,\n            baseline: 0.5,\n            showMark: false,\n            color: HOME_COLOR,\n          },\n          {\n            id: \"away\",\n            data: awayArea,\n            label: \"Away Win — City Blue FC\",\n            area: true,\n            baseline: 0.5,\n            showMark: false,\n            color: AWAY_COLOR,\n          },\n        ]}\n        margin={{ top: 16, bottom: 64, left: 96, right: 48 }}\n        sx={{\n          bgcolor: \"transparent\",\n          \"& .MuiAreaElement-series-home\": { fillOpacity: 0.82 },\n          \"& .MuiAreaElement-series-away\": { fillOpacity: 0.82 },\n          // Home line traces max(wp, 0.5): probability cursor when home leads\n          \"& .MuiLineElement-series-home\": { strokeWidth: 2.5 },\n          // Away line traces min(wp, 0.5): probability cursor when away leads\n          \"& .MuiLineElement-series-away\": { strokeWidth: 2.5 },\n        }}\n        slotProps={{\n          legend: {\n            position: { vertical: \"top\", horizontal: \"right\" },\n            padding: { top: 8, right: 28 },\n            itemMarkWidth: 14,\n            itemMarkHeight: 14,\n            labelStyle: { fontSize: 12, fill: t.inkSoft },\n          },\n        }}\n      >\n        {/* 50% neutral baseline */}\n        <ChartsReferenceLine\n          y={0.5}\n          label=\"50% — even odds\"\n          labelAlign=\"end\"\n          lineStyle={{\n            stroke: t.ink,\n            strokeWidth: 1.5,\n            strokeDasharray: \"8 4\",\n            opacity: 0.45,\n          }}\n          labelStyle={{ fontSize: 12, fill: t.inkSoft }}\n        />\n\n        {/* Away goal — probability falls */}\n        <ChartsReferenceLine\n          x={AWAY_GOAL}\n          label={`Goal (Away) ${AWAY_GOAL}'`}\n          labelAlign=\"start\"\n          lineStyle={{\n            stroke: AWAY_COLOR,\n            strokeWidth: 1.5,\n            strokeDasharray: \"5 3\",\n            opacity: 0.75,\n          }}\n          labelStyle={{ fontSize: 12, fill: t.inkSoft }}\n        />\n\n        {/* Half time */}\n        <ChartsReferenceLine\n          x={HALF_TIME}\n          label=\"Half Time\"\n          labelAlign=\"start\"\n          lineStyle={{\n            stroke: t.ink,\n            strokeWidth: 1,\n            strokeDasharray: \"4 3\",\n            opacity: 0.3,\n          }}\n          labelStyle={{ fontSize: 12, fill: t.inkSoft }}\n        />\n\n        {/* Home equaliser */}\n        <ChartsReferenceLine\n          x={HOME_GOAL_EQ}\n          label={`Goal (Home) ${HOME_GOAL_EQ}'`}\n          labelAlign=\"start\"\n          lineStyle={{\n            stroke: HOME_COLOR,\n            strokeWidth: 1.5,\n            strokeDasharray: \"5 3\",\n            opacity: 0.75,\n          }}\n          labelStyle={{ fontSize: 12, fill: t.inkSoft }}\n        />\n\n        {/* Home winner */}\n        <ChartsReferenceLine\n          x={HOME_GOAL_WIN}\n          label={`Goal (Home) ${HOME_GOAL_WIN}'`}\n          labelAlign=\"end\"\n          lineStyle={{\n            stroke: HOME_COLOR,\n            strokeWidth: 1.5,\n            strokeDasharray: \"5 3\",\n            opacity: 0.75,\n          }}\n          labelStyle={{ fontSize: 12, fill: t.inkSoft }}\n        />\n      </LineChart>\n    </Box>\n  );\n}\n"}