{"spec_id":"scatter-pitch-events","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nscatter-pitch-events: Soccer Pitch Event Map\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 87/100 | Updated: 2026-06-21\n\"\"\"\n\nimport math\nimport os\nimport sys\n\n\n# Remove script's own directory from sys.path so 'pygal' resolves to the\n# installed package, not this file.\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.realpath(p) != os.path.realpath(_here)]\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens — Imprint palette, theme-adaptive chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Soccer pitch background — semantic green (grass), adapts saturation per theme\nPITCH_BG = \"#276b2e\" if THEME == \"light\" else \"#1b3b1f\"\n\n# Imprint palette — 8 event series in canonical order, first = brand green\nIMPRINT_EVENTS = (\n    \"#009E73\",  # Pass ✓ — brand green, first data series\n    \"#C475FD\",  # Pass ✗ — lavender\n    \"#4467A3\",  # Shot ✓ — blue\n    \"#BD8233\",  # Shot ✗ — ochre\n    \"#AE3030\",  # Tackle ✓ — matte red (semantic: defensive stop)\n    \"#2ABCCD\",  # Tackle ✗ — cyan\n    \"#954477\",  # Intercept ✓ — rose\n    \"#99B314\",  # Intercept ✗ — lime\n)\n\n# Data — synthetic match events on a FIFA 105 m × 68 m pitch\nnp.random.seed(42)\n\nn_passes = 35\nn_shots = 12\nn_tackles = 18\nn_interceptions = 14\n\npass_x = np.random.uniform(10, 95, n_passes)\npass_y = np.random.uniform(5, 63, n_passes)\npass_end_x = np.clip(pass_x + np.random.uniform(-15, 25, n_passes), 0, 105)\npass_end_y = np.clip(pass_y + np.random.normal(0, 10, n_passes), 0, 68)\npass_outcome = np.random.choice([\"successful\", \"unsuccessful\"], n_passes, p=[0.75, 0.25])\n\nshot_x = np.random.uniform(70, 104, n_shots)\nshot_y = np.random.uniform(18, 50, n_shots)\nshot_end_x = np.full(n_shots, 105.0)\nshot_end_y = np.random.uniform(28, 40, n_shots)\nshot_outcome = np.random.choice([\"successful\", \"unsuccessful\"], n_shots, p=[0.35, 0.65])\n\ntackle_x = np.random.uniform(15, 85, n_tackles)\ntackle_y = np.random.uniform(5, 63, n_tackles)\ntackle_outcome = np.random.choice([\"successful\", \"unsuccessful\"], n_tackles, p=[0.6, 0.4])\n\ninterception_x = np.random.uniform(20, 80, n_interceptions)\ninterception_y = np.random.uniform(8, 60, n_interceptions)\ninterception_outcome = np.random.choice([\"successful\", \"unsuccessful\"], n_interceptions, p=[0.7, 0.3])\n\n# Pitch markings — FIFA standard coordinates\npitch_outline = [(0, 0), (105, 0), (105, 68), (0, 68), (0, 0)]\nhalfway_line = [(52.5, 0), (52.5, 68)]\nleft_penalty = [(0, 13.84), (16.5, 13.84), (16.5, 54.16), (0, 54.16)]\nright_penalty = [(105, 13.84), (88.5, 13.84), (88.5, 54.16), (105, 54.16)]\nleft_goal_area = [(0, 24.84), (5.5, 24.84), (5.5, 43.16), (0, 43.16)]\nright_goal_area = [(105, 24.84), (99.5, 24.84), (99.5, 43.16), (105, 43.16)]\n\ncenter_circle_angles = np.linspace(0, 2 * math.pi, 60)\ncenter_circle = [(52.5 + 9.15 * math.cos(a), 34 + 9.15 * math.sin(a)) for a in center_circle_angles]\n\nleft_arc_angles = np.linspace(-0.65, 0.65, 20)\nleft_penalty_arc = [(11 + 9.15 * math.cos(a), 34 + 9.15 * math.sin(a)) for a in left_arc_angles]\n\nright_arc_angles = np.linspace(math.pi - 0.65, math.pi + 0.65, 20)\nright_penalty_arc = [(94 + 9.15 * math.cos(a), 34 + 9.15 * math.sin(a)) for a in right_arc_angles]\n\ncorner_angles_bl = np.linspace(0, math.pi / 2, 10)\ncorner_bl = [(math.cos(a), math.sin(a)) for a in corner_angles_bl]\ncorner_angles_br = np.linspace(math.pi / 2, math.pi, 10)\ncorner_br = [(105 + math.cos(a), math.sin(a)) for a in corner_angles_br]\ncorner_angles_tr = np.linspace(math.pi, 3 * math.pi / 2, 10)\ncorner_tr = [(105 + math.cos(a), 68 + math.sin(a)) for a in corner_angles_tr]\ncorner_angles_tl = np.linspace(3 * math.pi / 2, 2 * math.pi, 10)\ncorner_tl = [(math.cos(a), 68 + math.sin(a)) for a in corner_angles_tl]\n\n# Goal posts — thin rectangles extending outward from each goal mouth\ngoal_y_min = 34 - 3.66  # 30.34\ngoal_y_max = 34 + 3.66  # 37.66\nleft_goal_post = [(-2.4, goal_y_min), (0, goal_y_min), (0, goal_y_max), (-2.4, goal_y_max), (-2.4, goal_y_min)]\nright_goal_post = [(107.4, goal_y_min), (105, goal_y_min), (105, goal_y_max), (107.4, goal_y_max), (107.4, goal_y_min)]\n\n# Style — 16 white pitch-marking colors + 8 Imprint event colors\nPITCH_LINE = \"#ffffffcc\"\nCOLORS = tuple([PITCH_LINE] * 16) + IMPRINT_EVENTS\n\nfont = \"DejaVu Sans, Helvetica, Arial, sans-serif\"\ntitle = \"scatter-pitch-events · python · pygal · anyplot.ai\"\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PITCH_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    guide_stroke_color=INK_MUTED,\n    colors=COLORS,\n    font_family=font,\n    title_font_family=font,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    legend_font_family=font,\n    value_font_size=36,\n    tooltip_font_size=30,\n    tooltip_font_family=font,\n    opacity=0.9,\n    opacity_hover=1.0,\n    stroke_opacity=0.9,\n    stroke_opacity_hover=1,\n)\n\n# Chart\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=title,\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=4,\n    legend_box_size=32,\n    stroke=True,\n    dots_size=0,\n    show_x_guides=False,\n    show_y_guides=False,\n    show_x_labels=False,\n    show_y_labels=False,\n    xrange=(-5, 110),\n    range=(-5, 73),\n    margin_bottom=120,\n    margin_left=20,\n    margin_right=20,\n    margin_top=60,\n    print_values=False,\n    print_zeroes=False,\n    tooltip_border_radius=8,\n    tooltip_fancy_mode=True,\n    js=[],\n)\n\n# Pitch markings as line series (None title = excluded from legend)\nthin = {\"width\": 4, \"linecap\": \"round\", \"linejoin\": \"round\"}\n\nchart.add(\n    None,\n    pitch_outline,\n    stroke=True,\n    show_dots=False,\n    stroke_style={\"width\": 6, \"linecap\": \"round\", \"linejoin\": \"round\"},\n)\nchart.add(None, halfway_line, stroke=True, show_dots=False, stroke_style=thin)\nchart.add(None, left_penalty, stroke=True, show_dots=False, stroke_style=thin)\nchart.add(None, right_penalty, stroke=True, show_dots=False, stroke_style=thin)\nchart.add(None, left_goal_area, stroke=True, show_dots=False, stroke_style=thin)\nchart.add(None, right_goal_area, stroke=True, show_dots=False, stroke_style=thin)\nchart.add(None, center_circle, stroke=True, show_dots=False, stroke_style=thin)\nchart.add(None, [(52.5, 34)], stroke=False, dots_size=6)\nchart.add(None, left_penalty_arc, stroke=True, show_dots=False, stroke_style=thin)\nchart.add(None, right_penalty_arc, stroke=True, show_dots=False, stroke_style=thin)\nchart.add(None, corner_bl, stroke=True, show_dots=False, stroke_style={\"width\": 3, \"linecap\": \"round\"})\nchart.add(None, corner_br, stroke=True, show_dots=False, stroke_style={\"width\": 3, \"linecap\": \"round\"})\nchart.add(None, corner_tr, stroke=True, show_dots=False, stroke_style={\"width\": 3, \"linecap\": \"round\"})\nchart.add(None, corner_tl, stroke=True, show_dots=False, stroke_style={\"width\": 3, \"linecap\": \"round\"})\nchart.add(\n    None,\n    left_goal_post,\n    stroke=True,\n    show_dots=False,\n    stroke_style={\"width\": 5, \"linecap\": \"round\", \"linejoin\": \"round\"},\n)\nchart.add(\n    None,\n    right_goal_post,\n    stroke=True,\n    show_dots=False,\n    stroke_style={\"width\": 5, \"linecap\": \"round\", \"linejoin\": \"round\"},\n)\n\n# Passes as directional line segments (pygal None-break technique)\npass_succ_mask = pass_outcome == \"successful\"\npass_succ_lines = []\nfor sx, sy, ex, ey in zip(\n    pass_x[pass_succ_mask], pass_y[pass_succ_mask], pass_end_x[pass_succ_mask], pass_end_y[pass_succ_mask], strict=True\n):\n    pass_succ_lines.append({\"value\": (float(sx), float(sy)), \"label\": f\"Pass → ({float(ex):.0f}, {float(ey):.0f})\"})\n    pass_succ_lines.append((float(ex), float(ey)))\n    pass_succ_lines.append(None)\nchart.add(\n    \"Pass ✓\",\n    pass_succ_lines,\n    stroke=True,\n    show_dots=True,\n    dots_size=7,\n    stroke_style={\"width\": 2, \"linecap\": \"round\", \"opacity\": \"0.85\"},\n)\n\npass_fail_mask = ~pass_succ_mask\npass_fail_lines = []\nfor sx, sy, ex, ey in zip(\n    pass_x[pass_fail_mask], pass_y[pass_fail_mask], pass_end_x[pass_fail_mask], pass_end_y[pass_fail_mask], strict=True\n):\n    pass_fail_lines.append({\"value\": (float(sx), float(sy)), \"label\": f\"Pass ✗ → ({float(ex):.0f}, {float(ey):.0f})\"})\n    pass_fail_lines.append((float(ex), float(ey)))\n    pass_fail_lines.append(None)\nchart.add(\n    \"Pass ✗\",\n    pass_fail_lines,\n    stroke=True,\n    show_dots=True,\n    dots_size=7,\n    stroke_style={\"width\": 2, \"linecap\": \"round\", \"dasharray\": \"8,6\", \"opacity\": \"0.75\"},\n)\n\n# Shots as directional lines (prominent — key events)\nshot_succ_mask = shot_outcome == \"successful\"\nshot_succ_lines = []\nfor sx, sy, ex, ey in zip(\n    shot_x[shot_succ_mask], shot_y[shot_succ_mask], shot_end_x[shot_succ_mask], shot_end_y[shot_succ_mask], strict=True\n):\n    shot_succ_lines.append({\"value\": (float(sx), float(sy)), \"label\": \"⚽ Goal!\"})\n    shot_succ_lines.append((float(ex), float(ey)))\n    shot_succ_lines.append(None)\nchart.add(\n    \"Shot ✓ (goal)\",\n    shot_succ_lines,\n    stroke=True,\n    show_dots=True,\n    dots_size=22,\n    stroke_style={\"width\": 7, \"linecap\": \"round\", \"opacity\": \"0.65\"},\n)\n\nshot_fail_mask = ~shot_succ_mask\nshot_fail_lines = []\nfor sx, sy, ex, ey in zip(\n    shot_x[shot_fail_mask], shot_y[shot_fail_mask], shot_end_x[shot_fail_mask], shot_end_y[shot_fail_mask], strict=True\n):\n    shot_fail_lines.append({\"value\": (float(sx), float(sy)), \"label\": \"Shot missed\"})\n    shot_fail_lines.append((float(ex), float(ey)))\n    shot_fail_lines.append(None)\nchart.add(\n    \"Shot ✗\",\n    shot_fail_lines,\n    stroke=True,\n    show_dots=True,\n    dots_size=14,\n    stroke_style={\"width\": 5, \"linecap\": \"round\", \"dasharray\": \"6,4\", \"opacity\": \"0.65\"},\n)\n\n# Tackles as scatter dots\ntackle_succ_mask = tackle_outcome == \"successful\"\ntackle_succ_pts = [\n    {\"value\": (float(x), float(y)), \"label\": \"Tackle won\"}\n    for x, y in zip(tackle_x[tackle_succ_mask], tackle_y[tackle_succ_mask], strict=True)\n]\nchart.add(\"Tackle ✓\", tackle_succ_pts, stroke=False, dots_size=18)\n\ntackle_fail_mask = ~tackle_succ_mask\ntackle_fail_pts = [\n    {\"value\": (float(x), float(y)), \"label\": \"Tackle lost\"}\n    for x, y in zip(tackle_x[tackle_fail_mask], tackle_y[tackle_fail_mask], strict=True)\n]\nchart.add(\"Tackle ✗\", tackle_fail_pts, stroke=False, dots_size=12)\n\n# Interceptions as scatter dots\nint_succ_mask = interception_outcome == \"successful\"\nint_succ_pts = [\n    {\"value\": (float(x), float(y)), \"label\": \"Interception\"}\n    for x, y in zip(interception_x[int_succ_mask], interception_y[int_succ_mask], strict=True)\n]\nchart.add(\"Intercept ✓\", int_succ_pts, stroke=False, dots_size=16)\n\nint_fail_mask = ~int_succ_mask\nint_fail_pts = [\n    {\"value\": (float(x), float(y)), \"label\": \"Intercept missed\"}\n    for x, y in zip(interception_x[int_fail_mask], interception_y[int_fail_mask], strict=True)\n]\nchart.add(\"Intercept ✗\", int_fail_pts, stroke=False, dots_size=11)\n\n# Save\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}