Activity Chart
Compact rounded-bar metric card with a Polar title, headline value, tooltip, and hover dimming.
components/ui/activity-chart.tsx
"use client";
import * as React from "react";
import {
Bar,
BarChart as RechartsBarChart,
Cell,
ResponsiveContainer,
XAxis,
} from "recharts";
import {
ChartContainer,
ChartHeading,
ChartTooltip,
colorVar,
type ChartConfig,
} from "@/components/ui/chart";
import { Card } from "@/components/ui/card";
import { type ChartReactionOptions } from "@/components/ui/chart-reactions";
import { cn } from "@/lib/utils";
export type ActivityDatum = { label: string; value: number };
const defaultConfig = { value: { label: "Value", color: "var(--chart-2)" } } satisfies ChartConfig;
function ActivityChartRoot({
title,
value,
data,
config = defaultConfig,
className,
isLoading,
reaction,
onBarClick,
}: {
title: string;
value: string;
data: ActivityDatum[];
config?: ChartConfig;
className?: string;
isLoading?: boolean;
reaction?: ChartReactionOptions;
onBarClick?: (datum: ActivityDatum, index: number) => void;
}) {
const [hovered, setHovered] = React.useState<number>();
const [focused, setFocused] = React.useState<number>();
const active = hovered ?? focused;
return (
<Card className={cn("p-5 sm:p-6", className)}>
<ChartContainer
isLoading={isLoading}
loadingVariant="bar"
reaction={reaction}
config={{ ...defaultConfig, ...config }}
data={data}
variant="plain"
className="w-full justify-start"
>
<ChartHeading title={title} value={value} />
<div className="mt-3 h-44 w-full">
<ResponsiveContainer width="100%" height="100%">
<RechartsBarChart data={data} barCategoryGap="28%" margin={{ top: 4, right: 0, left: 0, bottom: 0 }}>
<ChartTooltip />
<XAxis
dataKey="label"
tickLine={false}
axisLine={false}
interval="preserveStartEnd"
minTickGap={44}
tick={{ fill: "var(--muted-foreground)", fontSize: 11 }}
/>
<Bar dataKey="value" radius={[4, 4, 4, 4]} maxBarSize={10} isAnimationActive={false}>
{data.map((datum, index) => (
<Cell
key={datum.label}
fill={colorVar("value")}
opacity={active !== undefined && active !== index ? 0.6 : 1}
tabIndex={0}
role={onBarClick ? "button" : "img"}
aria-label={`${datum.label}: ${datum.value.toLocaleString("en-US")}`}
className="cursor-pointer transition-opacity duration-150 motion-reduce:transition-none focus-visible:outline-none"
onMouseEnter={() => setHovered(index)}
onMouseLeave={() => setHovered(undefined)}
onFocus={() => setFocused(index)}
onBlur={() => setFocused(undefined)}
onClick={() => onBarClick?.(datum, index)}
onKeyDown={(event) => {
if (onBarClick && (event.key === "Enter" || event.key === " ")) {
event.preventDefault();
onBarClick(datum, index);
}
}}
/>
))}
</Bar>
</RechartsBarChart>
</ResponsiveContainer>
</div>
</ChartContainer>
</Card>
);
}
export const ActivityChart = Object.assign(ActivityChartRoot, {});
- Where it goes
- stats
- Interaction
- animated-countertooltip