| 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> |
| ); |
| } |
|
|