/** * Shared utilities and sub-components for all shadcn chart components. * Not part of the public package API — internal use only. */ import React from "react"; import { Button } from "./button"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export type ChartPeriod = 1 | 3 | 6 | 12; export type ChartGranularity = "monthly" | "daily"; // --------------------------------------------------------------------------- // Period / slice config // --------------------------------------------------------------------------- /** * How many data points to slice per period × granularity combination. * monthly: slice by calendar month count. * daily: slice by approximate day count (1M=30d, 3M=90d, 6M=180d, 12M=365d). */ export const CHART_SLICE_COUNT: Record< ChartGranularity, Record > = { monthly: { 1: 1, 3: 3, 6: 6, 12: 12 }, daily: { 1: 30, 3: 90, 6: 180, 12: 365 }, }; /** Period buttons shown per granularity. monthly hides 1M; daily shows all four. */ export const CHART_PERIODS: Record = { monthly: [3, 6, 12], daily: [1, 3, 6, 12], }; // --------------------------------------------------------------------------- // Chart.js helpers // --------------------------------------------------------------------------- export function hexToRgba(hex: string, alpha: number): string { const clean = hex.replace("#", ""); const full = clean.length === 3 ? clean .split("") .map((c) => c + c) .join("") : clean; const r = parseInt(full.slice(0, 2), 16); const g = parseInt(full.slice(2, 4), 16); const b = parseInt(full.slice(4, 6), 16); return `rgba(${r},${g},${b},${alpha})`; } /** Opacity steps to derive distinct shades from a single brand color (up to 6 datasets). */ export const DATASET_ALPHAS = [1, 0.72, 0.52, 0.36, 0.24, 0.15]; // --------------------------------------------------------------------------- // Fallback resolved colors for Chart.js canvas API. // Chart.js requires actual hex/rgb strings — CSS variables cannot be used directly. // All values match the :root defaults defined in globals.css. // --------------------------------------------------------------------------- /** Fallback for --primary (WealthX green) */ export const FALLBACK_PRIMARY = "#33FF99"; /** Fallback for --brand-secondary (WealthX dark navy) */ export const FALLBACK_SECONDARY = "#162029"; /** Fallback for --background (white) */ export const FALLBACK_BG = "#ffffff"; /** Fallback for --foreground (near-black) */ export const FALLBACK_FOREGROUND = "#040D13"; /** Fallback for --muted-foreground used as axis tick color */ export const FALLBACK_TICK = "#9EAAB5"; /** Fallback neutral grey for comparison / suburb lines */ export const FALLBACK_NEUTRAL = "#B9BCBF"; /** Fallback semi-transparent grid line color */ export const FALLBACK_GRID_COLOR = "rgba(0,0,0,0.06)"; /** * Semantic severity colors for alert / status charts. * Match WealthX design tokens: --destructive, --warning, --success. * Intentionally fixed — severity meaning must be recognisable regardless of tenant theme. */ export const SEVERITY_COLORS = { high: "#F44336", // default --destructive oklch(0.643 0.215 28.8) medium: "#FF9800", // default --warning oklch(0.77 0.174 64.1) low: "#4CAF50", // default --success oklch(0.673 0.162 144.2) } as const; /** * Re-export abbreviated currency formatter from shared lib. * Kept as `formatAbbrev` for backward compatibility with existing chart consumers. */ import { formatCurrencyAbbrev as formatAbbrev } from "@/lib/format-currency"; export { formatAbbrev }; // --------------------------------------------------------------------------- // Chart date label — "Jan 15" for x-axis tick labels (daily charts) // --------------------------------------------------------------------------- /** Format an ISO date string as "Jan 15" for daily-chart x-axis labels. */ export function formatChartDateLabel(iso: string): string { const d = new Date(iso); return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); } // --------------------------------------------------------------------------- // Tooltip date format // --------------------------------------------------------------------------- /** * Format an ISO date string for the chart tooltip title. * Daily granularity includes the day; monthly shows only month + year. */ export function formatTooltipDate( iso: string, granularity: ChartGranularity, ): string { const d = new Date(iso); return d.toLocaleDateString( "en-US", granularity === "daily" ? { month: "short", day: "numeric", year: "numeric" } : { month: "short", year: "numeric" }, ); } // --------------------------------------------------------------------------- // Date / count formatters (shared across bar and doughnut charts) // --------------------------------------------------------------------------- /** Format an ISO date string as "Mon 'YY" for X-axis month labels. */ export function formatMonthLabel(iso: string): string { return new Date(iso).toLocaleDateString("en-US", { month: "short", year: "2-digit", }); } /** Format a raw integer count for axis ticks and tooltips. */ export function formatCount(n: number): string { if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`; return String(Math.round(n)); } // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- export function ChartLegendItem({ label, color, lineStyle, }: { label: string; color: string; /** When provided, renders a line indicator instead of a square swatch */ lineStyle?: "solid" | "dashed"; }) { return (
{lineStyle ? ( ) : (
)} {label}
); } /** * One row in a doughnut chart legend — circle swatch, label, dollar value, percent. * The label truncates when the container is constrained. */ export function DoughnutLegendRow({ label, color, value, percent, }: { label: string; color: string; value: number; percent: string; }) { return (
{label}
{formatAbbrev(value)} {percent}
); } export function ChartPeriodButton({ period, active, onClick, unit = "M", }: { period: number; active: boolean; onClick: () => void; unit?: string; }) { return ( ); }