npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

multi-layer-radial-chart

v1.0.0

Published

A lightweight, dependency-free React 19 + TypeScript library for animated multi-layer radial (activity-ring) charts, rendered with pure SVG.

Downloads

319

Readme

Multi-Layer Radial Chart

npm version npm downloads minzipped size CI provenance license: MIT

Animated, Apple-Watch-style activity rings for React — rendered with pure SVG, zero chart dependencies.

Live Demo · Component Explorer (Storybook) · Open in StackBlitz

Overview

A high-performance, dependency-free React 19 + TypeScript library for multi-layer radial (activity-ring) charts. It takes an array of { value, max, color, label } items and renders concentric progress rings with smooth animated transitions. Built with a focus on clean architecture (math fully separated from the UI), performance (rAF-based tweening with proper cleanup), and robust, strict TypeScript (no any).

Key Features

  • Pure SVG rendering: crisp at any DPI, responsive via viewBox, no canvas or heavy chart libs.
  • Smooth animations: requestAnimationFrame tweening with ease-out cubic; honors prefers-reduced-motion.
  • Fully responsive: recalculates radius and stroke widths from the container size via ResizeObserver, or pass a fixed size.
  • Accessible by design: each ring is a role="progressbar" with aria-valuenow/min/max; optional legend and pattern (dashed) support so color is never the only indicator.
  • Flexible layouts: full circle, gauge (270°) or semicircle (180°) via maxSweepDegrees; optional hover tooltip and centered content via children.
  • Strictly typed & tree-shakeable: precise interfaces, ESM + CJS builds, sideEffects-aware, ships its own tiny CSS.

Architectural Highlights & Challenges Solved

Challenge: An SVG elliptical arc (A command) cannot draw a full 360° circle — start and end points coincide, so the arc collapses and renders nothing. Solution: describeArc detects a full sweep and emits two half-arcs instead, while still supporting partial sweeps (gauges/semicircles) through a single sweepDegrees parameter. All of this lives in a pure, unit-tested core/geometry module with zero React, keeping trigonometry completely separate from the rendering layer.

Challenge: Animating many rings without stale closures or leaked animation frames. Solution: A dedicated useAnimatedValue hook owns one requestAnimationFrame loop per ring, restarts cleanly from the currently displayed value when the target changes, and always cancels the frame on cleanup. Per-ring hooks live inside the RadialRing component (never inside a .map()), respecting the Rules of Hooks.

Tech Stack

  • Frontend: TypeScript, React 19, plain CSS + CSS variables (library); Tailwind CSS v4 (demo only)
  • Core APIs: SVG, requestAnimationFrame, ResizeObserver, matchMedia
  • Tooling: Vite (library mode), Vitest + Testing Library, ESLint (flat config + a local plugin), Prettier, Storybook, Changesets, size-limit

Installation

npm install multi-layer-radial-chart

React 19 is a peer dependency.

Usage

import { RadialChart } from "multi-layer-radial-chart";
import "multi-layer-radial-chart/styles.css";

const data = [
  { value: 82, max: 100, color: "#fb2576", label: "Move" },
  { value: 45, max: 60, color: "#22d3ee", label: "Exercise" },
  { value: 9, max: 12, color: "#a3e635", label: "Stand" },
];

export function Dashboard() {
  return (
    <RadialChart data={data} showLegend showTooltip>
      <strong>82%</strong>
    </RadialChart>
  );
}

Props

