channel3
componentprice-history-chartsource: ok · hit
Price History Chart
Area chart of a product's price over time, built on the shadcn chart wrapper.
npx shadcn add "https://ui.trychannel3.com/r/price-history-chart.json"free
Where it goes
pricing
Use cases
saas-landing
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumchartsrecharts
Registry dependencies
chartemptyhttps://ui.trychannel3.com/r/format.json
Files1 · source cached 10/14/2026, 2:05:17 PM
registry/default/components/price-history-chart.tsx
import * as React from "react";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
import type { PriceHistoryPoint } from "@channel3/sdk/resources";
import { cn } from "@/lib/utils";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty";
import { formatCurrency } from "@/registry/default/lib/format";
const chartConfig = {
price: { label: "Price", color: "var(--chart-1)" },
} satisfies ChartConfig;
export interface PriceHistoryChartProps extends React.ComponentProps<"div"> {
history: ReadonlyArray<PriceHistoryPoint>;
currency?: string;
locale?: string;
}
export function PriceHistoryChart({
history,
currency,
locale,
className,
...props
}: PriceHistoryChartProps) {
const points = React.useMemo(
() => [...history].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()),
[history],
);
if (points.length === 0) {
return (
<Empty className={className} {...props}>
<EmptyHeader>
<EmptyTitle>No price history</EmptyTitle>
<EmptyDescription>Price tracking hasn't recorded any data points yet.</EmptyDescription>
</EmptyHeader>
</Empty>
);
}
const code = currency ?? points[0]?.currency ?? "USD";
const prices = points.map((p) => p.price);
const lo = Math.min(...prices);
const hi = Math.max(...prices);
const pad = (hi - lo || hi || 1) * 0.15; // flat or all-zero series
const yDomain: [number, number] = [Math.max(0, lo - pad), hi + pad];
const formatDate = (value: Date) =>
value.toLocaleDateString(locale, { month: "short", day: "numeric" });
const formatAxis = (value: number) =>
new Intl.NumberFormat(locale, {
style: "currency",
currency: code,
notation: "compact",
maximumFractionDigits: 1,
}).format(value);
return (
<ChartContainer config={chartConfig} className={cn("aspect-16/7 w-full", className)} {...props}>
<AreaChart data={points} margin={{ left: 4, right: 4, top: 8, bottom: 0 }}>
<defs>
<linearGradient id="channel3-price-fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-price)" stopOpacity={0.3} />
<stop offset="95%" stopColor="var(--color-price)" stopOpacity={0.02} />
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={24}
tickFormatter={formatDate}
/>
<YAxis
tickLine={false}
axisLine={false}
tickMargin={8}
width={56}
domain={yDomain}
tickFormatter={formatAxis}
/>
<ChartTooltip
content={
<ChartTooltipContent
labelFormatter={(_label, payload) => {
const raw = payload?.[0]?.payload as PriceHistoryPoint | undefined;
return raw ? raw.timestamp.toLocaleDateString(locale, {
year: "numeric",
month: "short",
day: "numeric",
}) : "";
}}
formatter={(value) => formatCurrency(Number(value), code, locale)}
/>
}
/>
<Area
dataKey="price"
type="monotone"
stroke="var(--color-price)"
strokeWidth={2}
fill="url(#channel3-price-fill)"
/>
</AreaChart>
</ChartContainer>
);
}