const series = [
{ dataKey: "merged", label: "Merged", color: "var(--chart-1)", rampFrom: 0.4 },
{ dataKey: "ready", label: "Ready for merge", color: "var(--chart-2)" },
{ dataKey: "draft", label: "Draft", color: "var(--chart-3)", emphasize: true },
];
<RadialChart data={chartData} xDataKey="time" series={series}>
<RadialAxis centerGradient />
<RadialDotBar
ghostDataKey="yesterday"
annotations={[{ index: 8, label: "02:14", color: "var(--chart-3)" }]}
/>
<RadialScrubber />
<RadialCenter>
{({ datum, isFuture, totals, values, label }) => {
// Today vs yesterday over the same bins: a count, not a rate
let merged = totals.merged;
let delta = merged - yesterdayMergedSoFar;
let caption = "merged by 14:30";
if (datum) {
merged = isFuture ? datum.yesterdayMerged : values.merged;
delta = isFuture ? null : merged - datum.yesterdayMerged;
caption = isFuture ? `merged at ${label} yesterday` : `merged at ${label}`;
}
return (
<>
<ChartStatFlow value={merged} label={caption} />
{delta !== null && (
<span style={{ color: delta < 0 ? "var(--chart-negative)" : "var(--chart-positive)" }}>
{delta < 0 ? "−" : "+"}
{Math.abs(delta).toLocaleString()} vs yesterday
</span>
)}
</>
);
}}
</RadialCenter>
<RadialLegend />
</RadialChart>
// 96 × 15-minute bins, clockwise from 00:00.
// Bins after "now" have null counts and are drawn as ghost dots from `yesterday`.
const chartData = [
{ time: "00:00", merged: 29, ready: 0, draft: 0, yesterday: 29, yesterdayMerged: 28 },
// …
{ time: "02:00", merged: 47, ready: 0, draft: 720, yesterday: 86, yesterdayMerged: 83 },
// …
{ time: "14:15", merged: 778, ready: 60, draft: 0, yesterday: 881, yesterdayMerged: 848 },
{ time: "14:30", merged: null, ready: null, draft: null, yesterday: 804, yesterdayMerged: 778 },
// …
];
// Yesterday's merged PRs over the bins that have data today (00:00–14:30)
const yesterdayMergedSoFar = chartData
.filter((d) => d.merged !== null)
.reduce((total, d) => total + d.yesterdayMerged, 0);const series = [
{ dataKey: "solar", label: "Solar", color: "var(--chart-2)" },
{ dataKey: "grid", label: "Grid", color: "var(--chart-1)" },
];
<RadialChart data={chartData} xDataKey="time" series={series}>
<RadialAxis />
<RadialDotBar
layout="columns"
rows={5}
dotSize={0.9}
maxValue={3.2}
ghostDataKey="forecast"
ghostColor="var(--muted-foreground)"
ghostDotSize={0.9}
ghostOpacity={0.6}
patterns={{
grid: { preset: "diagonal", strokeWidth: 1.5 },
ghost: { preset: "diagonal", stroke: "none" },
}}
/>
<RadialScrubber variant="dashed" />
<RadialCenter>
{({ datum, isFuture, totals, values, label }) => {
const counts = datum ? values : totals;
let caption = "kWh so far";
if (datum) {
caption = isFuture ? `forecast ${label}` : `kWh at ${label}`;
}
return (
<ChartStatFlow
value={isFuture ? datum.forecast : counts.solar + counts.grid}
formatOptions={{ minimumFractionDigits: 1, maximumFractionDigits: 1 }}
label={caption}
/>
);
}}
</RadialCenter>
<RadialLegend formatValue={(v) => `${v.toFixed(1)} kWh`} />
</RadialChart>
// 24 hourly bins, clockwise from 00:00. Metered until 14:00;
// later hours have null readings and show the striped forecast.
const chartData = [
{ time: "00:00", solar: 0, grid: 0.6, forecast: 0.6 },
// …
{ time: "12:00", solar: 1.9, grid: 0.2, forecast: 2 },
// …
{ time: "18:00", solar: null, grid: null, forecast: 3.2 },
// …
];const series = [
{ dataKey: "merged", label: "Merged today", color: "var(--chart-1)" },
];
<RadialChart data={chartData} xDataKey="time" series={series}>
<RadialAxis />
{/* Not a series: drawn as a comparison line for the whole day */}
<RadialLine
dataKey="yesterday"
dash
stroke="var(--muted-foreground)"
strokeOpacity={0.7}
strokeWidth={1.5}
maxValue={maxValue}
/>
<RadialLine dataKey="merged" strokeWidth={2.25} maxValue={maxValue} />
<RadialScrubber variant="gradient" />
<RadialCenter>
{({ datum, totals, label }) => (
<ChartStatFlow
value={datum ? (datum.merged ?? datum.yesterday) : totals.merged}
label={datum ? `merged at ${label}` : "merged by 14:30"}
/>
)}
</RadialCenter>
<RadialLegend />
</RadialChart>
// 96 × 15-minute bins, clockwise from 00:00.
// Today's line ends at "now"; yesterday closes the loop.
const chartData = [
{ time: "00:00", merged: 44, yesterday: 58 },
// …
{ time: "14:15", merged: 846, yesterday: 870 },
{ time: "14:30", merged: null, yesterday: 862 },
// …
];
// Share one scale between the lines
const maxValue = Math.max(...chartData.map((d) => Math.max(d.merged ?? 0, d.yesterday)));const series = [
{ dataKey: "avg", label: "Daily average", color: "var(--chart-1)" },
];
<RadialChart
angleType="time"
data={days}
xDataKey="date"
series={series}
valueKeys={["minmin", "maxmax"]}
minValue="auto"
maxValue="auto"
niceValues={false}
>
<RadialAxis showCircle={false} valueTicks={4} valueTickFormat={(v) => `${v}°F`} />
{/* Record extremes */}
<RadialArea
dataKey="maxmax"
baseDataKey="minmin"
color="var(--chart-1)"
fillOpacity={0.14}
rampFrom={1}
stroke="none"
curve="linear"
/>
{/* Normal range: average low → average high */}
<RadialArea
dataKey="max"
baseDataKey="min"
color="var(--chart-1)"
fillOpacity={0.28}
rampFrom={1}
stroke="none"
curve="linear"
/>
<RadialLine dataKey="avg" strokeWidth={1.5} />
<RadialScrubber variant="gradient" />
<RadialCenter>
{({ datum }) => (
<ChartStatFlow
value={datum ? datum.avg : annualAverage}
label={datum ? formatDay(datum.date) : "annual average"}
suffix="°F"
formatOptions={{ maximumFractionDigits: 1 }}
/>
)}
</RadialCenter>
</RadialChart>
// 366 days (plotted on the leap year 2000), °F — SFO daily temperatures
// 1999–2018 grouped by day of year (NOAA, via d3's radial area chart).
const days = [
{ date: Date.UTC(2000, 0, 1), avg: 50.2, min: 43.6, max: 55.7, minmin: 37, maxmax: 62 },
{ date: Date.UTC(2000, 0, 2), avg: 49.3, min: 43.9, max: 55.2, minmin: 37, maxmax: 62 },
// …
];
// Shown in the center at rest
const annualAverage = days.reduce((total, d) => total + d.avg, 0) / days.length;
const dayFormat = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
const formatDay = (date) => dayFormat.format(date);const series = [
{ dataKey: "under14", label: "Under 14" },
{ dataKey: "age14to24", label: "14–24" },
{ dataKey: "age25to44", label: "25–44" },
{ dataKey: "age45to64", label: "45–64" },
{ dataKey: "age65plus", label: "65+" },
]; // colors default to --chart-1 … --chart-5 by position
const compact = new Intl.NumberFormat("en-US", { notation: "compact" });
<RadialChart
angleType="band"
data={states} // sorted by total, largest first
xDataKey="state"
series={series}
maxValue="stacked"
>
<RadialBar />
{/* after the bars, so value rings + labels sit on top */}
<RadialAxis
showCircle={false}
valueTicks={[10e6, 20e6, 30e6, 40e6]}
valueTickFormat={(v) => compact.format(v)}
/>
<RadialCenter>
{({ datum, totals, values }) => {
const counts = datum ? values : totals;
return (
<ChartStatFlow
value={Object.values(counts).reduce((a, b) => a + b, 0)}
formatOptions={{ notation: "compact", maximumFractionDigits: 1 }}
label={datum ? String(datum.name) : "US population"}
/>
);
}}
</RadialCenter>
<RadialLegend showValues={false} />
</RadialChart>
// 50 states + DC (ACS 2008), sorted by total population.
const states = [
{ state: "CA", name: "California", under14: 7205000, age14to24: 6014000, age25to44: 10605000, age45to64: 8819000, age65plus: 4114000 },
{ state: "TX", name: "Texas", under14: 5305000, age14to24: 3875000, age25to44: 7018000, age45to64: 5657000, age65plus: 2472000 },
// …
{ state: "WY", name: "Wyoming", under14: 99000, age14to24: 83000, age25to44: 137000, age45to64: 147000, age65plus: 66000 },
];const series = [
{ dataKey: "photos", label: "Photos", color: "var(--chart-1)" },
{ dataKey: "apps", label: "Apps", color: "var(--chart-2)" },
{ dataKey: "documents", label: "Documents", color: "var(--chart-3)" },
{ dataKey: "system", label: "System", color: "var(--chart-4)" },
];
<RadialChart
data={chartData}
xDataKey="device"
series={series}
orientation="angular"
startAngle={-90}
endAngle={90}
maxValue={100}
ringGap={0.18}
>
<RadialAxis
showCircle={false}
valueTicks={[0, 25, 50, 75, 100]}
valueTickFormat={(v) => `${v}%`}
/>
<RadialRingBar cornerRadius={3} segmentGap={3} />
{/* The hole sits on the arc's center: lift the content into the half-disc */}
<RadialCenter className="-translate-y-[30%]">
{({ datum }) => (
<ChartStatFlow
value={datum ? percentUsed(datum) : overallUsed}
formatOptions={{ maximumFractionDigits: 0 }}
suffix="%"
label={datum ? `${datum.device} used` : "used across devices"}
/>
)}
</RadialCenter>
<RadialLegend className="mt-8" showValues={false} />
</RadialChart>
// One ring per device, innermost first. Values are % of that device's
// capacity, stacked end to end along the arc.
const chartData = [
{ device: "Phone", capacity: 256, photos: 46.1, apps: 21.1, documents: 3.5, system: 8.6 },
{ device: "Laptop", capacity: 1024, photos: 20.7, apps: 16.4, documents: 23, system: 7.2 },
{ device: "Cloud", capacity: 2048, photos: 41.3, apps: 0, documents: 20.1, system: 0 },
];
// % of one device in use
const percentUsed = (d) => d.photos + d.apps + d.documents + d.system;
// Shown in the center at rest: % of all capacity (GB) in use
const totalCapacity = chartData.reduce((total, d) => total + d.capacity, 0);
const overallUsed =
chartData.reduce((total, d) => total + percentUsed(d) * d.capacity, 0) /
totalCapacity;