| Prop | Type | Default | Description | | --------------------- | ------------------------- | -------------- | ------------------------------------------------------- | | data | RadialDatum[] | — | Series to render, outermost ring first. | | size | number | responsive | Fixed pixel size; omit to size to the container width. | | startAngle | number | -90 | Angle (deg) where arcs begin (-90 = 12 o'clock). | | gap | number | 6 | Pixel gap between rings. | | ringWidth | number | auto | Fixed stroke width; auto-derived when omitted. | | rounded | boolean | true | Round the arc line caps. | | allowOverflow | boolean | false | Let values > max overrun as an overlapping extra lap. | | animate | boolean | true | Animate value transitions. | | animationDurationMs | number | 800 | Tween duration. | | clockwise | boolean | true | Draw arcs clockwise. | | maxSweepDegrees | number | 360 | Total sweep; 270 gauge, 180 semicircle. | | showLegend | boolean | false | Render the built-in legend. | | showTooltip | boolean | false | Show a hover tooltip per ring. | | onSegmentClick | (datum, index) => void | — | Click/keyboard activate a ring (makes rings focusable). | | onSegmentHover | (datum, index) => void | — | Fires on pointer enter/move over a ring. | | onSegmentLeave | (datum, index) => void | — | Fires when the pointer leaves a ring. | | className | string | — | Class applied to the outer container. | | children | ReactNode | — | Content rendered in the chart's center. |

Each RadialDatum is { value, max, color, label, trackColor?, pattern?, gradient?, threshold? } where pattern is "solid" | "dashed".

Overflow (values past 100%)

Set allowOverflow so a value greater than its max overruns the ring as an overlapping extra lap (Apple-Watch style) instead of clamping at 100%:

<RadialChart allowOverflow data={[{ value: 135, max: 100, color: "#fb2576", label: "Move" }]} />

Goal markers (threshold)

Give a datum a threshold (in the same units as value) to draw a tick on its track marking a goal. Style it with the --rc-marker-color CSS variable:

<RadialChart data={[{ value: 82, max: 100, color: "#fb2576", label: "Move", threshold: 90 }]} />

Interactivity (onSegmentClick / onSegmentHover)

Provide onSegmentClick to make each ring clickable and keyboard-focusable (Tab to focus, Enter/Space to activate). onSegmentHover / onSegmentLeave report pointer enter/leave and are independent of the built-in tooltip:

<RadialChart
  data={data}
  onSegmentClick={(datum, index) => console.log("clicked", index, datum.label)}
  onSegmentHover={(datum) => setActive(datum.label)}
/>

Focused rings show a :focus-visible outline you can theme with the --rc-focus-color CSS variable.

Gradient rings

Give any datum a gradient to fill its progress arc with an SVG-native linear or radial gradient (it overrides color):

const data = [
  {
    value: 82,
    max: 100,
    label: "Move",
    color: "#fb2576", // fallback if gradient is ignored
    gradient: {
      type: "linear", // "linear" | "radial"
      angle: 45,       // degrees, linear only (0 = left→right)
      stops: [
        { offset: 0, color: "#f472b6" },
        { offset: 1, color: "#8b5cf6" },
      ],
    },
  },
];

Note: SVG paint supports linear and radial gradients natively. A true conic (angular) gradient is not part of the SVG spec, so it is intentionally not offered.

Center content & count-up

Anything you pass as children is centered inside the rings. Use the useCountUp hook for an animated number:

import { RadialChart, useCountUp } from "multi-layer-radial-chart";

function Ring({ percent }: { percent: number }) {
  const value = useCountUp(percent, { durationMs: 1200 });
  return (
    <RadialChart data={[{ value: percent, max: 100, color: "#6366f1", label: "Goal" }]}>
      <strong>{value}%</strong>
    </RadialChart>
  );
}

Headless usage (useRadialChart)

Need full control over the markup? useRadialChart computes all the geometry (radii, stroke widths and ready-to-use SVG path strings) and renders nothing, so you can build your own SVG:

import { useRadialChart } from "multi-layer-radial-chart";

function CustomRings({ data }: { data: RadialDatum[] }) {
  const { size, rings } = useRadialChart(data, { size: 240, gap: 6 });
  return (
    <svg viewBox={`0 0 ${size} ${size}`} width={size} height={size}>
      {rings.map((ring, i) => (
        <g key={i}>
          <path d={ring.trackPath} fill="none" stroke="#eee" strokeWidth={ring.strokeWidth} />
          <path
            d={ring.progressPath}
            fill="none"
            stroke={ring.datum.color}
            strokeWidth={ring.strokeWidth}
            strokeLinecap="round"
          />
        </g>
      ))}
    </svg>
  );
}

The library also exports the lower-level building blocks — useAnimatedValue, useElementSize, useReducedMotion, describeArc, gradientVector, polarToCartesian, computeRingLayout, toFraction, toPercent — for advanced compositions.

Framework-agnostic core (multi-layer-radial-chart/core)

The math is fully decoupled from React. Import the /core subpath to get the pure geometry, scaling, layout, validation and color helpers with zero React in the dependency graph — use it from Vue, Svelte, Solid, vanilla JS, a <canvas> renderer, or on the server:

import { validateData, computeRingLayout, describeArc } from "multi-layer-radial-chart/core";

const data = [{ value: 82, max: 100, color: "#fb2576", label: "Move" }];
const rings = validateData(data);
const layout = computeRingLayout(rings.length, 240, 6);
const path = describeArc(120, 120, layout[0].radius, rings[0].fraction, -90, true);
// → feed `path` into any SVG/Canvas renderer

The /core entry exports describeArc, describeArcSegment, polarToCartesian, gradientVector, markerLine, computeRingLayout, toFraction, toPercent, toRawFraction, validateData, normalizeDatum, contrastingColor, contrastShadow, and all non-React types (RadialDatum, NormalizedDatum, RingGradient, Point, RingLayout, …).

Development

# Clone the repository
git clone https://github.com/MosheHatab/multi-layer-radial-chart.git

# Install dependencies
npm install

# Run the demo playground
npm run dev

# Storybook
npm run storybook

# Verify everything
npm run typecheck && npm run lint && npm run test && npm run build && npm run size

Optional: the repository includes the ui-ux-pro-max design skill under .cursor/skills/ (used only for designing the demo). Its scripts require Python 3 (python --version; on Windows winget install Python.Python.3.12). It is not needed to build or use the library.

Deployment

Publishing to npm

The package is versioned with Changesets. Pick one of the two flows below — don't mix them.

Automated (recommended): commit a changeset and let CI do the rest.

npm run changeset   # describe the change; pick the semver bump
git commit -am "feat: ..." && git push

On push to main, the Release workflow opens a "Version Packages" PR. Merging that PR bumps the version, updates CHANGELOG.md, and publishes to npm (requires the NPM_TOKEN repository secret).

Manual (from local): build, validate, and publish in one step.

npm run version       # applies pending changesets + updates CHANGELOG.md (skip for first publish)
npm run publish:safe  # build → publint + are-the-types-wrong → npm publish
git commit -am "release" && git push   # commit the version bump / changelog

publish:safe is idempotent-friendly: npm publish refuses to overwrite an existing version, and CI's changeset publish skips versions already on npm — so pushing after a manual publish never double-publishes. Package exports/types can be validated anytime without publishing via npm run check:package.

License

MIT © 2026 Moshe Hatab