{"spec_id":"scatter-pitch-events","library":"makie","language":"julia","code":"# anyplot.ai\n# scatter-pitch-events: Soccer Pitch Event Map\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 93/100 | Created: 2026-06-21\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# Theme tokens\nconst THEME       = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG     = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst ELEVATED_BG = THEME == \"light\" ? colorant\"#FFFDF6\" : colorant\"#242420\"\nconst INK         = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT    = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nconst INK_MUTED   = THEME == \"light\" ? colorant\"#6B6A63\" : colorant\"#A8A79F\"\n\n# Imprint palette — event types mapped semantically\nconst COL_PASS          = colorant\"#009E73\"  # green — pass (constructive, positive action)\nconst COL_SHOT          = colorant\"#AE3030\"  # matte red — shot (aggressive, goal attempt)\nconst COL_TACKLE        = colorant\"#4467A3\"  # blue — tackle (defensive)\nconst COL_INTERCEPTION  = colorant\"#BD8233\"  # ochre — interception (opportunistic)\n\n# Pitch surface: green tones that work on both themes\nconst PITCH_FILL  = THEME == \"light\" ? RGBAf(0.13, 0.55, 0.13, 0.10) : RGBAf(0.13, 0.55, 0.13, 0.18)\nconst PITCH_LINE  = THEME == \"light\" ? RGBAf(0.1, 0.1, 0.1, 0.55) : RGBAf(0.9, 0.9, 0.85, 0.70)\n\n# Pitch dimensions (FIFA standard)\nconst PW = 105.0   # pitch width  (x)\nconst PH = 68.0    # pitch height (y)\n\n# --- Data -------------------------------------------------------------------\nn_events = 220\n\n# Passes — distributed across pitch, mostly in middle and attacking third\nn_pass = 110\npass_x = clamp.(randn(n_pass) .* 25 .+ 58, 2, 103)\npass_y = clamp.(randn(n_pass) .* 18 .+ 34, 2, 66)\npass_dx = randn(n_pass) .* 6\npass_dy = randn(n_pass) .* 4\npass_success = rand(n_pass) .> 0.25  # 75% success rate\n\n# Shots — clustered in attacking third, near penalty area\nn_shot = 22\nshot_x = clamp.(randn(n_shot) .* 8 .+ 88, 75, 103)\nshot_y = clamp.(randn(n_shot) .* 12 .+ 34, 10, 58)\nshot_dx = clamp.(randn(n_shot) .* 4 .+ 6, 1, 14)\nshot_dy = randn(n_shot) .* 3\nshot_success = rand(n_shot) .> 0.72  # ~28% on target\n\n# Tackles — spread across defensive and middle thirds\nn_tackle = 48\ntackle_x = clamp.(randn(n_tackle) .* 22 .+ 42, 2, 95)\ntackle_y = clamp.(randn(n_tackle) .* 18 .+ 34, 2, 66)\ntackle_success = rand(n_tackle) .> 0.40\n\n# Interceptions — defensive half\nn_inter = 40\ninter_x = clamp.(randn(n_inter) .* 18 .+ 35, 2, 80)\ninter_y = clamp.(randn(n_inter) .* 18 .+ 34, 2, 66)\ninter_success = rand(n_inter) .> 0.35\n\n# --- Figure -----------------------------------------------------------------\nfig = Figure(\n    size            = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\ntitle_str = \"scatter-pitch-events · julia · makie · anyplot.ai\"\nn_chars = length(title_str)\ntitle_sz = n_chars > 67 ? round(Int, 20 * 67 / n_chars) : 20\n\nax = Axis(\n    fig[1, 1];\n    title              = title_str,\n    titlesize          = title_sz,\n    titlecolor         = INK,\n    backgroundcolor    = PAGE_BG,\n    aspect             = DataAspect(),\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinevisible   = false,\n    bottomspinevisible = false,\n    xgridvisible       = false,\n    ygridvisible       = false,\n    xticksvisible      = false,\n    yticksvisible      = false,\n    xticklabelsvisible = false,\n    yticklabelsvisible = false,\n)\n\n# --- Draw pitch -------------------------------------------------------------\n# Pitch fill\npoly!(ax,\n    [Point2f(0, 0), Point2f(PW, 0), Point2f(PW, PH), Point2f(0, PH)];\n    color = PITCH_FILL, strokecolor = PITCH_LINE, strokewidth = 2.0,\n)\n\n# Subtle zone shading: very light red tint on the attacking third\nattack_zone_alpha = THEME == \"light\" ? 0.045f0 : 0.08f0\npoly!(ax,\n    [Point2f(70, 0), Point2f(PW, 0), Point2f(PW, PH), Point2f(70, PH)];\n    color = RGBAf(COL_SHOT.r, COL_SHOT.g, COL_SHOT.b, attack_zone_alpha), strokewidth = 0,\n)\n\nlw = 1.8  # line width for markings\n\n# Halfway line\nlines!(ax, [PW/2, PW/2], [0, PH]; color = PITCH_LINE, linewidth = lw)\n\n# Centre circle (radius 9.15m)\nθ = LinRange(0, 2π, 120)\nlines!(ax, PW/2 .+ 9.15 .* cos.(θ), PH/2 .+ 9.15 .* sin.(θ);\n       color = PITCH_LINE, linewidth = lw)\nscatter!(ax, [PW/2], [PH/2]; color = PITCH_LINE, markersize = 6)\n\n# Left penalty area (16.5m deep × 40.32m wide, centred)\npenalty_y_lo = (PH - 40.32) / 2\npenalty_y_hi = (PH + 40.32) / 2\npoly!(ax,\n    [Point2f(0, penalty_y_lo), Point2f(16.5, penalty_y_lo),\n     Point2f(16.5, penalty_y_hi), Point2f(0, penalty_y_hi)];\n    color = RGBAf(0, 0, 0, 0), strokecolor = PITCH_LINE, strokewidth = lw,\n)\n\n# Right penalty area\npoly!(ax,\n    [Point2f(PW - 16.5, penalty_y_lo), Point2f(PW, penalty_y_lo),\n     Point2f(PW, penalty_y_hi), Point2f(PW - 16.5, penalty_y_hi)];\n    color = RGBAf(0, 0, 0, 0), strokecolor = PITCH_LINE, strokewidth = lw,\n)\n\n# Left goal area (5.5m deep × 18.32m wide)\ngoal_y_lo = (PH - 18.32) / 2\ngoal_y_hi = (PH + 18.32) / 2\npoly!(ax,\n    [Point2f(0, goal_y_lo), Point2f(5.5, goal_y_lo),\n     Point2f(5.5, goal_y_hi), Point2f(0, goal_y_hi)];\n    color = RGBAf(0, 0, 0, 0), strokecolor = PITCH_LINE, strokewidth = lw,\n)\n\n# Right goal area\npoly!(ax,\n    [Point2f(PW - 5.5, goal_y_lo), Point2f(PW, goal_y_lo),\n     Point2f(PW, goal_y_hi), Point2f(PW - 5.5, goal_y_hi)];\n    color = RGBAf(0, 0, 0, 0), strokecolor = PITCH_LINE, strokewidth = lw,\n)\n\n# Left penalty arc (radius 9.15m, centred on penalty spot at 11m)\narc_angles_left = LinRange(-0.93, 0.93, 60)\nlines!(ax,\n    11.0 .+ 9.15 .* cos.(arc_angles_left),\n    PH/2 .+ 9.15 .* sin.(arc_angles_left);\n    color = PITCH_LINE, linewidth = lw,\n)\n\n# Right penalty arc\narc_angles_right = LinRange(π - 0.93, π + 0.93, 60)\nlines!(ax,\n    (PW - 11.0) .+ 9.15 .* cos.(arc_angles_right),\n    PH/2 .+ 9.15 .* sin.(arc_angles_right);\n    color = PITCH_LINE, linewidth = lw,\n)\n\n# Corner arcs (radius 1m)\ncorner_r = 1.0\nfor (cx, cy, a1, a2) in [\n        (0.0, 0.0,  0.0,      π/2),\n        (PW,  0.0,  π/2,      π),\n        (PW,  PH,   π,        3π/2),\n        (0.0, PH,   3π/2,     2π),\n    ]\n    θc = LinRange(a1, a2, 30)\n    lines!(ax, cx .+ corner_r .* cos.(θc), cy .+ corner_r .* sin.(θc);\n           color = PITCH_LINE, linewidth = lw)\nend\n\n# Left goal posts\nlines!(ax, [-2.0, -2.0], [(PH - 7.32)/2, (PH + 7.32)/2]; color = PITCH_LINE, linewidth = lw + 0.5)\nlines!(ax, [-2.0, 0.0],  [(PH - 7.32)/2, (PH - 7.32)/2]; color = PITCH_LINE, linewidth = lw + 0.5)\nlines!(ax, [-2.0, 0.0],  [(PH + 7.32)/2, (PH + 7.32)/2]; color = PITCH_LINE, linewidth = lw + 0.5)\n\n# Right goal posts\nlines!(ax, [PW + 2.0, PW + 2.0], [(PH - 7.32)/2, (PH + 7.32)/2]; color = PITCH_LINE, linewidth = lw + 0.5)\nlines!(ax, [PW, PW + 2.0], [(PH - 7.32)/2, (PH - 7.32)/2]; color = PITCH_LINE, linewidth = lw + 0.5)\nlines!(ax, [PW, PW + 2.0], [(PH + 7.32)/2, (PH + 7.32)/2]; color = PITCH_LINE, linewidth = lw + 0.5)\n\n# Pitch-third dividers (subtle dashed lines) and zone labels for tactical context\ndivider_color = RGBAf(PITCH_LINE.r, PITCH_LINE.g, PITCH_LINE.b, 0.28)\nlines!(ax, [35.0, 35.0], [0.0, PH]; color = divider_color, linewidth = 1.1, linestyle = :dash)\nlines!(ax, [70.0, 70.0], [0.0, PH]; color = divider_color, linewidth = 1.1, linestyle = :dash)\n\n# Zone labels (inside pitch, near top edge)\nzone_label_color = RGBAf(INK_SOFT.r, INK_SOFT.g, INK_SOFT.b, 0.55)\nfor (xc, lbl) in [(17.5, \"Defensive\"), (52.5, \"Midfield\"), (87.5, \"Attacking\")]\n    text!(ax, xc, PH - 1.5;\n        text     = lbl,\n        color    = zone_label_color,\n        fontsize = 8,\n        align    = (:center, :top),\n    )\nend\n\n# \"Shot Zone\" annotation near the right penalty arc\ntext!(ax, 97.5, 56.0;\n    text     = \"Shot Zone\",\n    color    = RGBAf(COL_SHOT.r, COL_SHOT.g, COL_SHOT.b, 0.60),\n    fontsize = 8,\n    align    = (:center, :center),\n)\n\n# --- Plot events ------------------------------------------------------------\n# Alpha for unsuccessful (lower opacity)\npass_alpha   = [s ? 0.85f0 : 0.30f0 for s in pass_success]\nshot_alpha   = [s ? 0.90f0 : 0.35f0 for s in shot_success]\ntackle_alpha = [s ? 0.85f0 : 0.30f0 for s in tackle_success]\ninter_alpha  = [s ? 0.85f0 : 0.30f0 for s in inter_success]\n\n# Passes — circles with directional arrows\nscatter!(ax, pass_x, pass_y;\n    color      = [RGBAf(COL_PASS.r, COL_PASS.g, COL_PASS.b, a) for a in pass_alpha],\n    marker     = :circle,\n    markersize = 9,\n    strokewidth = 0.6,\n    strokecolor = PITCH_LINE,\n)\n# Arrows for a subset of passes (every 3rd) — styled by outcome for visual refinement\nfor i in 1:3:n_pass\n    is_succ = pass_success[i]\n    arrows!(ax,\n        [pass_x[i]], [pass_y[i]],\n        [pass_dx[i] * 0.6], [pass_dy[i] * 0.6];\n        color     = RGBAf(COL_PASS.r, COL_PASS.g, COL_PASS.b, pass_alpha[i] * (is_succ ? 0.82 : 0.35)),\n        arrowsize = is_succ ? 8 : 5,\n        linewidth  = is_succ ? 1.3 : 0.5,\n    )\nend\n\n# Shots — stars with directional arrows\nscatter!(ax, shot_x, shot_y;\n    color      = [RGBAf(COL_SHOT.r, COL_SHOT.g, COL_SHOT.b, a) for a in shot_alpha],\n    marker     = :star5,\n    markersize = 18,\n    strokewidth = 0.8,\n    strokecolor = PITCH_LINE,\n)\nfor i in 1:n_shot\n    arrows!(ax,\n        [shot_x[i]], [shot_y[i]],\n        [shot_dx[i] * 0.7], [shot_dy[i] * 0.7];\n        color     = RGBAf(COL_SHOT.r, COL_SHOT.g, COL_SHOT.b, shot_alpha[i] * 0.8),\n        arrowsize = 9,\n        linewidth  = 1.2,\n    )\nend\n\n# Tackles — upward triangles\nscatter!(ax, tackle_x, tackle_y;\n    color      = [RGBAf(COL_TACKLE.r, COL_TACKLE.g, COL_TACKLE.b, a) for a in tackle_alpha],\n    marker     = :utriangle,\n    markersize = 12,\n    strokewidth = 0.6,\n    strokecolor = PITCH_LINE,\n)\n\n# Interceptions — diamonds\nscatter!(ax, inter_x, inter_y;\n    color      = [RGBAf(COL_INTERCEPTION.r, COL_INTERCEPTION.g, COL_INTERCEPTION.b, a) for a in inter_alpha],\n    marker     = :diamond,\n    markersize = 12,\n    strokewidth = 0.6,\n    strokecolor = PITCH_LINE,\n)\n\n# Set axis limits with small padding for goals\nxlims!(ax, -4, PW + 4)\nylims!(ax, -3, PH + 3)\n\n# --- Legend -----------------------------------------------------------------\nleg_elements = [\n    MarkerElement(color = COL_PASS,         marker = :circle,    markersize = 14, strokewidth = 0.5, strokecolor = INK_SOFT),\n    MarkerElement(color = COL_SHOT,         marker = :star5,     markersize = 18, strokewidth = 0.5, strokecolor = INK_SOFT),\n    MarkerElement(color = COL_TACKLE,       marker = :utriangle, markersize = 14, strokewidth = 0.5, strokecolor = INK_SOFT),\n    MarkerElement(color = COL_INTERCEPTION, marker = :diamond,   markersize = 14, strokewidth = 0.5, strokecolor = INK_SOFT),\n]\nleg_labels = [\"Pass (n=$n_pass)\", \"Shot (n=$n_shot)\", \"Tackle (n=$n_tackle)\", \"Interception (n=$n_inter)\"]\n\nLegend(\n    fig[1, 2], leg_elements, leg_labels;\n    framecolor   = INK_SOFT,\n    framevisible = true,\n    backgroundcolor = ELEVATED_BG,\n    labelcolor   = INK,\n    titlecolor   = INK,\n    title        = \"Event Type\\n(filled = successful)\",\n    titlesize    = 13,\n    labelsize    = 12,\n    rowgap       = 6,\n    padding      = (10, 10, 8, 8),\n)\n\n# Outcome note\ntext!(ax, 52.5, -2.0;\n    text      = \"Filled = successful · Faded = unsuccessful\",\n    color     = INK_MUTED,\n    fontsize  = 12,\n    align     = (:center, :top),\n)\n\ncolsize!(fig.layout, 1, Relative(0.82))\n\n# --- Save -------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}