docs: add toggle UI components, submission chart, and update stats schema

This commit is contained in:
Prad Nukala committed 2026-09-29 17:45:01 -04:00
1 parent 5d2d1dff3c
commit d194cedf7a
6 files changed
+449 -65

No files matched your search

+78 -52
View File
@@ -1,18 +1,20 @@
/**
* The past year of attempts — LeetCode's calendar heatmap: one cell per day,
* weeks as columns, each month its own block with a gap between, and the
* year's attempt count, active days and longest streak above it. The colour
* ramp is the README/email heatmap's (theme.css `--heat-0..4`, level =
* attempts capped at 4) and rows start on Monday like it, so the three
* heatmaps read the same. Drawn as SVG in a viewBox, so it scales to the
* column instead of scrolling.
* The last six months of attempts — LeetCode's calendar heatmap, halved to
* share its row with the submissions chart: one cell per day, weeks as
* columns, each month its own block, and the window's attempt count, active
* days and longest streak above it. The figures are counted from exactly the
* cells drawn, so they cannot disagree with the grid. The colour ramp is the
* README/email heatmap's (theme.css `--heat-0..4`, level = attempts capped at
* 4) and rows start on Monday like it, so the three heatmaps read the same.
* Drawn as SVG in a viewBox, so it scales to its column instead of scrolling.
*/
import { StatsPending, TipRow, useHoverTip } from "../components/progress/shared";
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
import { type Stats, useStats, utcDate } from "../lib/stats";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
import { type Day, useStats, utcDate } from "../lib/stats";
export const client = "load";
const MONTHS = 6;
const CELL = 10;
const STEP = 13; // cell + gap
const MONTH_GAP = 8;
@@ -22,42 +24,54 @@ const HEAT = ["var(--heat-0)", "var(--heat-1)", "var(--heat-2)", "var(--heat-3)"
const MONTH = new Intl.DateTimeFormat("en-US", { month: "short", timeZone: "UTC" });
const DAY = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
interface Cell {
x: number;
y: number;
date: Date;
attempts: number;
passes: number;
}
interface Block {
label: string;
x: number;
width: number;
cells: { x: number; y: number; date: Date; attempts: number; passes: number }[];
cells: Cell[];
}
/**
* Month blocks laid left to right. Within a block a day sits at row =
* weekday (Mon = 0) and column = its week counted from the block's first day,
* so a month starting on a Thursday opens with a short first column.
* Month blocks laid left to right over the last MONTHS calendar months. Within
* a block a day sits at row = weekday (Mon = 0) and column = its week counted
* from the 1st, so a month starting on a Thursday opens with a short column.
*/
function layout(days: Stats["year"]["days"]): { blocks: Block[]; width: number } {
function layout(year: Day[]): { blocks: Block[]; width: number } {
const last = utcDate(year.at(-1)?.date ?? "1970-01-01");
const from = Date.UTC(last.getUTCFullYear(), last.getUTCMonth() - (MONTHS - 1), 1);
const blocks: Block[] = [];
let x = 0;
let month = -1;
let block: Block | undefined;
let lead = 0;
let index = 0;
for (const day of days) {
for (const day of year) {
const date = utcDate(day.date);
if (date.getUTCMonth() !== month) {
if (date.getTime() < from) continue;
let block = blocks.at(-1);
if (date.getUTCDate() === 1 || !block) {
if (block) x += block.width + MONTH_GAP;
month = date.getUTCMonth();
lead = (date.getUTCDay() + 6) % 7;
index = 0;
block = { label: MONTH.format(date), x, width: 0, cells: [] };
blocks.push(block);
}
const row = (lead + index) % 7;
const col = Math.floor((lead + index) / 7);
block!.cells.push({ x: x + col * STEP, y: row * STEP, date, attempts: day.attempts, passes: day.passes });
block!.width = (col + 1) * STEP - (STEP - CELL);
index++;
const first = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1));
const index = (first.getUTCDay() + 6) % 7 + date.getUTCDate() - 1;
const col = Math.floor(index / 7);
block.cells.push({
x: x + col * STEP,
y: (index % 7) * STEP,
date,
attempts: day.attempts.easy + day.attempts.medium + day.attempts.hard,
passes: day.passes,
});
block.width = col * STEP + CELL;
}
return { blocks, width: block ? x + block.width : 0 };
const end = blocks.at(-1);
return { blocks, width: end ? end.x + end.width : 0 };
}
export default function SubmissionHeatmap() {
@@ -68,33 +82,47 @@ export default function SubmissionHeatmap() {
return (
<Card>
<CardContent>
<StatsPending load={load} className="h-32 w-full" />
<StatsPending load={load} className="h-40 w-full" />
</CardContent>
</Card>
);
}
const { year } = load.stats;
const { blocks, width } = layout(year.days);
const { blocks, width } = layout(load.stats.year);
const cells = blocks.flatMap((b) => b.cells);
let run = 0;
let streak = 0;
for (const c of cells) {
run = c.attempts > 0 ? run + 1 : 0;
streak = Math.max(streak, run);
}
const height = 7 * STEP - (STEP - CELL) + LABEL_H;
return (
<Card>
<CardHeader>
<CardTitle className="flex items-baseline gap-2">
<span className="font-semibold text-2xl tabular-nums">{year.attempts}</span>
<span className="font-normal text-muted-foreground">attempts in the past one year</span>
<span className="font-semibold text-2xl tabular-nums">{cells.reduce((n, c) => n + c.attempts, 0)}</span>
<span className="font-normal text-muted-foreground text-sm">attempts in the past {MONTHS} months</span>
</CardTitle>
<CardDescription>
Total active days: <span className="text-foreground tabular-nums">{year.active_days}</span>
<CardDescription className="flex gap-4">
<span>
Active days:{" "}
<span className="text-foreground tabular-nums">{cells.filter((c) => c.attempts > 0).length}</span>
</span>
<span>
Max streak: <span className="text-foreground tabular-nums">{streak}</span>
</span>
</CardDescription>
<CardAction className="text-muted-foreground text-sm">
Max streak: <span className="text-foreground tabular-nums">{year.max_streak}</span>
</CardAction>
</CardHeader>
<CardContent>
<div ref={hover.ref} className="relative" onPointerLeave={hover.hide}>
<svg viewBox={`0 0 ${width} ${height}`} className="h-auto w-full" role="img" aria-label="Attempts per day over the past year">
<CardContent className="flex flex-1 items-center">
<div ref={hover.ref} className="relative w-full" onPointerLeave={hover.hide}>
<svg
viewBox={`0 0 ${width} ${height}`}
className="h-auto w-full"
role="img"
aria-label={`Attempts per day over the past ${MONTHS} months`}
>
{blocks.map((block) => (
<g key={`${block.label}-${block.x}`}>
{block.cells.map((cell) => (
@@ -118,16 +146,14 @@ export default function SubmissionHeatmap() {
}
/>
))}
{block.width >= 2 * STEP && (
<text
x={block.x + block.width / 2}
y={height - 3}
textAnchor="middle"
className="fill-muted-foreground text-[9px]"
>
{block.label}
</text>
)}
<text
x={block.x + block.width / 2}
y={height - 3}
textAnchor="middle"
className="fill-muted-foreground text-[9px]"
>
{block.label}
</text>
</g>
))}
</svg>