From cee04bf20cb7fbce9fe17cdaf4353e29b111e24a Mon Sep 17 00:00:00 2001 From: OuOu2021 <1113117424@qq.com> Date: Mon, 9 Dec 2024 14:44:05 +0800 Subject: [PATCH] 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 --- CONTRIBUTORS | 1 + ftl/core/card-stats.ftl | 2 + proto/anki/stats.proto | 1 + rslib/src/stats/card.rs | 1 + ts/routes/card-info/CardInfo.svelte | 3 +- ts/routes/card-info/ForgettingCurve.svelte | 9 +++- ts/routes/card-info/forgetting-curve.ts | 57 +++++++++++++++++++++- 7 files changed, 70 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 84089f868..e6e1da9a2 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -197,6 +197,7 @@ twwn Cy Pokhrel Park Hyunwoo Tomas Fabrizio Orsi +Dongjin Ouyang <1113117424@qq.com> Sawan Sunar hideo aoyama Ross Brown diff --git a/ftl/core/card-stats.ftl b/ftl/core/card-stats.ftl index bd5b52a87..65c3b95f2 100644 --- a/ftl/core/card-stats.ftl +++ b/ftl/core/card-stats.ftl @@ -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 diff --git a/proto/anki/stats.proto b/proto/anki/stats.proto index a5e3401cc..42d02029c 100644 --- a/proto/anki/stats.proto +++ b/proto/anki/stats.proto @@ -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 { diff --git a/rslib/src/stats/card.rs b/rslib/src/stats/card.rs index 97005e07f..c2bf9e562 100644 --- a/rslib/src/stats/card.rs +++ b/rslib/src/stats/card.rs @@ -80,6 +80,7 @@ impl Collection { } else { None }, + desired_retention: card.desired_retention, }) } diff --git a/ts/routes/card-info/CardInfo.svelte b/ts/routes/card-info/CardInfo.svelte index 44599c489..ebafacc36 100644 --- a/ts/routes/card-info/CardInfo.svelte +++ b/ts/routes/card-info/CardInfo.svelte @@ -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; @@ -31,7 +32,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {/if} {#if fsrsEnabled} - + {/if} {:else} diff --git a/ts/routes/card-info/ForgettingCurve.svelte b/ts/routes/card-info/ForgettingCurve.svelte index 227d2c108..b3fffc16a 100644 --- a/ts/routes/card-info/ForgettingCurve.svelte +++ b/ts/routes/card-info/ForgettingCurve.svelte @@ -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, + );
diff --git a/ts/routes/card-info/forgetting-curve.ts b/ts/routes/card-info/forgetting-curve.ts index 66d4ebb4f..b5c71b761 100644 --- a/ts/routes/card-info/forgetting-curve.ts +++ b/ts/routes/card-info/forgetting-curve.ts @@ -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() .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 += `
${tr.cardStatsFsrsForgettingCurveDesiredRetention()}: ${desiredRetention.toFixed(2)}`; + } + showTooltip(text, x1, y1); }) .on("mouseout", () => { focusLine.style("opacity", 0);