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

View file

@ -16,6 +16,7 @@ import {
select,
} from "d3";
import * as tr from "@generated/ftl";
import { timeSpan } from "@tslib/time";
import type { GraphBounds, TableDatum } from "./graph-helpers";
import { setDataAvailable } from "./graph-helpers";
@ -23,7 +24,8 @@ import { hideTooltip, showTooltip } from "./tooltip-utils.svelte";
export interface Point {
x: number;
y: number;
timeCost: number;
count: number;
label: number;
}
@ -31,6 +33,7 @@ export function renderSimulationChart(
svgElem: SVGElement,
bounds: GraphBounds,
data: Point[],
showTime: boolean,
): TableDatum[] {
const svg = select(svgElem);
svg.selectAll(".lines").remove();
@ -62,10 +65,10 @@ export function renderSimulationChart(
// y scale
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()
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
.domain([0, yMax])
@ -91,10 +94,10 @@ export function renderSimulationChart(
.attr("dy", "1em")
.attr("fill", "currentColor")
.style("text-anchor", "middle")
.text("Review Time per day");
.text(showTime ? "Review Time per day" : "Review Count per day");
// 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 color = schemeCategory10;
@ -161,7 +164,9 @@ export function renderSimulationChart(
const days = +((date.getTime() - Date.now()) / (60 * 60 * 24 * 1000)).toFixed();
let tooltipContent = `Date: ${localizedDate(date)}<br>In ${days} Days<br>`;
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);