Activity Rings Progress Chart in lets-plot (Python)

The same plot in 14 other libraries — Python: Altair, Bokeh, Matplotlib, Plotly, plotnine, 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 lets-plot

Renders

Python source (lets-plot)

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

import os

import numpy as np
import pandas as pd
from lets_plot import *


LetsPlot.setup_html()

# 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"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Imprint palette — positions 1-3 for the three rings
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]

# Data — daily fitness tracker activity rings
rings_data = [
    {"name": "Move", "value": 420, "goal": 600, "unit": "kcal"},
    {"name": "Exercise", "value": 25, "goal": 30, "unit": "min"},
    {"name": "Stand", "value": 9, "goal": 12, "unit": "hr"},
]
ring_color_map = {r["name"]: IMPRINT_PALETTE[i] for i, r in enumerate(rings_data)}

# Ring geometry — outer to inner
radii = [2.5, 1.7, 0.9]
n_pts = 300

# Build arc path data
track_rows, arc_rows = [], []
for ring, r in zip(rings_data, radii, strict=False):
    frac = min(ring["value"] / ring["goal"], 1.0)
    pct = ring["value"] / ring["goal"] * 100

    # Background track (full circle, low opacity)
    a_track = np.linspace(np.pi / 2, np.pi / 2 - 2 * np.pi, n_pts + 1)
    for a in a_track:
        track_rows.append(
            {"x": r * np.cos(a), "y": r * np.sin(a), "group": f"track_{ring['name']}", "name": ring["name"]}
        )

    # Progress arc sweeping clockwise from 12 o'clock
    n_arc = max(4, int(n_pts * frac) + 1)
    a_arc = np.linspace(np.pi / 2, np.pi / 2 - 2 * np.pi * frac, n_arc)
    for a in a_arc:
        arc_rows.append(
            {
                "x": r * np.cos(a),
                "y": r * np.sin(a),
                "group": f"arc_{ring['name']}",
                "name": ring["name"],
                "pct_label": f"{pct:.0f}%",
                "progress": f"{ring['value']} / {ring['goal']} {ring['unit']}",
            }
        )

track_df = pd.DataFrame(track_rows)
arc_df = pd.DataFrame(arc_rows)

# Labels below the rings — centered under each ring's approximate x position
label_x = [-2.4, 0.0, 2.4]
label_rows, sub_rows = [], []
for ring, xp in zip(rings_data, label_x, strict=False):
    pct = ring["value"] / ring["goal"] * 100
    label_rows.append(
        {
            "x": xp,
            "y": -3.2,
            "label": f"{ring['name']}  {pct:.0f}%",
            "name": ring["name"],
            "pct_label": f"{pct:.0f}%",
            "progress": f"{ring['value']} / {ring['goal']} {ring['unit']}",
        }
    )
    sub_rows.append(
        {"x": xp, "y": -3.7, "label": f"{ring['value']} / {ring['goal']} {ring['unit']}", "name": ring["name"]}
    )

label_df = pd.DataFrame(label_rows)
sub_df = pd.DataFrame(sub_rows)

# Center annotation
center_df = pd.DataFrame([{"x": 0, "y": 0.22, "label": "Daily"}, {"x": 0, "y": -0.30, "label": "Activity"}])

# Title — scaled for length
title = "gauge-activity-rings · python · letsplot · anyplot.ai"
n_chars = len(title)
ratio = 67 / n_chars if n_chars > 67 else 1.0
title_size = max(11, round(16 * ratio))

anyplot_theme = theme_void() + theme(
    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
    panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
    panel_border=element_blank(),
    plot_title=element_text(color=INK, size=title_size, hjust=0.5),
    legend_position="none",
)

plot = (
    ggplot()
    # Background tracks (full circles, faint)
    + geom_path(
        data=track_df, mapping=aes(x="x", y="y", group="group", color="name"), size=8, alpha=0.15, show_legend=False
    )
    # Progress arcs with rounded end caps for the iconic activity-ring look
    + geom_path(
        data=arc_df,
        mapping=aes(x="x", y="y", group="group", color="name"),
        size=8,
        show_legend=False,
        lineend="round",
        tooltips=layer_tooltips().line("@name").line("Progress: @pct_label").line("@progress"),
    )
    # Ring name + percentage labels (with letsplot interactive tooltips)
    + geom_text(
        data=label_df,
        mapping=aes(x="x", y="y", label="label", color="name"),
        size=4.0,
        hjust=0.5,
        show_legend=False,
        tooltips=layer_tooltips().line("@name").line("@pct_label").line("@progress"),
    )
    # Value / goal sub-labels
    + geom_text(
        data=sub_df, mapping=aes(x="x", y="y", label="label"), color=INK_MUTED, size=3.0, hjust=0.5, show_legend=False
    )
    # Center text
    + geom_text(data=center_df, mapping=aes(x="x", y="y", label="label"), color=INK_SOFT, size=4.0, show_legend=False)
    + scale_color_manual(values=ring_color_map)
    + scale_x_continuous(limits=[-3.5, 3.5])
    + scale_y_continuous(limits=[-4.3, 3.3])
    + coord_fixed()
    + labs(title=title)
    + ggsize(600, 600)
    + anyplot_theme
)

# Save PNG (square canvas: 600 × 600 × scale=4 = 2400 × 2400 px)
ggsave(plot, f"plot-{THEME}.png", path=".", scale=4)
ggsave(plot, f"plot-{THEME}.html", path=".")

Retrieve this implementation

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

Part of Activity Rings Progress Chart on anyplot.ai.

Other implementations