Basic Radar Chart — plotnine

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 plotnine

Python source (plotnine)

""" anyplot.ai
radar-basic: Basic Radar Chart
Library: plotnine 0.15.7 | Python 3.13.14
Quality: 87/100 | Updated: 2026-07-24
"""

import math
import os
import sys


# Prevent this file from shadowing the plotnine library when run from its own directory
sys.path = [p for p in sys.path if not p or os.path.abspath(p) != os.path.abspath(os.path.dirname(__file__))]

import numpy as np
import pandas as pd
from plotnine import (
    aes,
    coord_fixed,
    element_blank,
    element_rect,
    element_text,
    geom_path,
    geom_point,
    geom_polygon,
    geom_text,
    ggplot,
    labs,
    scale_color_manual,
    scale_fill_manual,
    scale_x_continuous,
    scale_y_continuous,
    theme,
)


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
GRID_COLOR = "#C8C8C0" if THEME == "light" else "#4A4A44"

IMPRINT = ["#009E73", "#C475FD"]

# Data — employee performance metrics
categories = ["Technical", "Communication", "Leadership", "Creativity", "Teamwork", "Problem Solving"]
values_alice = [85, 70, 60, 90, 75, 80]
values_bob = [70, 85, 75, 65, 90, 70]

n = len(categories)
angles = [i * 2 * math.pi / n for i in range(n)]

# Build cartesian coordinates for each series
data_rows = []
for i, (cat, val_a, val_b, angle) in enumerate(zip(categories, values_alice, values_bob, angles, strict=True)):
    data_rows.append({"category": cat, "value": val_a, "angle": angle, "series": "Alice", "order": i})
    data_rows.append({"category": cat, "value": val_b, "angle": angle, "series": "Bob", "order": i})

# Close polygons by repeating the first point
data_rows.append(
    {"category": categories[0], "value": values_alice[0], "angle": angles[0], "series": "Alice", "order": n}
)
data_rows.append({"category": categories[0], "value": values_bob[0], "angle": angles[0], "series": "Bob", "order": n})

df = pd.DataFrame(data_rows)
df["x"] = df["value"] * np.cos(df["angle"] - math.pi / 2)
df["y"] = df["value"] * np.sin(df["angle"] - math.pi / 2)

# Gridlines (concentric circles at 20, 40, 60, 80, 100)
grid_rows = []
grid_angles = np.linspace(0, 2 * math.pi, 101)
for radius in [20, 40, 60, 80, 100]:
    for a in grid_angles:
        grid_rows.append(
            {"x": radius * math.cos(a - math.pi / 2), "y": radius * math.sin(a - math.pi / 2), "radius": radius}
        )
grid_df = pd.DataFrame(grid_rows)

# Spokes (axis lines from center to edge)
spoke_rows = []
for angle in angles:
    spoke_rows.append({"x": 0, "y": 0, "angle_group": angle})
    spoke_rows.append(
        {"x": 105 * math.cos(angle - math.pi / 2), "y": 105 * math.sin(angle - math.pi / 2), "angle_group": angle}
    )
spoke_df = pd.DataFrame(spoke_rows)

# Category labels positioned just outside the chart
label_rows = []
for cat, angle in zip(categories, angles, strict=True):
    label_rows.append(
        {"label": cat, "x": 115 * math.cos(angle - math.pi / 2), "y": 115 * math.sin(angle - math.pi / 2)}
    )
label_df = pd.DataFrame(label_rows)

# Plot
plot = (
    ggplot()
    + geom_path(aes(x="x", y="y", group="radius"), data=grid_df, color=GRID_COLOR, size=0.25, linetype="dashed")
    + geom_path(aes(x="x", y="y", group="angle_group"), data=spoke_df, color=GRID_COLOR, size=0.25)
    + geom_polygon(aes(x="x", y="y", fill="series", group="series"), data=df, alpha=0.25)
    + geom_path(aes(x="x", y="y", color="series", group="series"), data=df, size=0.75)
    + geom_point(aes(x="x", y="y", color="series"), data=df[df["order"] < n], size=2.5)
    + geom_text(aes(x="x", y="y", label="label"), data=label_df, size=8, color=INK)
    + scale_fill_manual(values=IMPRINT)
    + scale_color_manual(values=IMPRINT)
    + scale_x_continuous(limits=(-138, 138))
    + scale_y_continuous(limits=(-138, 138))
    + coord_fixed(ratio=1)
    + labs(title="radar-basic · python · plotnine · anyplot.ai", fill="Employee", color="Employee")
    + theme(
        figure_size=(6, 6),
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_background=element_rect(fill=PAGE_BG),
        plot_title=element_text(size=12, weight="bold", color=INK),
        legend_title=element_text(size=9, color=INK),
        legend_text=element_text(size=8, color=INK_SOFT),
        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
        legend_margin=8,
        legend_key_width=24,
        axis_title=element_blank(),
        axis_text=element_blank(),
        axis_ticks=element_blank(),
        axis_line=element_blank(),
        panel_grid_major=element_blank(),
        panel_grid_minor=element_blank(),
    )
)

# Save
plot.save(f"plot-{THEME}.png", dpi=400, width=6, height=6, units="in")

Part of Basic Radar Chart on anyplot.ai.

Other implementations