diff --git a/rslib/ftl/statistics.ftl b/rslib/ftl/statistics.ftl
index 1a5ff5f3e..3f1cec54c 100644
--- a/rslib/ftl/statistics.ftl
+++ b/rslib/ftl/statistics.ftl
@@ -180,3 +180,11 @@ statistics-due-tomorrow = Due tomorrow
# eg 5 of 15 (33.3%)
statistics-amount-of-total-with-percentage = { $amount } of { $total } ({ $percent }%)
statistics-average-over-period = If you studied every day
+statistics-reviews-per-day = { $count ->
+ [one] { $count } review/day
+ *[other] { $count } reviews/day
+ }
+statistics-minutes-per-day = { $count ->
+ [one] { $count } minute/day
+ *[other] { $count } minutes/day
+ }
diff --git a/ts/src/i18n.ts b/ts/src/i18n.ts
index d21226506..c7266570a 100644
--- a/ts/src/i18n.ts
+++ b/ts/src/i18n.ts
@@ -46,6 +46,19 @@ export class I18n {
);
}
+ direction(): string {
+ const firstLang = this.bundles[0].locales[0];
+ if (
+ firstLang.startsWith("ar") ||
+ firstLang.startsWith("he") ||
+ firstLang.startsWith("fa")
+ ) {
+ return "rtl";
+ } else {
+ return "ltr";
+ }
+ }
+
private keyName(msg: pb.BackendProto.FluentString): string {
return this.TR[msg].toLowerCase().replace(/_/g, "-");
}
diff --git a/ts/src/stats/ReviewsGraph.svelte b/ts/src/stats/ReviewsGraph.svelte
index a08d04b9d..fd089e4be 100644
--- a/ts/src/stats/ReviewsGraph.svelte
+++ b/ts/src/stats/ReviewsGraph.svelte
@@ -22,8 +22,16 @@
graphData = gatherData(sourceData);
}
+ let tableStrings: [string, string][] = [];
$: if (graphData) {
- renderReviews(svg as SVGElement, bounds, graphData, graphRange, showTime, i18n);
+ tableStrings = renderReviews(
+ svg as SVGElement,
+ bounds,
+ graphData,
+ graphRange,
+ showTime,
+ i18n
+ );
}
const title = i18n.tr(i18n.TR.STATISTICS_REVIEWS_TITLE);
@@ -61,4 +69,14 @@
+
+
+ {#each tableStrings as [label, value]}
+
+ {label}: |
+ {value} |
+
+ {/each}
+
+
diff --git a/ts/src/stats/graphs.scss b/ts/src/stats/graphs.scss
index 716a83050..cebd0b3a4 100644
--- a/ts/src/stats/graphs.scss
+++ b/ts/src/stats/graphs.scss
@@ -184,5 +184,14 @@ body.night-mode {
}
.centered {
- text-align: center;
+ display: flex;
+ justify-content: center;
+}
+
+.align-end {
+ text-align: end;
+}
+
+.align-start {
+ text-align: start;
}
diff --git a/ts/src/stats/reviews.ts b/ts/src/stats/reviews.ts
index d944fb1f9..f03c800d7 100644
--- a/ts/src/stats/reviews.ts
+++ b/ts/src/stats/reviews.ts
@@ -108,7 +108,7 @@ export function renderReviews(
range: GraphRange,
showTime: boolean,
i18n: I18n
-): void {
+): [string, string][] {
const svg = select(svgElem);
const trans = svg.transition().duration(600) as any;
@@ -143,9 +143,10 @@ export function renderReviews(
.thresholds(x.ticks(desiredBars))(sourceMap.entries() as any);
// empty graph?
- if (!sum(bins, (bin) => bin.length)) {
+ const totalDays = sum(bins, (bin) => bin.length);
+ if (!totalDays) {
setDataAvailable(svg, false);
- return;
+ return [];
} else {
setDataAvailable(svg, true);
}
@@ -304,9 +305,10 @@ export function renderReviews(
const areaCounts = bins.map((d: any) => cumulativeBinValue(d, 4));
areaCounts.unshift(0);
const areaData = cumsum(areaCounts);
- const yAreaScale = y.copy().domain([0, areaData.slice(-1)[0]]);
+ const yCumMax = areaData.slice(-1)[0];
+ const yAreaScale = y.copy().domain([0, yCumMax]);
- if (areaData.slice(-1)[0]) {
+ if (yCumMax) {
svg.select("path.area")
.datum(areaData as any)
.attr(
@@ -339,4 +341,74 @@ export function renderReviews(
showTooltip(tooltipText(d, areaData[idx + 1]), x, y);
})
.on("mouseout", hideTooltip);
+
+ const periodDays = -xMin;
+ const studiedDays = sum(bins, (bin) => bin.length);
+ const total = yCumMax;
+ const periodAvg = total / periodDays;
+ const studiedAvg = total / studiedDays;
+
+ let totalString: string,
+ averageForDaysStudied: string,
+ averageForPeriod: string,
+ averageAnswerTime: string,
+ averageAnswerTimeLabel: string;
+ if (showTime) {
+ totalString = timeSpan(i18n, total / 1000, false);
+ averageForDaysStudied = i18n.tr(i18n.TR.STATISTICS_MINUTES_PER_DAY, {
+ count: Math.round(studiedAvg / 1000 / 60),
+ });
+ averageForPeriod = i18n.tr(i18n.TR.STATISTICS_MINUTES_PER_DAY, {
+ count: Math.round(periodAvg / 1000 / 60),
+ });
+ averageAnswerTimeLabel = i18n.tr(i18n.TR.STATISTICS_AVERAGE_ANSWER_TIME_LABEL);
+
+ // need to get total review count to calculate average time
+ const countBins = histogram()
+ .value((m) => {
+ return m[0];
+ })
+ .domain(x.domain() as any)(sourceData.reviewCount.entries() as any);
+ const totalReviews = sum(countBins, (bin) => cumulativeBinValue(bin as any, 4));
+ const totalSecs = total / 1000;
+ console.log(`total secs ${totalSecs} total reviews ${totalReviews}`);
+ const avgSecs = totalSecs / totalReviews;
+ const cardsPerMin = (totalReviews * 60) / totalSecs;
+ averageAnswerTime = i18n.tr(i18n.TR.STATISTICS_AVERAGE_ANSWER_TIME, {
+ "average-seconds": avgSecs,
+ "cards-per-minute": cardsPerMin,
+ });
+ } else {
+ totalString = i18n.tr(i18n.TR.STATISTICS_REVIEWS, { reviews: total });
+ averageForDaysStudied = i18n.tr(i18n.TR.STATISTICS_REVIEWS_PER_DAY, {
+ count: Math.round(studiedAvg),
+ });
+ averageForPeriod = i18n.tr(i18n.TR.STATISTICS_REVIEWS_PER_DAY, {
+ count: Math.round(periodAvg),
+ });
+ averageAnswerTime = averageAnswerTimeLabel = "";
+ }
+
+ const tableData: [string, string][] = [
+ [
+ i18n.tr(i18n.TR.STATISTICS_DAYS_STUDIED),
+ i18n.tr(i18n.TR.STATISTICS_AMOUNT_OF_TOTAL_WITH_PERCENTAGE, {
+ amount: studiedDays,
+ total: periodDays,
+ percent: Math.round((studiedDays / periodDays) * 100),
+ }),
+ ],
+
+ [i18n.tr(i18n.TR.STATISTICS_TOTAL), totalString],
+
+ [i18n.tr(i18n.TR.STATISTICS_AVERAGE_FOR_DAYS_STUDIED), averageForDaysStudied],
+
+ [i18n.tr(i18n.TR.STATISTICS_AVERAGE_OVER_PERIOD), averageForPeriod],
+ ];
+
+ if (averageAnswerTime) {
+ tableData.push([averageAnswerTimeLabel, averageAnswerTime]);
+ }
+
+ return tableData;
}