"use client"; import { cn } from "@/lib/utils"; import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; import * as RechartsCore from "recharts"; import { z } from "zod"; /** * Represents a graph data object * @property {string} type - Type of graph to render * @property {string[]} labels - Labels for the graph * @property {Object[]} datasets - Data for the graph */ export const graphDataSchema = z.object({ type: z.enum(["bar", "line", "pie"]).describe("Type of graph to render"), labels: z.array(z.string()).describe("Labels for the graph"), datasets: z .array( z.object({ label: z.string().describe("Label for the dataset"), data: z.array(z.number()).describe("Data points for the dataset"), color: z.string().optional().describe("Optional color for the dataset"), }), ) .describe("Data for the graph"), }); export const graphSchema = z.object({ data: graphDataSchema.describe( "Data object containing chart configuration and values", ), title: z.string().describe("Title for the chart"), showLegend: z .boolean() .optional() .describe("Whether to show the legend (default: true)"), variant: z .enum(["default", "solid", "bordered"]) .optional() .describe("Visual style variant of the graph"), size: z .enum(["default", "sm", "lg"]) .optional() .describe("Size of the graph"), }); // Define the base type from the Zod schema export type GraphDataType = z.infer; // Extend the GraphProps with additional tambo properties export interface GraphProps extends Omit, "data" | "title" | "size">, Omit, "size" | "variant"> { /** Data object containing chart configuration and values */ data?: GraphDataType; /** Optional title for the chart */ title?: string; /** Whether to show the legend (default: true) */ showLegend?: boolean; /** Visual style variant of the graph */ variant?: "default" | "solid" | "bordered"; /** Size of the graph */ size?: "default" | "sm" | "lg"; } const graphVariants = cva( "w-full rounded-lg overflow-hidden transition-all duration-200", { variants: { variant: { default: "bg-background", solid: [ "shadow-lg shadow-zinc-900/10 dark:shadow-zinc-900/20", "bg-muted", ].join(" "), bordered: ["border-2", "border-border"].join(" "), }, size: { default: "h-64", sm: "h-48", lg: "h-96", }, }, defaultVariants: { variant: "default", size: "default", }, }, ); const defaultColors = [ "hsl(220, 100%, 62%)", // Blue "hsl(160, 82%, 47%)", // Green "hsl(32, 100%, 62%)", // Orange "hsl(340, 82%, 66%)", // Pink ]; /** * A component that renders various types of charts using Recharts * @component * @example * ```tsx * * ``` */ export const Graph = React.forwardRef( ( { className, variant, size, data, title, showLegend = true, ...props }, ref, ) => { // If no data received yet, show loading if (!data) { return (
Awaiting data...
); } // Check if we have the minimum viable data structure const hasValidStructure = data.type && data.labels && data.datasets && Array.isArray(data.labels) && Array.isArray(data.datasets) && data.labels.length > 0 && data.datasets.length > 0; if (!hasValidStructure) { return (

Building chart...

); } try { // Filter datasets to only include those with valid data const validDatasets = data.datasets.filter( (dataset) => dataset.label && dataset.data && Array.isArray(dataset.data) && dataset.data.length > 0, ); if (validDatasets.length === 0) { return (

Preparing datasets...

); } // Use the minimum length between labels and the shortest dataset const maxDataPoints = Math.min( data.labels.length, Math.min(...validDatasets.map((d) => d.data.length)), ); // Transform data for Recharts using only available data points const chartData = data.labels .slice(0, maxDataPoints) .map((label, index) => ({ name: label, ...Object.fromEntries( validDatasets.map((dataset) => [ dataset.label, dataset.data[index] ?? 0, ]), ), })); const renderChart = () => { if (!["bar", "line", "pie"].includes(data.type)) { return (

Unsupported chart type: {data.type}

); } switch (data.type) { case "bar": return ( {showLegend && ( )} {validDatasets.map((dataset, index) => ( ))} ); case "line": return ( {showLegend && ( )} {validDatasets.map((dataset, index) => ( ))} ); case "pie": { // For pie charts, use the first valid dataset const pieDataset = validDatasets[0]; if (!pieDataset) { return (

No valid dataset for pie chart

); } return ( ({ name: data.labels[index], value, fill: defaultColors[index % defaultColors.length], }))} dataKey="value" nameKey="name" cx="50%" cy="50%" labelLine={false} outerRadius={80} fill="#8884d8" /> {showLegend && ( )} ); } } }; return (
{title && (

{title}

)}
{renderChart()}
); } catch (error) { console.error("Error rendering chart:", error); return (

Error loading chart

An error occurred while rendering. Please try again.

); } }, ); Graph.displayName = "Graph";