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-native

v1.0.0

Published

A lightweight, dependency-light React Native + TypeScript library for animated multi-layer radial (activity-ring) charts, rendered with react-native-svg.

Readme

multi-layer-radial-chart-native

Animated, multi-layer radial (activity-ring) charts for React Native — powered by react-native-svg, written in strict TypeScript.

npm version npm downloads minzipped size CI provenance types license

Give it an array of { value, max, color, label } and it renders concentric rings — like Apple Watch activity rings — with smooth requestAnimationFrame animations, responsive sizing, gradients, overflow laps, goal markers, and a built-in legend. All the math is pure and framework-agnostic; the view layer is a thin react-native-svg wrapper.

Using React on the web? See the sibling package multi-layer-radial-chart — this is its React Native port. Same core engine and API surface, adapted to react-native-svg and native platform APIs.

Demo

A runnable Expo demo lives in example/ — it showcases activity rings with a count-up center, live controls (animate, rounded, legend, overflow), plus gradient, overflow, goal-marker, gauge, and dashed cards with automatic light/dark theming:

cd example
npm install
npm run start        # press a / i / w for Android / iOS / web

Prefer a hosted playground? Try it on Expo Snack — the React-Native equivalent of StackBlitz (runs in the browser and on a device via QR). Goes live once the package is published to npm.


Features

  • Multi-layer rings — any number of concentric series, outermost first.
  • Smooth animationrequestAnimationFrame tween with ease-out cubic; honors the OS "Reduce Motion" setting via AccessibilityInfo.
  • Responsive — omit size and the chart measures its container with onLayout, or pass a fixed size.
  • Gradients — per-ring linear / radial gradients using native <LinearGradient> / <RadialGradient>.
  • Overflow laps — Apple-Watch-style overlapping extra lap when a value exceeds max (allowOverflow).
  • Goal / threshold markers — a tick on the track marking a target, with a light outline so it stays visible on any color.
  • Gauge & semicircle — set maxSweepDegrees (e.g. 270 gauge, 180 semicircle).
  • InteractivityonSegmentPress(datum, index) makes each ring pressable.
  • Center content — render any node (count-up total, icon, label) in the middle.
  • Accessible — the chart exposes an image role with a summary label, and each ring exposes a progressbar role with accessibilityValue. The legend is color-independent.
  • HeadlessuseRadialChart() returns validated geometry and ready-to-use SVG path strings so you can draw the rings yourself.
  • Framework-agnostic core — import multi-layer-radial-chart-native/core for pure geometry/scale/validation with zero React/RN at runtime.
  • Strict typing — no any, fully typed public API, math fully decoupled from the UI.

Architecture highlights

Challenge: An SVG elliptical arc (A command) cannot draw a full 360° circle — the 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. This lives in a pure, unit-tested core/geometry module with zero React/RN, 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.

Challenge: The web version leans on the DOM (ResizeObserver, matchMedia, CSS variables) that don't exist in React Native. Solution: Only the thin adapter layer changed — sizing moved to View onLayout, reduced-motion to AccessibilityInfo, and theming to explicit props. The entire core/ + utils/ math layer is shared verbatim with the web package.

Tech stack

  • Language / UI: TypeScript (strict), React, React Native
  • Rendering: react-native-svg
  • Native APIs: requestAnimationFrame, View onLayout, AccessibilityInfo
  • Tooling: react-native-builder-bob (build), Vitest (core tests), ESLint, Changesets, Expo (demo)

Installation

npm install multi-layer-radial-chart-native react-native-svg
# or
yarn add multi-layer-radial-chart-native react-native-svg

react, react-native, and react-native-svg are peer dependencies.

Expo:

npx expo install react-native-svg
npm install multi-layer-radial-chart-native

Bare React Native: after installing react-native-svg, run cd ios && pod install (iOS).


Quick start

import { RadialChart } from "multi-layer-radial-chart-native";
import { View } from "react-native";

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 Activity() {
	return (
		<View style={{ padding: 24 }}>
			<RadialChart data={data} size={220} showLegend />
		</View>
	);
}

Props (RadialChart)

