{"spec_id":"scatter-pitch-events","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' scatter-pitch-events: Soccer Pitch Event Map\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 90/100 | Created: 2026-06-21\n\nlibrary(ggplot2)\nlibrary(ragg)\n\nset.seed(42)\n\n# Theme tokens\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\n\n# Imprint palette — canonical order\nIMPRINT_PALETTE <- c(\n  \"#009E73\",  # 1 — brand green (passes)\n  \"#C475FD\",  # 2 — lavender (shots)\n  \"#4467A3\",  # 3 — blue (tackles)\n  \"#BD8233\"   # 4 — ochre (interceptions)\n)\n\n# Pitch fill: green for light, muted dark-green for dark theme\nPITCH_FILL  <- if (THEME == \"light\") \"#3a7d2e\" else \"#1e4a18\"\nPITCH_LINE  <- if (THEME == \"light\") \"#FFFFFF\" else \"#C8C8B8\"\n\n# Data — synthetic match event data (first half attacking build-up)\nn_events <- 220\n\n# Generate events distributed across pitch zones with realistic patterns\nevent_types <- c(\"pass\", \"shot\", \"tackle\", \"interception\")\nevent_probs <- c(0.55, 0.12, 0.20, 0.13)\n\nevents <- data.frame(\n  event_type = sample(event_types, n_events, replace = TRUE, prob = event_probs)\n)\n\n# Passes cluster in midfield and wide areas\npass_idx  <- which(events$event_type == \"pass\")\nshot_idx  <- which(events$event_type == \"shot\")\ntack_idx  <- which(events$event_type == \"tackle\")\ninter_idx <- which(events$event_type == \"interception\")\n\nevents$x <- NA_real_\nevents$y <- NA_real_\n\n# Passes — spread across attacking two-thirds\nevents$x[pass_idx]  <- runif(length(pass_idx), 30, 100)\nevents$y[pass_idx]  <- runif(length(pass_idx), 4, 64)\n\n# Shots — concentrated in attacking box (x: 82–105, y: 13–55)\nevents$x[shot_idx]  <- runif(length(shot_idx), 82, 105)\nevents$y[shot_idx]  <- runif(length(shot_idx), 13, 55)\n\n# Tackles — midfield and defensive third\nevents$x[tack_idx]  <- runif(length(tack_idx), 20, 70)\nevents$y[tack_idx]  <- runif(length(tack_idx), 5, 63)\n\n# Interceptions — defensive and middle thirds\nevents$x[inter_idx] <- runif(length(inter_idx), 10, 65)\nevents$y[inter_idx] <- runif(length(inter_idx), 5, 63)\n\n# Outcome: passes/shots ~70% successful; tackles/interceptions ~60% successful\nevents$outcome <- ifelse(\n  events$event_type %in% c(\"pass\", \"shot\"),\n  sample(c(\"successful\", \"unsuccessful\"), n_events, replace = TRUE,\n         prob = c(0.70, 0.30)),\n  sample(c(\"successful\", \"unsuccessful\"), n_events, replace = TRUE,\n         prob = c(0.60, 0.40))\n)\n\n# Pass arrows — subset of passes with destination\npass_df <- events[pass_idx, ]\npass_df$xend <- pmin(pass_df$x + rnorm(length(pass_idx), 12, 6), 105)\npass_df$yend <- pass_df$y   + rnorm(length(pass_idx), 0,  8)\npass_df$yend <- pmax(pmin(pass_df$yend, 68), 0)\n\n# Shot arrows — toward goal centre (105, 34)\nshot_df <- events[shot_idx, ]\nshot_df$xend <- shot_df$x + (105 - shot_df$x) * runif(length(shot_idx), 0.4, 0.9)\nshot_df$yend <- shot_df$y + (34  - shot_df$y) * runif(length(shot_idx), 0.4, 0.9)\n\n# Pitch geometry helpers\n# Penalty area: 16.5m deep, 40.32m wide centred on goal (y = 34)\nPA_X1 <- 88.5; PA_X2 <- 105\nPA_Y1 <- 13.84; PA_Y2 <- 54.16\n# Goal area: 5.5m deep, 18.32m wide\nGA_X1 <- 99.5; GA_X2 <- 105\nGA_Y1 <- 24.84; GA_Y2 <- 43.16\n# Opp penalty area (left)\nOPA_X1 <- 0; OPA_X2 <- 16.5\nOPA_Y1 <- 13.84; OPA_Y2 <- 54.16\nOGA_X1 <- 0; OGA_X2 <- 5.5\nOGA_Y1 <- 24.84; OGA_Y2 <- 43.16\n\ntitle_str <- \"scatter-pitch-events · r · ggplot2 · anyplot.ai\"\n\np <- ggplot() +\n  # --- Pitch background ---\n  annotate(\"rect\", xmin = 0, xmax = 105, ymin = 0, ymax = 68,\n           fill = PITCH_FILL, color = PITCH_LINE, linewidth = 1.2) +\n  # Halfway line\n  annotate(\"segment\", x = 52.5, xend = 52.5, y = 0, yend = 68,\n           color = PITCH_LINE, linewidth = 0.8) +\n  # Centre circle (radius 9.15m)\n  annotate(\"path\",\n           x = 52.5 + 9.15 * cos(seq(0, 2 * pi, length.out = 100)),\n           y = 34   + 9.15 * sin(seq(0, 2 * pi, length.out = 100)),\n           color = PITCH_LINE, linewidth = 0.8) +\n  # Centre spot\n  annotate(\"point\", x = 52.5, y = 34, color = PITCH_LINE, size = 1.5) +\n  # Right penalty area\n  annotate(\"rect\", xmin = PA_X1, xmax = PA_X2,\n           ymin = PA_Y1, ymax = PA_Y2,\n           fill = NA, color = PITCH_LINE, linewidth = 0.8) +\n  # Right goal area\n  annotate(\"rect\", xmin = GA_X1, xmax = GA_X2,\n           ymin = GA_Y1, ymax = GA_Y2,\n           fill = NA, color = PITCH_LINE, linewidth = 0.8) +\n  # Left penalty area\n  annotate(\"rect\", xmin = OPA_X1, xmax = OPA_X2,\n           ymin = OPA_Y1, ymax = OPA_Y2,\n           fill = NA, color = PITCH_LINE, linewidth = 0.8) +\n  # Left goal area\n  annotate(\"rect\", xmin = OGA_X1, xmax = OGA_X2,\n           ymin = OGA_Y1, ymax = OGA_Y2,\n           fill = NA, color = PITCH_LINE, linewidth = 0.8) +\n  # Right penalty spot (11m from goal line)\n  annotate(\"point\", x = 94, y = 34, color = PITCH_LINE, size = 1.2) +\n  # Left penalty spot\n  annotate(\"point\", x = 11, y = 34, color = PITCH_LINE, size = 1.2) +\n  # Right penalty arc (partial circle outside box, radius 9.15m from spot)\n  {\n    arc_angles <- seq(pi * 0.82, pi * 1.18, length.out = 60)\n    annotate(\"path\",\n             x = 94 + 9.15 * cos(arc_angles),\n             y = 34 + 9.15 * sin(arc_angles),\n             color = PITCH_LINE, linewidth = 0.8)\n  } +\n  # Left penalty arc\n  {\n    arc_angles_l <- seq(-pi * 0.18, pi * 0.18, length.out = 60)\n    annotate(\"path\",\n             x = 11 + 9.15 * cos(arc_angles_l),\n             y = 34 + 9.15 * sin(arc_angles_l),\n             color = PITCH_LINE, linewidth = 0.8)\n  } +\n  # Corner arcs (radius 1m)\n  {\n    ca1 <- seq(0, pi / 2, length.out = 30)\n    ca2 <- seq(pi / 2, pi,     length.out = 30)\n    ca3 <- seq(pi, 3 * pi / 2, length.out = 30)\n    ca4 <- seq(3 * pi / 2, 2 * pi, length.out = 30)\n    list(\n      annotate(\"path\", x = 0   + cos(ca1), y = 0  + sin(ca1), color = PITCH_LINE, linewidth = 0.6),\n      annotate(\"path\", x = 0   + cos(ca2), y = 68 + sin(ca2), color = PITCH_LINE, linewidth = 0.6),\n      annotate(\"path\", x = 105 + cos(ca3), y = 68 + sin(ca3), color = PITCH_LINE, linewidth = 0.6),\n      annotate(\"path\", x = 105 + cos(ca4), y = 0  + sin(ca4), color = PITCH_LINE, linewidth = 0.6)\n    )\n  } +\n  # Goals (2.44m post-to-post depth outside pitch)\n  annotate(\"rect\", xmin = 105, xmax = 107.44,\n           ymin = 30.34, ymax = 37.66,\n           fill = NA, color = PITCH_LINE, linewidth = 1.0) +\n  annotate(\"rect\", xmin = -2.44, xmax = 0,\n           ymin = 30.34, ymax = 37.66,\n           fill = NA, color = PITCH_LINE, linewidth = 1.0) +\n\n  # --- Pass arrows ---\n  geom_segment(\n    data = pass_df,\n    aes(x = x, y = y, xend = xend, yend = yend),\n    arrow = arrow(length = unit(0.008, \"npc\"), type = \"closed\"),\n    color = IMPRINT_PALETTE[1], alpha = 0.55, linewidth = 0.7\n  ) +\n\n  # --- Shot arrows ---\n  geom_segment(\n    data = shot_df,\n    aes(x = x, y = y, xend = xend, yend = yend),\n    arrow = arrow(length = unit(0.010, \"npc\"), type = \"closed\"),\n    color = IMPRINT_PALETTE[2], alpha = 0.55, linewidth = 0.7\n  ) +\n\n  # --- Event markers ---\n  geom_point(\n    data = events,\n    aes(x = x, y = y,\n        color = event_type,\n        shape = event_type,\n        alpha = outcome),\n    size = 2.8,\n    stroke = 0.6\n  ) +\n\n  scale_color_manual(\n    name   = \"Event type\",\n    values = c(\n      \"pass\"          = IMPRINT_PALETTE[1],\n      \"shot\"          = IMPRINT_PALETTE[2],\n      \"tackle\"        = IMPRINT_PALETTE[3],\n      \"interception\"  = IMPRINT_PALETTE[4]\n    ),\n    labels = c(\"pass\" = \"Pass\", \"shot\" = \"Shot\",\n               \"tackle\" = \"Tackle\", \"interception\" = \"Interception\")\n  ) +\n  scale_shape_manual(\n    name   = \"Event type\",\n    values = c(\n      \"pass\"          = 16,  # circle\n      \"shot\"          = 18,  # diamond\n      \"tackle\"        = 17,  # triangle up\n      \"interception\"  = 15   # square\n    ),\n    labels = c(\"pass\" = \"Pass\", \"shot\" = \"Shot\",\n               \"tackle\" = \"Tackle\", \"interception\" = \"Interception\")\n  ) +\n  scale_alpha_manual(\n    name   = \"Outcome\",\n    values = c(\"successful\" = 0.90, \"unsuccessful\" = 0.35),\n    labels = c(\"successful\" = \"Successful\", \"unsuccessful\" = \"Unsuccessful\")\n  ) +\n\n  # Correct aspect ratio: 105m wide, 68m tall\n  coord_fixed(ratio = 1, xlim = c(-4, 109), ylim = c(-2, 70)) +\n\n  labs(\n    title = title_str,\n    x = \"Pitch length (m)\",\n    y = \"Pitch width (m)\"\n  ) +\n\n  theme_minimal(base_size = 8) +\n  theme(\n    plot.background   = element_rect(fill = PAGE_BG, color = PAGE_BG),\n    panel.background  = element_rect(fill = PAGE_BG, color = NA),\n    panel.grid.major  = element_blank(),\n    panel.grid.minor  = element_blank(),\n    panel.border      = element_blank(),\n    axis.title        = element_text(color = INK,      size = 10),\n    axis.text         = element_text(color = INK_SOFT, size = 8),\n    axis.ticks        = element_line(color = INK_SOFT),\n    plot.title        = element_text(color = INK,      size = 12,\n                                     margin = margin(b = 8)),\n    legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT,\n                                     linewidth = 0.4),\n    legend.text       = element_text(color = INK_SOFT, size = 8),\n    legend.title      = element_text(color = INK,      size = 9),\n    legend.position   = \"right\",\n    legend.key        = element_rect(fill = NA),\n    plot.margin       = margin(12, 12, 12, 12)\n  )\n\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 8,\n  height   = 4.5,\n  units    = \"in\",\n  dpi      = 400\n)\n"}