0
0
mirror of https://github.com/ankidroid/Anki-Android.git synced 2024-09-20 12:02:16 +02:00

more overview statistics

This commit is contained in:
Pascal Seitz 2016-03-17 07:05:36 +01:00 committed by Pascal Seitz
parent 219a140bd5
commit b89ddbc4e0
7 changed files with 297 additions and 43 deletions

View File

@ -144,7 +144,7 @@ public class DeckPicker extends NavigationDrawerActivity implements
private DeckAdapter mDeckListAdapter;
private FloatingActionsMenu mActionsMenu; // Note this will be null below SDK 14
private TextView mTodayTextView;
private TextView mReviewSummaryTextView;
private BroadcastReceiver mUnmountReceiver = null;
@ -404,7 +404,7 @@ public class DeckPicker extends NavigationDrawerActivity implements
});
}
mTodayTextView = (TextView) findViewById(R.id.today_stats_text_view);
mReviewSummaryTextView = (TextView) findViewById(R.id.today_stats_text_view);
// Hide the fragment until the counts have been loaded so that the Toolbar fills the whole screen on tablets
if (mFragmented) {
@ -1849,7 +1849,7 @@ public class DeckPicker extends NavigationDrawerActivity implements
}
// Update the mini statistics bar as well
AnkiStatsTaskHandler.createSmallTodayOverview(getCol(), mTodayTextView);
AnkiStatsTaskHandler.createReviewSummaryStatistics(getCol(), mReviewSummaryTextView);
}
@Override

View File

