[Userscript] Overall Progress Bars

I’ll look into it when I get the time

1 Like

Thanks for the userscript :slight_smile:
Fast question: for a breeze dark theme, is it possible to set the white background to a dark gray one instead? (The same shade that’s used for srs boxes)

UPD. Successfully found the attribute, no need for help anymore!

1 Like

Coul you share details? I would also like to use this progress bar with the dark breeze theme

1 Like

In the edit mode for the script, you’ll nee to find this setion:

image

And change background-color attribute value to #232629 (or any other different color of your liking). After that save the changes and refresh the page.

1 Like

Pretty sure it used to have a dark background. I wonder what changed. Eventually I’ll get to fixing it for everyone

Looks like it worked with the original Breeze Dark, but not Breeze Dark 2.
It should work again in version 1.4.1

1 Like

Version 1.4.2 should work better on small screens. Let me know if you still have any issues after updating

1 Like

Looks perfect! You are a-ma-zing! :slight_smile:

I tried with 1.4.2 but on my machine with breeze dark 2 (v0.8.1) I get:

@Kumirei to have easy support both with and without BD2 I’d recommend using the WK CSS variables as much as possible - i.e. in this case setting the background color to var(--color-wk-panel-background) instead of hardcoding it. That way it works without any custom styles applied (and will match the other Dashboard panels if WK were to slightly change the colour or something) and also works with BD2 without any issues, as for that as much as possible I just change the built-in WK CSS variables, to make it easier to support!
And if you want to be super safe around WK potentially making changes and breaking your styling, you could for example keep the hardcoded values as backup like this? var(--color-wk-panel-background, #f4f4f4)

EDIT: Oh, I hadn’t actually realised that there’s a setting in the script to change color scheme. I’d probably still recommend though switching to use the built-in CSS variables as much as possible - there’s ones for almost everything now. That way you have to change less when the use changes colour scheme, and you could probably even remove the setting completely and just let the colours auto-change based on the CSS variables. All just my opinion of course, you might have other reasons not to do that!

1 Like

Ah with the theme selection it works. Thanks @Hubbit200 I wasn’t aware of this!

Since I already have the theme chooser in the settings it’s not a high priority to get it switched over to CSS variables. I agree that it would be for the best, but I have little time to work on scripts as it is

1 Like

It’s broken since de update. :confused:

1 Like

Fix - you need to add one line at the top and adjust 2 CSS parameters:

Top part - add line to header

// @require https://code.jquery.com/jquery-3.6.0.min.js

Adjust CSS for the .srs-level-graph item, I changed only padding and grid-auto-rows

        const css = `.srs-level-graph {
    justify-content: space-evenly;
    gap: 0.2em;
    padding: 12px 7px;
    background-color: ${settings.theme === 'breeze' ? '#232629' : '#f4f4f4'};
    border-radius: 5px;
    margin: 0 0 5px 0 !important;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(16px, 1fr));
    grid-auto-rows: 44px;
}
5 Likes

Thank you for sharing this tip!

1 Like

Doesn’t seem to work for me after adding the line to the header and adjusting the .srs-level-graph section. :thinking:

Edit: Actually, it works, but only when set to display above SRS Counts in position in the settings. Any other position, it just does not appear.

1 Like

I looked into the positioning as well. I have a solution, there are many changes so I’ll post the whole script.

// ==UserScript==
// @name         Wanikani: Overall Progress Bars
// @namespace    http://tampermonkey.net/
// @version      1.4.2
// @description  Creates a progress bar on the dashboard for every level
// @author       Kumirei
// @include      /^https://(www|preview).wanikani.com/(dashboard)?$/
// @grant        none
// @license      MIT
// @downloadURL https://update.greasyfork.org/scripts/441185/Wanikani%3A%20Overall%20Progress%20Bars.user.js
// @updateURL https://update.greasyfork.org/scripts/441185/Wanikani%3A%20Overall%20Progress%20Bars.meta.js
// @require https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==

;(async () => {
    const { wkof, $ } = window

    // Script info
    const script_name = 'Overall Progress Bars'
    const script_id = 'overall_progress_bar'

    // Constants
    const srs_names = {
        '-1': 'Locked',
        0: 'In lessons',
        1: 'Apprentice 1',
        2: 'Apprentice 2',
        3: 'Apprentice 3',
        4: 'Apprentice 4',
        5: 'Guru 1',
        6: 'Guru 2',
        7: 'Master',
        8: 'Enlightened',
        9: 'Burned',
    }

    // Init
    confirm_wkof()
    wkof.include('Menu,Settings,ItemData')
    await wkof.ready('Menu,Settings,ItemData').then(load_settings).then(install_menu)

    const settings = wkof.settings[script_id]
    let color

    // Get items by level
    const items = await wkof.ItemData.get_items('assignments')
    const items_by_level = wkof.ItemData.get_index(items, 'level')

    // Get counts per SRS level per level
    const counts_by_level_and_srs = {}
    for (let [level, items] of Object.entries(items_by_level)) {
        counts_by_level_and_srs[level] = Object.fromEntries(
            Object.entries(wkof.ItemData.get_index(items, 'srs_stage')).map(([srs, items]) => [srs, items.length]),
        )
    }
    display()

    function display() {
        set_color_theme()
        injectCss()
        $('.srs-level-graph').remove()
        $('.dashboard__content').prepend(
            `<section class="srs-level-graph position-${settings.position}">${Object.entries(counts_by_level_and_srs)
                .filter(([level]) => {
                    if (level > settings.max_level) return false
                    if (settings.hide_locked && level > wkof.user.level) return false
                    return true
                })
                .map(get_level)
                .join('')}</section`,
        )
    }

    function set_color_theme() {
        color = {
            '-1': settings.theme === 'breeze' ? '#31363B' : '#aaaaaa',
            0: settings.theme === 'breeze' ? '#31363B' : '#aaaaaa',
            1: settings.theme === 'breeze' ? '#3fbbf3' : '#ff00bb',
            2: settings.theme === 'breeze' ? '#2eaaf4' : '#ee00aa',
            3: settings.theme === 'breeze' ? '#1d99f3' : '#dd0099',
            4: settings.theme === 'breeze' ? '#0c88e2' : '#cc0088',
            5: settings.theme === 'breeze' ? '#1cdc9a' : '#9339aa',
            6: settings.theme === 'breeze' ? '#1cdc9a' : '#882d9e',
            7: settings.theme === 'breeze' ? '#c9ce3b' : '#294ddb',
            8: settings.theme === 'breeze' ? '#f67400' : '#0093dd',
            9: settings.theme === 'breeze' ? '#da4453' : '#FAAF0E',
        }
    }

    function get_level([level, counts_by_srs]) {
        const total = Object.values(counts_by_srs).reduce((sum, val) => sum + val, 0)
        const bars = Object.entries(counts_by_srs)
            .map((item) => get_bar(...item, total))
            .join('')
        return `<div class="level"><div class="bars" style="background: ${get_color(
            counts_by_srs,
        )};">${bars}</div><div class="lbl" title="Level ${level}">${level}</div></div>`
    }

    function get_bar(srs_level, count, total) {
        const percent = Math.round((count / total) * 100)
        return `<div class="srs" data-srs="${srs_level}" title="${srs_names[srs_level]}: ${count} / ${total} items (${percent}%)" style="flex-grow: ${count}"></div>`
    }

    function get_color(counts_by_srs) {
        if (!['blend', 'avg_srs'].includes(settings.display)) return ''
        const srs_levels = Object.entries(counts_by_srs).reduce(
            (srs_items, [srs_level, count]) => srs_items.concat(new Array(count).fill(srs_level < 0 ? 0 : srs_level)),
            [],
        )
        const avg_srs = srs_levels.reduce((sum, val) => sum + Number(val), 0) / srs_levels.length
        if (settings.display === 'blend') {
            return averageColors(srs_levels)
        } else if (settings.display === 'avg_srs') {
            return interpolate_color(color[Math.floor(avg_srs)], color[Math.ceil(avg_srs)], avg_srs % 1)
        }
        return ''
    }

    function averageColors(levelItems) {
        let itemValues = [0, 0, 0]
        let averageColor = '#'

        // For each level, a mean average is taken of each item's color
        for (let i = 0; i < levelItems.length; i++) {
            for (let j = 0; j < 3; j++) {
                itemValues[j] += parseInt('0x0' + color[levelItems[i]].slice(2 * j + 1, 2 * j + 3), 16)
            }
        }

        // Divide by the total to get the means for RGB
        for (let k = 0; k < itemValues.length; k++) {
            itemValues[k] = itemValues[k] / levelItems.length
            itemValues[k] = Math.round(itemValues[k])
        }

        // Convert back into hex
        averageColor = '#' + itemValues.map((a) => `00${parseInt(a, 10).toString(16)}`.slice(-2)).join('')

        return averageColor
    }

    function interpolate_color(a, b, amount) {
        var ah = parseInt(a.replace(/#/g, ''), 16),
            ar = ah >> 16,
            ag = (ah >> 8) & 0xff,
            ab = ah & 0xff,
            bh = parseInt(b.replace(/#/g, ''), 16),
            br = bh >> 16,
            bg = (bh >> 8) & 0xff,
            bb = bh & 0xff,
            rr = ar + amount * (br - ar),
            rg = ag + amount * (bg - ag),
            rb = ab + amount * (bb - ab)

        return '#' + (((1 << 24) + (rr << 16) + (rg << 8) + rb) | 0).toString(16).slice(1)
    }

    function injectCss() {
        let srs_css = ''
        for (let i = -1; i < 10; i++) {
            srs_css += `
.srs-level-graph .srs[data-srs="${i}"] {
    background-color: ${color[i]};
    ${settings.display !== 'bars' ? 'width: 100' : 'height: ' + (i + 1) * 10}%;
}`
        }

        const css = `.srs-level-graph {
    justify-content: space-evenly;
    gap: 0.2em;
    padding: 12px 7px;
    background-color: ${settings.theme === 'breeze' ? '#232629' : '#f4f4f4'};
    border-radius: 5px;
    margin: 0 0 5px 0 !important;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(16px, 1fr));
    grid-auto-rows: 44px;
}

.srs-level-graph .level {
    width: 100%;
    display: flex;
    flex-direction: column;
}

.srs-level-graph .bars {
    display: flex;
    align-items: flex-end;
    flex-grow: 1;
    flex-direction: ${settings.display !== 'bars' ? 'column' : 'row-reverse'};
}

.srs-level-graph .lbl {
    font-size: 0.75em;
    line-height: 1.5em;
    text-align: center;
    vertical-align: bottom;
}

.srs-level-graph .srs {
    ${['blend', 'avg_srs'].includes(settings.display) ? 'display:none;' : ''}
    ${settings.display !== 'bars' ? '' : 'border-radius: 0.1em 0.1em 0 0;'}
}

.srs-level-graph .srs[data-srs="-1"] {
    order: -1;
}

/* Above Lessons (wide) */
.position-0 {grid-column: 1/span 6; grid-row: 1/2;}
.position-0 ~ .dashboard__lessons-and-reviews {grid-row: 2/3;}
.position-0 ~ .dashboard__recent-mistakes {grid-row: 3/4;}
.position-0 ~ .dashboard__extra-study {grid-row: 4/5;}
.position-0 ~ .dashboard__level-progress {grid-row: 5/6;}
.position-0 ~ .dashboard__review-forecast {grid-row: 2/6;}
.position-0 ~ .dashboard__srs-progress {grid-row: 6/7;}
.position-0 ~ .dashboard__item-lists {grid-row: 7/8;}
.position-0 ~ .dashboard__community-banner {grid-row: 8/9;}

/* Above Lessons (narrow) */
.position-1 {grid-column: 1/span 4; grid-row: 1/2;}
.position-1 ~ .dashboard__lessons-and-reviews {grid-row: 2/3;}
.position-1 ~ .dashboard__recent-mistakes {grid-row: 3/4;}
.position-1 ~ .dashboard__extra-study {grid-row: 4/5;}
.position-1 ~ .dashboard__level-progress {grid-row: 5/6;}
.position-1 ~ .dashboard__review-forecast {grid-row: 1/6;}
.position-1 ~ .dashboard__srs-progress {grid-row: 6/7;}
.position-1 ~ .dashboard__item-lists {grid-row: 7/8;}
.position-1 ~ .dashboard__community-banner {grid-row: 8/9;}

/* Above Recent Mistakes */
.position-2 ~ .dashboard__lessons-and-reviews {grid-row: 1/2;}
.position-2 {grid-column: 1/span 4; grid-row: 2/3;}
.position-2 ~ .dashboard__recent-mistakes {grid-row: 3/4;}
.position-2 ~ .dashboard__extra-study {grid-row: 4/5;}
.position-2 ~ .dashboard__level-progress {grid-row: 5/6;}
.position-2 ~ .dashboard__review-forecast {grid-row: 1/6;}
.position-2 ~ .dashboard__srs-progress {grid-row: 6/7;}
.position-2 ~ .dashboard__item-lists {grid-row: 7/8;}
.position-2 ~ .dashboard__community-banner {grid-row: 8/9;}

/* Above Extra study */
.position-3 ~ .dashboard__lessons-and-reviews {grid-row: 1/2;}
.position-3 ~ .dashboard__recent-mistakes {grid-row: 2/3;}
.position-3 {grid-column: 1/span 4; grid-row: 3/4;}
.position-3 ~ .dashboard__extra-study {grid-row: 4/5;}
.position-3 ~ .dashboard__level-progress {grid-row: 5/6;}
.position-3 ~ .dashboard__review-forecast {grid-row: 1/6;}
.position-3 ~ .dashboard__srs-progress {grid-row: 6/7;}
.position-3 ~ .dashboard__item-lists {grid-row: 7/8;}
.position-3 ~ .dashboard__community-banner {grid-row: 8/9;}

/* Above Level Progress */
.position-4 ~ .dashboard__lessons-and-reviews {grid-row: 1/2;}
.position-4 ~ .dashboard__recent-mistakes {grid-row: 2/3;}
.position-4 ~ .dashboard__extra-study {grid-row: 3/4;}
.position-4 {grid-column: 1/span 4; grid-row: 4/5;}
.position-4 ~ .dashboard__level-progress {grid-row: 5/6;}
.position-4 ~ .dashboard__review-forecast {grid-row: 1/6;}
.position-4 ~ .dashboard__srs-progress {grid-row: 6/7;}
.position-4 ~ .dashboard__item-lists {grid-row: 7/8;}
.position-4 ~ .dashboard__community-banner {grid-row: 8/9;}

/* Above SRS Progress (narrow) */
.position-5 ~ .dashboard__lessons-and-reviews {grid-row: 1/2;}
.position-5 ~ .dashboard__recent-mistakes {grid-row: 2/3;}
.position-5 ~ .dashboard__extra-study {grid-row: 3/4;}
.position-5 ~ .dashboard__level-progress {grid-row: 4/5;}
.position-5 {grid-column: 1/span 4; grid-row: 5/6;}
.position-5 ~ .dashboard__review-forecast {grid-row: 1/6;}
.position-5 ~ .dashboard__srs-progress {grid-row: 6/7;}
.position-5 ~ .dashboard__item-lists {grid-row: 7/8;}
.position-5 ~ .dashboard__community-banner {grid-row: 8/9;}

/* Above SRS Progress (wide) */
.position-6 ~ .dashboard__lessons-and-reviews {grid-row: 1/2;}
.position-6 ~ .dashboard__recent-mistakes {grid-row: 2/3;}
.position-6 ~ .dashboard__extra-study {grid-row: 3/4;}
.position-6 ~ .dashboard__level-progress {grid-row: 4/5;}
.position-6 ~ .dashboard__review-forecast {grid-row: 1/5;}
.position-6 {grid-column: 1/span 6; grid-row: 5/6;}
.position-6 ~ .dashboard__srs-progress {grid-row: 6/7;}
.position-6 ~ .dashboard__item-lists {grid-row: 7/8;}
.position-6 ~ .dashboard__community-banner {grid-row: 8/9;}

/* Above Item Lists */
.position-7 ~ .dashboard__lessons-and-reviews {grid-row: 1/2;}
.position-7 ~ .dashboard__recent-mistakes {grid-row: 2/3;}
.position-7 ~ .dashboard__extra-study {grid-row: 3/4;}
.position-7 ~ .dashboard__level-progress {grid-row: 4/5;}
.position-7 ~ .dashboard__review-forecast {grid-row: 1/5;}
.position-7 ~ .dashboard__srs-progress {grid-row: 5/6;}
.position-7 {grid-column: 1/span 6; grid-row: 6/7;}
.position-7 ~ .dashboard__item-lists {grid-row: 7/8;}
.position-7 ~ .dashboard__community-banner {grid-row: 8/9;}

/* Above Community Banner */
.position-8 ~ .dashboard__lessons-and-reviews {grid-row: 1/2;}
.position-8 ~ .dashboard__recent-mistakes {grid-row: 2/3;}
.position-8 ~ .dashboard__extra-study {grid-row: 3/4;}
.position-8 ~ .dashboard__level-progress {grid-row: 4/5;}
.position-8 ~ .dashboard__review-forecast {grid-row: 1/5;}
.position-8 ~ .dashboard__srs-progress {grid-row: 5/6;}
.position-8 ~ .dashboard__item-lists {grid-row: 6/7;}
.position-8 {grid-column: 1/span 6; grid-row: 7/8;}
.position-8 ~ .dashboard__community-banner {grid-row: 8/9;}

/* Bottom */
.position-9 ~ .dashboard__lessons-and-reviews {grid-row: 1/2;}
.position-9 ~ .dashboard__recent-mistakes {grid-row: 2/3;}
.position-9 ~ .dashboard__extra-study {grid-row: 3/4;}
.position-9 ~ .dashboard__level-progress {grid-row: 4/5;}
.position-9 ~ .dashboard__review-forecast {grid-row: 1/5;}
.position-9 ~ .dashboard__srs-progress {grid-row: 5/6;}
.position-9 ~ .dashboard__item-lists {grid-row: 6/7;}
.position-9 ~ .dashboard__community-banner {grid-row: 7/8;}
.position-9 {grid-column: 1/span 6; grid-row: 8/9;}

${srs_css}`

        $(`#overall-progress-bars-css`).remove()
        $('head').append(`<style id="overall-progress-bars-css">${css}</style>`)
    }

    /* ----------------------------------------------------------*/
    // WKOF setup
    /* ----------------------------------------------------------*/

    // Makes sure that WKOF is installed
    async function confirm_wkof() {
        if (!wkof) {
            let response = confirm(
                `${script_name} requires WaniKani Open Framework.\nClick "OK" to be forwarded to installation instructions.`,
            )
            if (response) {
                window.location.href =
                    'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549'
            }
            return
        }
    }

    // Load WKOF settings
    function load_settings() {
        const defaults = {
            display: 'stack',
            max_level: 60,
            hide_locked: false,
            theme: 'default',
            position: 0,
        }
        return wkof.Settings.load(script_id, defaults)
    }

    // Installs the options button in the menu
    function install_menu() {
        const config = {
            name: script_id,
            submenu: 'Settings',
            title: script_name,
            on_click: open_settings,
        }
        wkof.Menu.insert_script_link(config)
    }

    // Opens settings dialogue when button is pressed
    function open_settings() {
        let config = {
            script_id: script_id,
            title: script_name,
            on_save: display,
            content: {
                display: {
                    type: 'dropdown',
                    label: 'Display as',
                    default: 'stack',
                    hover_tip: 'Changes how the bars look',
                    content: {
                        stack: 'Stack',
                        bars: 'Bars',
                        avg_srs: 'Single Color (average SRS)',
                        blend: 'Single Color (blend)',
                    },
                },
                max_level: {
                    type: 'number',
                    label: 'Max Level',
                    hover_tip: 'The highest level to display',
                    max: 60,
                    min: 1,
                },
                hide_locked: {
                    type: 'checkbox',
                    default: false,
                    label: 'Hide Locked Levels',
                    hover_tip: 'Do not display bars above your current level',
                },
                theme: {
                    type: 'dropdown',
                    label: 'Theme',
                    default: 0,
                    hover_tip: 'Changes the colors of the bars',
                    content: { default: 'Default', breeze: 'Breeze Dark' },
                },
                position: {
                    type: 'dropdown',
                    label: 'Position',
                    default: 0,
                    hover_tip: 'Changes the colors of the bars',
                    content: ['Above Lessons (wide)', 'Above Lessons (narrow)', 'Above Recent Mistakes', 'Above Extra study', 'Above Level Progress', 'Above SRS Progress (narrow)', 'Above SRS Progress (wide)', 'Above Item Lists', 'Above Community Banner', 'Bottom'],
                },
            },
        }
        let dialog = new wkof.Settings(config)
        dialog.open()
    }
})()

I am not sure how this interacts with other scripts, but if you need to change it, here is what I did:

  • removed positions and instead the section added by script has new class position-N where N is position number
  • when adding element to page, it is appended to top of dashboard above all other sections so it can affect their positions
  • the CSS lists new grid positions for each position, and for all page sections to fit added section
6 Likes

This worked, many thanks!

thanks buddy, worked for me as well

I guess there’s been a recent WK update as it doesn’t fit on one line anymore: