{"spec_id":"scatter-pitch-events","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nscatter-pitch-events: Soccer Pitch Event Map\nLibrary: plotly 6.8.0 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-06-21\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme-adaptive chrome tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\n\n# Imprint palette — event type colors (semantic assignment)\nPASS_COLOR = \"#009E73\"  # Imprint brand green — passes (first categorical series)\nTACKLE_COLOR = \"#C475FD\"  # Imprint lavender — tackles\nINTERCEPT_COLOR = \"#4467A3\"  # Imprint blue — interceptions\nSHOT_COLOR = \"#AE3030\"  # Imprint matte red — shots (semantic: danger/goal)\n\n# Pitch domain colors — always green (real-world field, not theme-chrome)\nPITCH_GRASS = \"#3A9D5C\"  # bright green playing surface\nPITCH_SURROUND = \"#1B5E20\"  # dark green out-of-bounds surround\nPITCH_LINE = \"rgba(255,255,255,0.90)\"\nlw = 2.5\n\n# Data\nnp.random.seed(42)\nn_passes = 70\nn_shots = 16\nn_tackles = 28\nn_interceptions = 20\n\n# Passes — distributed across pitch, biased toward attacking half\npass_x = np.random.beta(2, 1.5, n_passes) * 105\npass_y = np.random.normal(34, 16, n_passes).clip(2, 66)\npass_end_x = (pass_x + np.random.normal(15, 8, n_passes)).clip(0, 105)\npass_end_y = (pass_y + np.random.normal(0, 10, n_passes)).clip(0, 68)\npass_success = np.random.choice([True, False], n_passes, p=[0.78, 0.22])\n\n# Shots — concentrated in final third\nshot_x = np.random.uniform(72, 98, n_shots)\nshot_y = np.random.normal(34, 12, n_shots).clip(14, 54)\nshot_end_x = np.full(n_shots, 105.0)\nshot_end_y = np.random.normal(34, 4, n_shots).clip(27, 41)\nshot_success = np.random.choice([True, False], n_shots, p=[0.33, 0.67])\n\n# Tackles — midfield and defensive zones\ntackle_x = np.random.beta(1.5, 2, n_tackles) * 80 + 10\ntackle_y = np.random.uniform(5, 63, n_tackles)\ntackle_success = np.random.choice([True, False], n_tackles, p=[0.65, 0.35])\n\n# Interceptions — defensive and midfield zones\ninterception_x = np.random.beta(1.2, 2.5, n_interceptions) * 70 + 5\ninterception_y = np.random.uniform(8, 60, n_interceptions)\ninterception_success = np.random.choice([True, False], n_interceptions, p=[0.85, 0.15])\n\n# Figure\nfig = go.Figure()\n\n# --- Pitch markings (layer=\"below\" so event traces render on top) ---\n\n# Playing surface fill\nfig.add_shape(\n    type=\"rect\",\n    x0=0,\n    y0=0,\n    x1=105,\n    y1=68,\n    line={\"color\": PITCH_LINE, \"width\": lw},\n    fillcolor=PITCH_GRASS,\n    layer=\"below\",\n)\n\n# Halfway line\nfig.add_shape(type=\"line\", x0=52.5, y0=0, x1=52.5, y1=68, line={\"color\": PITCH_LINE, \"width\": lw}, layer=\"below\")\n\n# Penalty areas\nfig.add_shape(type=\"rect\", x0=0, y0=13.84, x1=16.5, y1=54.16, line={\"color\": PITCH_LINE, \"width\": lw}, layer=\"below\")\nfig.add_shape(type=\"rect\", x0=88.5, y0=13.84, x1=105, y1=54.16, line={\"color\": PITCH_LINE, \"width\": lw}, layer=\"below\")\n\n# Goal areas (6-yard box)\nfig.add_shape(type=\"rect\", x0=0, y0=24.84, x1=5.5, y1=43.16, line={\"color\": PITCH_LINE, \"width\": lw}, layer=\"below\")\nfig.add_shape(type=\"rect\", x0=99.5, y0=24.84, x1=105, y1=43.16, line={\"color\": PITCH_LINE, \"width\": lw}, layer=\"below\")\n\n# Goal posts\nfig.add_shape(\n    type=\"rect\",\n    x0=-2,\n    y0=30.34,\n    x1=0,\n    y1=37.66,\n    line={\"color\": PITCH_LINE, \"width\": 2},\n    fillcolor=\"rgba(255,255,255,0.25)\",\n    layer=\"below\",\n)\nfig.add_shape(\n    type=\"rect\",\n    x0=105,\n    y0=30.34,\n    x1=107,\n    y1=37.66,\n    line={\"color\": PITCH_LINE, \"width\": 2},\n    fillcolor=\"rgba(255,255,255,0.25)\",\n    layer=\"below\",\n)\n\n# Center circle\ntheta = np.linspace(0, 2 * np.pi, 100)\nfig.add_trace(\n    go.Scatter(\n        x=52.5 + 9.15 * np.cos(theta),\n        y=34 + 9.15 * np.sin(theta),\n        mode=\"lines\",\n        line={\"color\": PITCH_LINE, \"width\": lw},\n        showlegend=False,\n        hoverinfo=\"skip\",\n    )\n)\n\n# Center spot\nfig.add_trace(\n    go.Scatter(\n        x=[52.5], y=[34], mode=\"markers\", marker={\"size\": 5, \"color\": PITCH_LINE}, showlegend=False, hoverinfo=\"skip\"\n    )\n)\n\n# Penalty spots\nfig.add_trace(\n    go.Scatter(\n        x=[11, 94],\n        y=[34, 34],\n        mode=\"markers\",\n        marker={\"size\": 4, \"color\": PITCH_LINE},\n        showlegend=False,\n        hoverinfo=\"skip\",\n    )\n)\n\n# Penalty arcs (outside penalty area)\nla = np.linspace(-0.65, 0.65, 50)\nfig.add_trace(\n    go.Scatter(\n        x=11 + 9.15 * np.cos(la),\n        y=34 + 9.15 * np.sin(la),\n        mode=\"lines\",\n        line={\"color\": PITCH_LINE, \"width\": lw},\n        showlegend=False,\n        hoverinfo=\"skip\",\n    )\n)\nra = np.linspace(np.pi - 0.65, np.pi + 0.65, 50)\nfig.add_trace(\n    go.Scatter(\n        x=94 + 9.15 * np.cos(ra),\n        y=34 + 9.15 * np.sin(ra),\n        mode=\"lines\",\n        line={\"color\": PITCH_LINE, \"width\": lw},\n        showlegend=False,\n        hoverinfo=\"skip\",\n    )\n)\n\n# Corner arcs\nfor cx_pos, cy_pos in [(0, 0), (0, 68), (105, 0), (105, 68)]:\n    start = (\n        0\n        if cx_pos == 0 and cy_pos == 0\n        else (\n            np.pi * 1.5 if cx_pos == 0 and cy_pos == 68 else (np.pi * 0.5 if cx_pos == 105 and cy_pos == 0 else np.pi)\n        )\n    )\n    ct = np.linspace(start, start + np.pi / 2, 25)\n    fig.add_trace(\n        go.Scatter(\n            x=cx_pos + 1.5 * np.cos(ct),\n            y=cy_pos + 1.5 * np.sin(ct),\n            mode=\"lines\",\n            line={\"color\": PITCH_LINE, \"width\": lw},\n            showlegend=False,\n            hoverinfo=\"skip\",\n        )\n    )\n\n# --- Pass direction lines (None-separator batching) ---\npass_s_xs, pass_s_ys = [], []\nfor i in np.where(pass_success)[0]:\n    pass_s_xs.extend([pass_x[i], pass_end_x[i], None])\n    pass_s_ys.extend([pass_y[i], pass_end_y[i], None])\nfig.add_trace(\n    go.Scatter(\n        x=pass_s_xs,\n        y=pass_s_ys,\n        mode=\"lines\",\n        line={\"color\": \"rgba(0,158,115,0.22)\", \"width\": 1},\n        showlegend=False,\n        hoverinfo=\"skip\",\n    )\n)\n\npass_u_xs, pass_u_ys = [], []\nfor i in np.where(~pass_success)[0]:\n    pass_u_xs.extend([pass_x[i], pass_end_x[i], None])\n    pass_u_ys.extend([pass_y[i], pass_end_y[i], None])\nfig.add_trace(\n    go.Scatter(\n        x=pass_u_xs,\n        y=pass_u_ys,\n        mode=\"lines\",\n        # raised from 0.08 → 0.18 so unsuccessful lines are visible in static render\n        line={\"color\": \"rgba(0,158,115,0.18)\", \"width\": 1, \"dash\": \"dot\"},\n        showlegend=False,\n        hoverinfo=\"skip\",\n    )\n)\n\n# --- Shot direction lines ---\nshot_s_xs, shot_s_ys = [], []\nfor i in np.where(shot_success)[0]:\n    shot_s_xs.extend([shot_x[i], shot_end_x[i], None])\n    shot_s_ys.extend([shot_y[i], shot_end_y[i], None])\nfig.add_trace(\n    go.Scatter(\n        x=shot_s_xs,\n        y=shot_s_ys,\n        mode=\"lines\",\n        line={\"color\": \"rgba(174,48,48,0.8)\", \"width\": 2.5},\n        showlegend=False,\n        hoverinfo=\"skip\",\n    )\n)\n\nshot_u_xs, shot_u_ys = [], []\nfor i in np.where(~shot_success)[0]:\n    shot_u_xs.extend([shot_x[i], shot_end_x[i], None])\n    shot_u_ys.extend([shot_y[i], shot_end_y[i], None])\nfig.add_trace(\n    go.Scatter(\n        x=shot_u_xs,\n        y=shot_u_ys,\n        mode=\"lines\",\n        line={\"color\": \"rgba(174,48,48,0.35)\", \"width\": 2, \"dash\": \"dot\"},\n        showlegend=False,\n        hoverinfo=\"skip\",\n    )\n)\n\n# --- Event markers ---\n\n# Passes (circles)\nfig.add_trace(\n    go.Scatter(\n        x=pass_x[pass_success],\n        y=pass_y[pass_success],\n        mode=\"markers\",\n        marker={\n            \"size\": 11,\n            \"color\": PASS_COLOR,\n            \"opacity\": 0.92,\n            \"symbol\": \"circle\",\n            \"line\": {\"width\": 1.5, \"color\": \"white\"},\n        },\n        name=\"Pass (successful)\",\n        hovertemplate=\"<b>Pass</b><br>x: %{x:.0f}m, y: %{y:.0f}m<extra></extra>\",\n    )\n)\nfig.add_trace(\n    go.Scatter(\n        x=pass_x[~pass_success],\n        y=pass_y[~pass_success],\n        mode=\"markers\",\n        marker={\n            \"size\": 11,\n            \"color\": PASS_COLOR,\n            \"opacity\": 0.6,\n            \"symbol\": \"circle-open\",\n            \"line\": {\"width\": 2, \"color\": PASS_COLOR},\n        },\n        name=\"Pass (unsuccessful)\",\n        hovertemplate=\"<b>Pass (missed)</b><br>x: %{x:.0f}m, y: %{y:.0f}m<extra></extra>\",\n    )\n)\n\n# Tackles (triangles)\nfig.add_trace(\n    go.Scatter(\n        x=tackle_x[tackle_success],\n        y=tackle_y[tackle_success],\n        mode=\"markers\",\n        marker={\n            \"size\": 16,\n            \"color\": TACKLE_COLOR,\n            \"opacity\": 0.95,\n            \"symbol\": \"triangle-up\",\n            \"line\": {\"width\": 1.5, \"color\": \"white\"},\n        },\n        name=\"Tackle (successful)\",\n        hovertemplate=\"<b>Tackle</b><br>x: %{x:.0f}m, y: %{y:.0f}m<extra></extra>\",\n    )\n)\nfig.add_trace(\n    go.Scatter(\n        x=tackle_x[~tackle_success],\n        y=tackle_y[~tackle_success],\n        mode=\"markers\",\n        marker={\n            \"size\": 16,\n            \"color\": TACKLE_COLOR,\n            \"opacity\": 0.6,\n            \"symbol\": \"triangle-up-open\",\n            \"line\": {\"width\": 2.5, \"color\": TACKLE_COLOR},\n        },\n        name=\"Tackle (unsuccessful)\",\n        hovertemplate=\"<b>Tackle (missed)</b><br>x: %{x:.0f}m, y: %{y:.0f}m<extra></extra>\",\n    )\n)\n\n# Interceptions (diamonds)\nfig.add_trace(\n    go.Scatter(\n        x=interception_x[interception_success],\n        y=interception_y[interception_success],\n        mode=\"markers\",\n        marker={\n            \"size\": 15,\n            \"color\": INTERCEPT_COLOR,\n            \"opacity\": 0.95,\n            \"symbol\": \"diamond\",\n            \"line\": {\"width\": 1.5, \"color\": \"white\"},\n        },\n        name=\"Interception (successful)\",\n        hovertemplate=\"<b>Interception</b><br>x: %{x:.0f}m, y: %{y:.0f}m<extra></extra>\",\n    )\n)\nfig.add_trace(\n    go.Scatter(\n        x=interception_x[~interception_success],\n        y=interception_y[~interception_success],\n        mode=\"markers\",\n        marker={\n            \"size\": 15,\n            \"color\": INTERCEPT_COLOR,\n            \"opacity\": 0.6,\n            \"symbol\": \"diamond-open\",\n            \"line\": {\"width\": 2.5, \"color\": INTERCEPT_COLOR},\n        },\n        name=\"Interception (unsuccessful)\",\n        hovertemplate=\"<b>Interception (missed)</b><br>x: %{x:.0f}m, y: %{y:.0f}m<extra></extra>\",\n    )\n)\n\n# Shots (stars — drawn last to sit above other event markers)\nfig.add_trace(\n    go.Scatter(\n        x=shot_x[shot_success],\n        y=shot_y[shot_success],\n        mode=\"markers\",\n        marker={\n            \"size\": 22,\n            \"color\": SHOT_COLOR,\n            \"opacity\": 0.95,\n            \"symbol\": \"star\",\n            \"line\": {\"width\": 2, \"color\": \"white\"},\n        },\n        name=\"Shot (on target)\",\n        hovertemplate=\"<b>Shot (on target)</b><br>x: %{x:.0f}m, y: %{y:.0f}m<extra></extra>\",\n    )\n)\nfig.add_trace(\n    go.Scatter(\n        x=shot_x[~shot_success],\n        y=shot_y[~shot_success],\n        mode=\"markers\",\n        marker={\n            \"size\": 22,\n            \"color\": SHOT_COLOR,\n            \"opacity\": 0.6,\n            \"symbol\": \"star-open\",\n            \"line\": {\"width\": 2.5, \"color\": SHOT_COLOR},\n        },\n        name=\"Shot (off target)\",\n        hovertemplate=\"<b>Shot (off target)</b><br>x: %{x:.0f}m, y: %{y:.0f}m<extra></extra>\",\n    )\n)\n\n# Tactical annotation — highlight the shot-dense zone\nfig.add_annotation(\n    x=85,\n    y=62,\n    text=\"<b>Danger Zone</b><br>Shots cluster in<br>the final third\",\n    showarrow=True,\n    arrowhead=2,\n    arrowsize=1,\n    arrowwidth=1.5,\n    arrowcolor=\"rgba(255,255,255,0.75)\",\n    ax=0,\n    ay=40,\n    font={\"size\": 13, \"color\": \"white\", \"family\": \"Arial, sans-serif\"},\n    bgcolor=\"rgba(27,27,23,0.82)\",\n    bordercolor=\"rgba(255,255,255,0.45)\",\n    borderwidth=1,\n    borderpad=6,\n)\n\n# Layout\nfig.update_layout(\n    autosize=False,\n    title={\n        \"text\": \"scatter-pitch-events · python · plotly · anyplot.ai\",\n        \"font\": {\"size\": 16, \"color\": INK, \"family\": \"Arial Black, Arial, sans-serif\"},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n        \"y\": 0.97,\n    },\n    xaxis={\n        \"range\": [-4, 109],\n        \"showgrid\": False,\n        \"zeroline\": False,\n        \"showticklabels\": False,\n        \"showline\": False,\n        \"fixedrange\": True,\n    },\n    yaxis={\n        \"range\": [-4, 72],\n        \"showgrid\": False,\n        \"zeroline\": False,\n        \"showticklabels\": False,\n        \"showline\": False,\n        \"fixedrange\": True,\n        \"scaleanchor\": \"x\",\n        \"scaleratio\": 1,\n    },\n    plot_bgcolor=PITCH_SURROUND,\n    paper_bgcolor=PAGE_BG,\n    margin={\"l\": 30, \"r\": 30, \"t\": 60, \"b\": 20},\n    legend={\n        \"font\": {\"size\": 10, \"color\": \"white\", \"family\": \"Arial, sans-serif\"},\n        \"bgcolor\": \"rgba(0,0,0,0.58)\",\n        \"bordercolor\": \"rgba(255,255,255,0.35)\",\n        \"borderwidth\": 1,\n        # shifted right to sit clearly inside the pitch (avoids left-boundary overlap)\n        \"x\": 0.04,\n        \"y\": 0.99,\n        \"xanchor\": \"left\",\n        \"yanchor\": \"top\",\n        \"itemsizing\": \"constant\",\n        \"tracegroupgap\": 3,\n    },\n    hoverlabel={\"bgcolor\": \"white\", \"font_size\": 14},\n)\n\n# Save — theme-suffixed filenames; 3200×1800 landscape target\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}