-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Feat/true retention stats * ./ninja fix:minilints * use translatable strings & update style * remove card couts & add more translatable strings * Update statistics.ftl Co-authored-by: user1823 <[email protected]> * add Estimated total knowledge (cards) --------- Co-authored-by: user1823 <[email protected]>
- Loading branch information
1 parent
5dfef8a
commit 3912db3
Showing
9 changed files
with
282 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
// Copyright: Ankitects Pty Ltd and contributors | ||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html | ||
use std::collections::HashMap; | ||
|
||
use anki_proto::stats::graphs_response::true_retention_stats::TrueRetention; | ||
use anki_proto::stats::graphs_response::TrueRetentionStats; | ||
|
||
use super::GraphsContext; | ||
use super::TimestampSecs; | ||
use crate::revlog::RevlogReviewKind; | ||
|
||
impl GraphsContext { | ||
pub fn calculate_true_retention(&self) -> TrueRetentionStats { | ||
let mut stats = TrueRetentionStats::default(); | ||
|
||
// create periods | ||
let day = 86400; | ||
let periods = vec![ | ||
( | ||
"today", | ||
self.next_day_start.adding_secs(-day), | ||
self.next_day_start, | ||
), | ||
( | ||
"yesterday", | ||
self.next_day_start.adding_secs(-2 * day), | ||
self.next_day_start.adding_secs(-day), | ||
), | ||
( | ||
"week", | ||
self.next_day_start.adding_secs(-7 * day), | ||
self.next_day_start, | ||
), | ||
( | ||
"month", | ||
self.next_day_start.adding_secs(-30 * day), | ||
self.next_day_start, | ||
), | ||
( | ||
"year", | ||
self.next_day_start.adding_secs(-365 * day), | ||
self.next_day_start, | ||
), | ||
("all_time", TimestampSecs(0), self.next_day_start), | ||
]; | ||
|
||
// create period stats | ||
let mut period_stats: HashMap<&str, TrueRetention> = periods | ||
.iter() | ||
.map(|(name, _, _)| (*name, TrueRetention::default())) | ||
.collect(); | ||
|
||
for review in &self.revlog { | ||
for (period_name, start, end) in &periods { | ||
if review.id.as_secs() >= *start && review.id.as_secs() < *end { | ||
let period_stat = period_stats.get_mut(period_name).unwrap(); | ||
const MATURE_IVL: i32 = 21; // mature interval is 21 days | ||
|
||
match review.review_kind { | ||
RevlogReviewKind::Learning | ||
| RevlogReviewKind::Review | ||
| RevlogReviewKind::Relearning => { | ||
if review.last_interval < MATURE_IVL | ||
&& review.button_chosen == 1 | ||
&& (review.review_kind == RevlogReviewKind::Review | ||
|| review.last_interval <= -86400 | ||
|| review.last_interval >= 1) | ||
{ | ||
period_stat.young_failed += 1; | ||
} else if review.last_interval < MATURE_IVL | ||
&& review.button_chosen > 1 | ||
&& (review.review_kind == RevlogReviewKind::Review | ||
|| review.last_interval <= -86400 | ||
|| review.last_interval >= 1) | ||
{ | ||
period_stat.young_passed += 1; | ||
} else if review.last_interval >= MATURE_IVL | ||
&& review.button_chosen == 1 | ||
&& (review.review_kind == RevlogReviewKind::Review | ||
|| review.last_interval <= -86400 | ||
|| review.last_interval >= 1) | ||
{ | ||
period_stat.mature_failed += 1; | ||
} else if review.last_interval >= MATURE_IVL | ||
&& review.button_chosen > 1 | ||
&& (review.review_kind == RevlogReviewKind::Review | ||
|| review.last_interval <= -86400 | ||
|| review.last_interval >= 1) | ||
{ | ||
period_stat.mature_passed += 1; | ||
} | ||
} | ||
RevlogReviewKind::Filtered | RevlogReviewKind::Manual => {} | ||
} | ||
} | ||
} | ||
} | ||
|
||
stats.today = Some(period_stats["today"].clone()); | ||
stats.yesterday = Some(period_stats["yesterday"].clone()); | ||
stats.week = Some(period_stats["week"].clone()); | ||
stats.month = Some(period_stats["month"].clone()); | ||
stats.year = Some(period_stats["year"].clone()); | ||
stats.all_time = Some(period_stats["all_time"].clone()); | ||
|
||
stats | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<!-- | ||
Copyright: Ankitects Pty Ltd and contributors | ||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html | ||
--> | ||
<script lang="ts"> | ||
import type { GraphsResponse } from "@generated/anki/stats_pb"; | ||
import * as tr from "@generated/ftl"; | ||
import { renderTrueRetention } from "./true-retention"; | ||
import Graph from "./Graph.svelte"; | ||
import type { RevlogRange } from "./graph-helpers"; | ||
export let revlogRange: RevlogRange; | ||
export let sourceData: GraphsResponse | null = null; | ||
let trueRetentionHtml: string; | ||
$: if (sourceData) { | ||
trueRetentionHtml = renderTrueRetention(sourceData, revlogRange); | ||
} | ||
const title = tr.statisticsTrueRetentionTitle(); | ||
const subtitle = tr.statisticsTrueRetentionSubtitle(); | ||
</script> | ||
|
||
<Graph {title} {subtitle}> | ||
{#if trueRetentionHtml} | ||
<div class="true-retention-table"> | ||
{@html trueRetentionHtml} | ||
</div> | ||
{/if} | ||
</Graph> | ||
|
||
<style> | ||
.true-retention-table { | ||
overflow-x: auto; | ||
margin-top: 1rem; | ||
display: flex; | ||
justify-content: center; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
// Copyright: Ankitects Pty Ltd and contributors | ||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html | ||
import type { GraphsResponse } from "@generated/anki/stats_pb"; | ||
import * as tr from "@generated/ftl"; | ||
import { localizedNumber } from "@tslib/i18n"; | ||
import { RevlogRange } from "./graph-helpers"; | ||
|
||
interface TrueRetentionData { | ||
youngPassed: number; | ||
youngFailed: number; | ||
maturePassed: number; | ||
matureFailed: number; | ||
} | ||
|
||
function calculateRetention(passed: number, failed: number): string { | ||
const total = passed + failed; | ||
if (total === 0) { | ||
return "0%"; | ||
} | ||
return localizedNumber((passed / total) * 100) + "%"; | ||
} | ||
|
||
function createStatsRow(name: string, data: TrueRetentionData): string { | ||
const youngRetention = calculateRetention(data.youngPassed, data.youngFailed); | ||
const matureRetention = calculateRetention(data.maturePassed, data.matureFailed); | ||
const totalPassed = data.youngPassed + data.maturePassed; | ||
const totalFailed = data.youngFailed + data.matureFailed; | ||
const totalRetention = calculateRetention(totalPassed, totalFailed); | ||
|
||
return ` | ||
<tr> | ||
<td class="trl">${name}</td> | ||
<td class="trr">${localizedNumber(data.youngPassed)}</td> | ||
<td class="trr">${localizedNumber(data.youngFailed)}</td> | ||
<td class="trr">${youngRetention}</td> | ||
<td class="trr">${localizedNumber(data.maturePassed)}</td> | ||
<td class="trr">${localizedNumber(data.matureFailed)}</td> | ||
<td class="trr">${matureRetention}</td> | ||
<td class="trr">${localizedNumber(totalPassed)}</td> | ||
<td class="trr">${localizedNumber(totalFailed)}</td> | ||
<td class="trr">${totalRetention}</td> | ||
</tr>`; | ||
} | ||
|
||
export function renderTrueRetention(data: GraphsResponse, revlogRange: RevlogRange): string { | ||
const trueRetention = data.trueRetention!; | ||
|
||
const tableContent = ` | ||
<style> | ||
td.trl { border: 1px solid; text-align: left; padding: 5px; } | ||
td.trr { border: 1px solid; text-align: right; padding: 5px; } | ||
td.trc { border: 1px solid; text-align: center; padding: 5px; } | ||
</style> | ||
<table style="border-collapse: collapse;" cellspacing="0" cellpadding="2"> | ||
<tr> | ||
<td class="trl" rowspan=3><b>${tr.statisticsTrueRetentionRange()}</b></td> | ||
<td class="trc" colspan=9><b>${tr.statisticsReviewsTitle()}</b></td> | ||
</tr> | ||
<tr> | ||
<td class="trc" colspan=3><b>${tr.statisticsCountsYoungCards()}</b></td> | ||
<td class="trc" colspan=3><b>${tr.statisticsCountsMatureCards()}</b></td> | ||
<td class="trc" colspan=3><b>${tr.statisticsCountsTotalCards()}</b></td> | ||
</tr> | ||
<tr> | ||
<td class="trc">${tr.statisticsTrueRetentionPass()}</td> | ||
<td class="trc">${tr.statisticsTrueRetentionFail()}</td> | ||
<td class="trc">${tr.statisticsTrueRetentionRetention()}</td> | ||
<td class="trc">${tr.statisticsTrueRetentionPass()}</td> | ||
<td class="trc">${tr.statisticsTrueRetentionFail()}</td> | ||
<td class="trc">${tr.statisticsTrueRetentionRetention()}</td> | ||
<td class="trc">${tr.statisticsTrueRetentionPass()}</td> | ||
<td class="trc">${tr.statisticsTrueRetentionFail()}</td> | ||
<td class="trc">${tr.statisticsTrueRetentionRetention()}</td> | ||
</tr> | ||
${createStatsRow(tr.statisticsTrueRetentionToday(), trueRetention.today!)} | ||
${createStatsRow(tr.statisticsTrueRetentionYesterday(), trueRetention.yesterday!)} | ||
${createStatsRow(tr.statisticsTrueRetentionWeek(), trueRetention.week!)} | ||
${createStatsRow(tr.statisticsTrueRetentionMonth(), trueRetention.month!)} | ||
${ | ||
revlogRange === RevlogRange.Year | ||
? createStatsRow(tr.statisticsTrueRetentionYear(), trueRetention.year!) | ||
: createStatsRow(tr.statisticsTrueRetentionAllTime(), trueRetention.allTime!) | ||
} | ||
</table>`; | ||
|
||
return tableContent; | ||
} |