feat: add interactivity to DashboardPieChart and ChartLegendContent components

This commit is contained in:
Yoga Pangestu 2026-08-15 13:14:25 +07:00
parent fd6793af50
commit e8c46ec18b
3 changed files with 73 additions and 6 deletions

View File

@ -278,9 +278,13 @@ function ChartLegendContent({
payload, payload,
verticalAlign = "bottom", verticalAlign = "bottom",
nameKey, nameKey,
onItemClick,
activeName,
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
hideIcon?: boolean hideIcon?: boolean
nameKey?: string nameKey?: string
onItemClick?: (name: string) => void
activeName?: string
} & RechartsPrimitive.DefaultLegendContentProps) { } & RechartsPrimitive.DefaultLegendContentProps) {
const { config } = useChart() const { config } = useChart()
@ -301,13 +305,20 @@ function ChartLegendContent({
.map((item, index) => { .map((item, index) => {
const key = `${nameKey ?? item.dataKey ?? "value"}` const key = `${nameKey ?? item.dataKey ?? "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key)
const label = typeof itemConfig?.label === 'string' ? itemConfig.label : ''
const isInactive = activeName !== undefined && activeName !== label
return ( return (
<div <div
key={index} key={index}
className={cn( className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground" "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
onItemClick && "cursor-pointer select-none hover:opacity-80",
isInactive && "opacity-40"
)} )}
onClick={() => {
if (label) onItemClick?.(label)
}}
> >
{itemConfig?.icon && !hideIcon ? ( {itemConfig?.icon && !hideIcon ? (
<itemConfig.icon /> <itemConfig.icon />

View File

@ -267,6 +267,8 @@ type DashboardPieChartProps = {
function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartProps) { function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartProps) {
const hasData = data.length > 0 && data.some((d) => d.count > 0); const hasData = data.length > 0 && data.some((d) => d.count > 0);
const [activeName, setActiveName] = useState<string | undefined>(undefined);
const pieColors = useMemo(() => generateRandomColors(5), []); const pieColors = useMemo(() => generateRandomColors(5), []);
const chartConfig = useMemo(() => { const chartConfig = useMemo(() => {
@ -287,6 +289,17 @@ function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartP
})); }));
}, [data, pieColors]); }, [data, pieColors]);
const handlePieClick = (_: unknown, index: number) => {
const name = chartData[index]?.name;
if (name) {
setActiveName((prev) => (prev === name ? undefined : name));
}
};
const handleLegendClick = (name: string) => {
setActiveName((prev) => (prev === name ? undefined : name));
};
return ( return (
<Card> <Card>
<CardHeader className="items-center pb-0"> <CardHeader className="items-center pb-0">
@ -311,9 +324,24 @@ function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartP
data={chartData} data={chartData}
dataKey={dataKey} dataKey={dataKey}
nameKey={nameKey} nameKey={nameKey}
/> onClick={handlePieClick}
>
{chartData.map((item, index) => (
<Cell
key={`cell-${index}`}
opacity={activeName === undefined || activeName === item.name ? 1 : 0.3}
style={{ cursor: 'pointer' }}
/>
))}
</Pie>
<ChartLegend <ChartLegend
content={<ChartLegendContent nameKey={nameKey} />} content={
<ChartLegendContent
nameKey={nameKey}
onItemClick={handleLegendClick}
activeName={activeName}
/>
}
className="-translate-y-2 flex-wrap gap-2 *:basis-1/4 *:justify-center" className="-translate-y-2 flex-wrap gap-2 *:basis-1/4 *:justify-center"
/> />
</PieChart> </PieChart>

View File

@ -18,7 +18,7 @@ import {
UserCheck, UserCheck,
} from 'lucide-react'; } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Pie, PieChart } from 'recharts'; import { Cell, Pie, PieChart } from 'recharts';
import { toast } from 'sonner'; import { toast } from 'sonner';
type TodayAttendance = { type TodayAttendance = {
@ -463,6 +463,8 @@ type DashboardPieChartProps = {
function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartProps) { function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartProps) {
const hasData = data.length > 0 && data.some((d) => d.count > 0); const hasData = data.length > 0 && data.some((d) => d.count > 0);
const [activeName, setActiveName] = useState<string | undefined>(undefined);
const pieColors = useMemo(() => generateRandomColors(5), []); const pieColors = useMemo(() => generateRandomColors(5), []);
const chartConfig = useMemo(() => { const chartConfig = useMemo(() => {
@ -483,6 +485,17 @@ function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartP
})); }));
}, [data, pieColors]); }, [data, pieColors]);
const handlePieClick = (_: unknown, index: number) => {
const name = chartData[index]?.name;
if (name) {
setActiveName((prev) => (prev === name ? undefined : name));
}
};
const handleLegendClick = (name: string) => {
setActiveName((prev) => (prev === name ? undefined : name));
};
return ( return (
<Card> <Card>
<CardHeader className="items-center pb-0"> <CardHeader className="items-center pb-0">
@ -507,9 +520,24 @@ function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartP
data={chartData} data={chartData}
dataKey={dataKey} dataKey={dataKey}
nameKey={nameKey} nameKey={nameKey}
/> onClick={handlePieClick}
>
{chartData.map((item, index) => (
<Cell
key={`cell-${index}`}
opacity={activeName === undefined || activeName === item.name ? 1 : 0.3}
style={{ cursor: 'pointer' }}
/>
))}
</Pie>
<ChartLegend <ChartLegend
content={<ChartLegendContent nameKey={nameKey} />} content={
<ChartLegendContent
nameKey={nameKey}
onItemClick={handleLegendClick}
activeName={activeName}
/>
}
className="-translate-y-2 flex-wrap gap-2 *:basis-1/4 *:justify-center" className="-translate-y-2 flex-wrap gap-2 *:basis-1/4 *:justify-center"
/> />
</PieChart> </PieChart>