Feat/add a toggle in the simulator to display time or review count (#3523)

Co-authored-by: Damien Elmes <dae@users.noreply.github.com>
This commit is contained in:
Jarrett Ye 2024-10-26 17:42:57 +08:00 committed by GitHub
parent 1aa734ad28
commit eacd5bf908
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 48 additions and 15 deletions

View file

@ -40,6 +40,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import NoDataOverlay from "../graphs/NoDataOverlay.svelte"; import NoDataOverlay from "../graphs/NoDataOverlay.svelte";
import TableData from "../graphs/TableData.svelte"; import TableData from "../graphs/TableData.svelte";
import { defaultGraphBounds, type TableDatum } from "../graphs/graph-helpers"; import { defaultGraphBounds, type TableDatum } from "../graphs/graph-helpers";
import InputBox from "../graphs/InputBox.svelte";
export let state: DeckOptionsState; export let state: DeckOptionsState;
export let openHelpModal: (String) => void; export let openHelpModal: (String) => void;
@ -73,6 +74,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
| ComputeRetentionProgress | ComputeRetentionProgress
| undefined; | undefined;
let showTime = false;
const optimalRetentionRequest = new ComputeOptimalRetentionRequest({ const optimalRetentionRequest = new ComputeOptimalRetentionRequest({
daysToSimulate: 365, daysToSimulate: 365,
lossAversion: 2.5, lossAversion: 2.5,
@ -305,6 +308,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
return result; return result;
} }
function addArrays(arr1: number[], arr2: number[]): number[] {
return arr1.map((value, index) => value + arr2[index]);
}
$: simulateProgressString = ""; $: simulateProgressString = "";
async function simulateFsrs(): Promise<void> { async function simulateFsrs(): Promise<void> {
@ -328,23 +335,37 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
resp.dailyTimeCost, resp.dailyTimeCost,
Math.ceil(simulateFsrsRequest.daysToSimulate / 50), Math.ceil(simulateFsrsRequest.daysToSimulate / 50),
); );
const dailyReviewCount = movingAverage(
addArrays(resp.dailyReviewCount, resp.dailyNewCount),
Math.ceil(simulateFsrsRequest.daysToSimulate / 50),
);
points = points.concat( points = points.concat(
dailyTimeCost.map((v, i) => ({ dailyTimeCost.map((v, i) => ({
x: i, x: i,
y: v, timeCost: v,
count: dailyReviewCount[i],
label: simulationNumber, label: simulationNumber,
})), })),
); );
tableData = renderSimulationChart(svg as SVGElement, bounds, points); tableData = renderSimulationChart(
svg as SVGElement,
bounds,
points,
showTime,
);
} }
} }
} }
$: tableData = renderSimulationChart(svg as SVGElement, bounds, points, showTime);
function clearSimulation(): void { function clearSimulation(): void {
points = points.filter((p) => p.label !== simulationNumber); points = points.filter((p) => p.label !== simulationNumber);
simulationNumber = Math.max(0, simulationNumber - 1); simulationNumber = Math.max(0, simulationNumber - 1);
tableData = renderSimulationChart(svg as SVGElement, bounds, points); tableData = renderSimulationChart(svg as SVGElement, bounds, points, showTime);
} }
const label = tr.statisticsReviewsTimeCheckbox();
</script> </script>
<SpinBoxFloatRow <SpinBoxFloatRow
@ -472,7 +493,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
max={3650} max={3650}
> >
<SettingTitle on:click={() => openHelpModal("simulateFsrsReview")}> <SettingTitle on:click={() => openHelpModal("simulateFsrsReview")}>
Days to simulate {tr.deckConfigDaysToSimulate()}
</SettingTitle> </SettingTitle>
</SpinBoxRow> </SpinBoxRow>
@ -491,10 +512,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
bind:value={simulateFsrsRequest.newLimit} bind:value={simulateFsrsRequest.newLimit}
defaultValue={defaults.newPerDay} defaultValue={defaults.newPerDay}
min={0} min={0}
max={1000} max={9999}
> >
<SettingTitle on:click={() => openHelpModal("simulateFsrsReview")}> <SettingTitle on:click={() => openHelpModal("simulateFsrsReview")}>
New cards/day {tr.schedulingNewCardsday()}
</SettingTitle> </SettingTitle>
</SpinBoxRow> </SpinBoxRow>
@ -502,10 +523,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
bind:value={simulateFsrsRequest.reviewLimit} bind:value={simulateFsrsRequest.reviewLimit}
defaultValue={defaults.reviewsPerDay} defaultValue={defaults.reviewsPerDay}
min={0} min={0}
max={1000} max={9999}
> >
<SettingTitle on:click={() => openHelpModal("simulateFsrsReview")}> <SettingTitle on:click={() => openHelpModal("simulateFsrsReview")}>
Maximum reviews/day {tr.schedulingMaximumReviewsday()}
</SettingTitle> </SettingTitle>
</SpinBoxRow> </SpinBoxRow>
@ -516,7 +537,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
max={36500} max={36500}
> >
<SettingTitle on:click={() => openHelpModal("simulateFsrsReview")}> <SettingTitle on:click={() => openHelpModal("simulateFsrsReview")}>
Maximum interval {tr.schedulingMaximumInterval()}
</SettingTitle> </SettingTitle>
</SpinBoxRow> </SpinBoxRow>
@ -538,6 +559,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<div>{simulateProgressString}</div> <div>{simulateProgressString}</div>
<Graph {title}> <Graph {title}>
<InputBox>
<label>
<input type="checkbox" bind:checked={showTime} />
{label}
</label>
</InputBox>
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}> <svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
<CumulativeOverlay /> <CumulativeOverlay />
<HoverColumns /> <HoverColumns />