@ -324,7 +324,7 @@ public class Statistics extends NavigationDrawerActivity implements
switch (position) {
case TODAYS_STATS_TAB_POSITION:
return getString(R.string.stats_today).toUpperCase(l);
return getString(R.string.stats_overview).toUpperCase(l);
case FORECAST_TAB_POSITION:
return getString(R.string.stats_forecast).toUpperCase(l);
case REVIEW_COUNT_TAB_POSITION:

View File

@ -72,10 +72,10 @@ public class AnkiStatsTaskHandler {
createChartTask.execute(views);
return createChartTask;
}
public static CreateSmallTodayOverview createSmallTodayOverview(Collection col, TextView view){
CreateSmallTodayOverview createSmallTodayOverview = new CreateSmallTodayOverview();
createSmallTodayOverview.execute(col, view);
return createSmallTodayOverview;
public static DeckPreviewStatistics createReviewSummaryStatistics(Collection col, TextView view){
DeckPreviewStatistics deckPreviewStatistics = new DeckPreviewStatistics();
deckPreviewStatistics.execute(col, view);
return deckPreviewStatistics;
}
public static CreateFirstStatisticChooserTask createFirstStatisticChooserTask(Collection col, ViewPager viewPager){
@ -162,8 +162,8 @@ public class AnkiStatsTaskHandler {
mWebView = (WebView) params[0];
mProgressBar = (ProgressBar) params[1];
String html = "";
InfoStatsBuilder infoStatsBuilder = new InfoStatsBuilder(mWebView, mCollectionData, mIsWholeCollection);
html = infoStatsBuilder.createInfoHtmlString();
OverviewStatsBuilder overviewStatsBuilder = new OverviewStatsBuilder(mWebView, mCollectionData, mIsWholeCollection, mStatType);
html = overviewStatsBuilder.createInfoHtmlString();
return html;
}finally {
sLock.unlock();
@ -195,12 +195,12 @@ public class AnkiStatsTaskHandler {
}
private static class CreateSmallTodayOverview extends AsyncTask<Object, Void, String>{
private static class DeckPreviewStatistics extends AsyncTask<Object, Void, String>{
private TextView mTextView;
private boolean mIsRunning = false;
public CreateSmallTodayOverview(){
public DeckPreviewStatistics(){
super();
mIsRunning = true;
}
@ -213,10 +213,10 @@ public class AnkiStatsTaskHandler {
try {
Collection collection = (Collection) params[0];
if (!mIsRunning || collection == null || collection.getDb() == null) {
Timber.d("quiting CreateSmallTodayOverview before execution");
Timber.d("quiting DeckPreviewStatistics before execution");
return null;
} else
Timber.d("starting CreateSmallTodayOverview" );
Timber.d("starting DeckPreviewStatistics" );
mTextView = (TextView) params[1];
//eventually put this in Stats (in desktop it is not though)
@ -224,7 +224,7 @@ public class AnkiStatsTaskHandler {
int minutes;
Cursor cur = null;
String query = "select count(), sum(time)/1000 from revlog where id > " + ((collection.getSched().getDayCutoff()-86400)*1000);
Timber.d("CreateSmallTodayOverview query: " + query);
Timber.d("DeckPreviewStatistics query: " + query);
try {
cur = collection.getDb()
@ -291,7 +291,7 @@ public class AnkiStatsTaskHandler {
int cards;
Cursor cur = null;
String query = "select count() from revlog where id > " + ((collection.getSched().getDayCutoff()-86400)*1000);
Timber.d("CreateSmallTodayOverview query: " + query);
Timber.d("DeckPreviewStatistics query: " + query);
try {
cur = collection.getDb()

View File

@ -17,14 +17,20 @@ package com.ichi2.anki.stats;
import android.content.res.Resources;
import android.database.Cursor;
import android.webkit.WebView;
import com.ichi2.anki.R;
import com.ichi2.libanki.Collection;
import com.ichi2.libanki.Stats;
import com.ichi2.libanki.Utils;
import com.ichi2.themes.Themes;
public class InfoStatsBuilder {
import java.util.ArrayList;
import timber.log.Timber;
public class OverviewStatsBuilder {
private final int CARDS_INDEX = 0;
private final int THETIME_INDEX = 1;
private final int FAILED_INDEX = 2;
@ -37,12 +43,30 @@ public class InfoStatsBuilder {
;
private final WebView mWebView; //for resources access
private final Collection mCollectionData;
private final boolean mIsWholeCollection;
private final boolean mWholeCollection;
private final Stats.AxisType mType;
public InfoStatsBuilder(WebView chartView, Collection collectionData, boolean isWholeCollection){
public class OverviewStats {
public double reviewsPerDayOnAll;
public double reviewsPerDayOnStudyDays;
public int allDays;
public int daysStudied;
public double timePerDayOnAll;
public double timePerDayOnStudyDays;
public double totalTime;
public int totalReviews;
public double newCardsPerDay;
public int totalNewCards;
public double averageInterval;
public double longestInterval;
}
public OverviewStatsBuilder(WebView chartView, Collection collectionData, boolean isWholeCollection, Stats.AxisType mStatType){
mWebView = chartView;
mCollectionData = collectionData;
mIsWholeCollection = isWholeCollection;
mWholeCollection = isWholeCollection;
mType = mStatType;
}
public String createInfoHtmlString(){
@ -61,12 +85,61 @@ public class InfoStatsBuilder {
stringBuilder.append(css);
appendTodaysStats(stringBuilder);
appendOverViewStats(stringBuilder);
stringBuilder.append("</center>");
return stringBuilder.toString();
}
private void appendOverViewStats(StringBuilder stringBuilder) {
Stats stats = new Stats(mCollectionData, mWholeCollection);
OverviewStats oStats = new OverviewStats();
stats.calculateOverviewStatistics(mType, oStats, mWebView.getContext());
Resources res = mWebView.getResources();
stringBuilder.append(_title(res.getString(mType.descriptionId)));
boolean allDaysStudied = oStats.daysStudied == oStats.allDays;
stringBuilder.append(_subtitle(res.getString(R.string.stats_review_count).toUpperCase()));
stringBuilder.append(res.getString(R.string.stats_overview_days_studied,(int)((float)oStats.daysStudied/(float)oStats.allDays*100), oStats.daysStudied, oStats.allDays));
stringBuilder.append(res.getString(R.string.stats_overview_total_reviews,oStats.totalReviews));
stringBuilder.append("<br>");
stringBuilder.append(res.getString(R.string.stats_overview_reviews_per_day_studydays,oStats.reviewsPerDayOnStudyDays));
if (!allDaysStudied){
stringBuilder.append("<br>");
stringBuilder.append(res.getString(R.string.stats_overview_reviews_per_day_all,oStats.reviewsPerDayOnAll));
}
stringBuilder.append("<br><br>");
stringBuilder.append(_subtitle(res.getString(R.string.stats_review_time).toUpperCase()));
stringBuilder.append(res.getString(R.string.stats_overview_time_per_day_studydays,oStats.timePerDayOnStudyDays));
if (!allDaysStudied){
stringBuilder.append("<br>");
stringBuilder.append(res.getString(R.string.stats_overview_time_per_day_all,oStats.timePerDayOnAll));
}
stringBuilder.append("<br><br>");
stringBuilder.append(_subtitle(res.getString(R.string.stats_progress).toUpperCase()));
stringBuilder.append(res.getString(R.string.stats_overview_total_new_cards,oStats.totalNewCards));
stringBuilder.append("<br>");
stringBuilder.append(res.getString(R.string.stats_overview_new_cards_per_day,oStats.newCardsPerDay));
stringBuilder.append("<br><br>");
stringBuilder.append(_subtitle(res.getString(R.string.stats_review_intervals).toUpperCase()));
stringBuilder.append(res.getString(R.string.stats_overview_average_interval));
stringBuilder.append(Utils.roundedTimeSpan(mWebView.getContext(), (int)Math.round(oStats.averageInterval*Stats.SECONDS_PER_DAY)));
stringBuilder.append("<br>");
stringBuilder.append(res.getString(R.string.stats_overview_longest_interval));
stringBuilder.append(Utils.roundedTimeSpan(mWebView.getContext(), (int)Math.round(oStats.longestInterval*Stats.SECONDS_PER_DAY)));
}
private void appendTodaysStats(StringBuilder stringBuilder){
Stats stats = new Stats(mCollectionData, mIsWholeCollection);
Stats stats = new Stats(mCollectionData, mWholeCollection);
int[] todayStats = stats.calculateTodayStats();
stringBuilder.append(_title(mWebView.getResources().getString(R.string.stats_today)));
Resources res = mWebView.getResources();
@ -88,15 +161,17 @@ public class InfoStatsBuilder {
} else {
stringBuilder.append(res.getString(R.string.stats_today_no_mature_cards));
}
}
private String _title(String title){
return _title(title, "");
return "<h1>" + title + "</h1>";
}
private String _title(String title, String subtitle){
return "<h1>" + title + "</h1>" + subtitle;
private String _subtitle(String title){
return "<h3>" + title + "</h3>";
}
private String bold(String s) {

View File

@ -19,9 +19,11 @@ package com.ichi2.libanki;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import com.ichi2.anki.AnkiDroidApp;
import com.ichi2.anki.R;
import com.ichi2.anki.stats.OverviewStatsBuilder;
import com.ichi2.anki.stats.StatsMetaInfo;
import com.ichi2.libanki.hooks.Hooks;
@ -37,10 +39,19 @@ import timber.log.Timber;
*/
public class Stats {
public static enum AxisType{
TYPE_MONTH,
TYPE_YEAR,
TYPE_LIFE
TYPE_MONTH(30, R.string.stats_period_month),
TYPE_YEAR(365, R.string.stats_period_year),
TYPE_LIFE(-1, R.string.stats_period_lifetime);
public int days;
public int descriptionId;
AxisType(int dayss, int descriptionId) {
this.days = dayss;
this.descriptionId = descriptionId;
}
}
public static enum ChartType {FORECAST, REVIEW_COUNT, REVIEW_TIME,
@ -75,6 +86,9 @@ public class Stats {
private double mPeak;
private double mMcount;
public static double SECONDS_PER_DAY = 86400.0;
public Stats(Collection col, boolean wholeCollection) {
mCol = col;
mWholeCollection = wholeCollection;
@ -116,7 +130,7 @@ public class Stats {
* Todays statistics
*/
public int[] calculateTodayStats(){
String lim = _revlogLimit();
String lim = _getDeckFilter();
if (lim.length() > 0)
lim = " and " + lim;
@ -127,7 +141,7 @@ public class Stats {
"sum(case when type = 1 then 1 else 0 end), "+ /* review */
"sum(case when type = 2 then 1 else 0 end), "+ /* relearn */
"sum(case when type = 3 then 1 else 0 end) "+ /* filter */
"from revlog where id > " + ((mCol.getSched().getDayCutoff()-86400)*1000) + " " + lim;
"from revlog where id > " + ((mCol.getSched().getDayCutoff()-SECONDS_PER_DAY)*1000) + " " + lim;
Timber.d("todays statistics query: %s", query);
int cards, thetime, failed, lrn, rev, relrn, filt;
@ -153,7 +167,7 @@ public class Stats {
}
}
query = "select count(), sum(case when ease = 1 then 0 else 1 end) from revlog " +
"where lastIvl >= 21 and id > " + ((mCol.getSched().getDayCutoff()-86400)*1000) + " " + lim;
"where lastIvl >= 21 and id > " + ((mCol.getSched().getDayCutoff()-SECONDS_PER_DAY)*1000) + " " + lim;
Timber.d("todays statistics query 2: %s", query);
int mcnt, msum;
@ -174,6 +188,128 @@ public class Stats {
return new int[]{cards, thetime, failed, lrn, rev, relrn, filt, mcnt, msum};
}
private String getRevlogTimeFilter(AxisType timespan, boolean inverse){
if (timespan == AxisType.TYPE_LIFE){
return "";
}
else {
String operator = null;
if (inverse){
operator = "<= ";
}else{
operator = "> ";
}
return "id "+ operator + ((mCol.getSched().getDayCutoff() - (timespan.days * SECONDS_PER_DAY)) * 1000);
}
}
public int getNewCards(AxisType timespan){
String filter = getRevlogFilter(timespan,false);
String queryNeg = "";
if (timespan != AxisType.TYPE_LIFE){
String invfilter = getRevlogFilter(timespan,true);
queryNeg = " EXCEPT SELECT distinct cid FROM revlog " + invfilter;
}
String query = "SELECT COUNT(*) FROM(\n" +
" SELECT distinct cid\n" +
" FROM revlog \n" +
filter+
queryNeg+
" )";
Timber.d("New cards query: %s", query);
Cursor cur = null;
int res = 0;
try {
cur = mCol.getDb().getDatabase().rawQuery(query, null);
while (cur.moveToNext()) {
res = cur.getInt(0);
}
} finally {
if (cur != null && !cur.isClosed()) {
cur.close();
}
}
return res;
}
String getRevlogFilter(AxisType timespan,boolean inverseTimeSpan){
ArrayList<String> lims = new ArrayList<>();
String dayFilter = getRevlogTimeFilter(timespan, inverseTimeSpan);
if (dayFilter != "") lims.add(dayFilter);
String lim = _getDeckFilter().replaceAll("[\\[\\]]", "");
if (lim.length() > 0){
lims.add(lim);
}
if (!lims.isEmpty()) {
lim = "WHERE ";
lim += TextUtils.join(" AND ",lims.toArray());
}
return lim;
}
public void calculateOverviewStatistics(AxisType timespan, OverviewStatsBuilder.OverviewStats oStats, Context context) {
oStats.allDays = timespan.days;
String lim = getRevlogFilter(timespan,false);
Cursor cur = null;
try {
cur = mCol.getDb().getDatabase().rawQuery( "SELECT COUNT(*) as num_reviews, sum(case when type = 0 then 1 else 0 end) as new_cards FROM revlog "+ lim, null);
while (cur.moveToNext()) {
oStats.totalReviews = cur.getInt(0);
}
} finally {
if (cur != null && !cur.isClosed())
cur.close();
}
String cntquery = "SELECT COUNT(*) numDays, MIN(day) firstDay, SUM(time_per_day) sum_time from (" +
" SELECT (cast((id/1000 - " + mCol.getSched().getDayCutoff() + ") / "+SECONDS_PER_DAY+" AS INT)) AS day, sum(time/1000.0/60.0) AS time_per_day"
+ " FROM revlog " + lim + " GROUP BY day ORDER BY day)";
Timber.d("Count cntquery: %s", cntquery);
try {
cur = mCol.getDb().getDatabase().rawQuery(cntquery, null);
while (cur.moveToNext()) {
oStats.daysStudied = cur.getInt(0);
oStats.totalTime = cur.getDouble(2);
if (timespan == AxisType.TYPE_LIFE)
oStats.allDays = Math.abs(cur.getInt(1))+1; // +1 for today
}
} finally {
if (cur != null && !cur.isClosed()) {
cur.close();
}
}
try {
cur = mCol.getDb().getDatabase().rawQuery("select avg(ivl), max(ivl) from cards where did in " +_limit() +
" and queue = 2", null);
cur.moveToFirst();
oStats.averageInterval = cur.getDouble(0);
oStats.longestInterval = cur.getDouble(1);
} finally {
if (cur != null && !cur.isClosed()) {
cur.close();
}
}
oStats.reviewsPerDayOnAll = (double)oStats.totalReviews / oStats.allDays;
oStats.reviewsPerDayOnStudyDays = (double)oStats.totalReviews / oStats.daysStudied;
oStats.timePerDayOnAll = oStats.totalTime / oStats.allDays;
oStats.timePerDayOnStudyDays = oStats.totalTime / oStats.daysStudied;
oStats.totalNewCards = getNewCards(timespan);
oStats.newCardsPerDay = (double)oStats.totalNewCards/(double) oStats.allDays;
}
public boolean calculateDue(Context context, AxisType type) {
// Not in libanki
@ -414,9 +550,9 @@ public class Stats {
}
ArrayList<String> lims = new ArrayList<>();
if (num != -1) {
lims.add("id > " + ((mCol.getSched().getDayCutoff() - ((num + 1) * chunk * 86400)) * 1000));
lims.add("id > " + ((mCol.getSched().getDayCutoff() - ((num + 1) * chunk * SECONDS_PER_DAY)) * 1000));
}
String lim = _revlogLimit().replaceAll("[\\[\\]]", "");
String lim = _getDeckFilter().replaceAll("[\\[\\]]", "");
if (lim.length() > 0) {
lims.add(lim);
}
@ -432,7 +568,7 @@ public class Stats {
String ti;
String tf;
if (charType == ChartType.REVIEW_TIME) {
ti = "time/1000";
ti = "time/1000.0";
if (mType == AxisType.TYPE_MONTH) {
tf = "/60.0"; // minutes
mAxisTitles = new int[] { type.ordinal(), R.string.stats_minutes, R.string.stats_cumulative_time_minutes };
@ -446,7 +582,7 @@ public class Stats {
}
ArrayList<double[]> list = new ArrayList<>();
Cursor cur = null;
String query = "SELECT (cast((id/1000 - " + mCol.getSched().getDayCutoff() + ") / 86400.0 AS INT))/"
String query = "SELECT (cast((id/1000 - " + mCol.getSched().getDayCutoff() + ") / "+SECONDS_PER_DAY+" AS INT))/"
+ chunk + " AS day, " + "sum(CASE WHEN type = 0 THEN " + ti + " ELSE 0 END)"
+ tf
+ ", " // lrn
@ -476,6 +612,7 @@ public class Stats {
}
}
// small adjustment for a proper chartbuilding with achartengine
if (type != AxisType.TYPE_LIFE && (list.size() == 0 || list.get(0)[0] > -num)) {
list.add(0, new double[] { -num, 0, 0, 0, 0, 0 });
@ -681,8 +818,8 @@ public class Stats {
mFirstElement = 0;
mMaxElements = list.size()-1;
mAverage = Utils.timeSpan(context, (int)Math.round(avg*86400));
mLongest = Utils.timeSpan(context, (int)Math.round(max_*86400));
mAverage = Utils.timeSpan(context, (int)Math.round(avg*SECONDS_PER_DAY));
mLongest = Utils.timeSpan(context, (int)Math.round(max_*SECONDS_PER_DAY));
//some adjustments to not crash the chartbuilding with emtpy data
if(mMaxElements == 0){
@ -711,7 +848,7 @@ public class Stats {
mColors = new int[] { R.attr.stats_counts, R.attr.stats_hours};
mType = type;
String lim = _revlogLimit().replaceAll("[\\[\\]]", "");
String lim = _getDeckFilter().replaceAll("[\\[\\]]", "");
if (lim.length() > 0) {
lim = " and " + lim;
@ -722,7 +859,7 @@ public class Stats {
int pd = _periodDays();
if(pd > 0){
lim += " and id > "+ ((mCol.getSched().getDayCutoff()-(86400*pd))*1000);
lim += " and id > "+ ((mCol.getSched().getDayCutoff()-(SECONDS_PER_DAY*pd))*1000);
}
long cutoff = mCol.getSched().getDayCutoff();
long cut = cutoff - sd.get(Calendar.HOUR_OF_DAY)*3600;
@ -849,7 +986,7 @@ public class Stats {
mColors = new int[] { R.attr.stats_counts, R.attr.stats_hours};
mType = type;
String lim = _revlogLimit().replaceAll("[\\[\\]]", "");
String lim = _getDeckFilter().replaceAll("[\\[\\]]", "");
if (lim.length() > 0) {
lim = " and " + lim;
@ -861,7 +998,7 @@ public class Stats {
int pd = _periodDays();
if(pd > 0){
lim += " and id > "+ ((mCol.getSched().getDayCutoff()-(86400*pd))*1000);
lim += " and id > "+ ((mCol.getSched().getDayCutoff()-(SECONDS_PER_DAY*pd))*1000);
}
long cutoff = mCol.getSched().getDayCutoff();
@ -977,7 +1114,7 @@ public class Stats {
mColors = new int[] { R.attr.stats_learn, R.attr.stats_young, R.attr.stats_mature};
mType = type;
String lim = _revlogLimit().replaceAll("[\\[\\]]", "");
String lim = _getDeckFilter().replaceAll("[\\[\\]]", "");
Vector<String> lims = new Vector<>();
int days = 0;
@ -993,7 +1130,7 @@ public class Stats {
days = -1;
if (days > 0)
lims.add("id > " + ((mCol.getSched().getDayCutoff()-(days*86400))*1000));
lims.add("id > " + ((mCol.getSched().getDayCutoff()-(days*SECONDS_PER_DAY))*1000));
if (lims.size() > 0) {
lim = "where " + lims.get(0);
for(int i=1; i<lims.size(); i++)
@ -1185,7 +1322,7 @@ public class Stats {
}
private String _revlogLimit() {
private String _getDeckFilter() {
if (mWholeCollection) {
return "";
} else {

View File

@ -193,6 +193,24 @@ public class Utils {
}
}
/**
* Return a proper string for a time value in seconds
*
* @param context The application's environment.
* @param time_s The time to format, in seconds
* @return The formatted, localized time string. The time is always a float.
*/
public static String roundedTimeSpan(Context context, int time_s) {
if (Math.abs(time_s) < TIME_DAY) {
return context.getResources().getString(R.string.stats_overview_hours, time_s/TIME_HOUR);
} else if (Math.abs(time_s) < TIME_MONTH) {
return context.getResources().getString(R.string.stats_overview_days, time_s/TIME_DAY);
} else if (Math.abs(time_s) < TIME_YEAR) {
return context.getResources().getString(R.string.stats_overview_months,time_s/TIME_MONTH);
} else {
return context.getResources().getString(R.string.stats_overview_years, time_s/TIME_YEAR);
}
}
/**
* Locale

View File

@ -53,6 +53,28 @@
<string name="stats_today_type_breakdown">Learn: &lt;b&gt;%1$s&lt;/b&gt;, review: &lt;b&gt;%2$s&lt;/b&gt;, relearn: &lt;b&gt;%3$s&lt;/b&gt;, filtered: &lt;b&gt;%4$s&lt;/b&gt;</string>
<string name="stats_today_mature_cards">Correct answers on mature cards: %1$d/%2$d (%3$.1f%%)</string>
<string name="stats_today_no_mature_cards">No mature cards were studied today</string>
<string name="stats_overview_days_studied">Days studied: &lt;b&gt;%1$d%%&lt;/b&gt; (%2$d of %3$d)</string>
<string name="stats_overview_total_reviews">Total: &lt;b&gt;%1$d&lt;/b&gt; reviews</string>
<string name="stats_overview_reviews_per_day_studydays">Average for days studied: &lt;b&gt;%1$.1f&lt;/b&gt; reviews/day</string>
<string name="stats_overview_reviews_per_day_all">If you studied every day: &lt;b&gt;%1$.1f&lt;/b&gt; reviews/day</string>
<string name="stats_overview_time_per_day_studydays">Average for days studied: &lt;b&gt;%1$.1f&lt;/b&gt; minutes/day</string>
<string name="stats_overview_time_per_day_all">If you studied every day: &lt;b&gt;%1$.1f&lt;/b&gt; minutes/day</string>
<string name="stats_overview_new_cards_per_day">Average: &lt;b&gt;%1$.1f&lt;/b&gt; new cards/day</string>
<string name="stats_overview_total_new_cards">Total: &lt;b&gt;%1$d&lt;/b&gt; new cards</string>
<string name="stats_overview_average_interval">"Average interval: "</string>
<string name="stats_overview_longest_interval">"Longest interval: "</string>
<string name="stats_overview_years">&lt;b&gt;%1$.1f&lt;/b&gt; years</string>
<string name="stats_overview_months">&lt;b&gt;%1$.1f&lt;/b&gt; months</string>
<string name="stats_overview_days">&lt;b&gt;%1$.1f&lt;/b&gt; days</string>
<string name="stats_overview_hours">&lt;b&gt;%1$.1f&lt;/b&gt; hours</string>
<string name="deck_summary_all_decks">All decks</string>
<string name="stats_select_time_scale">Select timescale</string>
<string-array name="due_x_axis_title">
@ -61,9 +83,11 @@
<item>Months</item>
</string-array>
<string name="stats_today">Today</string>
<string name="stats_overview">Overview</string>
<string name="stats_forecast">Forecast</string>
<string name="stats_review_count">Review count</string>
<string name="stats_review_time">Review time</string>
<string name="stats_progress">Progress</string>
<string name="stats_review_intervals">Intervals</string>
<string name="stats_breakdown">Hourly breakdown</string>
<string name="stats_weekly_breakdown">Weekly breakdown</string>