/** * Solved-by-difficulty ring — LeetCode's profile gauge. A 270° arc split into * one segment per difficulty, each sized by that difficulty's share of the * catalog; the faint track is the whole set, the solid arc the solved part. * The centre reads the total, or one difficulty while its segment (or its * stat box) is hovered. "Attempting" is the open-miss count: problems whose * latest attempt failed. */ import { useState } from "react"; import { Pie, PieChart } from "recharts"; import { Card, CardContent } from "../ui/card"; import { type ChartConfig, ChartContainer } from "../ui/chart"; import { DIFFICULTIES, type Difficulty, useStats } from "../../lib/stats"; import { cn } from "../../lib/utils"; import { StatsPending } from "./shared"; const chartConfig = { easy: { label: "Easy", color: "var(--chart-1)" }, medium: { label: "Med.", color: "var(--chart-2)" }, hard: { label: "Hard", color: "var(--chart-3)" }, } satisfies ChartConfig; // Static class names, so Tailwind's scanner sees every one (the chart's // --color-* variables exist only inside ChartContainer). const DIFFICULTY_TEXT: Record = { easy: "text-chart-1", medium: "text-chart-2", hard: "text-chart-3", }; // The gauge opens at the bottom: from 225° (lower left) clockwise to -45° // (lower right), in Recharts' counter-clockwise-from-3-o'clock degrees. const START = 225; const SWEEP = 270; const GAP = 8; export default function SolvedRing() { const load = useStats(); const [focus, setFocus] = useState(null); if (load.phase !== "ready") { return ( ); } const rows = DIFFICULTIES.map( (difficulty) => load.stats.difficulty.find((d) => d.difficulty === difficulty) ?? { difficulty, total: 0, done: 0 }, ); const total = rows.reduce((n, r) => n + r.total, 0); const done = rows.reduce((n, r) => n + r.done, 0); const span = SWEEP - GAP * (rows.length - 1); const segments: ((typeof rows)[number] & { from: number; to: number; solvedTo: number })[] = []; let cursor = START; for (const r of rows) { const size = total > 0 ? (span * r.total) / total : 0; const solved = r.total > 0 ? size * (r.done / r.total) : 0; segments.push({ ...r, from: cursor, to: cursor - size, solvedTo: cursor - solved }); cursor -= size + GAP; } const focused = focus ? rows.find((r) => r.difficulty === focus) : undefined; const arc = { innerRadius: "80%", outerRadius: "88%", cornerRadius: 99, stroke: "none", dataKey: "value" } as const; return (
{segments.map((s) => ( setFocus(s.difficulty)} onMouseLeave={() => setFocus(null)} /> ))} {segments .filter((s) => s.done > 0) .map((s) => ( setFocus(s.difficulty)} onMouseLeave={() => setFocus(null)} /> ))}
{focused ? focused.done : done} /{focused ? focused.total : total}
{focused ? chartConfig[focused.difficulty].label : "Solved"}
{load.stats.attempting} Attempting
{rows.map((r) => (
setFocus(r.difficulty)} onPointerLeave={() => setFocus(null)} > {chartConfig[r.difficulty].label} {r.done}/{r.total}
))}
); }