| Prop | Type | Default | Description | | -------------------- | ------------------------------------------- | -------------- | ---------------------------------------------------------------- | | data | RadialDatum[] | — | Series to render, outermost ring first. (required) | | size | number | container width | Fixed square size in px. Omit to size responsively via onLayout. | | startAngle | number | -90 | Angle (deg) where every arc begins (-90 = 12 o'clock). | | gap | number | 6 | Pixel gap between adjacent rings. | | ringWidth | number | auto | Fixed stroke width; auto-derived from size/count when omitted. | | rounded | boolean | true | Round the arc line caps. | | allowOverflow | boolean | false | Let values above max overrun as an overlapping extra lap. | | animate | boolean | true | Animate value transitions. | | animationDurationMs| number | 800 | Tween duration in ms. | | clockwise | boolean | true | Draw arcs clockwise. | | maxSweepDegrees | number | 360 | Total sweep in deg (270 gauge, 180 semicircle). | | showLegend | boolean | false | Render the built-in legend below the chart. | | onSegmentPress | (datum, index) => void | — | Press a ring. Providing this makes each ring pressable. | | trackColor | string | translucent gray | Background track color when a datum omits trackColor. | | markerColor | string | dark | Inner (dark) goal/threshold marker tick color. | | markerOutlineColor | string | light | Outer (light) outline of the marker tick. | | legendTextColor | string | #111827 | Text color for the built-in legend. | | style | StyleProp<ViewStyle> | — | Extra style for the outer container View. | | children | ReactNode | — | Content rendered in the centre (absolutely positioned). |

RadialDatum

interface RadialDatum {
	value: number; // current value (clamped to [0, max] unless allowOverflow)
	max: number; // upper bound (> 0)
	color: string; // progress color
	label: string; // legend + accessible name
	trackColor?: string; // per-ring track override
	pattern?: "solid" | "dashed"; // color-independent differentiation
	gradient?: RingGradient; // overrides `color`
	threshold?: number; // goal marker, in the same units as `value`
}

Recipes

Responsive sizing

Omit size and wrap the chart in a container with a known width — it measures itself via onLayout:

<View style={{ width: "100%" }}>
	<RadialChart data={data} />
</View>

Center content with a count-up total

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

function CenteredChart() {
	const total = 82;
	const shown = useCountUp(total, { durationMs: 900 });
	return (
		<RadialChart data={data} size={220}>
			<Text style={{ fontSize: 34, fontWeight: "700" }}>{shown}%</Text>
		</RadialChart>
	);
}

Gauge / semicircle

<RadialChart data={data} maxSweepDegrees={270} startAngle={135} />

Gradients

const data = [
	{
		value: 82,
		max: 100,
		label: "Move",
		color: "#fb2576", // fallback for the legend swatch
		gradient: {
			type: "linear",
			angle: 45,
			stops: [
				{ offset: 0, color: "#fb2576" },
				{ offset: 1, color: "#7b2ff7" },
			],
		},
	},
];

Overflow (values above 100%)

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

Goal / threshold marker

<RadialChart data={[{ value: 68, max: 100, color: "#22d3ee", label: "Exercise", threshold: 80 }]} />

Interactivity

<RadialChart
	data={data}
	onSegmentPress={(datum, index) => console.log("pressed", index, datum.label)}
/>

Headless usage (useRadialChart)

Compute geometry and draw the rings with your own react-native-svg markup:

import { useRadialChart } from "multi-layer-radial-chart-native";
import Svg, { Path } from "react-native-svg";

function Headless() {
	const { center, rings } = useRadialChart(data, { size: 240, gap: 8 });
	return (
		<Svg width={240} height={240} viewBox="0 0 240 240">
			{rings.map((ring) => (
				<Path
					key={ring.datum.label}
					d={ring.progressPath}
					fill="none"
					stroke={ring.datum.color}
					strokeWidth={ring.strokeWidth}
					strokeLinecap="round"
				/>
			))}
		</Svg>
	);
}

Framework-agnostic core (/core)

The math is fully decoupled from React and React Native. Import the /core subpath for pure geometry, scaling, layout, validation and color helpers with zero React/RN in the runtime graph — use it from a <canvas>/Skia renderer, a worker, or the server:

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

const rings = validateData([{ value: 82, max: 100, color: "#fb2576", label: "Move" }]);
const layout = computeRingLayout(rings.length, 240, 6);
const path = describeArc(120, 120, layout[0].radius, rings[0].fraction, -90, true);

Differences from the web version

This port keeps the same core engine and API names where they make sense on native. Web-only concepts were adapted:

| Web (multi-layer-radial-chart) | Native (-native) | | ------------------------------------------- | ------------------------------------------------------- | | SVG DOM elements (<svg>, <path>, <g>) | react-native-svg (Svg, Path, G, Line, …) | | ResizeObserver | View onLayout | | prefers-reduced-motion (matchMedia) | AccessibilityInfo.isReduceMotionEnabled() | | CSS variables / stylesheet | Props: trackColor, markerColor, legendTextColor, … | | className | style (StyleProp<ViewStyle>) | | onSegmentClick + keyboard focus | onSegmentPress | | Hover tooltip (onSegmentHover, tooltip) | Not applicable on touch — omitted | | CSS blur() on the overflow shadow | Approximated with a contrast-colored tip segment |


Accessibility

  • The chart <Svg> carries accessibilityRole="image" with a summary accessibilityLabel ("Move 82%, Exercise 75%, …").
  • Each ring carries accessibilityRole="progressbar", an accessibilityLabel of "{label}: {value}/{max} ({percent}%)", and accessibilityValue.
  • Motion honors the OS Reduce Motion setting (rings snap instead of tween).
  • The legend is color-independent (labels + values), and pattern: "dashed" differentiates rings without relying on color alone.

Project structure

src/
  core/          # pure math — geometry, scale, layout, constants (no React/RN)
  utils/         # math, color (WCAG contrast), validation (no React/RN)
  hooks/         # useAnimatedValue, useCountUp, useReducedMotion, useElementSize, useRadialChart
  components/    # RadialChart, RadialRing, RadialChartLabels (react-native-svg)
  types.ts       # public types
  index.ts       # main entry
  core.ts        # framework-agnostic /core entry
example/         # runnable Expo demo app (imports the library from ../src)

The core/ and utils/ folders are copied verbatim from the web package — the geometry and math are identical. Only the hooks and components differ.


Development

npm install          # install deps

# verify everything (same as CI)
npm run typecheck && npm run lint && npm run test && npm run build && npm run check:package

# run the demo app
cd example
npm install
npm run start        # press a / i / w for Android / iOS / web
  • npm run test runs the pure-core unit tests with Vitest (no React Native needed — the geometry/scale math is framework-agnostic).
  • npm run build uses react-native-builder-bob to emit ESM, CommonJS, and TypeScript declaration outputs. Metro consumes the TypeScript source directly during development, so edits hot-reload in the demo without a rebuild.
  • npm run check:package validates the published package layout with publint.

CI (.github/workflows/ci.yml) runs the same checks on every PR.


Publishing to npm

The package is versioned with Changesets and published automatically from CI.

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 with provenance. Publishing uses npm OIDC trusted publishing (configure the package's trusted publisher on npm, or set an NPM_TOKEN repository secret).


License

MIT © Moshe Hatab