radial-chart-07
Sprint progress ring chart in blue on a grained card, beside task counts; follows the page theme in light and dark.
pnpm dlx shadcn add @01orbital/radial-chart-07"use client"
import {
Label,
PolarAngleAxis,
PolarRadiusAxis,
RadialBar,
RadialBarChart,
} from "recharts"
import { CHART_NOISE } from "@/components/texture/grain-recipes"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { ChartContainer, type ChartConfig } from "@/components/ui/chart"
// Grain from the free Card Cascade Cool example.
const COPY = {
title: "Sprint progress",
description: "Design system refresh",
complete: "Complete",
remaining: "Remaining",
} as const
const TASKS = { complete: 18, total: 24 }
const REMAINING = TASKS.total - TASKS.complete
const PERCENT = Math.round((TASKS.complete / TASKS.total) * 100)
const DATA = [{ complete: TASKS.complete, fill: "var(--color-complete)" }]
const CHART_CONFIG = {
complete: { label: COPY.complete, color: "var(--chart-2)" },
} satisfies ChartConfig
/** A blue sprint ring and task counts on a finely grained card. */
export default function RadialChart07() {
return (
<Card className="relative isolate mx-auto w-full max-w-sm shadow-none ring-border dark:ring-border">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 -z-10 overflow-hidden rounded-[inherit] opacity-[0.06]"
style={{ backgroundImage: CHART_NOISE }}
/>
<CardHeader>
<CardTitle>
<h2>{COPY.title}</h2>
</CardTitle>
<CardDescription>{COPY.description}</CardDescription>
</CardHeader>
<CardContent className="grid grid-cols-1 items-center gap-6 min-[18rem]:grid-cols-2">
<ChartContainer
config={CHART_CONFIG}
role="img"
aria-label={`${TASKS.complete} of ${TASKS.total} tasks complete, ${PERCENT}%`}
className="aspect-square w-full max-w-40"
>
<RadialBarChart
accessibilityLayer={false}
data={DATA}
startAngle={90}
endAngle={-270}
innerRadius="76%"
outerRadius="100%"
margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
>
<PolarAngleAxis
type="number"
domain={[0, TASKS.total]}
tick={false}
/>
<RadialBar dataKey="complete" background cornerRadius={8} />
<PolarRadiusAxis tick={false} tickLine={false} axisLine={false}>
<Label
content={({ viewBox }) => {
if (!viewBox || !("cx" in viewBox)) return null
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
fontSize={24}
className="fill-foreground font-medium tabular-nums"
>
{PERCENT}%
</text>
)
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
<dl className="grid gap-4 text-sm">
<div className="space-y-1">
<dt className="text-muted-foreground">{COPY.complete}</dt>
<dd className="text-2xl font-medium tabular-nums">
{TASKS.complete}
</dd>
</div>
<div className="space-y-1">
<dt className="text-muted-foreground">{COPY.remaining}</dt>
<dd className="text-2xl font-medium tabular-nums">{REMAINING}</dd>
</div>
</dl>
</CardContent>
</Card>
)
}
// The grain tiles the kit paints with, as data URIs, each held once. Every
// recipe is a tiny SVG turbulence filter; the component using it chooses
// the tile size, opacity and blend. `*_URI` is the bare image for callers
// that wrap it themselves; the unsuffixed name is ready for `background-image`.
/** Stylized paper: coarse turbulence pushed to high contrast, the grain under most of the painted scenes and the Grain `paper` variant. */
export const PAPER_GRAIN_URI =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='250' height='250' viewBox='0 0 100 100'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='turbulence' baseFrequency='1.4' numOctaves='1' seed='2' stitchTiles='stitch' result='n'/%3E%3CfeComponentTransfer result='g'%3E%3CfeFuncR type='linear' slope='4' intercept='1'/%3E%3CfeFuncG type='linear' slope='4' intercept='1'/%3E%3CfeFuncB type='linear' slope='4' intercept='1'/%3E%3C/feComponentTransfer%3E%3CfeColorMatrix type='saturate' values='0' in='g'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"
export const PAPER_GRAIN = `url("${PAPER_GRAIN_URI}")`
/** Sandpaper: finer, two-octave turbulence, the Grain `sandpaper` variant and the pours and pools. */
export const SANDPAPER_GRAIN_URI =
"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200' viewBox='0 0 100 100'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='turbulence' baseFrequency='2.2' numOctaves='2' seed='5' stitchTiles='stitch' result='n'/%3E%3CfeComponentTransfer result='g'%3E%3CfeFuncR type='linear' slope='2.5' intercept='0.8'/%3E%3CfeFuncG type='linear' slope='2.5' intercept='0.8'/%3E%3CfeFuncB type='linear' slope='2.5' intercept='0.8'/%3E%3C/feComponentTransfer%3E%3CfeColorMatrix type='saturate' values='0' in='g'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"
export const SANDPAPER_GRAIN = `url("${SANDPAPER_GRAIN_URI}")`
/** Square: three-octave turbulence at high contrast, the Grain `square` variant. */
export const SQUARE_GRAIN_URI =
"data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%22200%22%20height=%22200%22%20viewBox=%220%200%20100%20100%22%3E%3Cfilter%20id=%22n%22%3E%3CfeTurbulence%20type=%22turbulence%22%20baseFrequency=%222.1%22%20numOctaves=%223%22%20seed=%2211%22%20stitchTiles=%22stitch%22%20result=%22n%22/%3E%3CfeComponentTransfer%20result=%22g%22%3E%3CfeFuncR%20type=%22linear%22%20slope=%225.5%22%20intercept=%221.4%22/%3E%3CfeFuncG%20type=%22linear%22%20slope=%225.5%22%20intercept=%221.4%22/%3E%3CfeFuncB%20type=%22linear%22%20slope=%225.5%22%20intercept=%221.4%22/%3E%3C/feComponentTransfer%3E%3CfeColorMatrix%20type=%22saturate%22%20values=%220%22%20in=%22g%22/%3E%3C/filter%3E%3Crect%20width=%22100%%22%20height=%22100%%22%20filter=%22url(%23n)%22/%3E%3C/svg%3E"
export const SQUARE_GRAIN = `url("${SQUARE_GRAIN_URI}")`
/** Plain fractal noise on a 200px tile, the Grain `single` variant. */
export const SINGLE_NOISE_URI =
"data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"
export const SINGLE_NOISE = `url("${SINGLE_NOISE_URI}")`
/** Fine fractal noise on a 512px tile, SoftGrain and the Limb scene. */
export const SOFT_NOISE_URI =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='5' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"
export const SOFT_NOISE = `url("${SOFT_NOISE_URI}")`
/** Fine fractal noise the field scenes lay over their art: Fracture Field, Gestural Zip, Solar Flare and Underwater Shaft. */
export const FIELD_NOISE_URI =
"data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"
export const FIELD_NOISE = `url("${FIELD_NOISE_URI}")`
/** Fine fractal noise on a 256px tile, the mist over the radial charts. */
export const CHART_NOISE_URI =
"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"
export const CHART_NOISE = `url("${CHART_NOISE_URI}")`
/** A one-octave dither on a 128px tile, the glass of the neon signs. */
export const DITHER_NOISE_URI =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E"
export const DITHER_NOISE = `url("${DITHER_NOISE_URI}")`
/** A thin film: paper turbulence with its alpha cut to 0.6, the grain GlowSection lays over its light. */
export const GLOW_FILM_URI =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='250' height='250'%3E%3Cfilter id='sf'%3E%3CfeTurbulence type='turbulence' baseFrequency='1.4' numOctaves='1' seed='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.6'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23sf)'/%3E%3C/svg%3E"
export const GLOW_FILM = `url("${GLOW_FILM_URI}")`
/** Plain fractal noise on a 400px tile, the canvas finish of ColorField and FeltOrb. */
export const CANVAS_NOISE_URI =
"data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"
export const CANVAS_NOISE = `url("${CANVAS_NOISE_URI}")`
Dependencies
npm: recharts@3.8.0. The CLI installs them for you.
Registry pieces it installs with it: