Usage trend chart
A usage-over-time line chart for one metric, wired to the usage router. Adds a near-cap upgrade link when billing is present.
A usage-over-time line chart for a single metric over the org-scoped usage
router. It reads the dense daily series from usage.daily ({ days } →
per-metric { day, quantity } buckets, zero-filled server-side) and renders it
with the usage-owned shadcn chart (recharts) — the same surface the dashboard's
usage cards use. Metrics are app-defined, so the block charts the first metric the
router returns unless you pass a metricKey.
When the billing feature is present, an optional region adds a footer link to
upgrade once the charted metric approaches its plan limit. That region never
imports @/features/billing — it links to the billing settings page, which only
exists in an app that kept billing — so add strips it cleanly for a build
without billing.
Requires: usage
Preview
"use client";import Link from "next/link";import { Line, LineChart, XAxis } from "recharts";import { Button } from "@/components/ui/button";import { Card, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card";import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig,} from "@/components/ui/chart";import { trpc } from "@/lib/trpc";import { firstMetricKey, toChartSeries } from "./to-chart-series";const chartConfig = { quantity: { label: "Used", color: "var(--chart-1)" },} satisfies ChartConfig;/** * A usage-over-time line chart for one metric, wired to the `usage` router. It * reads the dense daily series from `usage.daily` (`{ days }` → per-metric * `{ day, quantity }` buckets, zero-filled server-side) and renders it with the * usage-owned shadcn `chart` (recharts), the same surface the dashboard's * `UsageMetricCard` uses. Metrics are app-defined in `config/usage.ts`, so the * block doesn't hardcode a key: pass `metricKey` to pick one, else it charts the * first metric the router returns. Reads only — no mutation, no schema. * * The optional `@feature:billing` region adds a near-cap upgrade footer link * when the charted metric is approaching its plan limit. It never imports the * billing feature module (D-095) — the link points at the billing settings page, * which only exists in an app that kept billing — so `add` strips it cleanly for * a build without billing. Its near-cap read is `usage.summary` (usage-owned): * with no billing footer to drive, there's nothing to know, so the query lives * inside the region and strips with it. */export function UsageTrendChart({ metricKey, days = 30, className,}: { /** Which usage metric to chart; defaults to the first `usage.daily` returns. */ metricKey?: string; /** Window length in days (1–90, the router's bound). Defaults to 30. */ days?: number; className?: string;}) { const daily = trpc.usage.daily.useQuery({ days }); const resolvedKey = metricKey ?? firstMetricKey(daily.data); const series = resolvedKey ? toChartSeries(daily.data, resolvedKey) : []; // @feature:billing:start const summary = trpc.usage.summary.useQuery(); const metricSummary = resolvedKey ? summary.data?.find((row) => row.key === resolvedKey) : undefined; // Approaching (≥80% of) a finite plan limit; uncapped metrics never trigger it. const nearCap = metricSummary != null && metricSummary.limit !== null && metricSummary.limit > 0 && metricSummary.used / metricSummary.limit >= 0.8; // @feature:billing:end return ( <Card className={className}> <CardHeader> <CardTitle>Usage trend</CardTitle> <CardDescription> {daily.isPending ? "Loading usage…" : resolvedKey ? `Daily ${resolvedKey} over the last ${days} days.` : "No usage metrics to display."} </CardDescription> </CardHeader> <CardContent className="flex flex-col gap-4"> <ChartContainer config={chartConfig} className="h-40 w-full"> <LineChart data={series} accessibilityLayer> <XAxis dataKey="day" tickLine={false} axisLine={false} minTickGap={24} // "YYYY-MM-DD" → "MM-DD": the window is ≤90 days, the year is noise. tickFormatter={(day: string) => day.slice(5)} /> <ChartTooltip content={<ChartTooltipContent />} /> <Line dataKey="quantity" type="monotone" stroke="var(--color-quantity)" strokeWidth={2} dot={false} /> </LineChart> </ChartContainer> {/* @feature:billing:start */} {nearCap && ( <Button asChild variant="link" size="sm" className="self-start px-0"> <Link href="/settings/billing"> Approaching your plan limit — upgrade </Link> </Button> )} {/* @feature:billing:end */} </CardContent> </Card> );}Offline preview
The docs site has no backend, so the preview seeds a 14-day series for a
projects metric plus a summary near its plan limit, so both the chart and the
near-cap upgrade link render. In an app the series comes from the usage
router.
Install
npx create-saas-starter add usage-trend-chartRun it from your app's root. add only writes new files — wiring is up to you:
import { UsageTrendChart } from "@/components/blocks/usage-trend-chart/usage-trend-chart";