[Userscript] WaniKani Review Clock

I couldn’t find a userscript that had the time tracking features that I wanted and was visually appealing, so I made one. Took me a couple days, but I am happy with the result!

Main Features

  1. Display the duration of the current review session
  2. Display the rate of completing reviews (reviews / hour)
  3. Display the estimated time to complete a review session (based on current rate)

Install

To install the script, you will need a userscript manager such as Tampermonkey. Once you have it, you can install the script by clicking the link below.

Install here: WaniKani Review Clock

If you want to adjust settings, you will also need to install WaniKani Open Framework. The extension will work without it as well, but you will be stuck with default settings.

Other Features

  • Hide statistics in settings

  • Only display review rate and time estimate after a time delay from the beginning of a session (off by default):
    Screenshot 2021-08-02 165611
    This is useful because the statistics can be quite inaccurate at the beginning of a session.

  • Calculate an average review rate based on review sessions that are at least 3 minutes long (duration adjustable in settings)
    and display the statistics of the previous session (duration and rate) below the summary page:

  • Display a time estimate for the review queue based on average review rate (can be disabled):
    Screenshot 2021-08-02 164501

Settings

The settings menu is implemented using WaniKani Open Framework, so install it if you want to adjust settings.

The settings menu can be accessed via the scripts settings menu from the top left in a review session:
Screenshot 2021-08-02 172720

The settings are mostly useful for hiding information that you might not want to be displayed. There are also some additional settings for other features.

Review page settings:

Summary page settings:

Bugs & Suggestions

This script has been tested on Firefox with tampermonkey.

If you encounter any bugs or other annoyances, please let me know on this thread or on GitHub. Also if you have any suggestions, please let me know and I’ll see if I’ll implement them!

Source Code

The source code is hosted on my GitHub. If you are tech-savvy enough, you can take a look and even contribute improvements if you like. Also leave a star if you fancy :slight_smile:

14 Likes

I tried it and this is really very nice!

A few opinionated thoughts (feel free to ignore):

  1. I really want to set the update interval to 5 minutes, so it would be nice if there wasn’t a limit on how high these values can be set.

  2. It would be nice if it had the option to display the UI during reviews “out of the way” at the bottom of the page in a gray font similar to what the WaniKani Review Timer script does. (See where it says “Time Elapsed: 6m 2s” in that screenshot. You can ignore where it says “(4s)” in the black bar as that is from a different script.)

  3. I know that in past threads, when people have shared and compared their review times, they have sometimes used units of minutes per 100 reviews or reviews per minute. It would be nice if we could pick the units the script uses so that it is easier to compare.

  4. It would be nice if it counted radicals as 0.5 items since you only answer 1 question for radicals instead of 2.

  5. Sometimes, I will go to start a review but then something else will come up in the middle of the session, but I won’t end the session and will instead resume it later. Also, sometimes I start a review session but then do something else before actually reviewing anything. This means that my stats would show a much lower rate than my actual rate. I think this could be remedied by waiting to start the timer until the first question is answered and, after each question, if no questions are answered for several minutes, not counting the time gap between the last answer and the next answer.

Edit: removed suggestion that was already present in script

1 Like

Thank you for giving it a try! I also really appreciate the detailed suggestions and feedback.

As for the your points:

  1. I knew there would be someone who would like to have values for the settings that I didn’t even consider :D. I will probably remove the limits since it is mostly arbitrary anyways.
  2. I see your point. I would like this script to be as configurable as possible so that everyone can tweak it to their preference, but I’m too lazy to work on this for now. I will keep it in mind though if I have free time in the future.
  3. Yeah different units could be a setting. I will put this in the backlog.
  4. I agree, but due to how I made this technically, the effort to implement this would not be proportional to the gained benefit.
  5. This is also true, and I tried to keep it simple for now. Right now it ignores sessions with zero completed items and ones that are shorter than the set period (3 min by default).
    I could look into this a bit more in the future, but it will probably complicate things technically.

Once again thanks for the feedback! I will probably implement some of the suggestions. I will tag you again if I update the script :slight_smile:

1 Like

Below is a patch implementing #2 and #3 (ability to choose location and units). Note that it is a reverse diff where the minuses are the changes and the pluses are the original. In other words, what is says as “new version” is actually the old version. Most patch utilities have a flag that you can use to tell it the patch is reversed.

===================================================================
--- Current Version
+++ New Version
@@ -3,9 +3,9 @@
 // @namespace   wkreviewclock
 // @description Adds a clock to WaniKani review session statistics and estimates the remaining time.
 // @include     http://www.wanikani.com/review*
 // @include     https://www.wanikani.com/review*
-// @version     1.1
+// @version     1.0
 // @author      Markus Tuominen
 // @grant       none
 // @license     GPL version 3 or later: http://www.gnu.org/copyleft/gpl.html
 // @source      https://github.com/Markus98/wk-review-clock
