144 lines
5.2 KiB
TypeScript
144 lines
5.2 KiB
TypeScript
import * as React from "react";
|
||
|
||
export interface BarChartData {
|
||
label: string;
|
||
value: number;
|
||
maxValue?: number;
|
||
color?: string;
|
||
labelColor?: string;
|
||
valuePrefix?: string;
|
||
valueSuffix?: string;
|
||
}
|
||
|
||
interface CustomBarChartProps {
|
||
data: BarChartData[];
|
||
title?: string;
|
||
height?: string;
|
||
barHeight?: string;
|
||
showAxisLabels?: boolean;
|
||
className?: string;
|
||
loading?: boolean;
|
||
}
|
||
|
||
export function CustomBarChart({
|
||
data,
|
||
title,
|
||
height = "auto",
|
||
barHeight = "h-6",
|
||
showAxisLabels = true,
|
||
className = "",
|
||
loading = false
|
||
}: CustomBarChartProps) {
|
||
// Calculate the maximum value across all data points for consistent scaling
|
||
const globalMaxValue = Math.max(...data.map(item => item.maxValue || item.value));
|
||
|
||
// Function to format numbers with Persian digits
|
||
const formatNumber = (value: number): string => {
|
||
const str = String(value);
|
||
const map: Record<string, string> = {
|
||
"0": "۰", "1": "۱", "2": "۲", "3": "۳", "4": "۴",
|
||
"5": "۵", "6": "۶", "7": "۷", "8": "۸", "9": "۹"
|
||
};
|
||
return str.replace(/[0-9]/g, (d) => map[d] ?? d);
|
||
};
|
||
|
||
// Loading skeleton
|
||
if (loading) {
|
||
return (
|
||
<div className={`space-y-6 ${className}`} style={{ height }}>
|
||
{title && (
|
||
<div className="h-7 bg-gray-600 rounded animate-pulse mb-4 w-1/2"></div>
|
||
)}
|
||
|
||
<div className="space-y-4">
|
||
{Array.from({ length: 4 }).map((_, index) => (
|
||
<div key={index} className="flex items-center gap-3">
|
||
{/* Label skeleton */}
|
||
<div className="h-4 bg-gray-600 rounded animate-pulse min-w-[160px]"></div>
|
||
|
||
{/* Bar skeleton */}
|
||
<div className="flex-1 bg-gray-700 rounded-full h-6">
|
||
<div className="h-6 bg-gray-600 rounded-full animate-pulse" style={{ width: `${Math.random() * 60 + 20}%` }}></div>
|
||
</div>
|
||
|
||
{/* Value skeleton */}
|
||
<div className="h-4 bg-gray-600 rounded animate-pulse min-w-[60px]"></div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={`space-y-6 ${className}`} style={{ height }}>
|
||
{title && (
|
||
<h3 className="text-xl font-bold text-white font-persian text-right mb-4">
|
||
{title}
|
||
</h3>
|
||
)}
|
||
|
||
<div className="space-y-4">
|
||
{data.map((item, index) => {
|
||
const percentage = globalMaxValue > 0 ? (item.value / globalMaxValue) * 100 : 0;
|
||
const displayValue = item.value.toFixed(1);
|
||
|
||
return (
|
||
<div key={index} className="flex items-center gap-3">
|
||
{/* Label */}
|
||
<span
|
||
className={`font-persian text-sm min-w-[160px] text-right ${
|
||
item.labelColor || 'text-white'
|
||
}`}
|
||
>
|
||
{item.label}
|
||
</span>
|
||
|
||
{/* Bar Container */}
|
||
<div className={`flex-1 flex items-center bg-gray-700 rounded-full relative overflow-hidden ${barHeight}`}>
|
||
<div
|
||
className={`${barHeight} rounded-full transition-all duration-700 ease-out relative ${
|
||
item.color || 'bg-emerald-400'
|
||
}`}
|
||
style={{
|
||
width: `${Math.min(percentage, 100)}%`
|
||
}}
|
||
>
|
||
{/* Add a subtle gradient effect for better visual appeal */}
|
||
<div className="absolute inset-0 bg-gradient-to-r from-transparent to-white/10 rounded-full"></div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Value Label */}
|
||
<span className={`font-bold text-sm min-w-[60px] text-left ${
|
||
item.color?.includes('emerald') ? 'text-emerald-400' :
|
||
item.color?.includes('blue') ? 'text-blue-400' :
|
||
item.color?.includes('purple') ? 'text-purple-400' :
|
||
item.color?.includes('red') ? 'text-red-400' :
|
||
item.color?.includes('yellow') ? 'text-yellow-400' :
|
||
'text-emerald-400'
|
||
}`}>
|
||
{item.valuePrefix || ''}{formatNumber(parseFloat(displayValue))}{item.valueSuffix || '%'}
|
||
</span>
|
||
</div>
|
||
);
|
||
})}
|
||
|
||
{/* Axis Labels */}
|
||
{showAxisLabels && globalMaxValue > 0 && (
|
||
<div className="flex items-center gap-3 mt-6">
|
||
<span className="min-w-[160px]"></span>
|
||
<div className="flex-1 flex justify-between pt-2 border-t border-gray-700">
|
||
<span className="text-gray-400 text-xs">{formatNumber(0)}%</span>
|
||
<span className="text-gray-400 text-xs">{formatNumber(Math.round(globalMaxValue / 4))}%</span>
|
||
<span className="text-gray-400 text-xs">{formatNumber(Math.round(globalMaxValue / 2))}%</span>
|
||
<span className="text-gray-400 text-xs">{formatNumber(Math.round((globalMaxValue * 3) / 4))}%</span>
|
||
<span className="text-gray-400 text-xs">{formatNumber(Math.round(globalMaxValue))}%</span>
|
||
</div>
|
||
<span className="min-w-[60px]"></span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
} |