Enhanced the visual comparison chart for better readability: 📊 Logarithmic Scale: - Use log10 scale instead of linear for better visualization - Handles units with vastly different magnitudes (mm vs km) - Minimum bar width of 5% for visibility - Normalizes across the full range (minLog to maxLog) - Prevents tiny bars that are hard to see ✨ Visual Improvements: - Taller bars (h-3 instead of h-2) for better visibility - Larger, bolder value display (text-lg font-bold) - Better spacing (space-y-1.5) - Percentage indicator on each bar - White text on bars >50% filled - Shadow on bars for depth - Improved typography hierarchy - Better gap spacing between label and value 🎨 Layout Enhancements: - Unit name on left, value on right - Value with unit abbreviation in muted color - Flex layout with proper wrapping - Tabular numbers for alignment - Relative positioning for percentage labels This makes the chart view much more useful for comparing units with different orders of magnitude (e.g., nanometers to kilometers). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
'use client';
|
|
|
|
import { useMemo } from 'react';
|
|
import { type ConversionResult } from '@/lib/units';
|
|
import { formatNumber, cn } from '@/lib/utils';
|
|
|
|
interface VisualComparisonProps {
|
|
conversions: ConversionResult[];
|
|
color: string;
|
|
}
|
|
|
|
export default function VisualComparison({
|
|
conversions,
|
|
color,
|
|
}: VisualComparisonProps) {
|
|
// Calculate percentages for visual bars using logarithmic scale
|
|
const withPercentages = useMemo(() => {
|
|
if (conversions.length === 0) return [];
|
|
|
|
// Filter out zero or negative values for log scale
|
|
const validConversions = conversions.filter(c => c.value > 0);
|
|
if (validConversions.length === 0) {
|
|
return conversions.map(c => ({ ...c, percentage: 0 }));
|
|
}
|
|
|
|
// Use logarithmic scale for better visualization across different magnitudes
|
|
const logValues = validConversions.map(c => Math.log10(c.value));
|
|
const minLog = Math.min(...logValues);
|
|
const maxLog = Math.max(...logValues);
|
|
const logRange = maxLog - minLog;
|
|
|
|
return conversions.map(c => {
|
|
if (c.value <= 0) return { ...c, percentage: 0 };
|
|
|
|
const logValue = Math.log10(c.value);
|
|
// Normalize to 0-100 range, with minimum bar width of 5%
|
|
const percentage = logRange === 0
|
|
? 100
|
|
: Math.max(5, ((logValue - minLog) / logRange) * 100);
|
|
|
|
return {
|
|
...c,
|
|
percentage,
|
|
};
|
|
});
|
|
}, [conversions]);
|
|
|
|
if (conversions.length === 0) {
|
|
return (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
Enter a value to see conversions
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{withPercentages.map(item => (
|
|
<div key={item.unit} className="space-y-1.5">
|
|
<div className="flex items-baseline justify-between gap-4">
|
|
<span className="text-sm font-medium text-foreground min-w-0 flex-shrink">
|
|
{item.unitInfo.plural}
|
|
</span>
|
|
<span className="text-lg font-bold tabular-nums flex-shrink-0">
|
|
{formatNumber(item.value)}
|
|
<span className="text-sm font-normal text-muted-foreground ml-1">
|
|
{item.unit}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
<div className="relative h-3 bg-muted rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full rounded-full transition-all duration-500 ease-out shadow-sm"
|
|
style={{
|
|
width: `${item.percentage}%`,
|
|
backgroundColor: `var(--color-${color})`,
|
|
}}
|
|
/>
|
|
{/* Percentage indicator */}
|
|
<div
|
|
className="absolute top-0 right-0 h-full flex items-center pr-2 text-[10px] font-medium"
|
|
style={{
|
|
color: item.percentage > 50 ? 'white' : 'inherit',
|
|
}}
|
|
>
|
|
{item.percentage < 100 && (
|
|
<span className={item.percentage > 50 ? 'text-white' : 'text-muted-foreground'}>
|
|
{Math.round(item.percentage)}%
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|