{"spec_id":"line-win-probability","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// line-win-probability: Win Probability Chart\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 89/100 | Created: 2026-06-21\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Deterministic LCG for reproducible pseudo-random data\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1664525 + 1013904223) >>> 0;\n  return seed / 4294967295;\n}\n\n// NBA game: Lakers (home) vs Celtics (away), 48 minutes\n// Narrative: home team trails in Q2, stages comeback, seals win in Q4\nconst TOTAL_MINUTES = 48;\nconst N = 97; // ~one point per 30 seconds\n\nfunction trendAt(m) {\n  if (m < 4)  return 0.500 + m * 0.010;\n  if (m < 12) return 0.540 + (m - 4) * 0.004;\n  if (m < 18) return 0.572 - (m - 12) * 0.042;\n  if (m < 24) return 0.320 - (m - 18) * 0.005;\n  if (m < 30) return 0.290 + (m - 24) * 0.038;\n  if (m < 36) return 0.518 + (m - 30) * 0.008;\n  if (m < 42) return 0.566 + (m - 36) * 0.038;\n  return 0.794 + (m - 42) * 0.017;\n}\n\nconst points = Array.from({ length: N }, (_, i) => {\n  const m = (i / (N - 1)) * TOTAL_MINUTES;\n  const trend = trendAt(m);\n  const noise = (lcg() - 0.5) * 0.055;\n  const prob = Math.min(0.98, Math.max(0.02, trend + noise));\n  return { x: +m.toFixed(2), y: +(prob * 100).toFixed(1) };\n});\n\nconst refLine = points.map(p => ({ x: p.x, y: 50 }));\n\n// Imprint palette: home team = brand green, away team = matte red (semantic: win/loss)\nconst homeColor = t.palette[0]; // #009E73\nconst awayColor = t.palette[4]; // #AE3030\nconst HOME_FILL = \"rgba(0, 158, 115, 0.22)\";\nconst AWAY_FILL = \"rgba(174, 48, 48, 0.22)\";\n\n// Key scoring events annotated on the probability line\nconst EVENTS = [\n  { minute: 13, label: \"Celtics Run\" },   // Q2 start of big decline\n  { minute: 21, label: \"Celtics +12\" },   // deepest deficit, ~30%\n  { minute: 27, label: \"LeBron 3-PTR\" },  // comeback begins, ~40%\n  { minute: 39, label: \"AD Putback\" },    // decisive surge, ~68%\n];\n\n// Quarter boundary + event annotation plugin\nconst quarterPlugin = {\n  id: \"quarters\",\n  afterDraw(chart) {\n    const { ctx, chartArea: ca, scales } = chart;\n    ctx.save();\n\n    // Quarter dividers\n    [12, 24, 36].forEach((q, i) => {\n      const x = scales.x.getPixelForValue(q);\n      ctx.beginPath();\n      ctx.setLineDash([7, 5]);\n      ctx.strokeStyle = t.inkSoft + \"70\";\n      ctx.lineWidth = 1.5;\n      ctx.moveTo(x, ca.top);\n      ctx.lineTo(x, ca.bottom);\n      ctx.stroke();\n      ctx.setLineDash([]);\n      ctx.fillStyle = t.inkSoft;\n      ctx.font = \"bold 14px sans-serif\";\n      ctx.textAlign = \"left\";\n      ctx.fillText(`Q${i + 2}`, x + 5, ca.top + 20);\n    });\n\n    // Q1 label\n    ctx.setLineDash([]);\n    ctx.fillStyle = t.inkSoft;\n    ctx.font = \"bold 14px sans-serif\";\n    ctx.textAlign = \"left\";\n    ctx.fillText(\"Q1\", scales.x.getPixelForValue(0) + 5, ca.top + 20);\n\n    // Scoring event annotations: dot + dashed connector + label\n    EVENTS.forEach(ev => {\n      const closest = points.reduce((best, p) =>\n        Math.abs(p.x - ev.minute) < Math.abs(best.x - ev.minute) ? p : best\n      );\n      const ex = scales.x.getPixelForValue(closest.x);\n      const ey = scales.y.getPixelForValue(closest.y);\n      const isAbove = closest.y >= 50;\n      const color = isAbove ? homeColor : awayColor;\n      const dir = isAbove ? -1 : 1;\n      const connLen = 24;\n\n      // Dot on data line\n      ctx.beginPath();\n      ctx.arc(ex, ey, 4, 0, Math.PI * 2);\n      ctx.fillStyle = color;\n      ctx.fill();\n\n      // Dashed connector\n      ctx.beginPath();\n      ctx.strokeStyle = color;\n      ctx.lineWidth = 1;\n      ctx.setLineDash([3, 3]);\n      ctx.moveTo(ex, ey);\n      ctx.lineTo(ex, ey + dir * connLen);\n      ctx.stroke();\n      ctx.setLineDash([]);\n\n      // Event label\n      ctx.fillStyle = color;\n      ctx.font = \"11px sans-serif\";\n      ctx.textAlign = \"center\";\n      const labelY = isAbove ? ey - connLen - 6 : ey + connLen + 12;\n      ctx.fillText(ev.label, ex, labelY);\n    });\n\n    // HOME / AWAY directional labels\n    ctx.font = \"bold 14px sans-serif\";\n    ctx.textAlign = \"right\";\n    ctx.fillStyle = homeColor;\n    ctx.fillText(\"HOME WINS ↑\", ca.right - 10, ca.top + 22);\n    ctx.fillStyle = awayColor;\n    ctx.fillText(\"AWAY WINS ↓\", ca.right - 10, ca.bottom - 14);\n\n    // Final score\n    ctx.fillStyle = t.ink;\n    ctx.font = \"bold 15px sans-serif\";\n    ctx.textAlign = \"right\";\n    ctx.fillText(\"Final: Lakers 112 – Celtics 101\", ca.right - 10, ca.top + 44);\n\n    ctx.restore();\n  },\n};\n\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\nnew Chart(canvas, {\n  type: \"line\",\n  data: {\n    datasets: [\n      {\n        label: \"Home Win Probability\",\n        data: points,\n        borderColor: homeColor,\n        borderWidth: 2.5,\n        pointRadius: 0,\n        fill: {\n          target: { value: 50 },\n          above: HOME_FILL,\n          below: AWAY_FILL,\n        },\n        tension: 0.3,\n      },\n      {\n        label: \"_ref50\",\n        data: refLine,\n        borderColor: t.inkSoft + \"90\",\n        borderWidth: 1.5,\n        borderDash: [8, 5],\n        pointRadius: 0,\n        fill: false,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 30, right: 32, bottom: 20, left: 10 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"line-win-probability · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"600\" },\n        padding: { bottom: 20 },\n      },\n      legend: {\n        labels: {\n          color: t.ink,\n          font: { size: 14 },\n          filter: item => !item.text.startsWith(\"_\"),\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0,\n        max: 48,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n          stepSize: 6,\n          callback: v => `${v}'`,\n        },\n        grid: { color: t.grid },\n        title: {\n          display: true,\n          text: \"Game Time (minutes)\",\n          color: t.ink,\n          font: { size: 15 },\n        },\n      },\n      y: {\n        min: 0,\n        max: 100,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n          stepSize: 25,\n          callback: v => `${v}%`,\n        },\n        grid: { color: t.grid },\n        title: {\n          display: true,\n          text: \"Home Team Win Probability\",\n          color: t.ink,\n          font: { size: 15 },\n        },\n      },\n    },\n  },\n  plugins: [quarterPlugin],\n});\n"}