View file

@ -16,6 +16,7 @@ import {
select, select,
} from "d3"; } from "d3";
import * as tr from "@generated/ftl";
import { timeSpan } from "@tslib/time"; import { timeSpan } from "@tslib/time";
import type { GraphBounds, TableDatum } from "./graph-helpers"; import type { GraphBounds, TableDatum } from "./graph-helpers";
import { setDataAvailable } from "./graph-helpers"; import { setDataAvailable } from "./graph-helpers";
@ -23,7 +24,8 @@ import { hideTooltip, showTooltip } from "./tooltip-utils.svelte";
export interface Point { export interface Point {
x: number; x: number;
y: number; timeCost: number;
count: number;
label: number; label: number;
} }
@ -31,6 +33,7 @@ export function renderSimulationChart(
svgElem: SVGElement, svgElem: SVGElement,
bounds: GraphBounds, bounds: GraphBounds,
data: Point[], data: Point[],
showTime: boolean,
): TableDatum[] { ): TableDatum[] {
const svg = select(svgElem); const svg = select(svgElem);
svg.selectAll(".lines").remove(); svg.selectAll(".lines").remove();
@ -62,10 +65,10 @@ export function renderSimulationChart(
// y scale // y scale
const yTickFormat = (n: number): string => { const yTickFormat = (n: number): string => {
return timeSpan(n, true); return showTime ? timeSpan(n, true) : n.toString();
}; };
const yMax = max(convertedData, d => d.y)!; const yMax = showTime ? max(convertedData, d => d.timeCost)! : max(convertedData, d => d.count)!;
const y = scaleLinear() const y = scaleLinear()
.range([bounds.height - bounds.marginBottom, bounds.marginTop]) .range([bounds.height - bounds.marginBottom, bounds.marginTop])
.domain([0, yMax]) .domain([0, yMax])
@ -91,10 +94,10 @@ export function renderSimulationChart(
.attr("dy", "1em") .attr("dy", "1em")
.attr("fill", "currentColor") .attr("fill", "currentColor")
.style("text-anchor", "middle") .style("text-anchor", "middle")
.text("Review Time per day"); .text(showTime ? "Review Time per day" : "Review Count per day");
// x lines // x lines
const points = convertedData.map((d) => [x(d.date), y(d.y), d.label]); const points = convertedData.map((d) => [x(d.date), y(showTime ? d.timeCost : d.count), d.label]);
const groups = rollup(points, v => Object.assign(v, { z: v[0][2] }), d => d[2]); const groups = rollup(points, v => Object.assign(v, { z: v[0][2] }), d => d[2]);
const color = schemeCategory10; const color = schemeCategory10;
@ -161,7 +164,9 @@ export function renderSimulationChart(
const days = +((date.getTime() - Date.now()) / (60 * 60 * 24 * 1000)).toFixed(); const days = +((date.getTime() - Date.now()) / (60 * 60 * 24 * 1000)).toFixed();
let tooltipContent = `Date: ${localizedDate(date)}<br>In ${days} Days<br>`; let tooltipContent = `Date: ${localizedDate(date)}<br>In ${days} Days<br>`;
for (const [key, value] of Object.entries(groupData)) { for (const [key, value] of Object.entries(groupData)) {
tooltipContent += `#${key}: ${timeSpan(value)}<br>`; tooltipContent += `#${key}: ${
showTime ? timeSpan(value) : tr.statisticsReviews({ reviews: Math.round(value) })
}<br>`;
} }
showTooltip(tooltipContent, event.pageX, event.pageY); showTooltip(tooltipContent, event.pageX, event.pageY);