zazzy
Blocks

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

Usage trend
Daily projects over the last 30 days.
Approaching your plan limit — upgrade
"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-chart

Run 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";

On this page