{"spec_id":"scatter-pitch-events","library":"muix","language":"javascript","code":"// anyplot.ai\n// scatter-pitch-events: Soccer Pitch Event Map\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 87/100 | Created: 2026-06-21\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { useDrawingArea, useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Imprint palette for event types ---\nconst PASS_COLOR = t.palette[0];      // #009E73 brand green\nconst SHOT_COLOR = t.palette[1];      // #C475FD lavender\nconst TACKLE_COLOR = t.palette[2];    // #4467A3 blue\nconst INTERCEPT_COLOR = t.palette[3]; // #BD8233 ochre\n\nfunction hexToRgba(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r},${g},${b},${alpha})`;\n}\n\n// --- Deterministic seeded RNG (LCG) ---\nfunction makeLcg(seed) {\n  let s = seed >>> 0;\n  return () => {\n    s = (Math.imul(1664525, s) + 1013904223) >>> 0;\n    return s / 4294967296;\n  };\n}\n\nconst rng = makeLcg(42);\n\n// --- Synthetic match event data ---\n// Passes: start + end coords for directional arrows\nconst passes = Array.from({ length: 48 }, (_, i) => {\n  const x = rng() * 100 + 2.5;\n  const y = rng() * 63 + 2.5;\n  const dx = (rng() - 0.2) * 22;\n  const dy = (rng() - 0.5) * 16;\n  const x2 = Math.max(2, Math.min(103, x + dx));\n  const y2 = Math.max(2, Math.min(66, y + dy));\n  const successful = rng() > 0.22;\n  return { id: `pass-${i}`, x, y, x2, y2, successful };\n});\n\n// Shots: from attacking half toward goal at x=105\nconst shots = Array.from({ length: 10 }, (_, i) => {\n  const x = 70 + rng() * 30;\n  const y = 17 + rng() * 34;\n  const x2 = 102 + rng() * 3;\n  const y2 = 30.34 + rng() * 7.32;\n  const successful = rng() > 0.75;\n  return { id: `shot-${i}`, x, y, x2, y2, successful };\n});\n\n// Tackles: spread across the pitch\nconst tackles = Array.from({ length: 22 }, (_, i) => {\n  const x = rng() * 105;\n  const y = rng() * 68;\n  const successful = rng() > 0.35;\n  return { id: `tackle-${i}`, x, y, successful };\n});\n\n// Interceptions: midfield-heavy\nconst interceptions = Array.from({ length: 18 }, (_, i) => {\n  const x = rng() * 105;\n  const y = rng() * 68;\n  const successful = rng() > 0.12;\n  return { id: `intercept-${i}`, x, y, successful };\n});\n\n// --- MUI X scatter series (for legend and domain — markers drawn in EventMarkersLayer) ---\nconst scatterSeries = [\n  {\n    type: \"scatter\", id: \"pass-series\", label: \"Pass\",\n    data: passes.map(p => ({ x: p.x, y: p.y, id: p.id })),\n    color: PASS_COLOR,\n  },\n  {\n    type: \"scatter\", id: \"shot-series\", label: \"Shot\",\n    data: shots.map(s => ({ x: s.x, y: s.y, id: s.id })),\n    color: SHOT_COLOR,\n  },\n  {\n    type: \"scatter\", id: \"tackle-series\", label: \"Tackle\",\n    data: tackles.map(tk => ({ x: tk.x, y: tk.y, id: tk.id })),\n    color: TACKLE_COLOR,\n  },\n  {\n    type: \"scatter\", id: \"intercept-series\", label: \"Interception\",\n    data: interceptions.map(ic => ({ x: ic.x, y: ic.y, id: ic.id })),\n    color: INTERCEPT_COLOR,\n  },\n];\n\n// --- Pitch constants ---\nconst PITCH_GREEN = \"#2a6e1e\";\nconst LINE_W = \"rgba(255,255,255,0.88)\";\nconst LW = 1.8;\nconst DEG = Math.PI / 180;\n\n// Parametric arc → polyline points string (pitch coords → SVG via scale fns)\nfunction pitchArc(cx, cy, r, aFrom, aTo, X, Y) {\n  const steps = Math.ceil(Math.abs(aTo - aFrom));\n  const pts = [];\n  for (let i = 0; i <= steps; i++) {\n    const a = (aFrom + ((aTo - aFrom) * i) / steps) * DEG;\n    pts.push(`${X(cx + r * Math.cos(a))},${Y(cy + r * Math.sin(a))}`);\n  }\n  return pts.join(\" \");\n}\n\n// SVG shape helpers (cx/cy/r in SVG pixel space)\nfunction starPoints(cx, cy, r) {\n  const inner = r * 0.4;\n  const pts = [];\n  for (let i = 0; i < 10; i++) {\n    const angle = (i * Math.PI) / 5 - Math.PI / 2;\n    const rad = i % 2 === 0 ? r : inner;\n    pts.push(`${cx + rad * Math.cos(angle)},${cy + rad * Math.sin(angle)}`);\n  }\n  return pts.join(\" \");\n}\n\nfunction trianglePoints(cx, cy, r) {\n  const top = `${cx},${cy - r}`;\n  const bl = `${cx - r * 0.866},${cy + r * 0.5}`;\n  const br = `${cx + r * 0.866},${cy + r * 0.5}`;\n  return `${top} ${bl} ${br}`;\n}\n\nfunction diamondPoints(cx, cy, r) {\n  return `${cx},${cy - r} ${cx + r * 0.7},${cy} ${cx},${cy + r} ${cx - r * 0.7},${cy}`;\n}\n\n// --- Custom SVG layer: pitch markings ---\nfunction PitchBackground() {\n  const { left, top, width, height } = useDrawingArea();\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const X = (m) => xScale(m);\n  const Y = (m) => yScale(m);\n\n  // FIFA standard pitch dimensions (meters)\n  const circleR = 9.15;\n  const penDepth = 16.5;\n  const penHW = 20.16; // half-width of penalty area (40.32/2)\n  const goalDepth = 5.5;\n  const goalHW = 9.16; // half-width of goal area (18.32/2)\n  const goalHalfWidth = 3.66; // half-width of goal mouth (7.32/2)\n  const goalDepthPx = X(2) - X(0); // 2m goal depth shown as visual post\n\n  // Penalty arcs: only the portion outside the penalty area\n  const leftPenArc = (() => {\n    const pts = [];\n    for (let a = -60; a <= 60; a++) {\n      const px = 11 + circleR * Math.cos(a * DEG);\n      const py = 34 + circleR * Math.sin(a * DEG);\n      if (px > penDepth) pts.push(`${X(px)},${Y(py)}`);\n    }\n    return pts.join(\" \");\n  })();\n\n  const rightPenArc = (() => {\n    const pts = [];\n    for (let a = 120; a <= 240; a++) {\n      const px = 94 + circleR * Math.cos(a * DEG);\n      const py = 34 + circleR * Math.sin(a * DEG);\n      if (px < 105 - penDepth) pts.push(`${X(px)},${Y(py)}`);\n    }\n    return pts.join(\" \");\n  })();\n\n  // Helper: rectangle from pitch coords\n  const R = (x1, y1, x2, y2) => ({\n    x: X(Math.min(x1, x2)),\n    y: Y(Math.max(y1, y2)),\n    w: Math.abs(X(x2) - X(x1)),\n    h: Math.abs(Y(y2) - Y(y1)),\n  });\n\n  const lp = { fill: \"none\", stroke: LINE_W, strokeWidth: LW };\n\n  return (\n    <g>\n      {/* Pitch grass */}\n      <rect x={left} y={top} width={width} height={height} fill={PITCH_GREEN} />\n\n      {/* Pitch outline */}\n      {(() => { const r = R(0, 0, 105, 68); return <rect {...lp} x={r.x} y={r.y} width={r.w} height={r.h} />; })()}\n\n      {/* Halfway line */}\n      <line {...lp} x1={X(52.5)} y1={Y(0)} x2={X(52.5)} y2={Y(68)} />\n\n      {/* Center circle */}\n      <polyline {...lp} points={pitchArc(52.5, 34, circleR, 0, 360, X, Y)} />\n\n      {/* Center spot */}\n      <circle cx={X(52.5)} cy={Y(34)} r={4} fill={LINE_W} />\n\n      {/* Left penalty area */}\n      {(() => { const r = R(0, 34 - penHW, penDepth, 34 + penHW); return <rect {...lp} x={r.x} y={r.y} width={r.w} height={r.h} />; })()}\n\n      {/* Right penalty area */}\n      {(() => { const r = R(105 - penDepth, 34 - penHW, 105, 34 + penHW); return <rect {...lp} x={r.x} y={r.y} width={r.w} height={r.h} />; })()}\n\n      {/* Left goal area */}\n      {(() => { const r = R(0, 34 - goalHW, goalDepth, 34 + goalHW); return <rect {...lp} x={r.x} y={r.y} width={r.w} height={r.h} />; })()}\n\n      {/* Right goal area */}\n      {(() => { const r = R(105 - goalDepth, 34 - goalHW, 105, 34 + goalHW); return <rect {...lp} x={r.x} y={r.y} width={r.w} height={r.h} />; })()}\n\n      {/* Left goal (extends outside pitch) */}\n      <rect\n        x={X(0) - goalDepthPx} y={Y(34 + goalHalfWidth)}\n        width={goalDepthPx} height={Math.abs(Y(34 - goalHalfWidth) - Y(34 + goalHalfWidth))}\n        fill=\"none\" stroke={LINE_W} strokeWidth={LW}\n      />\n\n      {/* Right goal (extends outside pitch) */}\n      <rect\n        x={X(105)} y={Y(34 + goalHalfWidth)}\n        width={goalDepthPx} height={Math.abs(Y(34 - goalHalfWidth) - Y(34 + goalHalfWidth))}\n        fill=\"none\" stroke={LINE_W} strokeWidth={LW}\n      />\n\n      {/* Penalty spots */}\n      <circle cx={X(11)} cy={Y(34)} r={3} fill={LINE_W} />\n      <circle cx={X(94)} cy={Y(34)} r={3} fill={LINE_W} />\n\n      {/* Penalty arcs (portions visible outside penalty areas) */}\n      {leftPenArc && <polyline {...lp} points={leftPenArc} />}\n      {rightPenArc && <polyline {...lp} points={rightPenArc} />}\n\n      {/* Corner arcs (radius 1m from each corner) */}\n      <polyline {...lp} points={pitchArc(0, 0, 1, 0, 90, X, Y)} />\n      <polyline {...lp} points={pitchArc(105, 0, 1, 90, 180, X, Y)} />\n      <polyline {...lp} points={pitchArc(0, 68, 1, -90, 0, X, Y)} />\n      <polyline {...lp} points={pitchArc(105, 68, 1, 180, 270, X, Y)} />\n    </g>\n  );\n}\n\n// --- Custom SVG layer: directional arrows for passes and shots ---\nfunction ArrowsLayer() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const X = (m) => xScale(m);\n  const Y = (m) => yScale(m);\n\n  const passOk = hexToRgba(PASS_COLOR, 0.75);\n  const passFail = hexToRgba(PASS_COLOR, 0.3);\n  const shotOk = hexToRgba(SHOT_COLOR, 0.85);\n  const shotFail = hexToRgba(SHOT_COLOR, 0.38);\n\n  return (\n    <g>\n      <defs>\n        <marker id=\"arr-pass-ok\" viewBox=\"0 0 8 8\" refX=\"7\" refY=\"4\"\n                markerWidth=\"3\" markerHeight=\"3\" orient=\"auto\">\n          <path d=\"M 0 0 L 8 4 L 0 8 z\" fill={passOk} />\n        </marker>\n        <marker id=\"arr-pass-fail\" viewBox=\"0 0 8 8\" refX=\"7\" refY=\"4\"\n                markerWidth=\"3\" markerHeight=\"3\" orient=\"auto\">\n          <path d=\"M 0 0 L 8 4 L 0 8 z\" fill={passFail} />\n        </marker>\n        <marker id=\"arr-shot-ok\" viewBox=\"0 0 8 8\" refX=\"7\" refY=\"4\"\n                markerWidth=\"3.5\" markerHeight=\"3.5\" orient=\"auto\">\n          <path d=\"M 0 0 L 8 4 L 0 8 z\" fill={shotOk} />\n        </marker>\n        <marker id=\"arr-shot-fail\" viewBox=\"0 0 8 8\" refX=\"7\" refY=\"4\"\n                markerWidth=\"3.5\" markerHeight=\"3.5\" orient=\"auto\">\n          <path d=\"M 0 0 L 8 4 L 0 8 z\" fill={shotFail} />\n        </marker>\n      </defs>\n\n      {/* Pass trajectories */}\n      {passes.map(p => (\n        <line\n          key={p.id}\n          x1={X(p.x)} y1={Y(p.y)} x2={X(p.x2)} y2={Y(p.y2)}\n          stroke={p.successful ? passOk : passFail}\n          strokeWidth={p.successful ? 1.6 : 1.1}\n          strokeDasharray={p.successful ? undefined : \"4,3\"}\n          markerEnd={p.successful ? \"url(#arr-pass-ok)\" : \"url(#arr-pass-fail)\"}\n        />\n      ))}\n\n      {/* Shot trajectories */}\n      {shots.map(s => (\n        <line\n          key={s.id}\n          x1={X(s.x)} y1={Y(s.y)} x2={X(s.x2)} y2={Y(s.y2)}\n          stroke={s.successful ? shotOk : shotFail}\n          strokeWidth={s.successful ? 2.2 : 1.6}\n          strokeDasharray={s.successful ? undefined : \"5,3\"}\n          markerEnd={s.successful ? \"url(#arr-shot-ok)\" : \"url(#arr-shot-fail)\"}\n        />\n      ))}\n    </g>\n  );\n}\n\n// --- Custom SVG layer: distinct marker shapes per event type ---\n// Passes=circles, Shots=stars, Tackles=triangles, Interceptions=diamonds\nfunction EventMarkersLayer() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const X = (m) => xScale(m);\n  const Y = (m) => yScale(m);\n\n  return (\n    <g>\n      {/* Passes: circles */}\n      {passes.map(p => {\n        const cx = X(p.x), cy = Y(p.y);\n        const r = p.successful ? 7 : 5;\n        const fill = p.successful ? PASS_COLOR : hexToRgba(PASS_COLOR, 0.35);\n        return <circle key={p.id} cx={cx} cy={cy} r={r} fill={fill} />;\n      })}\n\n      {/* Shots: 5-pointed stars */}\n      {shots.map(s => {\n        const cx = X(s.x), cy = Y(s.y);\n        const r = s.successful ? 10 : 7;\n        const fill = s.successful ? SHOT_COLOR : hexToRgba(SHOT_COLOR, 0.4);\n        return <polygon key={s.id} points={starPoints(cx, cy, r)} fill={fill} />;\n      })}\n\n      {/* Tackles: upward-pointing triangles */}\n      {tackles.map(tk => {\n        const cx = X(tk.x), cy = Y(tk.y);\n        const r = tk.successful ? 8 : 6;\n        const fill = tk.successful ? TACKLE_COLOR : hexToRgba(TACKLE_COLOR, 0.35);\n        return <polygon key={tk.id} points={trianglePoints(cx, cy, r)} fill={fill} />;\n      })}\n\n      {/* Interceptions: diamonds */}\n      {interceptions.map(ic => {\n        const cx = X(ic.x), cy = Y(ic.y);\n        const r = ic.successful ? 8 : 6;\n        const fill = ic.successful ? INTERCEPT_COLOR : hexToRgba(INTERCEPT_COLOR, 0.35);\n        return <polygon key={ic.id} points={diamondPoints(cx, cy, r)} fill={fill} />;\n      })}\n    </g>\n  );\n}\n\n// --- Root chart component ---\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n\n  // Margins computed to maintain the FIFA 105:68 pitch aspect ratio\n  const topM = 68;\n  const botM = 92;\n  const pitchH = height - topM - botM;\n  const pitchW = Math.round(pitchH * (105 / 68));\n  const hM = Math.round((width - pitchW) / 2);\n  const margin = { top: topM, bottom: botM, left: hM, right: hM };\n\n  const TITLE = \"scatter-pitch-events · javascript · muix · anyplot.ai\";\n  const titleSize = Math.round(22 * Math.min(1, 67 / TITLE.length));\n\n  return (\n    <ChartContainer\n      width={width}\n      height={height}\n      margin={margin}\n      xAxis={[{ id: \"x\", min: 0, max: 105, scaleType: \"linear\", disableTicks: true }]}\n      yAxis={[{ id: \"y\", min: 0, max: 68, scaleType: \"linear\", disableTicks: true }]}\n      series={scatterSeries}\n      skipAnimation\n    >\n      {/* 1. Pitch background and markings */}\n      <PitchBackground />\n\n      {/* 2. Directional arrows (below markers) */}\n      <ArrowsLayer />\n\n      {/* 3. Event markers — circles/stars/triangles/diamonds per event type */}\n      <EventMarkersLayer />\n\n      {/* 4. Legend — row layout, centered below pitch */}\n      <ChartsLegend\n        position={{ horizontal: \"middle\", vertical: \"bottom\" }}\n        direction=\"row\"\n        itemMarkWidth={18}\n        itemMarkHeight={12}\n        markGap={8}\n        itemGap={24}\n        labelStyle={{ fontSize: 14, fill: t.ink }}\n      />\n\n      {/* 5. Title */}\n      <text\n        x={width / 2}\n        y={topM / 2}\n        textAnchor=\"middle\"\n        dominantBaseline=\"middle\"\n        fontSize={titleSize}\n        fontWeight=\"600\"\n        fill={t.ink}\n        fontFamily='-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif'\n      >\n        {TITLE}\n      </text>\n    </ChartContainer>\n  );\n}\n"}