/** * This calendar year by difficulty — LeetCode's solved/submissions bar chart, * sharing its row with the heatmap. Stacked Easy / Med. / Hard bars per month * or per week (Monday-start, like the heatmap rows); "Solved" counts each * problem once, on its first-pass date (the date `recent` uses), while * "Submissions" counts every logged attempt. The totals under the chart are * the year's, in the selected mode. Buckets are cut from `year`, which always * covers January 1 to today. */ import { useState } from "react"; import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { StatsPending } from "../components/progress/shared"; import { Card, CardAction, CardContent, CardFooter, CardHeader } from "../components/ui/card"; import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "../components/ui/chart"; import { ToggleGroup, ToggleGroupItem } from "../components/ui/toggle-group"; import { DIFFICULTIES, type Day, type Difficulty, useStats, utcDate } from "../lib/stats"; export const client = "load"; 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. const DIFFICULTY_TEXT: Record = { easy: "text-chart-1", medium: "text-chart-2", hard: "text-chart-3", }; type Mode = "solved" | "attempts"; type Grain = "month" | "week"; const DAY_MS = 86_400_000; const MONTH = new Intl.DateTimeFormat("en-US", { month: "short", timeZone: "UTC" }); const MONTH_YEAR = new Intl.DateTimeFormat("en-US", { month: "short", year: "numeric", timeZone: "UTC" }); const SHORT = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" }); interface Bucket extends Record { index: number; /** Axis tick: the month the bucket starts in. */ tick: string; /** Tooltip heading. */ label: string; } /** The year's buckets, every one present (future ones empty), filled from `days`. */ function buckets(days: Day[], year: number, grain: Grain, mode: Mode): Bucket[] { const jan1 = Date.UTC(year, 0, 1); const lead = (new Date(jan1).getUTCDay() + 6) % 7; // Jan 1's Monday-based weekday const length = Math.round((Date.UTC(year + 1, 0, 1) - jan1) / DAY_MS); const out: Bucket[] = []; if (grain === "month") { for (let m = 0; m < 12; m++) { const start = new Date(Date.UTC(year, m, 1)); out.push({ index: m, tick: MONTH.format(start), label: MONTH_YEAR.format(start), easy: 0, medium: 0, hard: 0 }); } } else { const weeks = Math.floor((length - 1 + lead) / 7) + 1; for (let w = 0; w < weeks; w++) { const start = new Date(Math.max(jan1, jan1 + (w * 7 - lead) * DAY_MS)); const end = new Date(Math.min(jan1 + (length - 1) * DAY_MS, jan1 + (w * 7 - lead + 6) * DAY_MS)); out.push({ index: w, tick: MONTH.format(start), label: `${SHORT.format(start)} – ${SHORT.format(end)}`, easy: 0, medium: 0, hard: 0, }); } } for (const day of days) { const date = utcDate(day.date); if (date.getUTCFullYear() !== year) continue; const i = grain === "month" ? date.getUTCMonth() : Math.floor(((date.getTime() - jan1) / DAY_MS + lead) / 7); const bucket = out[i]; if (!bucket) continue; for (const d of DIFFICULTIES) bucket[d] += day[mode][d]; } return out; } export default function SubmissionChart() { const load = useStats(); const [mode, setMode] = useState("solved"); const [grain, setGrain] = useState("month"); if (load.phase !== "ready") { return ( ); } const year = utcDate(load.stats.year.at(-1)?.date ?? "1970-01-01").getUTCFullYear(); const data = buckets(load.stats.year, year, grain, mode); const totals = Object.fromEntries( DIFFICULTIES.map((d) => [d, data.reduce((n, b) => n + b[d], 0)]), ); const peak = Math.max(...data.map((b) => b.easy + b.medium + b.hard)); // A round ceiling for the one labelled gridline, as LeetCode draws it. const top = peak <= 5 ? 5 : Math.ceil(peak / 10) * 10; return ( value && setMode(value as Mode)} className="rounded-full bg-muted p-0.5" aria-label="Count" > Solved Submissions
value && setGrain(value as Grain)} className="rounded-full bg-muted p-0.5" aria-label="Bucket size" > M W
( {data[Number(payload.value)]?.tick} )} /> ( items[0]?.payload?.label} indicator="dot" /> )} /> {(["hard", "medium", "easy"] as const).map((d) => ( ))} {DIFFICULTIES.map((d) => ( {chartConfig[d].label} {totals[d]} ))} {year}
); }