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.

""" 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")
Part of Activity Rings Progress Chart on anyplot.ai.