RYP / src /components /ComplexityGrowthChart.tsx
Soumya79's picture
Upload 1361 files
f91a684 verified
import {
CartesianGrid,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
type ChartTheme = 'dark' | 'light';
type ComplexityProfile = {
key: string;
label: string;
color: string;
inputSizes: number[];
getGrowth: (n: number) => number;
getAuxiliaryLabel?: (n: number) => string;
inputLabel?: string;
note?: string;
};
type ComplexityPoint = {
n: number;
estimatedGrowth: number;
normalizedGrowth: number;
auxiliaryLabel?: string;
};
type ComplexityGrowthChartProps = {
complexity?: string | null;
theme?: ChartTheme;
className?: string;
};
const STANDARD_INPUT_SIZES = [1, 2, 4, 8, 16, 32, 64, 128];
const EXPONENTIAL_INPUT_SIZES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const normalizeComplexity = (value?: string | null) =>
(value ?? '').replace(/\s+/g, '').toLowerCase();
const getComplexityProfile = (complexity?: string | null): ComplexityProfile | null => {
const normalized = normalizeComplexity(complexity);
if (!normalized) {
return null;
}
if (/^o\(1\)$/.test(normalized)) {
return {
key: 'constant',
label: 'O(1)',
color: '#22c55e',
inputSizes: STANDARD_INPUT_SIZES,
getGrowth: () => 1,
};
}
if (/nlogn/.test(normalized)) {
return {
key: 'linearithmic',
label: 'O(n log n)',
color: '#a855f7',
inputSizes: STANDARD_INPUT_SIZES,
getGrowth: (n) => n * Math.log2(Math.max(2, n)),
};
}
if (/nlogk/.test(normalized)) {
return {
key: 'k-way-heap',
label: 'O(N log k)',
color: '#8b5cf6',
inputSizes: STANDARD_INPUT_SIZES,
inputLabel: 'Total elements (N)',
getGrowth: (n) => {
const k = Math.max(2, Math.round(Math.sqrt(n)));
return n * Math.log2(k);
},
getAuxiliaryLabel: (n) => `representative k = ${Math.max(2, Math.round(Math.sqrt(n)))}`,
note: 'Representative multi-list plot: k grows with sqrt(N)',
};
}
if (/2\^n/.test(normalized)) {
return {
key: 'exponential',
label: 'O(2^n)',
color: '#e11d48',
inputSizes: EXPONENTIAL_INPUT_SIZES,
getGrowth: (n) => 2 ** n,
};
}
if (/n\^3/.test(normalized)) {
return {
key: 'cubic',
label: 'O(n^3)',
color: '#f97316',
inputSizes: STANDARD_INPUT_SIZES,
getGrowth: (n) => n ** 3,
};
}
if (/n\^2|m\*n|mn/.test(normalized)) {
return {
key: 'quadratic',
label: normalized.includes('m') ? 'O(m * n)' : 'O(n^2)',
color: '#ef4444',
inputSizes: STANDARD_INPUT_SIZES,
getGrowth: (n) => n ** 2,
note: normalized.includes('m') ? 'Representative grid case: m = n' : undefined,
};
}
if (/^o\(n\)$/.test(normalized)) {
return {
key: 'linear',
label: 'O(n)',
color: '#60a5fa',
inputSizes: STANDARD_INPUT_SIZES,
getGrowth: (n) => n,
};
}
if (/logn/.test(normalized)) {
return {
key: 'logarithmic',
label: 'O(log n)',
color: '#eab308',
inputSizes: STANDARD_INPUT_SIZES,
getGrowth: (n) => Math.log2(n),
};
}
return null;
};
const buildComplexityData = (profile: ComplexityProfile): ComplexityPoint[] => {
const rawPoints = profile.inputSizes.map((n) => ({
n,
estimatedGrowth: profile.getGrowth(n),
auxiliaryLabel: profile.getAuxiliaryLabel?.(n),
}));
const maxGrowth = Math.max(...rawPoints.map((point) => point.estimatedGrowth));
const minGrowth = Math.min(...rawPoints.map((point) => point.estimatedGrowth));
const hasFlatGrowth = maxGrowth === minGrowth;
return rawPoints.map((point) => ({
...point,
normalizedGrowth: hasFlatGrowth ? 48 : (point.estimatedGrowth / maxGrowth) * 100,
}));
};
const formatGrowth = (value: number) => {
if (!Number.isFinite(value)) {
return 'very high';
}
if (Math.abs(value) < 1000) {
return Number.isInteger(value) ? String(value) : value.toFixed(value < 10 ? 2 : 1);
}
if (Math.abs(value) < 1_000_000) {
return value.toLocaleString(undefined, { maximumFractionDigits: 0 });
}
return value.toExponential(2);
};
function ComplexityTooltip({
active,
payload,
label,
complexityLabel,
inputLabel,
theme,
}: {
active?: boolean;
payload?: Array<{ payload?: ComplexityPoint }>;
label?: number | string;
complexityLabel: string;
inputLabel: string;
theme: ChartTheme;
}) {
if (!active || !payload?.length) {
return null;
}
const point = payload[0]?.payload;
const isDark = theme === 'dark';
return (
<div
className={[
'rounded-md border px-3 py-2 text-xs shadow-xl',
isDark
? 'border-white/10 bg-[#111827] text-slate-100'
: 'border-slate-200 bg-white text-slate-900',
].join(' ')}
>
<div className="font-mono font-black">{complexityLabel}</div>
<div className={isDark ? 'mt-1 text-slate-300' : 'mt-1 text-slate-600'}>
{inputLabel} = {label}
</div>
{point?.auxiliaryLabel && (
<div className={isDark ? 'text-slate-300' : 'text-slate-600'}>{point.auxiliaryLabel}</div>
)}
<div className={isDark ? 'text-slate-300' : 'text-slate-600'}>
estimated growth = {formatGrowth(point?.estimatedGrowth ?? 0)}
</div>
</div>
);
}
export default function ComplexityGrowthChart({
complexity,
theme = 'dark',
className = '',
}: ComplexityGrowthChartProps) {
const profile = getComplexityProfile(complexity);
const isDark = theme === 'dark';
const axisColor = isDark ? '#94a3b8' : '#475569';
const gridColor = isDark ? 'rgba(148,163,184,0.16)' : 'rgba(100,116,139,0.18)';
const fallbackClass = isDark ? 'text-slate-400' : 'text-slate-500';
const inputLabel = profile?.inputLabel ?? 'Input size (n)';
if (!profile) {
return (
<div className={`flex h-full min-h-[220px] items-center justify-center rounded-md ${fallbackClass} ${className}`}>
<div className="text-center text-xs font-semibold">
No graph available for {complexity ?? 'unknown complexity'}.
</div>
</div>
);
}
const data = buildComplexityData(profile);
return (
<div className={`flex h-full min-h-[220px] flex-col ${className}`}>
<div className="min-h-0 flex-1">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} margin={{ top: 18, right: 22, left: 8, bottom: 24 }}>
<CartesianGrid stroke={gridColor} strokeDasharray="4 6" />
<XAxis
dataKey="n"
axisLine={{ stroke: axisColor }}
tickLine={{ stroke: axisColor }}
tick={{ fill: axisColor, fontSize: 11, fontWeight: 700 }}
label={{
value: inputLabel,
position: 'insideBottom',
offset: -14,
fill: axisColor,
fontSize: 12,
fontWeight: 800,
}}
/>
<YAxis
domain={[0, 100]}
axisLine={{ stroke: axisColor }}
tickLine={{ stroke: axisColor }}
tick={{ fill: axisColor, fontSize: 11, fontWeight: 700 }}
tickFormatter={(value) => `${value}%`}
label={{
value: 'Normalized growth',
angle: -90,
position: 'insideLeft',
fill: axisColor,
fontSize: 12,
fontWeight: 800,
}}
/>
<Tooltip
cursor={{ stroke: profile.color, strokeWidth: 1.5, strokeDasharray: '4 6' }}
content={<ComplexityTooltip complexityLabel={profile.label} inputLabel={inputLabel} theme={theme} />}
/>
<Line
type="monotone"
dataKey="normalizedGrowth"
name={profile.label}
stroke={profile.color}
strokeWidth={4}
dot={{ r: 3.5, fill: profile.color, strokeWidth: 0 }}
activeDot={{ r: 6, fill: profile.color, stroke: isDark ? '#111827' : '#ffffff', strokeWidth: 2 }}
isAnimationActive={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
<div className={`mt-2 flex items-center justify-between gap-3 text-[11px] font-bold ${isDark ? 'text-slate-400' : 'text-slate-600'}`}>
<span className="font-mono" style={{ color: profile.color }}>
{profile.label}
</span>
{profile.note && <span>{profile.note}</span>}
</div>
</div>
);
}