Wanikani: Progress Percentages

Calculates the percentage of known kanji for each JLPT level, Joyo grade, Frequency bracket, and various other sources.

// ==UserScript==
// @name         Wanikani: Progress Percentages
// @namespace    https://greasyfork.org/en/users/1364747
// @version      1.2.9
// @description  Calculates the percentage of known kanji for each JLPT level, Joyo grade, Frequency bracket, and various other sources.
// @author       sarmientoF
// @include      /^https://(www|preview).wanikani.com/(dashboard)?$/
// @require      https://greasyfork.org/scripts/377613-wanikani-open-framework-jlpt-joyo-and-frequency-filters/code/Wanikani%20Open%20Framework%20JLPT,%20Joyo,%20and%20Frequency%20filters.user.js
// @license      MIT; http://opensource.org/licenses/MIT
// @grant        none
// ==/UserScript==

(function () {
    // Make sure WKOF is installed
    var wkof = window.wkof;
    if (!wkof) {
        var response = confirm(
            'Wanikani: JLPT Progress requires WaniKani Open Framework.\n Click "OK" to be forwarded to installation instructions.'
        );
        if (response)
            window.location.href =
                "https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549";
        return;
    } else {
        // Install menu
        wkof.include("Menu,Settings");
        wkof.ready("Menu,Settings").then(load_settings).then(install_menu);

        // Initiate progress variable
        var progress = {
            jlpt: {
                1: { learned: 0, total: 1232 },
                2: { learned: 0, total: 367 },
                3: { learned: 0, total: 367 },
                4: { learned: 0, total: 166 },
                5: { learned: 0, total: 79 },
            },
            joyo: {
                1: { learned: 0, total: 80 },
                2: { learned: 0, total: 160 },
                3: { learned: 0, total: 200 },
                4: { learned: 0, total: 200 },
                5: { learned: 0, total: 185 },
                6: { learned: 0, total: 181 },
                9: { learned: 0, total: 1130 },
            },
            freq: {
                500: { learned: 0, total: 500 },
                1000: { learned: 0, total: 500 },
                1500: { learned: 0, total: 500 },
                2000: { learned: 0, total: 500 },
                2500: { learned: 0, total: 500 },
            },
            other: {
                nhk: { learned: 0, total: 0 },
                news: { learned: 0, total: 0 },
                aozora: { learned: 0, total: 0 },
                twitter: { learned: 0, total: 0 },
                wikipedia: { learned: 0, total: 0 },
            },
        };

        // Fetch lesson info then process it
        wkof.include("ItemData");
        wkof
            .ready("ItemData")
            .then(update_progress)
            .then(calculate_percentages)
            .then(display_data);
    }

    // Loads settings
    function load_settings() {
        var defaults = { cumulative: false, threshold: 1, position: "top" };
        wkof.Settings.load("progress_percentages", defaults);
    }

    // Installs the options button in the menu
    function install_menu() {
        var config = {
            name: "progress_percentages_settings",
            submenu: "Settings",
            title: "Progress Percentages",
            on_click: open_settings,
        };
        wkof.Menu.insert_script_link(config);
    }

    // Creates the options
    function open_settings(items) {
        var config = {
            script_id: "progress_percentages",
            title: "Progress Percentages",
            content: {
                cumulative: {
                    type: "checkbox",
                    label: "Cumulative percentages",
                    hover_tip: "Eg. N3 = N3 + N4 + N4",
                    default: false,
                },
                threshold: {
                    type: "list",
                    label: "Learned threshold",
                    hover_tip:
                        "Items at or above this SRS level will be counted as learned",
                    multi: false,
                    size: 9,
                    default: "1",
                    content: {
                        1: "Apprentice 1",
                        2: "Apprentice 2",
                        3: "Apprentice 3",
                        4: "Apprentice 4",
                        5: "Guru 1",
                        6: "Guru 2",
                        7: "Master",
                        8: "Enlightened",
                        9: "Burned",
                    },
                },
                position: {
                    type: "dropdown",
                    label: "Position",
                    hover_tip:
                        "Position of the Progress Percentages element on the dashboard",
                    default: "search",
                    content: {
                        top: "Top of page",
                        below_srs: "Below SRS boxes",
                    },
                    on_change: (setting, value) => {
                        let elem = $(".progress_percentages");
                        elem.toggleClass("span12", value == "top");
                        if (value == "top") $("#search-form").before(elem);
                        if (value == "below_srs") $(".srs-progress").append(elem);
                    },
                },
            },
            on_save: () => {
                progress = {
                    jlpt: {
                        1: { learned: 0, total: 1232 },
                        2: { learned: 0, total: 367 },
                        3: { learned: 0, total: 367 },
                        4: { learned: 0, total: 166 },
                        5: { learned: 0, total: 79 },
                    },
                    joyo: {
                        1: { learned: 0, total: 80 },
                        2: { learned: 0, total: 160 },
                        3: { learned: 0, total: 200 },
                        4: { learned: 0, total: 200 },
                        5: { learned: 0, total: 185 },
                        6: { learned: 0, total: 181 },
                        9: { learned: 0, total: 1130 },
                    },
                    freq: {
                        500: { learned: 0, total: 500 },
                        1000: { learned: 0, total: 500 },
                        1500: { learned: 0, total: 500 },
                        2000: { learned: 0, total: 500 },
                        2500: { learned: 0, total: 500 },
                    },
                    other: {
                        nhk: { learned: 0, total: 0 },
                        news: { learned: 0, total: 0 },
                        aozora: { learned: 0, total: 0 },
                        twitter: { learned: 0, total: 0 },
                        wikipedia: { learned: 0, total: 0 },
                    },
                };
                update_progress().then(calculate_percentages).then(update_element);
            },
        };
        var dialog = new wkof.Settings(config);
        dialog.open();
    }

    // Updates element
    function update_element(percentages) {
        console.log("percentages", percentages);
        for (var key in percentages) {
            console.log("key", key);
            for (var level in percentages[key]) {
                console.log("level", level);
                var stage = key == "jlpt" ? 6 - level : level;
                var elem = $("#" + key + "_" + stage)[0];
                console.log("elem", elem);
                elem.title =
                    percentages[key][stage].learned +
                    (key != "other" ? " of " + percentages[key][stage].total : "") +
                    " learned";
                elem.children[1].innerText = percentages[key][stage].percent + "%";
            }
        }
    }

    // Retreives lesson data
    function update_progress() {
        var resolve,
            promise = new Promise((res, rej) => {
                resolve = res;
            });
        var config = {
            wk_items: {
                options: { assignments: true },
                filters: {
                    item_type: "kan",
                    include_jlpt_data: true,
                    include_joyo_data: true,
                    include_frequency_data: true,
                },
            },
        };
        wkof.ItemData.get_items(config).then((data) => {
            for (var key in data) {
                if (data[key].assignments && data[key].assignments.started_at != null) {
                    var keys = [
                        ["jlpt_level", "jlpt"],
                        ["joyo_grade", "joyo"],
                        ["frequency", "freq"],
                        ["nhk_frequency", "nhk"],
                        ["news_frequency", "news"],
                        ["aozora_frequency", "aozora"],
                        ["twitter_frequency", "twitter"],
                        ["wikipedia_frequency", "wikipedia"],
                    ];
                    keys.forEach((val, i) => {
                        var level = data[key][val[0]];
                        if (
                            level &&
                            data[key].assignments.srs_stage >=
                            wkof.settings.progress_percentages.threshold
                        ) {
                            if (level < 1) {
                                progress.other[val[1]].learned++;
                                progress.other[val[1]].total += level;
                            } else progress[val[1]][level].learned++;
                        }
                    });
                }
            }
            resolve();
        });
        return promise;
    }

    function calculate_percentages() {
        var show_cum = wkof.settings.progress_percentages.cumulative;
        var percentages = {
            jlpt: { 1: {}, 2: {}, 3: {}, 4: {}, 5: {} },
            joyo: { 1: {}, 2: {}, 3: {}, 4: {}, 5: {}, 6: {}, 9: {} },
            freq: { 500: {}, 1000: {}, 1500: {}, 2000: {}, 2500: {} },
            other: { nhk: {}, news: {}, aozora: {}, twitter: {}, wikipedia: {} },
        };
        for (var key in percentages) {
            var cumulative = [0, 0];
            for (var level in percentages[key]) {
                var stage = key == "jlpt" ? 6 - level : level;
                var learned = progress[key][stage].learned;
                var total = progress[key][stage].total;
                cumulative[0] += learned;
                cumulative[1] += total;
                let percent;
                if (key != "other")
                    percent = show_cum ? cumulative[0] / cumulative[1] : learned / total;
                else percent = total;
                percent =
                    percent < 0.1
                        ? Math.floor(percent * 1000) / 10
                        : Math.floor(percent * 100);
                percentages[key][stage].percent = percent;
                percentages[key][stage].learned = show_cum ? cumulative[0] : learned;
                percentages[key][stage].total = show_cum ? cumulative[1] : total;
            }
        }
        return percentages;
    }

    function display_data(percentages) {
        // Add css
        $("head").append(`<style id="progress_percentages">
    .progress_percentages {
        display: flex;
        height: 28px;
        background: #434343;
        color: rgb(240, 240, 240);
        line-height: 28px;
        //margin-bottom: 0;
        border-radius: 5px;
        text-align: center;
        grid-row: 1;
        grid-column: 1 / span 2;
        //margin-top: 15px;
    }
    .srs-progress .progress_percentages {
        margin-top: 5px;
    }
    #search-form {
        grid-row: 1;
    }
    .progress_percentages .PPprogress {
        display: flex;
        width: 100%;
        justify-content: space-around;
    }
    .progress_percentages .PPbtn {
        width: 20px;
        height: auto;
        color: rgb(240,240,240);
        padding: 0 5px;
        cursor: pointer;
        font-size: 16px;
        z-index: 10;
    }
    .progress_percentages .level {
        font-weight: bold;
    }
    .progress_percentages .percent {
        font-weight: normal !important;
    }
    .progress_percentages span {
        font-size: 16px !important;
        display: inline !important;
    }
    </style>`);
        if (is_dark_theme()) {
            $("head").append(`<style id="progress_percentages_dark">
    .progress_percentages {
        box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7), 2px 2px 2px rgba(0, 0, 0, 0.7);
    }
    .progress_percentages > div {
        background: #232629 !important;
    }
    </style>`);
        }
        // Add elements
        console.log("percentages", percentages);
        var section = document.createElement("section");
        section.className = "progress_percentages";

        var active_set =
            localStorage.getItem("WKProgressPercentagesActiveSet") || "jlpt";
        var [next, prev] = get_new_sets(active_set);

        var next_button = document.createElement("div");
        next_button.className = "next PPbtn";
        next_button.innerHTML = '<i class="link icon-chevron-right">></i>';
        next_button.onclick = toggle_percentages;
        next_button.current = active_set;
        next_button.next = next;

        var prev_button = document.createElement("div");
        prev_button.className = "prev PPbtn";
        prev_button.innerHTML = '<i class="link icon-chevron-left"><</i>';
        prev_button.onclick = toggle_percentages;
        prev_button.current = active_set;
        prev_button.next = prev;

        var list = document.createElement("div");
        list.className = "PPprogress";
        for (var key in percentages) {
            for (var level in percentages[key]) {
                var stage = key == "jlpt" ? 6 - level : level;
                var prefix =
                    key == "jlpt" ? "N" : key == "joyo" ? "G" : key == "freq" ? "F" : "";
                var label =
                    key == "other"
                        ? stage == "nhk"
                            ? "NHK"
                            : stage.charAt(0).toUpperCase() + stage.slice(1)
                        : key == "freq"
                            ? stage / 500
                            : stage;
                $(list).append(
                    '<div class="' +
                    key +
                    "_percentages stage " +
                    (key == active_set ? "" : "hidden") +
                    '" id="' +
                    key +
                    "_" +
                    stage +
                    '" title="' +
                    percentages[key][stage].learned +
                    (key != "other" ? " of " + percentages[key][stage].total : "") +
                    ' learned"><span class="level">' +
                    prefix +
                    label +
                    ' </span><span class="percent">' +
                    percentages[key][stage].percent +
                    "%</span></div>"
                );
            }
        }
        section.appendChild(prev_button);
        section.appendChild(list);
        section.appendChild(next_button);
        if (wkof.settings.progress_percentages.position == "top") {
            //section.className += ' span12'
            $(".dashboard").before(section);
        } else if (wkof.settings.progress_percentages.position == "below_srs")
            $(".srs-progress").append(section);
        else $(".srs-progress__stages").before(section);
    }

    // Switches which percentages are showing
    function toggle_percentages(event) {
        var button = event.target;
        if (button.nodeName == "I") button = button.parentElement;
        var current_set = button.current;
        var next_set = button.next;
        $("." + current_set + "_percentages").toggleClass("hidden");
        $("." + next_set + "_percentages").toggleClass("hidden");
        var next_button = $(".progress_percentages .next")[0];
        var prev_button = $(".progress_percentages .prev")[0];
        var [next, prev] = get_new_sets(next_set);
        next_button.next = next;
        next_button.current = next_set;
        prev_button.next = prev;
        prev_button.current = next_set;
        localStorage.setItem("WKProgressPercentagesActiveSet", next_set);
    }

    // Returns the next and previous sets
    function get_new_sets(current_set) {
        var sets = ["jlpt", "joyo", "freq", "other"];
        var current_index = sets.indexOf(current_set);
        return [sets[(current_index + 1) % 4], sets[(current_index + 3) % 4]];
    }

    // Handy little function that rfindley wrote. Checks whether the theme is dark.
    function is_dark_theme() {
        // Grab the <html> background color, average the RGB.  If less than 50% bright, it's dark theme.
        return (
            $("body")
                .css("background-color")
                .match(/\((.*)\)/)[1]
                .split(",")
                .slice(0, 3)
                .map((str) => Number(str))
                .reduce((a, i) => a + i) /
            (255 * 3) <
            0.5
        );
    }
})();