From 36ec5c1b939c7659b6a8a8218ae514c349ff8b55 Mon Sep 17 00:00:00 2001 From: sudacode Date: Sun, 7 Jun 2026 00:27:13 -0700 Subject: [PATCH] feat(stats): add Reading Speed and Cards/Hour efficiency charts to Trend - Add cardsPerHour and readingSpeed series to TrendsDashboardQueryResult and TrendsDashboardData - Reorganize Trends tab into Activity, Cumulative Totals, Efficiency, Patterns, and Library sections - Disambiguate per-period vs cumulative chart titles - Label Activity section header with current groupBy period --- changes/stats-updates.md | 1 + docs-site/immersion-tracking.md | 2 +- .../immersion-tracker/__tests__/query.test.ts | 4 + .../immersion-tracker/query-trends.ts | 25 +++++ stats/src/components/trends/TrendsTab.tsx | 103 ++++++++++++------ stats/src/types/stats.ts | 2 + 6 files changed, 100 insertions(+), 37 deletions(-) diff --git a/changes/stats-updates.md b/changes/stats-updates.md index b422f083..b9604447 100644 --- a/changes/stats-updates.md +++ b/changes/stats-updates.md @@ -5,4 +5,5 @@ area: stats - Improved Stats mining from Search and vocabulary examples: empty `ankiConnect.deck` can use Yomitan's mining deck, sentence cards are created before slow media generation finishes, stored/requested secondary subtitles are preserved before falling back to sidecar files or temporary alass-retimed English sidecars for sentence Selection Text, invalid stored timings are blocked before FFmpeg runs, future out-of-order subtitle timing pairs are skipped until valid timings arrive, and partial media failures are shown. - Fixed Stats mining field/audio behavior so sentence clips update `SentenceAudio`, word audio uses the configured Yomitan sources, English subtitle text is not written onto word cards, and secondary subtitle auto-selection prefers regular English tracks over Signs/Songs tracks. - Improved vocabulary review with remembered Hide Known/Hide Kana filters, duplicate-collapsed exclusions across token variants, and Related Seen Words matching based on shared readings or kanji. +- Reorganized the Stats Trends tab into clearer Activity, Cumulative Totals, Efficiency, Patterns, and Library sections, disambiguated per-period vs cumulative charts, and added Reading Speed (words/min) and Cards/Hour efficiency charts. - Improved Stats browsing reliability by remembering library card size, retrying stored cover art without extra AniList lookups, preserving PNG/WebP cover MIME types, honoring custom AnkiConnect URLs for Browse, showing progress during session deletes, and making session deletes refresh faster. diff --git a/docs-site/immersion-tracking.md b/docs-site/immersion-tracking.md index d5f6b994..e954248c 100644 --- a/docs-site/immersion-tracking.md +++ b/docs-site/immersion-tracking.md @@ -54,7 +54,7 @@ When YouTube channel metadata is available, the Library tab groups videos by cre #### Trends -Watch time, sessions, words seen, and per-anime progress/pattern charts with configurable date ranges and grouping. +Grouped into Activity (per-day/month watch time, cards, words, sessions), Cumulative Totals (running totals incl. new words learned and episodes), Efficiency (reading speed, cards/hour, lookups per 100 words), Patterns (watch time by day of week and hour), and per-anime Library charts — all with configurable date ranges and grouping. ![Stats Trends](/screenshots/stats-trends.png) diff --git a/src/core/services/immersion-tracker/__tests__/query.test.ts b/src/core/services/immersion-tracker/__tests__/query.test.ts index 6b21f9bf..b7941443 100644 --- a/src/core/services/immersion-tracker/__tests__/query.test.ts +++ b/src/core/services/immersion-tracker/__tests__/query.test.ts @@ -761,6 +761,10 @@ test('getTrendsDashboard returns chart-ready aggregated series', () => { assert.equal(dashboard.progress.watchTime[1]?.value, 75); assert.equal(dashboard.progress.lookups[1]?.value, 18); assert.equal(dashboard.ratios.lookupsPerHundred[0]?.value, +((8 / 120) * 100).toFixed(1)); + assert.equal(dashboard.ratios.cardsPerHour[0]?.value, +(2 / (30 / 60)).toFixed(1)); + assert.equal(dashboard.ratios.cardsPerHour[1]?.value, +(3 / (45 / 60)).toFixed(1)); + assert.equal(dashboard.ratios.readingSpeed[0]?.value, +(120 / 30).toFixed(1)); + assert.equal(dashboard.ratios.readingSpeed[1]?.value, +(140 / 45).toFixed(1)); assert.equal(dashboard.librarySummary[0]?.title, 'Trend Dashboard Anime'); assert.equal(dashboard.animeCumulative.watchTime[1]?.value, 75); assert.equal( diff --git a/src/core/services/immersion-tracker/query-trends.ts b/src/core/services/immersion-tracker/query-trends.ts index f4e41cbc..0af6bc15 100644 --- a/src/core/services/immersion-tracker/query-trends.ts +++ b/src/core/services/immersion-tracker/query-trends.ts @@ -74,6 +74,8 @@ export interface TrendsDashboardQueryResult { }; ratios: { lookupsPerHundred: TrendChartPoint[]; + cardsPerHour: TrendChartPoint[]; + readingSpeed: TrendChartPoint[]; }; animeCumulative: { watchTime: TrendPerAnimePoint[]; @@ -225,6 +227,26 @@ function buildAggregatedTrendRows(rollups: ImmersionSessionRollupRow[]) { })); } +function buildEfficiencyRates(rows: ReturnType): { + cardsPerHour: TrendChartPoint[]; + readingSpeed: TrendChartPoint[]; +} { + const cardsPerHour: TrendChartPoint[] = []; + const readingSpeed: TrendChartPoint[] = []; + for (const row of rows) { + const hours = row.activeMin / 60; + cardsPerHour.push({ + label: row.label, + value: hours > 0 ? +(row.cards / hours).toFixed(1) : 0, + }); + readingSpeed.push({ + label: row.label, + value: row.activeMin > 0 ? +(row.words / row.activeMin).toFixed(1) : 0, + }); + } + return { cardsPerHour, readingSpeed }; +} + function buildWatchTimeByDayOfWeek(sessions: TrendSessionMetricRow[]): TrendChartPoint[] { const totals = new Array(7).fill(0); for (const session of sessions) { @@ -675,6 +697,7 @@ export function getTrendsDashboard( ); const aggregatedRows = buildAggregatedTrendRows(chartRollups); + const efficiency = buildEfficiencyRates(aggregatedRows); const activity = { watchTime: aggregatedRows.map((row) => ({ label: row.label, value: row.activeMin })), cards: aggregatedRows.map((row) => ({ label: row.label, value: row.cards })), @@ -724,6 +747,8 @@ export function getTrendsDashboard( }, ratios: { lookupsPerHundred: buildLookupsPerHundredWords(sessions, groupBy), + cardsPerHour: efficiency.cardsPerHour, + readingSpeed: efficiency.readingSpeed, }, animeCumulative: { watchTime: buildCumulativePerAnime(animePerDay.watchTime), diff --git a/stats/src/components/trends/TrendsTab.tsx b/stats/src/components/trends/TrendsTab.tsx index abfbf884..15467043 100644 --- a/stats/src/components/trends/TrendsTab.tsx +++ b/stats/src/components/trends/TrendsTab.tsx @@ -148,7 +148,7 @@ export function TrendsTab() { onGroupByChange={setGroupBy} />
- Activity + Activity (per {groupBy === 'month' ? 'month' : 'day'}) + + Cumulative Totals + + + + + + + + + Efficiency + + + + + Patterns - Period Trends - - - - - - - - - Library — Cumulative