feat(api): add difficulty‑segmented solved data to yearly stats

This commit is contained in:
Prad Nukala committed 2026-09-29 17:44:54 -04:00
1 parent 6e9c407698
commit dde06daea3
1 file changed
+44 -30
+44 -30
View File
@@ -10,8 +10,8 @@
* free to colour it however it likes.
*
* `recent`, `attempting` and `year` feed the docs page's LeetCode-style cards
* (recently solved, the solved ring, the year heatmap). They are additive: the
* terminal dashboard ignores them.
* (recently solved, the solved ring, the heatmap and the submissions chart).
* They are additive: the terminal dashboard ignores them.
*/
import {
CAMPAIGN_DAYS,
@@ -59,7 +59,7 @@ interface TopicStat {
temperature: Temperature;
}
/** Days in the year heatmap: the 365 ending today, like LeetCode's "past one year". */
/** Days in `year`: the 365 ending today, which always covers the current calendar year to date. */
const YEAR_DAYS = 365;
/** How many problems `recent` lists. */
@@ -252,38 +252,52 @@ export async function buildStats(db: D1Database, today: string): Promise<object>
)
.first<{ n: number }>();
// Attempts per day over the past year, every day present, plus the three
// figures LeetCode prints above its heatmap. `heat` stays campaign-scoped
// (the terminal dashboard lays it out by campaign week); this is the
// calendar-scoped view of the same table.
// The past year, one entry per day (zeros included), split by difficulty two
// ways: every logged attempt, and every first solve — the same first-pass
// date `recent` uses. `heat` stays campaign-scoped (the terminal dashboard
// lays it out by campaign week); this is the calendar-scoped view the docs
// heatmap and submissions chart window and bucket for themselves.
const yearStart = addDays(today, -(YEAR_DAYS - 1));
const { results: yearRows } = await db
const { results: attemptRows } = await db
.prepare(
`SELECT date,
COUNT(*) AS attempts,
SUM(CASE WHEN result = 'pass' THEN 1 ELSE 0 END) AS passes
FROM attempts WHERE date BETWEEN ?1 AND ?2 GROUP BY date`,
`SELECT a.date AS date, p.difficulty AS difficulty,
COUNT(*) AS n,
SUM(CASE WHEN a.result = 'pass' THEN 1 ELSE 0 END) AS passes
FROM attempts a JOIN problems p ON p.lc_number = a.lc_number
WHERE a.date BETWEEN ?1 AND ?2
GROUP BY a.date, p.difficulty`,
)
.bind(yearStart, today)
.all<{ date: string; attempts: number; passes: number | null }>();
const yearByDate = new Map(yearRows.map((r) => [r.date, r]));
const yearDays = Array.from({ length: YEAR_DAYS }, (_, i) => {
const date = addDays(yearStart, i);
const row = yearByDate.get(date);
return { date, attempts: row?.attempts ?? 0, passes: row?.passes ?? 0 };
});
let run = 0;
let maxStreak = 0;
for (const d of yearDays) {
run = d.attempts > 0 ? run + 1 : 0;
maxStreak = Math.max(maxStreak, run);
.all<{ date: string; difficulty: string; n: number; passes: number | null }>();
const { results: solvedRows } = await db
.prepare(
`SELECT date, difficulty, COUNT(*) AS n
FROM (SELECT MIN(a.date) AS date, p.difficulty AS difficulty
FROM attempts a JOIN problems p ON p.lc_number = a.lc_number
WHERE a.result = 'pass'
GROUP BY p.lc_number)
WHERE date BETWEEN ?1 AND ?2
GROUP BY date, difficulty`,
)
.bind(yearStart, today)
.all<{ date: string; difficulty: string; n: number }>();
const year = Array.from({ length: YEAR_DAYS }, (_, i) => ({
date: addDays(yearStart, i),
passes: 0,
attempts: Object.fromEntries(DIFFICULTIES.map((d) => [d, 0])),
solved: Object.fromEntries(DIFFICULTIES.map((d) => [d, 0])),
}));
const yearIndex = new Map(year.map((d, i) => [d.date, i]));
for (const row of attemptRows) {
const day = year[yearIndex.get(row.date) ?? -1];
if (!day || !(row.difficulty in day.attempts)) continue;
day.attempts[row.difficulty] = row.n;
day.passes += row.passes ?? 0;
}
for (const row of solvedRows) {
const day = year[yearIndex.get(row.date) ?? -1];
if (day && row.difficulty in day.solved) day.solved[row.difficulty] = row.n;
}
const year = {
days: yearDays,
attempts: yearDays.reduce((n, d) => n + d.attempts, 0),
active_days: yearDays.filter((d) => d.attempts > 0).length,
max_streak: maxStreak,
};
return {
generated: new Date().toISOString(),