radial-chart-08
Three daily target progress rings on a fine paper-grain card, in light or dark mode, each with a label and exact target.
pnpm dlx shadcn add @01orbital/radial-chart-08"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"
// The free Card Cascade grain tile, scaled down for a fine paper finish.
const COPY = {
title: "Daily targets",
description: "Today’s progress",
} as const
const GOALS = [
{
key: "focus",
label: "Focus",
value: 3,
target: 4,
unit: "hours",
color: "var(--chart-2)",
},
{
key: "tasks",
label: "Tasks",
value: 8,
target: 10,
unit: "tasks",
color: "var(--chart-3)",
},
{
key: "breaks",
label: "Breaks",
value: 3,
target: 5,
unit: "breaks",
color: "var(--primary)",
},
] as const
const CHART_CONFIG = Object.fromEntries(
GOALS.map((goal) => [goal.key, { label: goal.label, color: goal.color }])
) satisfies ChartConfig
/** Three daily target rings on a fine paper-grain surface, in light or dark mode. */
export default function RadialChart08() {
return (
<Card className="@container/targets relative isolate mx-auto w-full max-w-md shadow-none ring-border [--card-foreground:var(--foreground)] [--card:oklch(0.969_0.01_85)] [--foreground:oklch(0.25_0.016_85)] [--muted-foreground:oklch(0.46_0.014_85)] [--muted:oklch(0.915_0.012_85)] [--primary:var(--foreground)] dark:ring-border dark:[--card:oklch(0.205_0.012_255)] dark:[--foreground:oklch(0.94_0.008_255)] dark:[--muted-foreground:oklch(0.74_0.008_255)] dark:[--muted:oklch(0.285_0.012_255)]">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 -z-10 overflow-hidden rounded-[inherit] [background-size:128px_128px] opacity-[0.035]"
style={{ backgroundImage: CHART_NOISE }}
/>
<CardHeader>
<CardTitle>
<h2>{COPY.title}</h2>
</CardTitle>
<CardDescription>{COPY.description}</CardDescription>
</CardHeader>
<CardContent className="grid grid-cols-1 gap-6 @[16rem]/targets:grid-cols-3 @[16rem]/targets:gap-3">
{GOALS.map((goal) => (
<figure key={goal.key} className="min-w-0 space-y-3 text-center">
<ChartContainer
config={CHART_CONFIG}
role="img"
aria-label={`${goal.label}: ${goal.value} of ${goal.target} ${goal.unit}`}
className="mx-auto aspect-square w-full max-w-28"
>
<RadialBarChart
accessibilityLayer={false}
data={[{ value: goal.value, fill: `var(--color-${goal.key})` }]}
startAngle={90}
endAngle={-270}
innerRadius="72%"
outerRadius="100%"
margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
>
<PolarAngleAxis
type="number"
domain={[0, goal.target]}
tick={false}
/>
<RadialBar dataKey="value" background cornerRadius={6} />
<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={16}
className="fill-foreground font-medium tabular-nums"
>
{Math.round((goal.value / goal.target) * 100)}%
</text>
)
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
<figcaption className="space-y-1 text-sm">
<p className="font-medium">{goal.label}</p>
<p className="text-xs/5 text-muted-foreground tabular-nums">
{goal.value} / {goal.target} {goal.unit}
</p>
</figcaption>
</figure>
))}
</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: