298 lines
9.9 KiB
TypeScript
298 lines
9.9 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
|
|
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
import {
|
|
Card,
|
|
CardAction,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import {
|
|
ChartConfig,
|
|
ChartContainer,
|
|
ChartTooltip,
|
|
ChartTooltipContent,
|
|
} from "@/components/ui/chart";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|
|
|
export const description = "An interactive area chart";
|
|
|
|
type ChartDataPoint = {
|
|
date: string;
|
|
appointments: number;
|
|
};
|
|
|
|
const defaultChartData: ChartDataPoint[] = [
|
|
{ date: "2024-04-01", appointments: 12 },
|
|
{ date: "2024-04-02", appointments: 8 },
|
|
{ date: "2024-04-03", appointments: 15 },
|
|
{ date: "2024-04-04", appointments: 22 },
|
|
{ date: "2024-04-05", appointments: 18 },
|
|
{ date: "2024-04-06", appointments: 25 },
|
|
{ date: "2024-04-07", appointments: 10 },
|
|
{ date: "2024-04-08", appointments: 16 },
|
|
{ date: "2024-04-09", appointments: 9 },
|
|
{ date: "2024-04-10", appointments: 19 },
|
|
{ date: "2024-04-11", appointments: 27 },
|
|
{ date: "2024-04-12", appointments: 21 },
|
|
{ date: "2024-04-13", appointments: 14 },
|
|
{ date: "2024-04-14", appointments: 11 },
|
|
{ date: "2024-04-15", appointments: 17 },
|
|
{ date: "2024-04-16", appointments: 13 },
|
|
{ date: "2024-04-17", appointments: 24 },
|
|
{ date: "2024-04-18", appointments: 28 },
|
|
{ date: "2024-04-19", appointments: 20 },
|
|
{ date: "2024-04-20", appointments: 12 },
|
|
{ date: "2024-04-21", appointments: 16 },
|
|
{ date: "2024-04-22", appointments: 19 },
|
|
{ date: "2024-04-23", appointments: 23 },
|
|
{ date: "2024-04-24", appointments: 26 },
|
|
{ date: "2024-04-25", appointments: 15 },
|
|
{ date: "2024-04-26", appointments: 8 },
|
|
{ date: "2024-04-27", appointments: 29 },
|
|
{ date: "2024-04-28", appointments: 18 },
|
|
{ date: "2024-04-29", appointments: 22 },
|
|
{ date: "2024-04-30", appointments: 25 },
|
|
{ date: "2024-05-01", appointments: 14 },
|
|
{ date: "2024-05-02", appointments: 20 },
|
|
{ date: "2024-05-03", appointments: 17 },
|
|
{ date: "2024-05-04", appointments: 24 },
|
|
{ date: "2024-05-05", appointments: 28 },
|
|
{ date: "2024-05-06", appointments: 30 },
|
|
{ date: "2024-05-07", appointments: 21 },
|
|
{ date: "2024-05-08", appointments: 16 },
|
|
{ date: "2024-05-09", appointments: 19 },
|
|
{ date: "2024-05-10", appointments: 23 },
|
|
{ date: "2024-05-11", appointments: 26 },
|
|
{ date: "2024-05-12", appointments: 18 },
|
|
{ date: "2024-05-13", appointments: 13 },
|
|
{ date: "2024-05-14", appointments: 27 },
|
|
{ date: "2024-05-15", appointments: 25 },
|
|
{ date: "2024-05-16", appointments: 22 },
|
|
{ date: "2024-05-17", appointments: 29 },
|
|
{ date: "2024-05-18", appointments: 24 },
|
|
{ date: "2024-05-19", appointments: 17 },
|
|
{ date: "2024-05-20", appointments: 20 },
|
|
{ date: "2024-05-21", appointments: 11 },
|
|
{ date: "2024-05-22", appointments: 10 },
|
|
{ date: "2024-05-23", appointments: 21 },
|
|
{ date: "2024-05-24", appointments: 19 },
|
|
{ date: "2024-05-25", appointments: 16 },
|
|
{ date: "2024-05-26", appointments: 14 },
|
|
{ date: "2024-05-27", appointments: 28 },
|
|
{ date: "2024-05-28", appointments: 18 },
|
|
{ date: "2024-05-29", appointments: 12 },
|
|
{ date: "2024-05-30", appointments: 23 },
|
|
{ date: "2024-05-31", appointments: 17 },
|
|
{ date: "2024-06-01", appointments: 15 },
|
|
{ date: "2024-06-02", appointments: 26 },
|
|
{ date: "2024-06-03", appointments: 13 },
|
|
{ date: "2024-06-04", appointments: 25 },
|
|
{ date: "2024-06-05", appointments: 11 },
|
|
{ date: "2024-06-06", appointments: 19 },
|
|
{ date: "2024-06-07", appointments: 22 },
|
|
{ date: "2024-06-08", appointments: 24 },
|
|
{ date: "2024-06-09", appointments: 27 },
|
|
{ date: "2024-06-10", appointments: 16 },
|
|
{ date: "2024-06-11", appointments: 10 },
|
|
{ date: "2024-06-12", appointments: 28 },
|
|
{ date: "2024-06-13", appointments: 12 },
|
|
{ date: "2024-06-14", appointments: 25 },
|
|
{ date: "2024-06-15", appointments: 21 },
|
|
{ date: "2024-06-16", appointments: 23 },
|
|
{ date: "2024-06-17", appointments: 29 },
|
|
{ date: "2024-06-18", appointments: 14 },
|
|
{ date: "2024-06-19", appointments: 20 },
|
|
{ date: "2024-06-20", appointments: 26 },
|
|
{ date: "2024-06-21", appointments: 17 },
|
|
{ date: "2024-06-22", appointments: 22 },
|
|
{ date: "2024-06-23", appointments: 30 },
|
|
{ date: "2024-06-24", appointments: 15 },
|
|
{ date: "2024-06-25", appointments: 16 },
|
|
{ date: "2024-06-26", appointments: 24 },
|
|
{ date: "2024-06-27", appointments: 27 },
|
|
{ date: "2024-06-28", appointments: 18 },
|
|
{ date: "2024-06-29", appointments: 13 },
|
|
{ date: "2024-06-30", appointments: 25 },
|
|
];
|
|
|
|
const chartConfig = {
|
|
appointments: {
|
|
label: "Appointments",
|
|
color: "var(--primary)",
|
|
},
|
|
} satisfies ChartConfig;
|
|
|
|
export function ChartAreaInteractive({ data }: { data?: ChartDataPoint[] }) {
|
|
const isMobile = useIsMobile();
|
|
const [timeRange, setTimeRange] = React.useState("90d");
|
|
|
|
// Use provided data or fallback to default
|
|
const chartData = React.useMemo(() => {
|
|
if (!data || data.length === 0) {
|
|
return defaultChartData;
|
|
}
|
|
|
|
// Fill in missing dates with 0 appointments
|
|
const referenceDate = new Date();
|
|
const filledData: ChartDataPoint[] = [];
|
|
|
|
// Create a map of existing data
|
|
const dataMap = new Map(data.map((item) => [item.date, item.appointments]));
|
|
|
|
// Generate data for last 90 days
|
|
for (let i = 89; i >= 0; i--) {
|
|
const date = new Date(referenceDate);
|
|
date.setDate(date.getDate() - i);
|
|
const dateStr = date.toISOString().split("T")[0];
|
|
|
|
filledData.push({
|
|
date: dateStr,
|
|
appointments: dataMap.get(dateStr) || 0,
|
|
});
|
|
}
|
|
|
|
return filledData;
|
|
}, [data]);
|
|
|
|
React.useEffect(() => {
|
|
if (isMobile) {
|
|
setTimeRange("7d");
|
|
}
|
|
}, [isMobile]);
|
|
|
|
const filteredData = React.useMemo(() => {
|
|
const referenceDate = new Date();
|
|
let daysToSubtract = 90;
|
|
if (timeRange === "30d") {
|
|
daysToSubtract = 30;
|
|
} else if (timeRange === "7d") {
|
|
daysToSubtract = 7;
|
|
}
|
|
const startDate = new Date(referenceDate);
|
|
startDate.setDate(startDate.getDate() - daysToSubtract);
|
|
|
|
return chartData.filter((item) => {
|
|
const date = new Date(item.date);
|
|
return date >= startDate;
|
|
});
|
|
}, [chartData, timeRange]);
|
|
|
|
return (
|
|
<Card className="@container/card">
|
|
<CardHeader>
|
|
<CardTitle>Appointment Trends</CardTitle>
|
|
<CardDescription>
|
|
<span className="hidden @[540px]/card:block">
|
|
Appointments over time
|
|
</span>
|
|
<span className="@[540px]/card:hidden">Appointments</span>
|
|
</CardDescription>
|
|
<CardAction>
|
|
<ToggleGroup
|
|
type="single"
|
|
value={timeRange}
|
|
onValueChange={setTimeRange}
|
|
variant="outline"
|
|
className="hidden *:data-[slot=toggle-group-item]:!px-4 @[767px]/card:flex"
|
|
>
|
|
<ToggleGroupItem value="90d">Last 3 months</ToggleGroupItem>
|
|
<ToggleGroupItem value="30d">Last 30 days</ToggleGroupItem>
|
|
<ToggleGroupItem value="7d">Last 7 days</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
<Select value={timeRange} onValueChange={setTimeRange}>
|
|
<SelectTrigger
|
|
className="flex w-40 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate @[767px]/card:hidden"
|
|
size="sm"
|
|
aria-label="Select a value"
|
|
>
|
|
<SelectValue placeholder="Last 3 months" />
|
|
</SelectTrigger>
|
|
<SelectContent className="rounded-xl">
|
|
<SelectItem value="90d" className="rounded-lg">
|
|
Last 3 months
|
|
</SelectItem>
|
|
<SelectItem value="30d" className="rounded-lg">
|
|
Last 30 days
|
|
</SelectItem>
|
|
<SelectItem value="7d" className="rounded-lg">
|
|
Last 7 days
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</CardAction>
|
|
</CardHeader>
|
|
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
|
|
<ChartContainer
|
|
config={chartConfig}
|
|
className="aspect-auto h-[250px] w-full"
|
|
>
|
|
<AreaChart data={filteredData}>
|
|
<defs>
|
|
<linearGradient id="fillAppointments" x1="0" y1="0" x2="0" y2="1">
|
|
<stop
|
|
offset="5%"
|
|
stopColor="var(--color-appointments)"
|
|
stopOpacity={1.0}
|
|
/>
|
|
<stop
|
|
offset="95%"
|
|
stopColor="var(--color-appointments)"
|
|
stopOpacity={0.1}
|
|
/>
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid vertical={false} />
|
|
<XAxis
|
|
dataKey="date"
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickMargin={8}
|
|
minTickGap={32}
|
|
tickFormatter={(value) => {
|
|
const date = new Date(value);
|
|
return date.toLocaleDateString("en-US", {
|
|
month: "short",
|
|
day: "numeric",
|
|
});
|
|
}}
|
|
/>
|
|
<ChartTooltip
|
|
cursor={false}
|
|
content={
|
|
<ChartTooltipContent
|
|
labelFormatter={(value) => {
|
|
return new Date(value).toLocaleDateString("en-US", {
|
|
month: "short",
|
|
day: "numeric",
|
|
});
|
|
}}
|
|
indicator="dot"
|
|
/>
|
|
}
|
|
/>
|
|
<Area
|
|
dataKey="appointments"
|
|
type="natural"
|
|
fill="url(#fillAppointments)"
|
|
stroke="var(--color-appointments)"
|
|
/>
|
|
</AreaChart>
|
|
</ChartContainer>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|