Use the period selector to compare the last four or all eight sample weeks. Bars show revenue and the dashed line shows the target. The totals, achievement rate, and table use the same selected weeks.
import ChartRevenueTarget from "@/components/ui/chart-revenue-target";
export default ChartRevenueTarget;
#Installation
Complete the base setup, then install this recipe with the CLI tab below. It works in React 19 projects using Next.js or Vite and Tailwind CSS v4.
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/chart-revenue-target.jsonFor manual installation, install Chart and Card, then add Recharts:
npm install recharts@^3.9.2Create components/ui/chart-revenue-target.tsx:
"use client";
import { useState } from "react";
import { Bar, CartesianGrid, ComposedChart, Line, XAxis, YAxis } from "recharts";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
ChartContainer,
ChartSelect,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
const weeks = [
{ week: "W1", revenue: 12400, target: 14000 },
{ week: "W2", revenue: 15100, target: 14000 },
{ week: "W3", revenue: 13800, target: 15000 },
{ week: "W4", revenue: 17200, target: 15000 },
{ week: "W5", revenue: 16400, target: 16000 },
{ week: "W6", revenue: 18900, target: 16000 },
{ week: "W7", revenue: 17600, target: 17000 },
{ week: "W8", revenue: 21400, target: 17000 },
];
const config = {
revenue: { label: "Revenue", color: "var(--chart-1)" },
target: { label: "Target", color: "var(--foreground)" },
} satisfies ChartConfig;
const money = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
});
export default function Component() {
const [range, setRange] = useState("4");
const data = weeks.slice(-Number(range));
const revenue = data.reduce((total, row) => total + row.revenue, 0);
const target = data.reduce((total, row) => total + row.target, 0);
const gap = revenue - target;
const belowTarget = data.filter((row) => row.revenue < row.target).length;
return (
<Card className="h-full min-w-0" data-chart-recipe="revenue">
<CardHeader>
<p className="font-mono text-xs uppercase tracking-widest">01 / Plan and compare</p>
<CardTitle>
<h3>Revenue vs target</h3>
</CardTitle>
<CardDescription>
Weekly revenue against an explicit plan. Sample data in USD.
</CardDescription>
</CardHeader>
<CardContent className="grid min-w-0 gap-6">
<div className="flex flex-wrap items-end justify-between gap-4">
<ChartSelect
label="Revenue period"
value={range}
onValueChange={setRange}
options={[
{ value: "4", label: "Last 4 weeks" },
{ value: "8", label: "All 8 weeks" },
]}
/>
<p className="text-xs leading-5 text-foreground/75">
Bars: revenue / dashed line: target
</p>
</div>
<dl className="grid gap-4 border-y-2 border-border py-4 sm:grid-cols-3" aria-live="polite">
{[
["Revenue", money.format(revenue)],
["Target", money.format(target)],
["Variance", `${gap >= 0 ? "+" : "−"}${money.format(Math.abs(gap))}`],
].map(([label, value]) => (
<div key={label}>
<dt className="text-xs text-foreground/75">{label}</dt>
<dd className="mt-1 text-2xl font-heading tabular-nums">{value}</dd>
</div>
))}
</dl>
<figure className="min-w-0" aria-label="Weekly revenue and target">
<ChartContainer config={config} className="h-64 w-full min-w-0 aspect-auto">
<ComposedChart
accessibilityLayer
data={data}
margin={{ top: 12, right: 8, bottom: 0, left: 0 }}
>
<CartesianGrid vertical={false} />
<XAxis dataKey="week" tickLine={false} axisLine={false} tickMargin={10} />
<YAxis
tickLine={false}
axisLine={false}
width={48}
tickFormatter={(value) => `$${Number(value) / 1000}k`}
domain={[0, "auto"]}
/>
<ChartTooltip
content={
<ChartTooltipContent
formatter={(value, name) =>
`${name === "revenue" ? "Revenue" : "Target"}: ${money.format(Number(value))}`
}
/>
}
/>
<Bar
dataKey="revenue"
fill="var(--color-revenue)"
maxBarSize={48}
radius={[3, 3, 0, 0]}
isAnimationActive={false}
/>
<Line
dataKey="target"
type="linear"
stroke="var(--color-target)"
strokeWidth={2}
strokeDasharray="6 4"
dot={false}
isAnimationActive={false}
/>
</ComposedChart>
</ChartContainer>
</figure>
<output className="border-l-4 border-main pl-3 text-sm leading-6">
{((revenue / target) * 100).toFixed(1)}% of target achieved. {belowTarget} of{" "}
{data.length} weeks below plan.
</output>
<details className="min-w-0 border-t-2 border-border pt-4">
<summary className="cursor-pointer text-sm font-heading">View revenue data</summary>
{/* oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- Keyboard access to the data-table scroll area. */}
<section tabIndex={0} aria-label="Revenue values" className="mt-3 overflow-x-auto">
<table className="w-full text-left text-xs tabular-nums">
<caption className="pb-3 text-left text-foreground/75">
The same {data.length} weeks shown in the chart. USD.
</caption>
<thead>
<tr>
{["Week", "Revenue", "Target", "Variance"].map((label) => (
<th key={label} scope="col" className="border-b-2 border-border py-2 pr-3">
{label}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr key={row.week}>
<th scope="row" className="py-2 pr-3">
{row.week}
</th>
<td className="pr-3">{money.format(row.revenue)}</td>
<td className="pr-3">{money.format(row.target)}</td>
<td>{money.format(row.revenue - row.target)}</td>
</tr>
))}
</tbody>
</table>
</section>
</details>
</CardContent>
</Card>
);
}
#Usage
import ChartRevenueTarget from "@/components/ui/chart-revenue-target";
export default function Example() {
return <ChartRevenueTarget />;
}#API reference
The default export is an editable recipe with no required props. It includes illustrative data, not a live data connection. Edit the following constants in the installed source to adapt it.
| Source setting | Meaning |
|---|---|
weeks | Ordered rows containing week, revenue, and target. Amounts are USD. |
range | Initial period: "4" for the last four weeks or "8" for all weeks. |
money | Currency formatter shared by summaries, tooltip, and table. |
config | Series labels and shared theme colors. |
Keep weekly targets positive when adapting this sample. Achievement is total revenue divided by total target; it is not the average of weekly percentages. Variance is revenue minus target.
#Accessibility
The selector has a visible label and supports keyboard input. The chart enables Recharts keyboard navigation. Calculated summaries remain visible without hovering, and the expandable data table provides the exact values behind the plot. Preserve these alternatives when adapting the recipe. The plot does not animate and inherits the consuming project's light and dark theme tokens.