infoalloggi-monorepo/apps/infoalloggi/src/components/timeserieBarChartStacked.tsx

180 lines
4.6 KiB
TypeScript
Raw Normal View History

2025-08-04 17:45:44 +02:00
"use client";
2025-08-28 18:27:07 +02:00
import { enUS, it } from "date-fns/locale";
2025-08-04 17:45:44 +02:00
import { CalendarIcon } from "lucide-react";
2025-08-28 18:27:07 +02:00
import * as React from "react";
import type { DateRange } from "react-day-picker";
2025-08-04 17:45:44 +02:00
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
import { Button } from "~/components/ui/button";
import { Calendar } from "~/components/ui/calendar";
import {
2025-08-28 18:27:07 +02:00
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
2025-08-04 17:45:44 +02:00
} from "~/components/ui/card";
import {
2025-08-28 18:27:07 +02:00
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
2025-08-04 17:45:44 +02:00
} from "~/components/ui/chart";
import {
2025-08-28 18:27:07 +02:00
Popover,
PopoverContent,
PopoverTrigger,
2025-08-04 17:45:44 +02:00
} from "~/components/ui/popover";
import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
const chartConfig = {
2025-08-28 18:27:07 +02:00
valueA: {
color: "var(--chart-2)",
2025-08-29 16:18:32 +02:00
label: "valueA",
2025-08-28 18:27:07 +02:00
},
valueB: {
color: "var(--chart-3)",
2025-08-29 16:18:32 +02:00
label: "valueB",
2025-08-28 18:27:07 +02:00
},
2025-08-04 17:45:44 +02:00
} satisfies ChartConfig;
export type StackedTimeserie = { date: Date; valueA: number; valueB: number };
2025-08-04 17:45:44 +02:00
type AnalyticsChartProps = {
2025-08-28 18:27:07 +02:00
title: string;
description?: string;
valueALabel: string;
valueBLabel: string;
timeserie: StackedTimeserie[];
2025-08-04 17:45:44 +02:00
};
export default function TimeserieBarChartStacked({
2025-08-28 18:27:07 +02:00
timeserie,
title,
valueALabel,
valueBLabel,
description,
2025-08-04 17:45:44 +02:00
}: AnalyticsChartProps) {
2025-08-28 18:27:07 +02:00
const { locale } = useTranslation();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const configs = chartConfig;
configs.valueA.label = valueALabel;
configs.valueB.label = valueBLabel;
2025-08-28 18:27:07 +02:00
const startDate = timeserie[0]
? new Date(timeserie[0].date)
: new Date(2025, 1, 1);
const endDate = timeserie[timeserie.length - 1]
? // biome-ignore lint/style/noNonNullAssertion: <exists>
new Date(timeserie[timeserie.length - 1]!.date)
: new Date();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const [range, setRange] = React.useState<DateRange | undefined>({
from: startDate,
to: endDate,
});
const filteredData = React.useMemo(() => {
if (!range?.from && !range?.to) {
return timeserie;
}
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return timeserie.filter((item) => {
// biome-ignore lint/style/noNonNullAssertion: <exists>
return item.date >= range.from! && item.date <= range.to!;
});
}, [range]);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const DatePickerLocale = locale === "it" ? it : enUS;
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return (
<Card className="@container/card w-full pb-4">
<CardHeader className="flex flex-col border-b @md/card:grid">
<CardTitle>{title}</CardTitle>
<CardDescription className={cn(!description && "sr-only")}>
{description || "Analytics for the selected period."}
</CardDescription>
<CardAction className="mt-2 @md/card:mt-0">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">
<CalendarIcon />
{range?.from && range?.to
? `${range.from.toLocaleDateString()} - ${range.to.toLocaleDateString()}`
: "Seleziona un intervallo di date"}
</Button>
</PopoverTrigger>
2025-08-29 16:18:32 +02:00
<PopoverContent align="end" className="w-auto overflow-hidden p-0">
2025-08-28 18:27:07 +02:00
<Calendar
className="w-full"
2025-08-29 16:18:32 +02:00
defaultMonth={range?.from}
fixedWeeks
2025-08-28 18:27:07 +02:00
locale={DatePickerLocale}
mode="range"
onSelect={setRange}
2025-08-29 16:18:32 +02:00
selected={range}
2025-08-28 18:27:07 +02:00
showOutsideDays
/>
</PopoverContent>
</Popover>
</CardAction>
</CardHeader>
<CardContent className="px-4">
<ChartContainer
className="aspect-auto h-[250px] w-full"
2025-08-29 16:18:32 +02:00
config={chartConfig}
2025-08-28 18:27:07 +02:00
>
{filteredData.length === 0 ? (
<div className="text-muted-foreground flex h-full items-center justify-center text-base">
Nessun dato disponibile per il periodo selezionato.
</div>
) : (
<BarChart
accessibilityLayer
data={filteredData}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
axisLine={false}
2025-08-29 16:18:32 +02:00
dataKey="date"
2025-08-28 18:27:07 +02:00
minTickGap={20}
tickFormatter={(value) => {
const date = new Date(String(value));
return date.toLocaleDateString("it-IT", {
day: "numeric",
month: "short",
});
}}
2025-08-29 16:18:32 +02:00
tickLine={false}
tickMargin={8}
2025-08-28 18:27:07 +02:00
/>
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
<Bar
dataKey="valueA"
fill={`var(--color-valueA)`}
radius={[0, 0, 4, 4]}
2025-08-29 16:18:32 +02:00
stackId="a"
2025-08-28 18:27:07 +02:00
/>
<Bar
dataKey="valueB"
fill={`var(--color-valueB)`}
radius={[4, 4, 0, 0]}
2025-08-29 16:18:32 +02:00
stackId="a"
2025-08-28 18:27:07 +02:00
/>
</BarChart>
)}
</ChartContainer>
</CardContent>
<CardFooter className="flex justify-end border-t !pt-4">
<span className="text-muted-foreground text-sm">
Totale: <strong>{filteredData.reduce((acc) => acc + 1, 0)}</strong>
</span>
</CardFooter>
</Card>
);
2025-08-04 17:45:44 +02:00
}