{"spec_id":"radar-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' radar-basic: Basic Radar Chart\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 78/100 | Created: 2026-07-24\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(ragg)\n\nset.seed(42)\n\n# coord_polar() curves straight edges into arcs; radar polygons need straight\n# lines between vertices, so force linear interpolation via a custom coord.\ncoord_radar <- function(theta = \"x\", start = 0, direction = 1) {\n  theta <- match.arg(theta, c(\"x\", \"y\"))\n  r <- if (theta == \"x\") \"y\" else \"x\"\n  # clip = \"off\": CoordPolar defaults to clip = \"on\", which hard-clips\n  # panel content to the circular/hexagonal boundary. The category-label\n  # layer sits close to that boundary (label_radius near the y-scale max),\n  # and its text bounding box — laid out horizontally, not radially — pokes\n  # past the boundary on off-axis vertices, silently shaving off whichever\n  # glyph sits at the extreme edge (e.g. the leading \"P\" of a left-side\n  # label). Disabling clip removes that failure mode entirely.\n  ggproto(\"CoordRadar\", CoordPolar,\n    theta = theta, r = r, start = start,\n    direction = sign(direction),\n    is_linear = function(coord) TRUE,\n    clip = \"off\"\n  )\n}\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\"\nGRID_COLOR  <- scales::alpha(INK, 0.15)\n\n# Imprint palette (see prompts/default-style-guide.md \"Categorical Palette\")\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# --- Data -----------------------------------------------------------------\ncompetencies <- c(\"Communication\", \"Technical Skills\", \"Teamwork\",\n                   \"Leadership\", \"Problem Solving\", \"Adaptability\")\nn_axes <- length(competencies)\n\nreviews <- tibble::tibble(\n  competency = rep(competencies, 2),\n  employee   = factor(rep(c(\"Alice Chen\", \"Marcus Webb\"), each = n_axes),\n                       levels = c(\"Alice Chen\", \"Marcus Webb\")),\n  score      = c(85, 70, 90, 60, 80, 75,\n                 60, 88, 68, 92, 74, 82)\n) %>%\n  mutate(axis_pos = rep(seq_len(n_axes), 2))\n\n# Duplicate each series' first point at n_axes + 1 to close the polygon\nclosing_points <- reviews %>%\n  filter(axis_pos == 1) %>%\n  mutate(axis_pos = n_axes + 1)\n\nradar_df <- bind_rows(reviews, closing_points)\n\n# Category labels are drawn as a manual geom_text() layer, not axis.text.x:\n# ggplot2's CoordPolar computes a per-angle hjust for axis text, and at\n# certain angles that computation clips/mis-renders the leading glyph.\n# A fixed hjust/vjust text layer sidesteps that rendering path entirely.\nlabel_df <- tibble::tibble(\n  axis_pos   = seq_len(n_axes),\n  competency = competencies\n)\nlabel_radius <- 108\n\n# --- Plot -------------------------------------------------------------------\np <- ggplot(radar_df, aes(x = axis_pos, y = score, group = employee)) +\n  geom_polygon(aes(fill = employee), color = NA, alpha = 0.25) +\n  geom_line(aes(color = employee), linewidth = 1.1) +\n  geom_point(aes(color = employee), size = 2.8) +\n  geom_text(data = label_df,\n            aes(x = axis_pos, y = label_radius, label = competency),\n            inherit.aes = FALSE, color = INK, size = 3.9,\n            hjust = 0.5, vjust = 0.5) +\n  coord_radar(theta = \"x\", start = 0) +\n  scale_x_continuous(breaks = seq_len(n_axes), labels = competencies,\n                      limits = c(1, n_axes + 1), expand = c(0, 0)) +\n  scale_y_continuous(limits = c(0, 120), breaks = seq(0, 100, 20),\n                      expand = c(0, 0)) +\n  scale_fill_manual(values = IMPRINT_PALETTE[1:2], name = NULL) +\n  scale_color_manual(values = IMPRINT_PALETTE[1:2], name = NULL) +\n  labs(title = \"radar-basic · r · ggplot2 · anyplot.ai\",\n       subtitle = \"Score (0-100 scale)\") +\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_line(color = GRID_COLOR, linewidth = 0.4),\n    panel.grid.minor  = element_blank(),\n    axis.title        = element_blank(),\n    axis.text.x       = element_blank(),\n    axis.text.y       = element_text(color = INK_SOFT, size = 8),\n    axis.ticks        = element_blank(),\n    plot.title        = element_text(color = INK, size = 12, hjust = 0.5),\n    plot.subtitle     = element_text(color = INK_SOFT, size = 7.5, hjust = 0.5,\n                                      margin = margin(t = 4, b = 4)),\n    legend.position    = \"bottom\",\n    legend.background = element_rect(fill = ELEVATED_BG, color = NA),\n    legend.text       = element_text(color = INK_SOFT, size = 9),\n    legend.title       = element_blank()\n  )\n\n# --- Save -------------------------------------------------------------------\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 6,\n  height   = 6,\n  units    = \"in\",\n  dpi      = 400\n)\n"}