diff --git a/ftl/core/statistics.ftl b/ftl/core/statistics.ftl index f84e5a13f..006db0cf5 100644 --- a/ftl/core/statistics.ftl +++ b/ftl/core/statistics.ftl @@ -48,6 +48,11 @@ statistics-cards = [one] { $cards } card *[other] { $cards } cards } +statistics-notes = + { $notes -> + [one] { $notes } note + *[other] { $notes } notes + } # a count of how many cards have been answered, eg "Total: 34 reviews" statistics-reviews = { $reviews -> @@ -220,6 +225,7 @@ statistics-average-answer-time-label = Average answer time statistics-average = Average statistics-average-interval = Average interval statistics-due-tomorrow = Due tomorrow +statistics-daily-load = Daily load # eg 5 of 15 (33.3%) statistics-amount-of-total-with-percentage = { $amount } of { $total } ({ $percent }%) statistics-average-over-period = Average over period diff --git a/proto/anki/stats.proto b/proto/anki/stats.proto index 14d0eef6f..63799b071 100644 --- a/proto/anki/stats.proto +++ b/proto/anki/stats.proto @@ -85,11 +85,13 @@ message GraphsResponse { message Retrievability { map retrievability = 1; float average = 2; - float sum = 3; + float sum_by_card = 3; + float sum_by_note = 4; } message FutureDue { map future_due = 1; bool have_backlog = 2; + uint32 daily_load = 3; } message Today { uint32 answer_count = 1; diff --git a/rslib/src/stats/graphs/future_due.rs b/rslib/src/stats/graphs/future_due.rs index 4671766e5..2b7caec5e 100644 --- a/rslib/src/stats/graphs/future_due.rs +++ b/rslib/src/stats/graphs/future_due.rs @@ -13,6 +13,7 @@ impl GraphsContext { pub(super) fn future_due(&self) -> FutureDue { let mut have_backlog = false; let mut due_by_day: HashMap = Default::default(); + let mut daily_load = 0.0; for c in &self.cards { if matches!(c.queue, CardQueue::New | CardQueue::Suspended) { continue; @@ -31,10 +32,16 @@ impl GraphsContext { } have_backlog |= due_day < 0; *due_by_day.entry(due_day).or_default() += 1; + if matches!(c.queue, CardQueue::Review | CardQueue::DayLearn) { + daily_load += 1.0 / c.interval.max(1) as f32; + } else { + daily_load += 1.0; + } } FutureDue { future_due: due_by_day, have_backlog, + daily_load: daily_load as u32, } } } diff --git a/rslib/src/stats/graphs/retrievability.rs b/rslib/src/stats/graphs/retrievability.rs index da2830e8f..930a21c5b 100644 --- a/rslib/src/stats/graphs/retrievability.rs +++ b/rslib/src/stats/graphs/retrievability.rs @@ -19,25 +19,37 @@ impl GraphsContext { next_day_at: Default::default(), }; let fsrs = FSRS::new(None).unwrap(); + // note id -> (sum, count) + let mut note_retrievability: std::collections::HashMap = + std::collections::HashMap::new(); for card in &self.cards { + let entry = note_retrievability + .entry(card.note_id.0) + .or_insert((0.0, 0)); + entry.1 += 1; if let Some(state) = card.memory_state { - let r = fsrs.current_retrievability( - state.into(), - card.days_since_last_review(&timing).unwrap_or_default(), - ); + let elapsed_days = card.days_since_last_review(&timing).unwrap_or_default(); + let r = fsrs.current_retrievability(state.into(), elapsed_days); + *retrievability .retrievability .entry(percent_to_bin(r * 100.0)) .or_insert_with(Default::default) += 1; - retrievability.sum += r; + retrievability.sum_by_card += r; card_with_retrievability_count += 1; + entry.0 += r; + } else { + entry.0 += 0.0; } } if card_with_retrievability_count != 0 { retrievability.average = - retrievability.sum * 100.0 / card_with_retrievability_count as f32; + retrievability.sum_by_card * 100.0 / card_with_retrievability_count as f32; } - + retrievability.sum_by_note = note_retrievability + .values() + .map(|(sum, count)| sum / *count as f32) + .sum(); retrievability } } diff --git a/ts/routes/graphs/future-due.ts b/ts/routes/graphs/future-due.ts index 1acd221f5..b965874c5 100644 --- a/ts/routes/graphs/future-due.ts +++ b/ts/routes/graphs/future-due.ts @@ -19,11 +19,16 @@ import type { HistogramData } from "./histogram-graph"; export interface GraphData { dueCounts: Map; haveBacklog: boolean; + dailyLoad: number; } export function gatherData(data: GraphsResponse): GraphData { const msg = data.futureDue!; - return { dueCounts: numericMap(msg.futureDue), haveBacklog: msg.haveBacklog }; + return { + dueCounts: numericMap(msg.futureDue), + haveBacklog: msg.haveBacklog, + dailyLoad: msg.dailyLoad, + }; } export interface FutureDueResponse { @@ -138,6 +143,12 @@ export function buildHistogram( reviews: sourceData.dueCounts.get(1) ?? 0, }), }, + { + label: tr.statisticsDailyLoad(), + value: tr.statisticsReviews({ + reviews: sourceData.dailyLoad, + }), + }, ]; return { diff --git a/ts/routes/graphs/retrievability.ts b/ts/routes/graphs/retrievability.ts index 072e6ab73..dd54c2b8d 100644 --- a/ts/routes/graphs/retrievability.ts +++ b/ts/routes/graphs/retrievability.ts @@ -18,14 +18,16 @@ import type { HistogramData } from "./histogram-graph"; export interface GraphData { retrievability: Map; average: number; - sum: number; + sumByCard: number; + sumByNote: number; } export function gatherData(data: GraphsResponse): GraphData { return { retrievability: numericMap(data.retrievability!.retrievability), average: data.retrievability!.average, - sum: data.retrievability!.sum, + sumByCard: data.retrievability!.sumByCard, + sumByNote: data.retrievability!.sumByNote, }; } @@ -111,7 +113,9 @@ export function prepareData( }, { label: tr.statisticsEstimatedTotalKnowledge(), - value: tr.statisticsCards({ cards: +data.sum.toFixed(0) }), + value: `${tr.statisticsCards({ cards: +data.sumByCard.toFixed(0) })} / ${ + tr.statisticsNotes({ notes: +data.sumByNote.toFixed(0) }) + }`, }, ];