Skip to content

Commit cee04bf

Browse files
OuOu2021dae
andauthored
Apply gradient effect to forgetting curve (#3604)
* Add gradient color for forgetting curve * Add desiredRetention prop for CardInfo * update CONTRIBUTORS * Formatting * Tweak range of gradient * Tweak: salmon -> tomato * Get desired retention of the card from backend * Add a reference line for desired retention * Fix: Corrected the steel blue's height & Hide desired retention line when yMin is higher than desiredRetentionY * Add y axis title * Show desired retention in the tooltip * I18n: improve translation and vertical text display * Revert rotatation&writing-mode of vertical title * Tweak font-size of y axis title * Fix: delete old desired retention line when changing duration * Update ftl/core/card-stats.ftl --------- Co-authored-by: Damien Elmes <[email protected]>
1 parent 3b99ae4 commit cee04bf

File tree

7 files changed

+70
-4
lines changed

7 files changed

+70
-4
lines changed

CONTRIBUTORS

+1
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ twwn <github.com/twwn>
197197
Cy Pokhrel <[email protected]>
198198
Park Hyunwoo <[email protected]>
199199
Tomas Fabrizio Orsi <[email protected]>
200+
Dongjin Ouyang <[email protected]>
200201
Sawan Sunar <[email protected]>
201202
hideo aoyama <https://github.com/boukendesho>
202203
Ross Brown <[email protected]>

ftl/core/card-stats.ftl

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ card-stats-fsrs-forgetting-curve-first-week = First Week
3535
card-stats-fsrs-forgetting-curve-first-month = First Month
3636
card-stats-fsrs-forgetting-curve-first-year = First Year
3737
card-stats-fsrs-forgetting-curve-all-time = All Time
38+
card-stats-fsrs-forgetting-curve-probability-of-recalling = Probability of Recall
39+
card-stats-fsrs-forgetting-curve-desired-retention = Desired Retention
3840
3941
## Window Titles
4042

proto/anki/stats.proto

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ message CardStatsResponse {
6464
string custom_data = 20;
6565
string preset = 21;
6666
optional string original_deck = 22;
67+
optional float desired_retention = 23;
6768
}
6869

6970
message GraphsRequest {

rslib/src/stats/card.rs

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ impl Collection {
8080
} else {
8181
None
8282
},
83+
desired_retention: card.desired_retention,
8384
})
8485
}
8586

ts/routes/card-info/CardInfo.svelte

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
1616
export let stats: CardStatsResponse | null = null;
1717
export let showRevlog: boolean = true;
1818
export let fsrsEnabled: boolean = stats?.memoryState != null;
19+
export let desiredRetention: number = stats?.desiredRetention ?? 0.9;
1920
</script>
2021

2122
<Container breakpoint="md" --gutter-inline="1rem" --gutter-block="0.5rem">
@@ -31,7 +32,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
3132
{/if}
3233
{#if fsrsEnabled}
3334
<Row>
34-
<ForgettingCurve revlog={stats.revlog} />
35+
<ForgettingCurve revlog={stats.revlog} {desiredRetention} />
3536
</Row>
3637
{/if}
3738
{:else}

ts/routes/card-info/ForgettingCurve.svelte

+8-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
2020
import HoverColumns from "../graphs/HoverColumns.svelte";
2121
2222
export let revlog: RevlogEntry[];
23+
export let desiredRetention: number;
2324
let svg = null as HTMLElement | SVGElement | null;
2425
const bounds = defaultGraphBounds();
2526
const title = tr.cardStatsFsrsForgettingCurveTitle();
@@ -35,7 +36,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
3536
}
3637
const timeRange = writable(defaultTimeRange);
3738
38-
$: renderForgettingCurve(filteredRevlog, $timeRange, svg as SVGElement, bounds);
39+
$: renderForgettingCurve(
40+
filteredRevlog,
41+
$timeRange,
42+
svg as SVGElement,
43+
bounds,
44+
desiredRetention,
45+
);
3946
</script>
4047

4148
<div class="forgetting-curve">

ts/routes/card-info/forgetting-curve.ts

