Activity Rings Progress Chart in plotnine (Python)

The same plot in 14 other libraries — Python: Altair, Bokeh, lets-plot, Matplotlib, Plotly, Pygal, Seaborn; R: ggplot2; Julia: Makie.jl; JavaScript: Chart.js, D3.js, Apache ECharts, Highcharts, MUI X Charts. Compare all 15 side by side: Activity Rings Progress Chart in Python, R, Julia and JavaScript.

An activity rings chart displays goal completion for several metrics at once using concentric radial progress arcs, in the style popularized by fitness trackers. Each ring is a partial arc with rounded end caps that starts at the top (12 o'clock) and sweeps clockwise in proportion to how close the metric is to its goal, with a distinct color per metric and a faint background track showing the full circle. It is instantly recognizable and ideal for at-a-glance comparison of progress across a small set of related goals, filling the general radial-progress gap that needle gauges (`gauge-basic`) and polar bars (`polar-bar`) do not cover.

Activity Rings Progress Chart rendered with plotnine

Renders

Python source (plotnine)

""" anyplot.ai
gauge-activity-rings: Activity Rings Progress Chart
Library: plotnine 0.15.7 | Python 3.13.13
Quality: 88/100 | Created: 2026-06-14
"""

import os

import numpy as np
import pandas as pd
from plotnine import (
    aes,
    coord_equal,
    element_blank,
    element_rect,
    element_text,
    geom_path,
    geom_text,
    ggplot,
    labs,
    scale_color_manual,
    theme,
    theme_void,
)


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"

# Imprint palette — positions 1-3 for Move / Exercise / Stand
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3"]

# Fitness tracker: Move / Exercise / Stand goals (spec example data)
ring_configs = [
    {"metric": "Move", "value": 420, "goal": 600, "radius": 3.0},
    {"metric": "Exercise", "value": 25, "goal": 30, "radius": 2.0},
    {"metric": "Stand", "value": 9, "goal": 12, "radius": 1.0},
]

# Track alpha — slightly more visible on dark surfaces
TRACK_ALPHA = 0.20 if THEME == "light" else 0.32

# Decompose background color once for track blending
_bg = [int(PAGE_BG.lstrip("#")[j : j + 2], 16) for j in (0, 2, 4)]

# Build arc point data
# Clockwise from top: x = r*sin(theta), y = r*cos(theta), theta in [0, 2pi)
N_SMOOTH = 400

track_rows = []
arc_rows = []
color_dict = {}
legend_breaks = []
legend_labels = []
ann_rows = []

for i, ring in enumerate(ring_configs):
    progress = min(ring["value"] / ring["goal"], 1.0)
    r = ring["radius"]
    color = IMPRINT_PALETTE[i]

    # Blend ring color onto background for the faint track arc
    _fg = [int(color.lstrip("#")[j : j + 2], 16) for j in (0, 2, 4)]
    track_color = "#{:02X}{:02X}{:02X}".format(
        *[int(_fg[j] * TRACK_ALPHA + _bg[j] * (1 - TRACK_ALPHA)) for j in range(3)]
    )

    track_key = f"{ring['metric']}_track"
    arc_key = f"{ring['metric']}_arc"

    # Full-circle background track
    thetas = np.linspace(0, 2 * np.pi, N_SMOOTH + 1)
    track_rows.append(pd.DataFrame({"x": r * np.sin(thetas), "y": r * np.cos(thetas), "group": track_key}))

    # Progress arc from 12 o'clock, sweeping clockwise
    n_pts = max(3, int(N_SMOOTH * progress))
    arc_thetas = np.linspace(0, progress * 2 * np.pi, n_pts)
    arc_rows.append(pd.DataFrame({"x": r * np.sin(arc_thetas), "y": r * np.cos(arc_thetas), "group": arc_key}))

    color_dict[track_key] = track_color
    color_dict[arc_key] = color

    legend_breaks.append(arc_key)
    pct = round(ring["value"] / ring["goal"] * 100)
    legend_labels.append(f"{ring['metric']}   {ring['value']} / {ring['goal']}   ({pct}%)")

    # Endpoint annotation: % label placed just outside the arc tip
    theta_end = progress * 2 * np.pi
    r_lbl = r + 0.55
    ann_rows.append({"x": r_lbl * np.sin(theta_end), "y": r_lbl * np.cos(theta_end), "label": f"{pct}%"})

# Center headline: overall average completion
avg_pct = round(sum(min(r["value"] / r["goal"], 1.0) for r in ring_configs) / len(ring_configs) * 100)
df_center = pd.DataFrame({"x": [0.0, 0.0], "y": [0.22, -0.42], "label": [f"{avg_pct}%", "avg"]})
df_ann = pd.DataFrame(ann_rows)

# Tracks drawn first (underneath), arcs drawn on top
df_tracks = pd.concat(track_rows, ignore_index=True)
df_arcs = pd.concat(arc_rows, ignore_index=True)
df = pd.concat([df_tracks, df_arcs], ignore_index=True)

all_groups = list(color_dict.keys())
df["group"] = pd.Categorical(df["group"], categories=all_groups)

title = "gauge-activity-rings · python · plotnine · anyplot.ai"

plot = (
    ggplot(df, aes(x="x", y="y", group="group", color="group"))
    + geom_path(data=df_tracks, size=9, lineend="round")
    + geom_path(data=df_arcs, size=9, lineend="round")
    + geom_text(mapping=aes(x="x", y="y", label="label"), data=df_center, color=INK, size=4.5, inherit_aes=False)
    + geom_text(mapping=aes(x="x", y="y", label="label"), data=df_ann, color=INK_SOFT, size=2.8, inherit_aes=False)
    + coord_equal(xlim=(-4.0, 4.0), ylim=(-4.0, 4.0))
    + scale_color_manual(values=color_dict, breaks=legend_breaks, labels=legend_labels, name="")
    + labs(title=title, x=None, y=None)
    + theme_void()
    + 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(color=INK, size=12, hjust=0.5),
        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
        legend_text=element_text(color=INK_SOFT, size=8),
        legend_position="bottom",
        legend_title=element_blank(),
        legend_key=element_rect(fill=PAGE_BG, color=PAGE_BG),
    )
)

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

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/gauge-activity-rings/plotnine/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": "gauge-activity-rings",
  "language": "python",
  "library": "plotnine",
  "page": "https://anyplot.ai/gauge-activity-rings/python/plotnine",
  "hub": "https://anyplot.ai/gauge-activity-rings",
  "code_json": "https://api.anyplot.ai/specs/gauge-activity-rings/plotnine/code",
  "spec_json": "https://api.anyplot.ai/specs/gauge-activity-rings",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/gauge-activity-rings/python/plotnine/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/gauge-activity-rings/python/plotnine/plot-dark.png",
  "quality_score": 88.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Activity Rings Progress Chart on anyplot.ai.

Other implementations