From c985acb9fe36d3651eb83cf4cfe44d046ec7458f Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Sun, 22 Dec 2024 01:40:51 +0000 Subject: [PATCH] Add memorized option to FSRS simulation graph (#3655) * Added: Memorized option to graph * Count -> Reviews * Added: Margin to radio button input * Fix: Labels * ./check * Check errors? * bump fsrs to 1.4.6 * ./ninja fix:minilints * Added: Don't show hidden simulator values. * Bump to fsrs 1.4.7 --- Cargo.lock | 4 +- Cargo.toml | 2 +- cargo/licenses.json | 2 +- ftl/core/deck-config.ftl | 3 + ftl/core/statistics.ftl | 1 + ts/routes/deck-options/FsrsOptions.svelte | 68 ++++++++++++++++++----- ts/routes/graphs/simulator.ts | 48 ++++++++++++---- 7 files changed, 99 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 120401925..d7e43c53e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1860,9 +1860,9 @@ dependencies = [ [[package]] name = "fsrs" -version = "1.4.5" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00a50fe61228127da3faad87e871e99d4f47fcf4fd226a648300f89bb220dc1" +checksum = "61218a50bf5c8da66b62be02f495c5d0b43de52ec37fae036a0e68d4539ff59d" dependencies = [ "burn", "itertools 0.12.1", diff --git a/Cargo.toml b/Cargo.toml index a7f9a9d9f..3d6926043 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ git = "https://github.com/ankitects/linkcheck.git" rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" [workspace.dependencies.fsrs] -version = "=1.4.5" +version = "=1.4.7" # git = "https://github.com/open-spaced-repetition/fsrs-rs.git" # rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941" # path = "../open-spaced-repetition/fsrs-rs" diff --git a/cargo/licenses.json b/cargo/licenses.json index 0cc12bf7c..3d350ee2f 100644 --- a/cargo/licenses.json +++ b/cargo/licenses.json @@ -1225,7 +1225,7 @@ }, { "name": "fsrs", - "version": "1.4.5", + "version": "1.4.7", "authors": "Open Spaced Repetition", "repository": "https://github.com/open-spaced-repetition/fsrs-rs", "license": "BSD-3-Clause", diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index f60ef72f9..2cc0b2c4a 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -463,10 +463,13 @@ deck-config-days-to-simulate = Days to simulate deck-config-desired-retention-below-optimal = Your desired retention is below optimal. Increasing it is recommended. deck-config-fsrs-simulator-y-axis-title-time = Review Time/Day deck-config-fsrs-simulator-y-axis-title-count = Review Count/Day +deck-config-fsrs-simulator-y-axis-title-memorized = Memorized Total deck-config-fsrs-simulator-experimental = FSRS simulator (experimental) deck-config-additional-new-cards-to-simulate = Additional new cards to simulate deck-config-simulate = Simulate deck-config-clear-last-simulate = Clear last simulation +deck-config-fsrs-simulator-radio-count = Reviews +deck-config-fsrs-simulator-radio-memorized = Memorized ## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future. diff --git a/ftl/core/statistics.ftl b/ftl/core/statistics.ftl index 0a2b1d5e1..e6c7c5811 100644 --- a/ftl/core/statistics.ftl +++ b/ftl/core/statistics.ftl @@ -59,6 +59,7 @@ statistics-reviews = [one] { $reviews } review *[other] { $reviews } reviews } +statistics-memorized = {$memorized} memorized # Shown at the bottom of the deck list, and in the statistics screen. # eg "Studied 3 cards in 13 seconds today (4.33s/card)." # The { statistics-in-time-span-seconds } part should be pasted in from the English diff --git a/ts/routes/deck-options/FsrsOptions.svelte b/ts/routes/deck-options/FsrsOptions.svelte index 106fa599b..69bb53abc 100644 --- a/ts/routes/deck-options/FsrsOptions.svelte +++ b/ts/routes/deck-options/FsrsOptions.svelte @@ -32,7 +32,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import Warning from "./Warning.svelte"; import ParamsInputRow from "./ParamsInputRow.svelte"; import ParamsSearchRow from "./ParamsSearchRow.svelte"; - import { renderSimulationChart, type Point } from "../graphs/simulator"; + import { + renderSimulationChart, + SimulateSubgraph, + type Point, + } from "../graphs/simulator"; import Graph from "../graphs/Graph.svelte"; import HoverColumns from "../graphs/HoverColumns.svelte"; import CumulativeOverlay from "../graphs/CumulativeOverlay.svelte"; @@ -74,7 +78,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html | ComputeRetentionProgress | undefined; - let showTime = false; + let simulateSubgraph: SimulateSubgraph = SimulateSubgraph.count; const optimalRetentionRequest = new ComputeOptimalRetentionRequest({ daysToSimulate: 365, @@ -326,33 +330,46 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html resp.dailyReviewCount, resp.dailyNewCount, ); + + const dailyMemorizedCount = resp.accumulatedKnowledgeAcquisition; + points = points.concat( resp.dailyTimeCost.map((v, i) => ({ x: i, timeCost: v, count: dailyTotalCount[i], + memorized: dailyMemorizedCount[i], label: simulationNumber, })), ); + tableData = renderSimulationChart( svg as SVGElement, bounds, points, - showTime, + simulateSubgraph, ); } } } - $: tableData = renderSimulationChart(svg as SVGElement, bounds, points, showTime); + $: tableData = renderSimulationChart( + svg as SVGElement, + bounds, + points, + simulateSubgraph, + ); function clearSimulation(): void { points = points.filter((p) => p.label !== simulationNumber); simulationNumber = Math.max(0, simulationNumber - 1); - tableData = renderSimulationChart(svg as SVGElement, bounds, points, showTime); + tableData = renderSimulationChart( + svg as SVGElement, + bounds, + points, + simulateSubgraph, + ); } - - const label = tr.statisticsReviewsTimeCheckbox(); {simulateProgressString} - - - +
+ + + + + +
@@ -569,4 +608,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html diff --git a/ts/routes/graphs/simulator.ts b/ts/routes/graphs/simulator.ts index d2bc62a77..db2c51818 100644 --- a/ts/routes/graphs/simulator.ts +++ b/ts/routes/graphs/simulator.ts @@ -26,14 +26,21 @@ export interface Point { x: number; timeCost: number; count: number; + memorized: number; label: number; } +export enum SimulateSubgraph { + time, + count, + memorized, +} + export function renderSimulationChart( svgElem: SVGElement, bounds: GraphBounds, data: Point[], - showTime: boolean, + subgraph: SimulateSubgraph, ): TableDatum[] { const svg = select(svgElem); svg.selectAll(".lines").remove(); @@ -65,10 +72,22 @@ export function renderSimulationChart( // y scale const yTickFormat = (n: number): string => { - return showTime ? timeSpan(n, true) : n.toString(); + return subgraph == SimulateSubgraph.time ? timeSpan(n, true) : n.toString(); }; - const yMax = showTime ? max(convertedData, d => d.timeCost)! : max(convertedData, d => d.count)!; + const subgraph_data = ({ + [SimulateSubgraph.count]: convertedData.map(d => ({ ...d, y: d.count })), + [SimulateSubgraph.time]: convertedData.map(d => ({ ...d, y: d.timeCost })), + [SimulateSubgraph.memorized]: convertedData.map(d => ({ ...d, y: d.memorized })), + })[subgraph]; + + const subgraph_title = ({ + [SimulateSubgraph.count]: tr.deckConfigFsrsSimulatorYAxisTitleCount(), + [SimulateSubgraph.time]: tr.deckConfigFsrsSimulatorYAxisTitleTime(), + [SimulateSubgraph.memorized]: tr.deckConfigFsrsSimulatorYAxisTitleMemorized(), + })[subgraph]; + + const yMax = max(subgraph_data, d => d.y)!; const y = scaleLinear() .range([bounds.height - bounds.marginBottom, bounds.marginTop]) .domain([0, yMax]) @@ -95,14 +114,10 @@ export function renderSimulationChart( .attr("dy", "1.1em") .attr("fill", "currentColor") .style("text-anchor", "middle") - .text(`${ - showTime - ? tr.deckConfigFsrsSimulatorYAxisTitleTime() - : tr.deckConfigFsrsSimulatorYAxisTitleCount() - }`); + .text(subgraph_title); // x lines - const points = convertedData.map((d) => [x(d.date), y(showTime ? d.timeCost : d.count), d.label]); + const points = subgraph_data.map((d) => [x(d.date), y(d.y), d.label]); const groups = rollup(points, v => Object.assign(v, { z: v[0][2] }), d => d[2]); const color = schemeCategory10; @@ -169,9 +184,18 @@ export function renderSimulationChart( const days = +((date.getTime() - Date.now()) / (60 * 60 * 24 * 1000)).toFixed(); let tooltipContent = `Date: ${localizedDate(date)}
In ${days} Days
`; for (const [key, value] of Object.entries(groupData)) { - tooltipContent += `#${key}: ${ - showTime ? timeSpan(value) : tr.statisticsReviews({ reviews: Math.round(value) }) - }
`; + const path = svg.select(`path[data-group="${key}"]`); + const hidden = path.classed("hidden"); + + if (!hidden) { + const tooltip = ({ + [SimulateSubgraph.time]: timeSpan(value), + [SimulateSubgraph.count]: tr.statisticsReviews({ reviews: Math.round(value) }), + [SimulateSubgraph.memorized]: tr.statisticsMemorized({ memorized: Math.round(value).toFixed(0) }), + })[subgraph]; + + tooltipContent += `#${key}: ${tooltip}
`; + } } showTooltip(tooltipContent, event.pageX, event.pageY);