Limit study time to hours in reviews graph (#4086)

* Add maxUnit argument to naturalUnit

* Limit study time to hours in reviews graph

Relevant discussions:
- https://forums.ankiweb.net/t/reviews-graph-units-of-total-time-studied-suggestion/61237
- https://forums.ankiweb.net/t/why-does-anki-display-study-time-in-months/37722
- https://forums.ankiweb.net/t/poll-use-hours-in-total-time-stats/62076
- https://github.com/ankitects/anki/pull/3901#issuecomment-2973161601

* Use the new approach for native stability in Card Info

* Use a simpler approach
This commit is contained in:
user1823 2025-06-18 13:04:58 +05:30 committed by GitHub
parent 4040a3c1f9
commit 44f3bbbbc9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 19 additions and 9 deletions

View file

@ -7,8 +7,8 @@ export const SECOND = 1.0;
export const MINUTE = 60.0 * SECOND; export const MINUTE = 60.0 * SECOND;
export const HOUR = 60.0 * MINUTE; export const HOUR = 60.0 * MINUTE;
export const DAY = 24.0 * HOUR; export const DAY = 24.0 * HOUR;
export const MONTH = 30.417 * DAY; // 365/12 ≈ 30.417
export const YEAR = 365.0 * DAY; export const YEAR = 365.0 * DAY;
export const MONTH = YEAR / 12;
export enum TimespanUnit { export enum TimespanUnit {
Seconds, Seconds,
@ -146,8 +146,13 @@ function i18nFuncForUnit(
If precise is true, show to two decimal places, eg If precise is true, show to two decimal places, eg
eg 70 seconds -> "1.17 minutes" eg 70 seconds -> "1.17 minutes"
If false, seconds and days are shown without decimals. */ If false, seconds and days are shown without decimals. */
export function timeSpan(seconds: number, short = false, precise = true): string { export function timeSpan(
const unit = naturalUnit(seconds); seconds: number,
short = false,
precise = true,
maxUnit: TimespanUnit = TimespanUnit.Years,
): string {
const unit = Math.min(naturalUnit(seconds), maxUnit);
let amount = unitAmount(unit, seconds); let amount = unitAmount(unit, seconds);
if (!precise && unit < TimespanUnit.Months) { if (!precise && unit < TimespanUnit.Months) {
amount = Math.round(amount); amount = Math.round(amount);

View file

@ -5,7 +5,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<script lang="ts"> <script lang="ts">
import type { CardStatsResponse } from "@generated/anki/stats_pb"; import type { CardStatsResponse } from "@generated/anki/stats_pb";
import * as tr2 from "@generated/ftl"; import * as tr2 from "@generated/ftl";
import { DAY, timeSpan, Timestamp } from "@tslib/time"; import { DAY, timeSpan, TimespanUnit, Timestamp } from "@tslib/time";
export let stats: CardStatsResponse; export let stats: CardStatsResponse;
@ -58,7 +58,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
if (stats.memoryState) { if (stats.memoryState) {
let stability = timeSpan(stats.memoryState.stability * 86400, false, false); let stability = timeSpan(stats.memoryState.stability * 86400, false, false);
if (stats.memoryState.stability > 31) { if (stats.memoryState.stability > 31) {
const nativeStability = stats.memoryState.stability.toFixed(0); const nativeStability = timeSpan(
stats.memoryState.stability * 86400,
false,
false,
TimespanUnit.Days,
);
stability += ` (${nativeStability})`; stability += ` (${nativeStability})`;
} }
statsRows.push({ statsRows.push({

View file

@ -8,7 +8,7 @@
import type { GraphsResponse } from "@generated/anki/stats_pb"; import type { GraphsResponse } from "@generated/anki/stats_pb";
import * as tr from "@generated/ftl"; import * as tr from "@generated/ftl";
import { localizedNumber } from "@tslib/i18n"; import { localizedNumber } from "@tslib/i18n";
import { dayLabel, timeSpan } from "@tslib/time"; import { dayLabel, timeSpan, TimespanUnit } from "@tslib/time";
import type { Bin, ScaleSequential } from "d3"; import type { Bin, ScaleSequential } from "d3";
import { import {
area, area,
@ -141,7 +141,7 @@ export function renderReviews(
const yTickFormat = (n: number): string => { const yTickFormat = (n: number): string => {
if (showTime) { if (showTime) {
return timeSpan(n / 1000, true); return timeSpan(n / 1000, true, false, TimespanUnit.Hours);
} else { } else {
if (Math.round(n) != n) { if (Math.round(n) != n) {
return ""; return "";
@ -205,7 +205,7 @@ export function renderReviews(
function valueLabel(n: number): string { function valueLabel(n: number): string {
if (showTime) { if (showTime) {
return timeSpan(n / 1000); return timeSpan(n / 1000, false, true, TimespanUnit.Hours);
} else { } else {
return tr.statisticsReviews({ reviews: n }); return tr.statisticsReviews({ reviews: n });
} }
@ -340,7 +340,7 @@ export function renderReviews(
averageAnswerTime: string, averageAnswerTime: string,
averageAnswerTimeLabel: string; averageAnswerTimeLabel: string;
if (showTime) { if (showTime) {
totalString = timeSpan(total / 1000, false); totalString = timeSpan(total / 1000, false, true, TimespanUnit.Hours);
averageForDaysStudied = tr.statisticsMinutesPerDay({ averageForDaysStudied = tr.statisticsMinutesPerDay({
count: Math.round(studiedAvg / 1000 / 60), count: Math.round(studiedAvg / 1000 / 60),
}); });