/** * Beszel Chart Library * A comprehensive collection of chart components for monitoring dashboards * Can be exported to other projects */ import { type ReactNode, useEffect, useMemo, useState } from "react" import { Area, AreaChart, Line, LineChart, Bar, BarChart, Pie, PieChart, Cell, CartesianGrid, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, ReferenceLine, } from "recharts" // ============================================================================ // TYPES // ============================================================================ export type ChartType = "area" | "line" | "bar" | "pie" export type DataPoint = { label: string dataKey: (data: T) => number | null | undefined color: number | string opacity?: number stackId?: string | number order?: number strokeOpacity?: number activeDot?: boolean strokeWidth?: number } export type ChartTheme = { colors: string[] background: string gridColor: string textColor: string tooltipBg: string tooltipBorder: string } export type ChartProps = { data: T[] dataPoints: DataPoint[] type?: ChartType height?: number showGrid?: boolean showLegend?: boolean showTooltip?: boolean tickFormatter?: (value: number, index: number) => string contentFormatter?: (item: any, key: string) => ReactNode domain?: [number, number] | [string, string] stacked?: boolean showTotal?: boolean itemSorter?: (a: any, b: any) => number reverseStackOrder?: boolean referenceLines?: Array<{ y?: number; x?: number; label?: string; color?: string }> } // ============================================================================ // DEFAULT THEMES // ============================================================================ export const defaultThemes: Record = { light: { colors: [ "#3b82f6", // blue-500 "#22c55e", // green-500 "#f59e0b", // amber-500 "#ef4444", // red-500 "#8b5cf6", // violet-500 "#ec4899", // pink-500 "#14b8a6", // teal-500 "#f97316", // orange-500 ], background: "#ffffff", gridColor: "#e5e7eb", textColor: "#374151", tooltipBg: "#ffffff", tooltipBorder: "#e5e7eb", }, dark: { colors: [ "#60a5fa", // blue-400 "#4ade80", // green-400 "#fbbf24", // amber-400 "#f87171", // red-400 "#a78bfa", // violet-400 "#f472b6", // pink-400 "#2dd4bf", // teal-400 "#fb923c", // orange-400 ], background: "#1f2937", gridColor: "#374151", textColor: "#d1d5db", tooltipBg: "#1f2937", tooltipBorder: "#374151", }, } // ============================================================================ // UTILITY FUNCTIONS // ============================================================================ export function formatBytes(bytes: number, decimals = 2): string { if (bytes === 0) return "0 B" const k = 1024 const dm = decimals < 0 ? 0 : decimals const sizes = ["B", "KB", "MB", "GB", "TB", "PB"] const i = Math.floor(Math.log(bytes) / Math.log(k)) return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}` } export function formatPercentage(value: number, decimals = 2): string { return `${value.toFixed(decimals)}%` } export function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms` if (ms < 60000) return `${(ms / 1000).toFixed(1)}s` return `${(ms / 60000).toFixed(1)}m` } export function formatShortDate(date: Date | string | number): string { const d = new Date(date) return d.toLocaleDateString(undefined, { month: "short", day: "numeric" }) } export function getColor(index: number, theme: ChartTheme = defaultThemes.light): string { return theme.colors[index % theme.colors.length] } // ============================================================================ // CHART CARD COMPONENT // ============================================================================ export function ChartCard({ children, title, description, cornerEl, empty = false, className = "", }: { children: ReactNode title?: string description?: string cornerEl?: ReactNode empty?: boolean className?: string }) { if (empty) { return (
{title &&

{title}

} {description &&

{description}

}
{cornerEl}
No data available
) } return (
{title &&

{title}

} {description &&

{description}

}
{cornerEl}
{children}
) } // ============================================================================ // AREA CHART COMPONENT // ============================================================================ export function BeszelAreaChart({ data, dataPoints, height = 300, showGrid = true, showLegend = false, showTooltip = true, tickFormatter = (v) => `${v}`, contentFormatter = ({ value }) => `${value}`, domain, stacked = false, showTotal = false, itemSorter, reverseStackOrder = false, referenceLines = [], }: ChartProps) { const Areas = useMemo(() => { return dataPoints?.map((dataPoint, i) => { let { color } = dataPoint if (typeof color === "number") { color = defaultThemes.light.colors[color % defaultThemes.light.colors.length] } return ( ) }) }, [dataPoints, stacked]) return (
{showGrid && ( )} {showTooltip && ( [contentFormatter({ value }, name), name]} itemSorter={itemSorter} /> )} {showLegend && } {referenceLines?.map((line, i) => ( ))} {Areas}
) } // ============================================================================ // LINE CHART COMPONENT // ============================================================================ export function BeszelLineChart({ data, dataPoints, height = 300, showGrid = true, showLegend = false, showTooltip = true, tickFormatter = (v) => `${v}`, contentFormatter = ({ value }) => `${value}`, domain, itemSorter, referenceLines = [], }: ChartProps) { const Lines = useMemo(() => { return dataPoints?.map((dataPoint, i) => { let { color } = dataPoint if (typeof color === "number") { color = defaultThemes.light.colors[color % defaultThemes.light.colors.length] } return ( ) }) }, [dataPoints]) return (
{showGrid && ( )} {showTooltip && ( [contentFormatter({ value }, name), name]} itemSorter={itemSorter} /> )} {showLegend && } {referenceLines?.map((line, i) => ( ))} {Lines}
) } // ============================================================================ // BAR CHART COMPONENT // ============================================================================ export function BeszelBarChart({ data, dataPoints, height = 300, showGrid = true, showLegend = false, showTooltip = true, tickFormatter = (v) => `${v}`, contentFormatter = ({ value }) => `${value}`, domain, stacked = false, itemSorter, }: ChartProps) { const Bars = useMemo(() => { return dataPoints?.map((dataPoint, i) => { let { color } = dataPoint if (typeof color === "number") { color = defaultThemes.light.colors[color % defaultThemes.light.colors.length] } return ( ) }) }, [dataPoints, stacked]) return (
{showGrid && ( )} {showTooltip && ( [contentFormatter({ value }, name), name]} itemSorter={itemSorter} /> )} {showLegend && } {Bars}
) } // ============================================================================ // PIE CHART COMPONENT // ============================================================================ export function BeszelPieChart({ data, height = 300, showTooltip = true, innerRadius = 0, }: { data: Array<{ name: string; value: number; color?: string }> height?: number showTooltip?: boolean innerRadius?: number }) { return (
{showTooltip && ( )} {data.map((entry, index) => ( ))}
) } // ============================================================================ // PRE-BUILT MONITORING CHARTS // ============================================================================ export function CpuUsageChart({ data, showMax = false, }: { data: Array<{ time: string; cpu: number; cpum?: number }> showMax?: boolean }) { return ( (showMax ? d.cpum : d.cpu), color: 0, opacity: 0.4, }, ]} tickFormatter={(v) => `${v.toFixed(0)}%`} contentFormatter={({ value }) => `${value?.toFixed(2)}%`} domain={[0, 100]} /> ) } export function MemoryUsageChart({ data, totalMemory, }: { data: Array<{ time: string; mu: number; mm?: number; mb?: number }> totalMemory: number }) { return ( d.mu, color: 1, opacity: 0.4, stackId: "1", }, { label: "Buff/Cache", dataKey: (d) => d.mb, color: 2, opacity: 0.4, stackId: "1", }, ]} tickFormatter={(v) => `${v.toFixed(0)} MB`} contentFormatter={({ value }) => `${value?.toFixed(2)} MB`} domain={[0, totalMemory]} stacked showTotal /> ) } export function DiskUsageChart({ data, }: { data: Array<{ time: string; du: number; dr?: number; dw?: number }> }) { return ( d.dr, color: 0, strokeWidth: 2, }, { label: "Write", dataKey: (d) => d.dw, color: 1, strokeWidth: 2, }, ]} tickFormatter={(v) => `${v?.toFixed(0)} MB/s`} contentFormatter={({ value }) => `${value?.toFixed(2)} MB/s`} /> ) } export function NetworkTrafficChart({ data, }: { data: Array<{ time: string; ns: number; nr: number }> }) { return ( d.ns, color: 4, strokeWidth: 2, }, { label: "Received", dataKey: (d) => d.nr, color: 5, strokeWidth: 2, }, ]} tickFormatter={(v) => `${v?.toFixed(0)} MB/s`} contentFormatter={({ value }) => `${value?.toFixed(2)} MB/s`} /> ) } export function ResponseTimeChart({ data, }: { data: Array<{ time: string; ping: number; status?: number }> }) { return ( d.ping, color: 0, opacity: 0.3, }, ]} tickFormatter={(v) => `${v?.toFixed(0)}ms`} contentFormatter={({ value }) => `${value?.toFixed(0)}ms`} /> ) } export function UptimeChart({ data, }: { data: Array<{ time: string; uptime: number }> }) { return ( d.uptime, color: 1, opacity: 0.4, }, ]} tickFormatter={(v) => `${v?.toFixed(0)}%`} contentFormatter={({ value }) => `${value?.toFixed(2)}%`} domain={[0, 100]} referenceLines={[{ y: 99.9, label: "SLA", color: "#22c55e" }]} /> ) } export function DomainExpiryChart({ data, }: { data: Array<{ time: string; daysUntilExpiry: number; sslDaysUntil: number }> }) { return ( d.daysUntilExpiry, color: 0, opacity: 0.3, }, { label: "SSL Days", dataKey: (d) => d.sslDaysUntil, color: 1, opacity: 0.3, }, ]} tickFormatter={(v) => `${v?.toFixed(0)}d`} contentFormatter={({ value }) => `${value?.toFixed(0)} days`} /> ) } // ============================================================================ // STAT CARD COMPONENTS // ============================================================================ export function StatCard({ title, value, subtitle, trend, icon: Icon, onClick, }: { title: string value: string subtitle?: string trend?: "up" | "down" | "neutral" icon?: React.ComponentType<{ className?: string }> onClick?: () => void }) { const trendColors = { up: "text-green-500", down: "text-red-500", neutral: "text-gray-500", } const TrendIcon = trend ? trend === "up" ? () => ( ) : trend === "down" ? () => ( ) : () => ( ) : null return (

{title}

{value}

{trend && ( )}
{subtitle &&

{subtitle}

}
{Icon && (
)}
) } export function StatusCard({ status, title, subtitle, }: { status: "up" | "down" | "warning" | "paused" title: string subtitle?: string }) { const configs = { up: { bg: "bg-green-500", text: "text-green-500", label: "Up" }, down: { bg: "bg-red-500", text: "text-red-500", label: "Down" }, warning: { bg: "bg-yellow-500", text: "text-yellow-500", label: "Warning" }, paused: { bg: "bg-gray-500", text: "text-gray-500", label: "Paused" }, } const config = configs[status] return (

{title}

{subtitle &&

{subtitle}

}
) } // ============================================================================ // EXPORTS // ============================================================================ export { // Re-exports from recharts for convenience AreaChart, LineChart, BarChart, PieChart, ResponsiveContainer, CartesianGrid, XAxis, YAxis, Tooltip, Legend, ReferenceLine, }