@@ -21,10 +21,8 @@
 const averageStatsKey = 'reviewRateAverageStats';
 const scriptId = 'WKReviewClock'
 
 const defaultSettings = {
-    units: 'rph',
-    location: 'toprightright',
     showTimer: true,
     showRate: true,
     showRemaining: true,
     updateInterval: 1.0,
@@ -76,10 +74,10 @@
 
     const reviewsDoneNumber = parseInt(document.getElementById('completed-count').textContent);
     const reviewRate = time !== 0 ? reviewsDoneNumber/time : 0; // reviews/sec
     if (showRate) {
-        const formattedRate = formatRate(reviewRate, 'short');
-        statHtmlElems.rate.span.textContent = (hideRateRemaining ? '—' : formattedRate) + '';
+        const formattedRate = (reviewRate*3600).toFixed(1); // reviews/hour
+        statHtmlElems.rate.span.textContent = (hideRateRemaining ? '—' : formattedRate) + ' r/h';
     }
 
     const reviewsAvailableNumber = parseInt(document.getElementById('available-count').textContent);
     const timeRemaining = reviewsAvailableNumber / reviewRate; // seconds
@@ -148,23 +146,9 @@
     }
     statHtmlElems.updateVisibility();
 
     // append statsDiv to header
-    let parent;
-    const header = document.createElement('span');
-    const location = wkof.settings[scriptId].location;
-    if (location == 'toprightright') {
-        parent = document.getElementById('stats');
-        parent.append(header);
-    } else if (location == 'toprightleft') {
-        parent = document.getElementById('stats');
-        parent.prepend(header);
-        header.style.cssText = 'margin-right: 2em';
-    } else if (location == 'bottom') {
-        parent = document.getElementById('reviews');
-        parent.append(header);
-        header.classList.add('wkrc_bottom');
-    }
+    const header = document.getElementById('stats');
     header.appendChild(statHtmlElems.timer.icon);
     header.appendChild(statHtmlElems.timer.span);
     header.appendChild(statHtmlElems.rate.icon);
     header.appendChild(statHtmlElems.rate.span);
@@ -253,9 +237,9 @@
     // Saved time and rate
     const lastTime = parseFloat(localStorage.getItem(timerTimeKey));
     const lastTimeStr = isNaN(lastTime) ? '—' : getTimeString(splitToHourMinSec(lastTime));
     const lastRate = parseFloat(localStorage.getItem(timerRateKey));
-    const lastRateStr = formatRate(lastRate);
+    const lastRateStr = isNaN(lastRate) ? '—' : (lastRate*3600).toFixed(1);
 
     // Average rate
     const avgStats = getAverageStats();
     if (!avgStats.mostRecentAdded && lastTime > ignoreInterval && lastRate > 0) {
@@ -264,18 +248,18 @@
         avgStats.mostRecentAdded = true;
         setAverageStats(avgStats);
     }
     const avgRate = avgStats.rateSum / avgStats.reviews; // reviews/second
-    const avgRateStr = formatRate(avgRate, 'short');
+    const avgRateStr = isNaN(avgRate) ? '—' : (parseFloat(avgRate)*3600).toFixed(1);
 
     // Estimate time for current reviews
     const numOfReviews = parseInt(reviewCountSpan.textContent);
     const estimatedTime = numOfReviews / avgRate;
     const estimatedTimeStr = getTimeString(splitToHourMinSec(estimatedTime), false);
     
     // Set stats text content
     timeSpan.textContent = `Duration: ${lastTimeStr}`;
-    rateSpan.textContent = `Review rate: ${lastRateStr} (avg. ${avgRateStr}) (${avgStats.reviews} sessions)`;
+    rateSpan.textContent = `Review rate: ${lastRateStr} reviews per hour (avg. ${avgRateStr} r/h) (${avgStats.reviews} sessions)`;
     estimatedTimeDiv.textContent = 
         !showEstimatedSessionTime || isNaN(estimatedTime) || numOfReviews === 0 ? 
         '' : `~${estimatedTimeStr}`;
 
@@ -285,32 +269,8 @@
     footer.appendChild(resetAvgButton);
     reviewCountSpan.appendChild(estimatedTimeDiv);
 }
 
