diff --git a/ftl/core/statistics.ftl b/ftl/core/statistics.ftl
index d33fe7f96..ce6e757f8 100644
--- a/ftl/core/statistics.ftl
+++ b/ftl/core/statistics.ftl
@@ -94,6 +94,8 @@ statistics-range-collection = collection
statistics-range-search = Search
statistics-card-ease-title = Card Ease
statistics-card-difficulty-title = Card Difficulty
+statistics-card-stability-title = Card Stability
+statistics-card-stability-subtitle = Combined with desired retention to determine the next interval.
statistics-card-retrievability-title = Card Retrievability
statistics-card-ease-subtitle = The lower the ease, the more frequently a card will appear.
statistics-card-difficulty-subtitle = The higher the difficulty, the harder it is to remember.
diff --git a/proto/anki/stats.proto b/proto/anki/stats.proto
index 3f6814954..8c14b5803 100644
--- a/proto/anki/stats.proto
+++ b/proto/anki/stats.proto
@@ -155,6 +155,7 @@ message GraphsResponse {
uint32 rollover_hour = 10;
Retrievability retrievability = 12;
bool fsrs = 13;
+ Intervals stability = 14;
}
message GraphPreferences {
diff --git a/rslib/src/stats/graphs/intervals.rs b/rslib/src/stats/graphs/intervals.rs
index f69c33052..b8a261ae0 100644
--- a/rslib/src/stats/graphs/intervals.rs
+++ b/rslib/src/stats/graphs/intervals.rs
@@ -19,4 +19,19 @@ impl GraphsContext {
}
data
}
+
+ pub(super) fn stability(&self) -> Intervals {
+ let mut data = Intervals::default();
+ for card in &self.cards {
+ if matches!(card.ctype, CardType::Review | CardType::Relearn) {
+ if let Some(state) = &card.memory_state {
+ *data
+ .intervals
+ .entry(state.stability as u32)
+ .or_insert_with(Default::default) += 1;
+ }
+ }
+ }
+ data
+ }
}
diff --git a/rslib/src/stats/graphs/mod.rs b/rslib/src/stats/graphs/mod.rs
index 7c48666ec..0d78d9ba0 100644
--- a/rslib/src/stats/graphs/mod.rs
+++ b/rslib/src/stats/graphs/mod.rs
@@ -67,6 +67,7 @@ impl Collection {
reviews: Some(ctx.review_counts_and_times()),
future_due: Some(ctx.future_due()),
intervals: Some(ctx.intervals()),
+ stability: Some(ctx.stability()),
eases: Some(eases),
difficulty: Some(difficulty),
today: Some(ctx.today()),
diff --git a/ts/graphs/IntervalsGraph.svelte b/ts/graphs/IntervalsGraph.svelte
index d15ed0e47..4251d4f55 100644
--- a/ts/graphs/IntervalsGraph.svelte
+++ b/ts/graphs/IntervalsGraph.svelte
@@ -33,7 +33,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let range = IntervalRange.Percentile95;
$: if (sourceData) {
- intervalData = gatherIntervalData(sourceData);
+ intervalData = gatherIntervalData(sourceData.intervals!);
}
$: if (intervalData) {
diff --git a/ts/graphs/StabilityGraph.svelte b/ts/graphs/StabilityGraph.svelte
new file mode 100644
index 000000000..024c821e2
--- /dev/null
+++ b/ts/graphs/StabilityGraph.svelte
@@ -0,0 +1,87 @@
+
+
+
+{#if sourceData?.fsrs}
+
+
+
+
+
+
+
+
+
+
+
+
+{/if}
diff --git a/ts/graphs/index.ts b/ts/graphs/index.ts
index 57263d856..e8a5d61ed 100644
--- a/ts/graphs/index.ts
+++ b/ts/graphs/index.ts
@@ -50,6 +50,7 @@ import IntervalsGraph from "./IntervalsGraph.svelte";
import RangeBox from "./RangeBox.svelte";
import RetrievabilityGraph from "./RetrievabilityGraph.svelte";
import ReviewsGraph from "./ReviewsGraph.svelte";
+import StabilityGraph from "./StabilityGraph.svelte";
import TodayStats from "./TodayStats.svelte";
setupGraphs(
@@ -60,6 +61,7 @@ setupGraphs(
ReviewsGraph,
CardCounts,
IntervalsGraph,
+ StabilityGraph,
EaseGraph,
DifficultyGraph,
RetrievabilityGraph,
@@ -79,6 +81,7 @@ export const graphComponents = {
ReviewsGraph,
CardCounts,
IntervalsGraph,
+ StabilityGraph,
EaseGraph,
DifficultyGraph,
RetrievabilityGraph,
diff --git a/ts/graphs/intervals.ts b/ts/graphs/intervals.ts
index 8bb5917a1..06335248e 100644
--- a/ts/graphs/intervals.ts
+++ b/ts/graphs/intervals.ts
@@ -5,7 +5,7 @@
@typescript-eslint/no-explicit-any: "off",
*/
-import type { GraphsResponse } from "@tslib/anki/stats_pb";
+import type { GraphsResponse_Intervals } from "@tslib/anki/stats_pb";
import * as tr from "@tslib/ftl";
import { localizedNumber } from "@tslib/i18n";
import { timeSpan } from "@tslib/time";
@@ -27,11 +27,11 @@ export enum IntervalRange {
All = 3,
}
-export function gatherIntervalData(data: GraphsResponse): IntervalGraphData {
+export function gatherIntervalData(data: GraphsResponse_Intervals): IntervalGraphData {
// This could be made more efficient - this graph currently expects a flat list of individual intervals which it
// uses to calculate a percentile and then converts into a histogram, and the percentile/histogram calculations
// in JS are relatively slow.
- const map = numericMap(data.intervals!.intervals);
+ const map = numericMap(data.intervals);
const totalCards = sum(map, ([_k, v]) => v);
const allIntervals: number[] = Array(totalCards);
let position = 0;