financial-data-analyst/components/ChartRenderer.tsx (394 lines of code) (raw):

"use client"; import React from "react"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { TrendingUp, TrendingDown } from "lucide-react"; import { Area, AreaChart, Bar, BarChart, CartesianGrid, Label, Line, LineChart, Pie, PieChart, XAxis, } from "recharts"; import { ChartContainer, ChartTooltip, ChartTooltipContent, } from "@/components/ui/chart"; import type { ChartData } from "@/types/chart"; function BarChartComponent({ data }: { data: ChartData }) { const dataKey = Object.keys(data.chartConfig)[0]; return ( <Card> <CardHeader> <CardTitle className="text-xl">{data.config.title}</CardTitle> <CardDescription>{data.config.description}</CardDescription> </CardHeader> <CardContent> <ChartContainer config={data.chartConfig}> <BarChart accessibilityLayer data={data.data}> <CartesianGrid vertical={false} /> <XAxis dataKey={data.config.xAxisKey} tickLine={false} tickMargin={10} axisLine={false} tickFormatter={(value) => { return value.length > 20 ? `${value.substring(0, 17)}...` : value; }} /> <ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} /> <Bar dataKey={dataKey} fill={`var(--color-${dataKey})`} radius={8} /> </BarChart> </ChartContainer> </CardContent> <CardFooter className="flex-col items-start gap-2 text-sm"> {data.config.trend && ( <div className="flex gap-2 font-medium leading-none"> Trending {data.config.trend.direction} by{" "} {data.config.trend.percentage}% this period{" "} {data.config.trend.direction === "up" ? ( <TrendingUp className="h-4 w-4" /> ) : ( <TrendingDown className="h-4 w-4" /> )} </div> )} {data.config.footer && ( <div className="leading-none text-muted-foreground"> {data.config.footer} </div> )} </CardFooter> </Card> ); } function MultiBarChartComponent({ data }: { data: ChartData }) { return ( <Card> <CardHeader> <CardTitle className="text-xl">{data.config.title}</CardTitle> <CardDescription>{data.config.description}</CardDescription> </CardHeader> <CardContent> <ChartContainer config={data.chartConfig}> <BarChart accessibilityLayer data={data.data}> <CartesianGrid vertical={false} /> <XAxis dataKey={data.config.xAxisKey} tickLine={false} tickMargin={10} axisLine={false} tickFormatter={(value) => { return value.length > 20 ? `${value.substring(0, 17)}...` : value; }} /> <ChartTooltip cursor={false} content={<ChartTooltipContent indicator="dashed" />} /> {Object.keys(data.chartConfig).map((key) => ( <Bar key={key} dataKey={key} fill={`var(--color-${key})`} radius={4} /> ))} </BarChart> </ChartContainer> </CardContent> <CardFooter className="flex-col items-start gap-2 text-sm"> {data.config.trend && ( <div className="flex gap-2 font-medium leading-none"> Trending {data.config.trend.direction} by{" "} {data.config.trend.percentage}% this period{" "} {data.config.trend.direction === "up" ? ( <TrendingUp className="h-4 w-4" /> ) : ( <TrendingDown className="h-4 w-4" /> )} </div> )} {data.config.footer && ( <div className="leading-none text-muted-foreground"> {data.config.footer} </div> )} </CardFooter> </Card> ); } function LineChartComponent({ data }: { data: ChartData }) { return ( <Card> <CardHeader> <CardTitle className="text-xl">{data.config.title}</CardTitle> <CardDescription>{data.config.description}</CardDescription> </CardHeader> <CardContent> <ChartContainer config={data.chartConfig}> <LineChart accessibilityLayer data={data.data} margin={{ left: 12, right: 12, }} > <CartesianGrid vertical={false} /> <XAxis dataKey={data.config.xAxisKey} tickLine={false} axisLine={false} tickMargin={8} tickFormatter={(value) => { return value.length > 20 ? `${value.substring(0, 17)}...` : value; }} /> <ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} /> {Object.keys(data.chartConfig).map((key) => ( <Line key={key} type="natural" dataKey={key} stroke={`var(--color-${key})`} strokeWidth={2} dot={false} /> ))} </LineChart> </ChartContainer> </CardContent> <CardFooter className="flex-col items-start gap-2 text-sm"> {data.config.trend && ( <div className="flex gap-2 font-medium leading-none"> Trending {data.config.trend.direction} by{" "} {data.config.trend.percentage}% this period{" "} {data.config.trend.direction === "up" ? ( <TrendingUp className="h-4 w-4" /> ) : ( <TrendingDown className="h-4 w-4" /> )} </div> )} {data.config.footer && ( <div className="leading-none text-muted-foreground"> {data.config.footer} </div> )} </CardFooter> </Card> ); } function PieChartComponent({ data }: { data: ChartData }) { const totalValue = React.useMemo(() => { return data.data.reduce((acc, curr) => acc + curr.value, 0); }, [data.data]); const chartData = data.data.map((item, index) => { return { ...item, // Use the same color variable pattern as other charts fill: `hsl(var(--chart-${index + 1}))`, }; }); return ( <Card className="flex flex-col"> <CardHeader className="items-center pb-0"> <CardTitle className="text-xl">{data.config.title}</CardTitle> <CardDescription>{data.config.description}</CardDescription> </CardHeader> <CardContent className="flex-1 pb-0"> <ChartContainer config={data.chartConfig} className="mx-auto aspect-square max-h-[250px]" > <PieChart> <ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} /> <Pie data={chartData} dataKey="value" nameKey="segment" innerRadius={60} strokeWidth={5} > <Label content={({ viewBox }) => { if (viewBox && "cx" in viewBox && "cy" in viewBox) { return ( <text x={viewBox.cx} y={viewBox.cy} textAnchor="middle" dominantBaseline="middle" > <tspan x={viewBox.cx} y={viewBox.cy} className="fill-foreground text-3xl font-bold" > {totalValue.toLocaleString()} </tspan> <tspan x={viewBox.cx} y={(viewBox.cy || 0) + 24} className="fill-muted-foreground" > {data.config.totalLabel} </tspan> </text> ); } return null; }} /> </Pie> </PieChart> </ChartContainer> </CardContent> <CardFooter className="flex-col gap-2 text-sm"> {data.config.trend && ( <div className="flex items-center gap-2 font-medium leading-none"> Trending {data.config.trend.direction} by{" "} {data.config.trend.percentage}% this period{" "} {data.config.trend.direction === "up" ? ( <TrendingUp className="h-4 w-4" /> ) : ( <TrendingDown className="h-4 w-4" /> )} </div> )} {data.config.footer && ( <div className="leading-none text-muted-foreground"> {data.config.footer} </div> )} </CardFooter> </Card> ); } function AreaChartComponent({ data, stacked, }: { data: ChartData; stacked?: boolean; }) { return ( <Card> <CardHeader> <CardTitle className="text-xl">{data.config.title}</CardTitle> <CardDescription>{data.config.description}</CardDescription> </CardHeader> <CardContent> <ChartContainer config={data.chartConfig}> <AreaChart accessibilityLayer data={data.data} margin={{ left: 12, right: 12, }} > <CartesianGrid vertical={false} /> <XAxis dataKey={data.config.xAxisKey} tickLine={false} axisLine={false} tickMargin={8} tickFormatter={(value) => { return value.length > 20 ? `${value.substring(0, 17)}...` : value; }} /> <ChartTooltip cursor={false} content={ <ChartTooltipContent indicator={stacked ? "dot" : "line"} /> } /> {Object.keys(data.chartConfig).map((key) => ( <Area key={key} type="natural" dataKey={key} fill={`var(--color-${key})`} fillOpacity={0.4} stroke={`var(--color-${key})`} stackId={stacked ? "a" : undefined} /> ))} </AreaChart> </ChartContainer> </CardContent> <CardFooter> <div className="flex w-full items-start gap-2 text-sm"> <div className="grid gap-2"> {data.config.trend && ( <div className="flex items-center gap-2 font-medium leading-none"> Trending {data.config.trend.direction} by{" "} {data.config.trend.percentage}% this period{" "} {data.config.trend.direction === "up" ? ( <TrendingUp className="h-4 w-4" /> ) : ( <TrendingDown className="h-4 w-4" /> )} </div> )} {data.config.footer && ( <div className="leading-none text-muted-foreground"> {data.config.footer} </div> )} </div> </div> </CardFooter> </Card> ); } export function ChartRenderer({ data }: { data: ChartData }) { switch (data.chartType) { case "bar": return <BarChartComponent data={data} />; case "multiBar": return <MultiBarChartComponent data={data} />; case "line": return <LineChartComponent data={data} />; case "pie": return <PieChartComponent data={data} />; case "area": return <AreaChartComponent data={data} />; case "stackedArea": return <AreaChartComponent data={data} stacked />; default: return null; } }