Add stability graph

This commit is contained in:
Damien Elmes 2023-10-01 15:44:33 +10:00
parent 072cd37b42
commit 9fd8a8bb40
8 changed files with 113 additions and 4 deletions

View file

@ -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.

View file

@ -155,6 +155,7 @@ message GraphsResponse {
uint32 rollover_hour = 10;
Retrievability retrievability = 12;
bool fsrs = 13;
Intervals stability = 14;
}
message GraphPreferences {

View file

@ -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
}
}

View file

@ -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()),

View file

@ -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) {

View file

@ -0,0 +1,87 @@
<!--
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 "@tslib/anki/stats_pb";
import * as tr from "@tslib/ftl";
import { MONTH, timeSpan } from "@tslib/time";
import { createEventDispatcher } from "svelte";
import Graph from "./Graph.svelte";
import type { GraphPrefs } from "./graph-helpers";
import type { SearchEventMap, TableDatum } from "./graph-helpers";
import type { HistogramData } from "./histogram-graph";
import HistogramGraph from "./HistogramGraph.svelte";
import InputBox from "./InputBox.svelte";
import type { IntervalGraphData } from "./intervals";
import {
gatherIntervalData,
IntervalRange,
prepareIntervalData,
} from "./intervals";
import TableData from "./TableData.svelte";
export let sourceData: GraphsResponse | null = null;
export let prefs: GraphPrefs;
const dispatch = createEventDispatcher<SearchEventMap>();
let intervalData: IntervalGraphData | null = null;
let histogramData = null as HistogramData | null;
let tableData: TableDatum[] = [];
let range = IntervalRange.Percentile95;
$: if (sourceData) {
intervalData = gatherIntervalData(sourceData.stability!);
}
$: if (intervalData) {
[histogramData, tableData] = prepareIntervalData(
intervalData,
range,
dispatch,
$prefs.browserLinksSupported,
);
}
const title = tr.statisticsCardStabilityTitle();
const subtitle = tr.statisticsCardStabilitySubtitle();
const month = timeSpan(1 * MONTH);
const all = tr.statisticsRangeAllTime();
</script>
{#if sourceData?.fsrs}
<Graph {title} {subtitle}>
<InputBox>
<label>
<input type="radio" bind:group={range} value={IntervalRange.Month} />
{month}
</label>
<label>
<input
type="radio"
bind:group={range}
value={IntervalRange.Percentile50}
/>
50%
</label>
<label>
<input
type="radio"
bind:group={range}
value={IntervalRange.Percentile95}
/>
95%
</label>
<label>
<input type="radio" bind:group={range} value={IntervalRange.All} />
{all}
</label>
</InputBox>
<HistogramGraph data={histogramData} />
<TableData {tableData} />
</Graph>
{/if}

View file

@ -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,

View file

@ -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;