import React, { useMemo } from "react"; import { Chart as ChartJS, ArcElement, DoughnutController, Tooltip, type ChartOptions, type ChartData, } from "chart.js"; import { Chart } from "react-chartjs-2"; import { useThemeVars } from "@/lib/theme-provider"; import { Card, CardContent, CardHeader, CardTitle } from "./card"; import { Empty, EmptyDescription } from "./empty"; import { Skeleton } from "./skeleton"; import { cn } from "@/lib/utils"; import { formatAbbrev, hexToRgba, DATASET_ALPHAS, FALLBACK_PRIMARY, DoughnutLegendRow, } from "./chart-shared"; ChartJS.register(ArcElement, DoughnutController, Tooltip); // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface LiabilitySegment { label: string; /** Value in dollars */ value: number; } export interface TransactionsLiabilitiesBreakdownChartProps { segments?: LiabilitySegment[] | null; title?: string; showLegend?: boolean; /** Chart canvas height in pixels */ height?: number; /** Card max-width in pixels or CSS string */ width?: number | string; className?: string; /** Show skeleton loading state instead of the chart */ isLoading?: boolean; } // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export function TransactionsLiabilitiesBreakdownChart({ segments, title = "Liabilities Breakdown", showLegend = true, height = 200, width = "100%", className, isLoading = false, }: TransactionsLiabilitiesBreakdownChartProps) { const themeVars = useThemeVars(); const brandPrimary: string = (themeVars["--theme-primary"] as string | undefined) || FALLBACK_PRIMARY; const fontFamily: string = (themeVars["--font-sans"] as string | undefined) || "Figtree, sans-serif"; const hasData = !!segments?.length && segments.some((s) => s.value > 0); const total = useMemo( () => segments?.reduce((sum, s) => sum + s.value, 0) ?? 0, [segments], ); const colors = useMemo( () => (segments ?? []).map((_, i) => hexToRgba(brandPrimary, DATASET_ALPHAS[i % DATASET_ALPHAS.length]), ), [segments, brandPrimary], ); const chartData = useMemo>( () => ({ labels: segments?.map((s) => s.label) ?? [], datasets: [ { data: segments?.map((s) => s.value) ?? [], backgroundColor: colors, borderWidth: 0, hoverOffset: 4, }, ], }), [segments, colors], ); const options = useMemo>( () => ({ responsive: true, maintainAspectRatio: false, cutout: "68%", animation: { duration: 600, easing: "easeOutQuart" }, plugins: { legend: { display: false }, tooltip: { padding: 10, cornerRadius: 0, titleFont: { size: 11, weight: 600 }, bodyFont: { size: 12, weight: 500 }, callbacks: { label: (ctx) => { const val = ctx.raw as number; const pct = total > 0 ? `${((val / total) * 100).toFixed(1)}%` : "0%"; return ` ${ctx.label}: ${formatAbbrev(val)} (${pct})`; }, }, }, }, }), [total], ); return ( {title && ( {title} )} {isLoading ? ( ) : !hasData ? ( No data available ) : (
{formatAbbrev(total)}
Total
{showLegend && (
{segments!.map((s, i) => ( 0 ? `${((s.value / total) * 100).toFixed(1)}%` : "0%" } /> ))}
)}
)}
); }