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 <dae@users.noreply.github.com>
This commit is contained in:
OuOu2021 2024-12-09 14:44:05 +08:00 committed by GitHub
parent 3b99ae4b91
commit cee04bf20c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 70 additions and 4 deletions

View file

@ -197,6 +197,7 @@ twwn <github.com/twwn>
Cy Pokhrel <cy@cy7.sh>
Park Hyunwoo <phu54321@naver.com>
Tomas Fabrizio Orsi <torsi@fi.uba.ar>
Dongjin Ouyang <1113117424@qq.com>
Sawan Sunar <sawansunar24072002@gmail.com>
hideo aoyama <https://github.com/boukendesho>
Ross Brown <rbrownwsws@googlemail.com>

View file

@ -35,6 +35,8 @@ card-stats-fsrs-forgetting-curve-first-week = First Week
card-stats-fsrs-forgetting-curve-first-month = First Month
card-stats-fsrs-forgetting-curve-first-year = First Year
card-stats-fsrs-forgetting-curve-all-time = All Time
card-stats-fsrs-forgetting-curve-probability-of-recalling = Probability of Recall
card-stats-fsrs-forgetting-curve-desired-retention = Desired Retention
## Window Titles

View file

@ -64,6 +64,7 @@ message CardStatsResponse {
string custom_data = 20;
string preset = 21;
optional string original_deck = 22;
optional float desired_retention = 23;
}
message GraphsRequest {

View file

@ -80,6 +80,7 @@ impl Collection {
} else {
None
},
desired_retention: card.desired_retention,
})
}

View file

@ -16,6 +16,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let stats: CardStatsResponse | null = null;
export let showRevlog: boolean = true;
export let fsrsEnabled: boolean = stats?.memoryState != null;
export let desiredRetention: number = stats?.desiredRetention ?? 0.9;
</script>
<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
{/if}
{#if fsrsEnabled}
<Row>
<ForgettingCurve revlog={stats.revlog} />
<ForgettingCurve revlog={stats.revlog} {desiredRetention} />
</Row>
{/if}
{:else}

View file

@ -20,6 +20,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import HoverColumns from "../graphs/HoverColumns.svelte";
export let revlog: RevlogEntry[];
export let desiredRetention: number;
let svg = null as HTMLElement | SVGElement | null;
const bounds = defaultGraphBounds();
const title = tr.cardStatsFsrsForgettingCurveTitle();
@ -35,7 +36,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
const timeRange = writable(defaultTimeRange);
$: renderForgettingCurve(filteredRevlog, $timeRange, svg as SVGElement, bounds);
$: renderForgettingCurve(
filteredRevlog,
$timeRange,
svg as SVGElement,
bounds,
desiredRetention,
);
</script>
<div class="forgetting-curve">

View file

@ -161,6 +161,7 @@ export function renderForgettingCurve(
timeRange: TimeRange,
svgElem: SVGElement,
bounds: GraphBounds,
desiredRetention: number,
) {
const svg = select(svgElem);
const trans = svg.transition().duration(600) as any;
@ -204,18 +205,63 @@ export function renderForgettingCurve(
.call((selection) => selection.transition(trans).call(axisLeft(y).tickSizeOuter(0)))
.attr("direction", "ltr");
svg.select(".y-ticks .y-axis-title").remove();
svg.select(".y-ticks")
.append("text")
.attr("class", "y-axis-title")
.attr("transform", "rotate(-90)")
.attr("y", 0 - bounds.marginLeft)
.attr("x", 0 - (bounds.height / 2))
.attr("font-size", "1rem")
.attr("dy", "1.1em")
.attr("fill", "currentColor")
.style("text-anchor", "middle")
.text(`${tr.cardStatsFsrsForgettingCurveProbabilityOfRecalling()}(%)`);
const lineGenerator = line<DataPoint>()
.x((d) => x(d.date))
.y((d) => y(d.retrievability));
// gradient color
const desiredRetentionY = desiredRetention * 100;
svg.append("linearGradient")
.attr("id", "line-gradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", 0)
.attr("y1", y(0))
.attr("x2", 0)
.attr("y2", y(100))
.selectAll("stop")
.data([
{ offset: "0%", color: "tomato" },
{ offset: `${desiredRetentionY}%`, color: "steelblue" },
{ offset: "100%", color: "green" },
])
.enter().append("stop")
.attr("offset", d => d.offset)
.attr("stop-color", d => d.color);
svg.append("path")
.datum(data)
.attr("class", "forgetting-curve-line")
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke", "url(#line-gradient)")
.attr("stroke-width", 1.5)
.attr("d", lineGenerator);
svg.select(".desired-retention-line").remove();
if (desiredRetentionY > yMin) {
svg.append("line")
.attr("class", "desired-retention-line")
.attr("x1", bounds.marginLeft)
.attr("x2", bounds.width - bounds.marginRight)
.attr("y1", y(desiredRetentionY))
.attr("y2", y(desiredRetentionY))
.attr("stroke", "steelblue")
.attr("stroke-dasharray", "4 4")
.attr("stroke-width", 1.2);
}
const focusLine = svg.append("line")
.attr("class", "focus-line")
.attr("y1", bounds.marginTop)
@ -248,11 +294,18 @@ export function renderForgettingCurve(
.attr("fill", "transparent")
.on("mousemove", (event: MouseEvent, d: DataPoint) => {
const [x1, y1] = pointer(event, document.body);
const [_, y2] = pointer(event, svg.node());
const lineY = y(desiredRetentionY);
focusLine.attr("x1", x(d.date) - 1).attr("x2", x(d.date) + 1).style(
"opacity",
1,
);
showTooltip(tooltipText(d), x1, y1);
let text = tooltipText(d);
if (y2 >= lineY - 10 && y2 <= lineY + 10) {
text += `<br>${tr.cardStatsFsrsForgettingCurveDesiredRetention()}: ${desiredRetention.toFixed(2)}`;
}
showTooltip(text, x1, y1);
})
.on("mouseout", () => {
focusLine.style("opacity", 0);