Activity Rings Progress Chart — D3.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 D3.js

JavaScript source (D3.js)

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

const t = window.ANYPLOT_TOKENS;
const { width, height } = window.ANYPLOT_SIZE;

// --- Data: daily fitness goals (3-ring canonical layout) --------------------
const rings = [
  { metric: "Move",     value: 420, goal: 600, unit: "kcal" },
  { metric: "Exercise", value: 25,  goal: 30,  unit: "min"  },
  { metric: "Stand",    value: 9,   goal: 12,  unit: "hr"   },
];
const colors = [t.palette[0], t.palette[1], t.palette[2]];

// --- Layout -----------------------------------------------------------------
const cx     = width / 2;
const cy     = Math.round(height * 0.48);
const strokeW = Math.round(width * 0.05);    // 60px track width at 1200 CSS px
const step    = Math.round(width * 0.0708);  // 85px center-to-center ring spacing
const outerR  = Math.round(width * 0.25);    // 300px outermost ring radius
const radii   = rings.map((_, i) => outerR - i * step);

const START = -Math.PI / 2; // 12 o'clock position

// --- SVG mount --------------------------------------------------------------
const svg = d3.select("#container")
  .append("svg")
  .attr("width", width)
  .attr("height", height);

const g = svg.append("g").attr("transform", `translate(${cx},${cy})`);

// SVG arc path for a stroke-based circular arc (supports stroke-linecap: round)
const arcPath = (r, fraction) => {
  const f = Math.min(fraction, 0.9999);
  const angle = f * 2 * Math.PI;
  const x1 = r * Math.cos(START);
  const y1 = r * Math.sin(START);
  const x2 = r * Math.cos(START + angle);
  const y2 = r * Math.sin(START + angle);
  return `M ${x1},${y1} A ${r},${r} 0 ${angle > Math.PI ? 1 : 0},1 ${x2},${y2}`;
};

// --- Draw rings (outer → inner) ---------------------------------------------
rings.forEach((d, i) => {
  const r = radii[i];
  const fraction = d.value / d.goal;
  const color = colors[i];

  // Full-circle background track
  g.append("circle")
    .attr("r", r)
    .attr("fill", "none")
    .attr("stroke", color)
    .attr("stroke-width", strokeW)
    .attr("opacity", 0.18);

  // Progress arc with rounded linecaps (iconic activity-ring look)
  g.append("path")
    .attr("d", arcPath(r, fraction))
    .attr("fill", "none")
    .attr("stroke", color)
    .attr("stroke-width", strokeW)
    .attr("stroke-linecap", "round");
});

// --- Center label: average completion ---------------------------------------
const avgPct  = Math.round(rings.reduce((s, d) => s + d.value / d.goal, 0) / rings.length * 100);
const pctSize = Math.round(width * 0.038);  // 46px
const subSize = Math.round(width * 0.014);  // 17px

g.append("text")
  .attr("text-anchor", "middle")
  .attr("y", Math.round(pctSize * 0.36))
  .attr("fill", t.ink)
  .style("font-size", `${pctSize}px`)
  .style("font-weight", "700")
  .text(`${avgPct}%`);

g.append("text")
  .attr("text-anchor", "middle")
  .attr("y", Math.round(pctSize * 0.36 + subSize + 9))
  .attr("fill", t.inkSoft)
  .style("font-size", `${subSize}px`)
  .text("complete");

// --- Legend below rings -----------------------------------------------------
const legendY  = Math.round(cy + outerR + strokeW / 2 + height * 0.04);
const colW     = Math.round(width / 3);
const nameSize = Math.round(width * 0.017);  // 20px
const valSize  = Math.round(width * 0.013);  // 16px
const dotR     = Math.round(width * 0.008);  // 10px

rings.forEach((d, i) => {
  const lx   = colW * i + colW / 2;
  const pct  = Math.round(d.value / d.goal * 100);
  const color = colors[i];

  svg.append("circle")
    .attr("cx", lx - Math.round(nameSize * 4.5))
    .attr("cy", legendY)
    .attr("r", dotR)
    .attr("fill", color);

  svg.append("text")
    .attr("x", lx)
    .attr("y", legendY + 5)
    .attr("text-anchor", "middle")
    .attr("fill", t.ink)
    .style("font-size", `${nameSize}px`)
    .style("font-weight", "600")
    .text(`${d.metric}  ·  ${pct}%`);

  svg.append("text")
    .attr("x", lx)
    .attr("y", legendY + nameSize + 14)
    .attr("text-anchor", "middle")
    .attr("fill", t.inkSoft)
    .style("font-size", `${valSize}px`)
    .text(`${d.value} / ${d.goal} ${d.unit}`);
});

// --- Title ------------------------------------------------------------------
svg.append("text")
  .attr("x", width / 2)
  .attr("y", 44)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "22px")
  .style("font-weight", "600")
  .text("gauge-activity-rings · javascript · d3 · anyplot.ai");

Part of Activity Rings Progress Chart on anyplot.ai.

Other implementations