Activity Rings Progress Chart — Chart.js

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 Chart.js

JavaScript source (Chart.js)

// anyplot.ai
// gauge-activity-rings: Activity Rings Progress Chart
// Library: chartjs 4.4.7 | JavaScript 22.22.3
// Quality: 92/100 | Created: 2026-06-14
//# anyplot-orientation: square

const t = window.ANYPLOT_TOKENS;
const isDark = window.ANYPLOT_THEME === "dark";

// --- Helpers ---
function hexToRgba(hex, alpha) {
  const r = parseInt(hex.slice(1, 3), 16);
  const g = parseInt(hex.slice(3, 5), 16);
  const b = parseInt(hex.slice(5, 7), 16);
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}

// --- Data (daily fitness summary) ---
const rings = [
  { label: "Move",     value: 420, goal: 600, unit: "kcal" },
  { label: "Exercise", value: 38,  goal: 30,  unit: "min"  },
  { label: "Stand",    value: 9,   goal: 12,  unit: "hr"   },
];

// Theme-adaptive track opacity: dark bg absorbs translucent colours; increase alpha
const trackAlpha = isDark ? 0.28 : 0.15;

const datasets = rings.map((ring, i) => {
  const fraction = Math.min(ring.value / ring.goal, 1.0);
  const color = t.palette[i];
  return {
    label: ring.label,
    data: [fraction, 1 - fraction],
    backgroundColor: [color, hexToRgba(color, trackAlpha)],
    borderWidth: 0,
    borderRadius: (ctx) => (ctx.dataIndex === 0 ? 32 : 0),
  };
});

const avgPct = Math.round(
  rings.reduce((sum, r) => sum + Math.min(r.value / r.goal, 1), 0) / rings.length * 100
);

// --- Mount ---
const canvas = document.createElement("canvas");
document.getElementById("container").appendChild(canvas);

// --- Plugin: center summary + glowing progress-tip dots ---
const centerTextPlugin = {
  id: "centerText",
  afterDraw(chart) {
    const { ctx, chartArea } = chart;
    const cx = (chartArea.left + chartArea.right) / 2;
    const cy = (chartArea.top + chartArea.bottom) / 2;

    // Soft radial glow behind the percentage — lifts the focal point
    ctx.save();
    const glowR = 90;
    const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, glowR);
    grad.addColorStop(0, hexToRgba(t.palette[0], isDark ? 0.12 : 0.06));
    grad.addColorStop(1, "rgba(0,0,0,0)");
    ctx.fillStyle = grad;
    ctx.beginPath();
    ctx.arc(cx, cy, glowR, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();

    // Center percentage and sub-label
    ctx.save();
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.font = `bold 62px system-ui, sans-serif`;
    ctx.fillStyle = t.ink;
    ctx.fillText(`${avgPct}%`, cx, cy - 28);
    ctx.font = `20px system-ui, sans-serif`;
    ctx.fillStyle = t.inkSoft;
    ctx.fillText("avg. complete", cx, cy + 30);
    ctx.restore();

    // Glowing dot at each ring's progress endpoint (skips full/over-100% rings)
    rings.forEach((ring, i) => {
      if (ring.value / ring.goal >= 1.0) return;
      const meta = chart.getDatasetMeta(i);
      const arc = meta.data[0];
      if (!arc) return;
      const dotR = (arc.outerRadius - arc.innerRadius) / 2;
      const midR = (arc.outerRadius + arc.innerRadius) / 2;
      const tipX = arc.x + midR * Math.cos(arc.endAngle);
      const tipY = arc.y + midR * Math.sin(arc.endAngle);
      ctx.save();
      ctx.beginPath();
      ctx.arc(tipX, tipY, dotR, 0, Math.PI * 2);
      ctx.fillStyle = t.palette[i];
      ctx.shadowColor = hexToRgba(t.palette[i], 0.55);
      ctx.shadowBlur = 14;
      ctx.fill();
      ctx.restore();
    });
  },
};

// --- Chart ---
new Chart(canvas, {
  type: "doughnut",
  plugins: [centerTextPlugin],
  data: {
    labels: ["Progress", "Remaining"],
    datasets,
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    animation: false,
    rotation: -90,
    circumference: 360,
    cutout: "42%",
    plugins: {
      title: {
        display: true,
        text: "gauge-activity-rings · javascript · chartjs · anyplot.ai",
        color: t.ink,
        font: { size: 22 },
        padding: { top: 16, bottom: 16 },
      },
      legend: {
        display: true,
        position: "bottom",
        labels: {
          color: t.ink,
          font: { size: 17 },
          padding: 28,
          usePointStyle: true,
          pointStyle: "circle",
          generateLabels() {
            return rings.map((ring, i) => {
              const fraction = ring.value / ring.goal;
              const pct = Math.round(fraction * 100);
              const prefix = fraction > 1 ? ">" : "";
              return {
                text: `${ring.label}  ·  ${ring.value} / ${ring.goal} ${ring.unit}  ·  ${prefix}${pct}%`,
                fillStyle: t.palette[i],
                strokeStyle: "transparent",
                lineWidth: 0,
                hidden: false,
                datasetIndex: i,
                pointStyle: "circle",
              };
            });
          },
        },
      },
      tooltip: { enabled: false },
    },
  },
});

Part of Activity Rings Progress Chart on anyplot.ai.

Other implementations