{"spec_id":"scatter-pitch-events","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// scatter-pitch-events: Soccer Pitch Event Map\n// Library: highcharts 12.6.0 | JavaScript 22.22.3\n// Quality: 93/100 | Created: 2026-06-21\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME;\n\n// Deterministic LCG RNG (seed 42)\nlet _s = 42;\nconst rng = () => { _s = Math.imul(_s, 1664525) + 1013904223 | 0; return (_s >>> 0) / 4294967296; };\n\n// Hex → rgba helper\nconst rgba = (hex, a) => {\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},${a})`;\n};\n\n// FIFA pitch dimensions (meters)\nconst PW = 105, PH = 68;\n\n// Pitch appearance — grass color is semantic (always green regardless of theme)\nconst GRASS_BG = '#2d5a27';\nconst PITCH_LINE = 'rgba(255,255,255,0.85)';\n\n// Imprint palette — canonical order for 4 event categories (first = brand green)\nconst C_PASS  = t.palette[0]; // #009E73 brand green  — passes\nconst C_SHOT  = t.palette[1]; // #C475FD lavender     — shots\nconst C_TACK  = t.palette[2]; // #4467A3 blue         — tackles\nconst C_INTER = t.palette[3]; // #BD8233 ochre        — interceptions\n\n// White outline makes markers pop against dark grass\nconst MARK_LINE = 'rgba(255,255,255,0.65)';\nconst MARK_LW   = 1.5;\n\n// --- Circle / arc point generators ---\nconst circlePts = (cx, cy, r, n = 60) =>\n    Array.from({ length: n + 1 }, (_, i) => ({\n        x: cx + r * Math.cos((i / n) * 2 * Math.PI),\n        y: cy + r * Math.sin((i / n) * 2 * Math.PI)\n    }));\n\nconst arcPts = (cx, cy, r, a0, a1, n = 24) =>\n    Array.from({ length: n + 1 }, (_, i) => {\n        const a = a0 + (i / n) * (a1 - a0);\n        return { x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) };\n    });\n\n// --- Pitch markings as null-separated line segments ---\nfunction buildPitchLines() {\n    const d = [];\n    const seg = pts => { if (d.length) d.push(null); d.push(...pts); };\n\n    // Outer boundary\n    seg([{ x: 0, y: 0 }, { x: PW, y: 0 }, { x: PW, y: PH }, { x: 0, y: PH }, { x: 0, y: 0 }]);\n    // Halfway line\n    seg([{ x: PW / 2, y: 0 }, { x: PW / 2, y: PH }]);\n    // Center circle (r = 9.15 m)\n    seg(circlePts(PW / 2, PH / 2, 9.15));\n    // Center spot\n    seg(circlePts(PW / 2, PH / 2, 0.6, 12));\n    // Left penalty area (16.5 m deep, 40.32 m wide)\n    seg([{ x: 0, y: 13.84 }, { x: 16.5, y: 13.84 }, { x: 16.5, y: 54.16 }, { x: 0, y: 54.16 }]);\n    // Right penalty area\n    seg([{ x: PW, y: 13.84 }, { x: 88.5, y: 13.84 }, { x: 88.5, y: 54.16 }, { x: PW, y: 54.16 }]);\n    // Left goal area (5.5 m deep, 18.32 m wide)\n    seg([{ x: 0, y: 24.84 }, { x: 5.5, y: 24.84 }, { x: 5.5, y: 43.16 }, { x: 0, y: 43.16 }]);\n    // Right goal area\n    seg([{ x: PW, y: 24.84 }, { x: 99.5, y: 24.84 }, { x: 99.5, y: 43.16 }, { x: PW, y: 43.16 }]);\n    // Penalty spots (at 11 m from each goal line)\n    seg(circlePts(11, PH / 2, 0.6, 12));\n    seg(circlePts(94, PH / 2, 0.6, 12));\n    // Penalty arcs — portions of r=9.15 circle outside each penalty area\n    const la = Math.acos((16.5 - 11) / 9.15); // ~53.1°\n    seg(arcPts(11, PH / 2, 9.15, -la, la));\n    const ra = Math.acos((94 - 88.5) / 9.15);\n    seg(arcPts(94, PH / 2, 9.15, Math.PI - ra, Math.PI + ra));\n    // Corner arcs (r = 1 m, quarter circles facing pitch interior)\n    seg(arcPts(0,  0,  1, 0,            Math.PI / 2));\n    seg(arcPts(PW, 0,  1, Math.PI / 2,  Math.PI));\n    seg(arcPts(0,  PH, 1, -Math.PI / 2, 0));\n    seg(arcPts(PW, PH, 1, Math.PI,      3 * Math.PI / 2));\n    // Goals (2 m depth, 7.32 m wide — centered at y = 34)\n    seg([{ x: 0, y: 30.34 }, { x: -2, y: 30.34 }, { x: -2, y: 37.66 }, { x: 0, y: 37.66 }]);\n    seg([{ x: PW, y: 30.34 }, { x: PW + 2, y: 30.34 }, { x: PW + 2, y: 37.66 }, { x: PW, y: 37.66 }]);\n\n    return d;\n}\n\n// --- Event data generation (all using rng() in fixed order for determinism) ---\n\n// Passes: spread across the full pitch\nconst passes = Array.from({ length: 70 }, () => ({\n    x: 5 + rng() * 90, y: 2 + rng() * 64, suc: rng() > 0.28,\n    ex: 0, ey: 0\n}));\npasses.forEach(p => {\n    const dx = (rng() * 0.6 + 0.25) * 22 * (rng() > 0.3 ? 1 : -1);\n    const dy = (rng() - 0.5) * 14;\n    p.ex = Math.min(104, Math.max(1, p.x + dx));\n    p.ey = Math.min(67, Math.max(1, p.y + dy));\n});\n\n// Shots: concentrated in attacking third\nconst shots = Array.from({ length: 20 }, () => ({\n    x: 72 + rng() * 28, y: 12 + rng() * 44, suc: rng() > 0.72,\n    ex: 0, ey: 0\n}));\nshots.forEach(s => {\n    const goalY = 27 + rng() * 14; // target point in goal opening\n    const dx = PW - s.x, dy = goalY - s.y;\n    const dist = Math.sqrt(dx * dx + dy * dy);\n    const tLen = Math.min(dist * 0.65, 14); // arrow length (fraction toward goal)\n    s.ex = s.x + (dx / dist) * tLen;\n    s.ey = s.y + (dy / dist) * tLen;\n});\n\n// Tackles: distributed mid-field to defensive third\nconst tackles = Array.from({ length: 28 }, () => ({\n    x: 10 + rng() * 80, y: 3 + rng() * 62, suc: rng() > 0.42\n}));\n\n// Interceptions: mid-field zone\nconst inters = Array.from({ length: 22 }, () => ({\n    x: 15 + rng() * 70, y: 3 + rng() * 62, suc: rng() > 0.25\n}));\n\n// Build null-separated arrow line data from events with end coords\nconst arrowData = evts => {\n    const d = [];\n    evts.forEach(e => { if (d.length) d.push(null); d.push({ x: e.x, y: e.y }, { x: e.ex, y: e.ey }); });\n    return d;\n};\n\n// Per-point outcome encoding: solid fill = successful, faded fill = unsuccessful\nconst markerData = (evts, color) => evts.map(e => ({\n    x: e.x, y: e.y,\n    marker: { fillColor: e.suc ? color : rgba(color, 0.22) }\n}));\n\n// X-axis padding to maintain 105:68 pitch aspect ratio\n// Plot area CSS: (1600-80) × (900-170) = 1520 × 730 px  (margins top=70,right=40,bottom=100,left=40)\n// px/m constrained by height: 730 / (68+4) ≈ 10.14; x range = 1520 / 10.14 ≈ 150 m\nconst X_PAD = ((1520 / (730 / (PH + 4))) - PW) / 2; // ≈ 22.4 m each side\n\nHighcharts.chart(\"container\", {\n    chart: {\n        type: \"scatter\",\n        backgroundColor: \"transparent\",\n        plotBackgroundColor: GRASS_BG,\n        animation: false,\n        style: { fontFamily: \"inherit\" },\n        margin: [70, 40, 100, 40],\n    },\n    credits: { enabled: false },\n    colors: t.palette,\n    title: {\n        text: \"scatter-pitch-events · javascript · highcharts · anyplot.ai\",\n        style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" }\n    },\n    subtitle: {\n        text: \"Filled = successful · Faded = unsuccessful · Lines show pass/shot direction · Attacking direction →\",\n        style: { color: t.inkSoft, fontSize: \"13px\" }\n    },\n    xAxis: {\n        min: -X_PAD, max: PW + X_PAD,\n        lineWidth: 0, tickWidth: 0, gridLineWidth: 0,\n        labels: { enabled: false },\n        title: { text: null },\n    },\n    yAxis: {\n        min: -2, max: PH + 2,\n        lineWidth: 0, tickWidth: 0, gridLineWidth: 0,\n        labels: { enabled: false },\n        title: { text: null },\n    },\n    legend: {\n        enabled: true,\n        align: 'center',\n        verticalAlign: 'bottom',\n        layout: 'horizontal',\n        itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n        itemHoverStyle: { color: t.ink },\n        backgroundColor: t.elevatedBg,\n        borderColor: t.grid,\n        borderWidth: 1,\n        borderRadius: 4,\n        padding: 8,\n    },\n    tooltip: { enabled: false },\n    plotOptions: {\n        series: { animation: false },\n        scatter: {\n            enableMouseTracking: false,\n            marker: { lineWidth: MARK_LW, lineColor: MARK_LINE },\n        },\n        line: {\n            enableMouseTracking: false,\n            marker: { enabled: false },\n            connectNulls: false,\n        },\n    },\n    series: [\n        // Pitch markings (white lines on grass)\n        {\n            type: 'line', name: '_pitch',\n            data: buildPitchLines(),\n            color: PITCH_LINE, lineWidth: 1.5,\n            showInLegend: false, zIndex: 1,\n        },\n        // Pass trajectory lines (successful passes only)\n        {\n            type: 'line', name: '_passArrows',\n            data: arrowData(passes.filter(p => p.suc)),\n            color: rgba(C_PASS, 0.4), lineWidth: 1.2,\n            showInLegend: false, zIndex: 2,\n        },\n        // Shot trajectory lines (all shots, toward goal)\n        {\n            type: 'line', name: '_shotArrows',\n            data: arrowData(shots),\n            color: rgba(C_SHOT, 0.45), lineWidth: 1.5,\n            showInLegend: false, zIndex: 2,\n        },\n        // Pass events — circle markers\n        {\n            type: 'scatter', name: 'Pass',\n            data: markerData(passes, C_PASS),\n            color: C_PASS,\n            marker: { symbol: 'circle', radius: 5, lineColor: MARK_LINE, lineWidth: MARK_LW },\n            zIndex: 4,\n        },\n        // Shot events — diamond markers\n        {\n            type: 'scatter', name: 'Shot',\n            data: markerData(shots, C_SHOT),\n            color: C_SHOT,\n            marker: { symbol: 'diamond', radius: 7, lineColor: MARK_LINE, lineWidth: MARK_LW },\n            zIndex: 4,\n        },\n        // Tackle events — triangle markers\n        {\n            type: 'scatter', name: 'Tackle',\n            data: markerData(tackles, C_TACK),\n            color: C_TACK,\n            marker: { symbol: 'triangle', radius: 6, lineColor: MARK_LINE, lineWidth: MARK_LW },\n            zIndex: 4,\n        },\n        // Interception events — square markers\n        {\n            type: 'scatter', name: 'Interception',\n            data: markerData(inters, C_INTER),\n            color: C_INTER,\n            marker: { symbol: 'square', radius: 5, lineColor: MARK_LINE, lineWidth: MARK_LW },\n            zIndex: 4,\n        },\n    ],\n});\n"}