import { useMemo } from 'react'; import type { KanjiEntry } from '../../types/stats'; import { formatNumber } from '../../lib/formatters'; interface KanjiBreakdownProps { kanji: KanjiEntry[]; selectedKanjiId?: number | null; onSelectKanji?: (entry: KanjiEntry) => void; } // Heat scale from rare (cool) to very frequent (warm). Catppuccin Macchiato. const FREQ_TIERS = [ { min: 0.85, color: 'text-ctp-peach', swatch: 'bg-ctp-peach', label: 'Very frequent' }, { min: 0.6, color: 'text-ctp-yellow', swatch: 'bg-ctp-yellow', label: 'Frequent' }, { min: 0.35, color: 'text-ctp-green', swatch: 'bg-ctp-green', label: 'Common' }, { min: 0.15, color: 'text-ctp-teal', swatch: 'bg-ctp-teal', label: 'Occasional' }, { min: 0, color: 'text-ctp-subtext0', swatch: 'bg-ctp-subtext0', label: 'Rare' }, ] as const; function tierFor(intensity: number) { return FREQ_TIERS.find((tier) => intensity >= tier.min) ?? FREQ_TIERS[FREQ_TIERS.length - 1]!; } export function KanjiBreakdown({ kanji, selectedKanjiId = null, onSelectKanji, }: KanjiBreakdownProps) { const { totalOccurrences, maxLogFreq } = useMemo(() => { let total = 0; let maxFreq = 1; for (const entry of kanji) { total += entry.frequency; if (entry.frequency > maxFreq) maxFreq = entry.frequency; } return { totalOccurrences: total, maxLogFreq: Math.log(maxFreq + 1) }; }, [kanji]); if (kanji.length === 0) return null; return (

Kanji Encountered {formatNumber(kanji.length)} unique · {formatNumber(totalOccurrences)} seen

rare
{[...FREQ_TIERS].reverse().map((tier) => ( ))}
frequent
{kanji.map((k) => { // Log scale keeps the heavily-skewed frequency distribution readable. const intensity = maxLogFreq > 0 ? Math.log(k.frequency + 1) / maxLogFreq : 0; const tier = tierFor(intensity); const selected = selectedKanjiId === k.kanjiId; return ( ); })}
); }