Basic Radar Chart — ggplot2

A radar chart (also known as spider or web chart) displays multivariate data on axes starting from a common center point, with values connected to form a polygon. Each axis represents a different variable, making it ideal for comparing multiple quantitative variables at once or visualizing strengths and weaknesses across categories.

Basic Radar Chart rendered with ggplot2

Renders

R source (ggplot2)

#' anyplot.ai
#' radar-basic: Basic Radar Chart
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 78/100 | Created: 2026-07-24
library(ggplot2)
library(dplyr)
library(ragg)

set.seed(42)

# coord_polar() curves straight edges into arcs; radar polygons need straight
# lines between vertices, so force linear interpolation via a custom coord.
coord_radar <- function(theta = "x", start = 0, direction = 1) {
  theta <- match.arg(theta, c("x", "y"))
  r <- if (theta == "x") "y" else "x"
  # clip = "off": CoordPolar defaults to clip = "on", which hard-clips
  # panel content to the circular/hexagonal boundary. The category-label
  # layer sits close to that boundary (label_radius near the y-scale max),
  # and its text bounding box — laid out horizontally, not radially — pokes
  # past the boundary on off-axis vertices, silently shaving off whichever
  # glyph sits at the extreme edge (e.g. the leading "P" of a left-side
  # label). Disabling clip removes that failure mode entirely.
  ggproto("CoordRadar", CoordPolar,
    theta = theta, r = r, start = start,
    direction = sign(direction),
    is_linear = function(coord) TRUE,
    clip = "off"
  )
}

# --- Theme tokens -------------------------------------------------------
THEME       <- Sys.getenv("ANYPLOT_THEME", "light")
PAGE_BG     <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420"
INK         <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
INK_SOFT    <- if (THEME == "light") "#4A4A44" else "#B8B7B0"
GRID_COLOR  <- scales::alpha(INK, 0.15)

# Imprint palette (see prompts/default-style-guide.md "Categorical Palette")
IMPRINT_PALETTE <- c("#009E73", "#C475FD", "#4467A3", "#BD8233",
                     "#AE3030", "#2ABCCD", "#954477", "#99B314")

# --- Data -----------------------------------------------------------------
competencies <- c("Communication", "Technical Skills", "Teamwork",
                   "Leadership", "Problem Solving", "Adaptability")
n_axes <- length(competencies)

reviews <- tibble::tibble(
  competency = rep(competencies, 2),
  employee   = factor(rep(c("Alice Chen", "Marcus Webb"), each = n_axes),
                       levels = c("Alice Chen", "Marcus Webb")),
  score      = c(85, 70, 90, 60, 80, 75,
                 60, 88, 68, 92, 74, 82)
) %>%
  mutate(axis_pos = rep(seq_len(n_axes), 2))

# Duplicate each series' first point at n_axes + 1 to close the polygon
closing_points <- reviews %>%
  filter(axis_pos == 1) %>%
  mutate(axis_pos = n_axes + 1)

radar_df <- bind_rows(reviews, closing_points)

# Category labels are drawn as a manual geom_text() layer, not axis.text.x:
# ggplot2's CoordPolar computes a per-angle hjust for axis text, and at
# certain angles that computation clips/mis-renders the leading glyph.
# A fixed hjust/vjust text layer sidesteps that rendering path entirely.
label_df <- tibble::tibble(
  axis_pos   = seq_len(n_axes),
  competency = competencies
)
label_radius <- 108

# --- Plot -------------------------------------------------------------------
p <- ggplot(radar_df, aes(x = axis_pos, y = score, group = employee)) +
  geom_polygon(aes(fill = employee), color = NA, alpha = 0.25) +
  geom_line(aes(color = employee), linewidth = 1.1) +
  geom_point(aes(color = employee), size = 2.8) +
  geom_text(data = label_df,
            aes(x = axis_pos, y = label_radius, label = competency),
            inherit.aes = FALSE, color = INK, size = 3.9,
            hjust = 0.5, vjust = 0.5) +
  coord_radar(theta = "x", start = 0) +
  scale_x_continuous(breaks = seq_len(n_axes), labels = competencies,
                      limits = c(1, n_axes + 1), expand = c(0, 0)) +
  scale_y_continuous(limits = c(0, 120), breaks = seq(0, 100, 20),
                      expand = c(0, 0)) +
  scale_fill_manual(values = IMPRINT_PALETTE[1:2], name = NULL) +
  scale_color_manual(values = IMPRINT_PALETTE[1:2], name = NULL) +
  labs(title = "radar-basic · r · ggplot2 · anyplot.ai",
       subtitle = "Score (0-100 scale)") +
  theme_minimal(base_size = 8) +
  theme(
    plot.background   = element_rect(fill = PAGE_BG, color = PAGE_BG),
    panel.background  = element_rect(fill = PAGE_BG, color = NA),
    panel.grid.major  = element_line(color = GRID_COLOR, linewidth = 0.4),
    panel.grid.minor  = element_blank(),
    axis.title        = element_blank(),
    axis.text.x       = element_blank(),
    axis.text.y       = element_text(color = INK_SOFT, size = 8),
    axis.ticks        = element_blank(),
    plot.title        = element_text(color = INK, size = 12, hjust = 0.5),
    plot.subtitle     = element_text(color = INK_SOFT, size = 7.5, hjust = 0.5,
                                      margin = margin(t = 4, b = 4)),
    legend.position    = "bottom",
    legend.background = element_rect(fill = ELEVATED_BG, color = NA),
    legend.text       = element_text(color = INK_SOFT, size = 9),
    legend.title       = element_blank()
  )

# --- Save -------------------------------------------------------------------
ggsave(
  filename = sprintf("plot-%s.png", THEME),
  plot     = p,
  device   = ragg::agg_png,
  width    = 6,
  height   = 6,
  units    = "in",
  dpi      = 400
)

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/radar-basic/ggplot2/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "radar-basic",
  "language": "r",
  "library": "ggplot2",
  "page": "https://anyplot.ai/radar-basic/r/ggplot2",
  "hub": "https://anyplot.ai/radar-basic",
  "code_json": "https://api.anyplot.ai/specs/radar-basic/ggplot2/code",
  "spec_json": "https://api.anyplot.ai/specs/radar-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/radar-basic/r/ggplot2/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/radar-basic/r/ggplot2/plot-dark.png",
  "quality_score": 78.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Basic Radar Chart on anyplot.ai.

Other implementations