-let shortUnitNames = {'rph': 'r/h', 'rpm': 'r/m', 'mp100r': 'm/100r'}
-let unitNames = {'rph': 'reviews/hr', 'rpm': 'reviews/min', 'mp100r': 'min/100 reviews'}
-function formatRate(rps, format) {
-    if (isNaN(rps) || rps < 0.00001) {
-        return '—';
-    }
-    rps = parseFloat(rps);
-    const units = wkof.settings[scriptId].units;
-    let res;
-    if (units == 'rph') {
-        res = rps*3600;
-    } else if (units == 'rpm') {
-        res = rps*60;
-    } else if (units == 'mp100r') {
-        res = 1/rps/60*100;
-    }
-    if (format == 'short') {
-        return res.toFixed(1) + ' ' + shortUnitNames[units];
-    } else {
-        return res.toFixed(1) + ' ' + unitNames[units];
-    }
-
-}
-
 function openSettings() {
     var config = {
         script_id: scriptId,
         title: 'Review Clock Settings',
@@ -319,40 +279,12 @@
             statHtmlElems.updateVisibility();
             rateShowDelay = parseFloat(wkof.settings[scriptId].rateShowDelay)*60;
         },
         content: {
-            general: {
-                type: 'page',
-                label: 'General',
-                content: {
-                    units: {
-                        type: 'dropdown',
-                        label: 'Units for Speed',
-                        default: defaultSettings.units,
-                        hover_tip: 'What units the review rate of completion should be displayed in.',
-                        content: {
-                            rph: 'reviews/hr',
-                            rpm: 'reviews/min',
-                            mp100r: 'min/100 reviews',
-                        }
-                    },
-                }
-            },
             reviewPage: {
                 type: 'page',
                 label: 'Review Page',
                 content: {
-                    location: {
-                        type: 'dropdown',
-                        label: 'Display Location',
-                        default: defaultSettings.location,
-                        hover_tip: 'Where to show the below items (if checked) during reviews.',
-                        content: {
-                            toprightright: 'top right (right of other stats)',
-                            toprightleft: 'top right (left of other stats)',
-                            bottom: 'bottom in gray font',
-                        }
-                    },
                     showTimer: {
                         type: 'checkbox',
                         label: 'Show elapsed time',
                         default: defaultSettings.showTimer,
@@ -452,14 +384,8 @@
         console.warn('WaniKani Review Clock: Wanikani Open FrameWork required for adjusting settings. '
             + 'Installation instructions can be found here: https://community.wanikani.com/t/installing-wanikani-open-framework/28549');
     }
 
-    const style = document.createElement('style');
-    style.textContent = '.wkrc_bottom i { margin-right: 0.5em; margin-left: 0.8em; }' +
-        '.wkrc_bottom span { margin-right: 0.5em; }' +
-        '.wkrc_bottom { color:#BBB; letter-spacing: initial; display: block; text-align: center; }';
-    document.head.append(style);
-
     if(/session$/.exec(window.location.href)) { // review page
         await generateStatHtmlElems();
         startReviewTimer();
     } else { // review summary page
1 Like

It would also be nice if it updated the review numbers right when you finish the reviews in addition to the scheduled updates so that the statistics it computes include all your reviews instead of just the ones done prior to the final scheduled update.

Edit: Another thing is that refreshing the page resets the the timer / count. It would be nice if the behavior were changed so that on page load, it it checks to see if the timer is already running and resumes the timer in that case, rather than starting it over.

Wow! I am flattered. Could you post the whole script without the diff formatting? I can easily see the diff in vscode. This way I won’t have to do awkward copy-pasting and erasing diff formatting. I will take a closer look a bit later when I have time.

You could also fork the repository on GitHub and make a pull request (if you are familiar with working with git and github). This way you would be listed as a contributor on the repository. If you make a PR, allow edits from maintainers, so that I can edit it before merging if necessary.

Not sure I understand what you mean by this. What are you referring to when you say “the review numbers”?

This might be problematic to implement since it resets the completed items counter on refresh, which is used in conjecture with the timer to calculate the rate.

1 Like

You don’t have to do copy-pasting. Just use patch.

1 Like

Didn’t know about this tool, thanks!

2 Likes

Updated the script with your contributions, thank you very much!

I only had to make a small fix to make sure that it worked without WKOF, but otherwise didn’t really have to do anything! You can check out the changes I made on the PR: V1.1 by Markus98 · Pull Request #1 · Markus98/wk-review-clock · GitHub.

I also removed the max limit on the update interval.

2 Likes

Sorry, I mean that if in settings, you set it to update the statistics only once every (say) 5 minutes and do a review session that is 9 minutes long, the statistics that are displayed at the end and used to update your cumulative statistics are the statistics from 5 minutes in to the review session, not the full statistics from the entire review session. I would imagine that in most cases, the user would hope for the full information to be included.

I think the following way of implementing the above two changes might not require too many changes to the existing code:

  1. Use document.addEventListener('visibilitychange', callback) to get notified right before the user leaves the review page. This works for all reasons the browser leaves the page including navigating away, refreshes, etc. The only thing it doesn’t handle is if the browser crashes, but I don’t think it’s necessary to worry about something unlikely like that.
  2. In the callback above, call setStatsAndUpdateTime() to update the stored statistics. I believe this would immediately fix the first point mentioned earlier. And it will also be useful in the fix for the second point as well. (Also, you may need to check the url so it only runs on the .../review/session page.)
  3. Add a localStorage variable that stores whether the timer should be reset. Set this variable to true whenever the review summary page is visited.
  4. Modify the code in main() that runs on the .../review/session page to check the value of the localStorage variable that stores whether the timer should be reset. If it is true, reset the timer. If it is false, resume the timer. You could resume the timer by subtracting the stored elapsed time from startTime. That way, it won’t include time you were away from the page.
1 Like