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, DoughnutLegendRow, FALLBACK_PRIMARY, FALLBACK_SECONDARY, } from "./chart-shared"; ChartJS.register(ArcElement, DoughnutController, Tooltip); // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface PropertyCashflowDoughnutChartProps { /** Total rental income or inflows in dollars */ income?: number | null; /** Total expenses or outflows in dollars */ expenses?: number | 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 PropertyCashflowDoughnutChart({ income, expenses, title = "Property Cashflow", showLegend = true, height = 200, width = "100%", className, isLoading = false, }: PropertyCashflowDoughnutChartProps) { const themeVars = useThemeVars(); const brandPrimary: string = (themeVars["--theme-primary"] as string | undefined) || FALLBACK_PRIMARY; const brandSecondary: string = (themeVars["--theme-secondary"] as string | undefined) || FALLBACK_SECONDARY; const fontFamily: string = (themeVars["--font-sans"] as string | undefined) || "Figtree, sans-serif"; const incomeVal = Math.max(0, income ?? 0); const expensesVal = Math.max(0, expenses ?? 0); const total = incomeVal + expensesVal; const netCashflow = incomeVal - expensesVal; const hasData = total > 0; const chartData = useMemo>( () => ({ labels: ["Income", "Expenses"], datasets: [ { data: [incomeVal, expensesVal], backgroundColor: [brandPrimary, brandSecondary], borderWidth: 0, hoverOffset: 4, }, ], }), [incomeVal, expensesVal, brandPrimary, brandSecondary], ); 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], ); const incomePct = total > 0 ? `${((incomeVal / total) * 100).toFixed(1)}%` : "0%"; const expensesPct = total > 0 ? `${((expensesVal / total) * 100).toFixed(1)}%` : "0%"; const netLabel = netCashflow >= 0 ? "Surplus" : "Deficit"; return ( {title} {isLoading ? ( ) : !hasData ? ( No data available ) : (
{formatAbbrev(Math.abs(netCashflow))}
{netLabel}
{showLegend && (
)}
)}
); }