+55-2
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export function renderForgettingCurve(
161161
timeRange: TimeRange,
162162
svgElem: SVGElement,
163163
bounds: GraphBounds,
164+
desiredRetention: number,
164165
) {
165166
const svg = select(svgElem);
166167
const trans = svg.transition().duration(600) as any;
@@ -204,18 +205,63 @@ export function renderForgettingCurve(
204205
.call((selection) => selection.transition(trans).call(axisLeft(y).tickSizeOuter(0)))
205206
.attr("direction", "ltr");
206207

208+
svg.select(".y-ticks .y-axis-title").remove();
209+
svg.select(".y-ticks")
210+
.append("text")
211+
.attr("class", "y-axis-title")
212+
.attr("transform", "rotate(-90)")
213+
.attr("y", 0 - bounds.marginLeft)
214+
.attr("x", 0 - (bounds.height / 2))
215+
.attr("font-size", "1rem")
216+
.attr("dy", "1.1em")
217+
.attr("fill", "currentColor")
218+
.style("text-anchor", "middle")
219+
.text(`${tr.cardStatsFsrsForgettingCurveProbabilityOfRecalling()}(%)`);
220+
207221
const lineGenerator = line<DataPoint>()
208222
.x((d) => x(d.date))
209223
.y((d) => y(d.retrievability));
210224

225+
// gradient color
226+
const desiredRetentionY = desiredRetention * 100;
227+
svg.append("linearGradient")
228+
.attr("id", "line-gradient")
229+
.attr("gradientUnits", "userSpaceOnUse")
230+
.attr("x1", 0)
231+
.attr("y1", y(0))
232+
.attr("x2", 0)
233+
.attr("y2", y(100))
234+
.selectAll("stop")
235+
.data([
236+
{ offset: "0%", color: "tomato" },
237+
{ offset: `${desiredRetentionY}%`, color: "steelblue" },
238+
{ offset: "100%", color: "green" },
239+
])
240+
.enter().append("stop")
241+
.attr("offset", d => d.offset)
242+
.attr("stop-color", d => d.color);
243+
211244
svg.append("path")
212245
.datum(data)
213246
.attr("class", "forgetting-curve-line")
214247
.attr("fill", "none")
215-
.attr("stroke", "steelblue")
248+
.attr("stroke", "url(#line-gradient)")
216249
.attr("stroke-width", 1.5)
217250
.attr("d", lineGenerator);
218251

252+
svg.select(".desired-retention-line").remove();
253+
if (desiredRetentionY > yMin) {
254+
svg.append("line")
255+
.attr("class", "desired-retention-line")
256+
.attr("x1", bounds.marginLeft)
257+
.attr("x2", bounds.width - bounds.marginRight)
258+
.attr("y1", y(desiredRetentionY))
259+
.attr("y2", y(desiredRetentionY))
260+
.attr("stroke", "steelblue")
261+
.attr("stroke-dasharray", "4 4")
262+
.attr("stroke-width", 1.2);
263+
}
264+
219265
const focusLine = svg.append("line")
220266
.attr("class", "focus-line")
221267
.attr("y1", bounds.marginTop)
@@ -248,11 +294,18 @@ export function renderForgettingCurve(
248294
.attr("fill", "transparent")
249295
.on("mousemove", (event: MouseEvent, d: DataPoint) => {
250296
const [x1, y1] = pointer(event, document.body);
297+
const [_, y2] = pointer(event, svg.node());
298+
299+
const lineY = y(desiredRetentionY);
251300
focusLine.attr("x1", x(d.date) - 1).attr("x2", x(d.date) + 1).style(
252301
"opacity",
253302
1,
254303
);
255-
showTooltip(tooltipText(d), x1, y1);
304+
let text = tooltipText(d);
305+
if (y2 >= lineY - 10 && y2 <= lineY + 10) {
306+
text += `<br>${tr.cardStatsFsrsForgettingCurveDesiredRetention()}: ${desiredRetention.toFixed(2)}`;
307+
}
308+
showTooltip(text, x1, y1);
256309
})
257310
.on("mouseout", () => {
258311
focusLine.style("opacity", 0);

0 commit comments

Comments
 (0)