[Userscript] Stroke Order Diagram

ah, that did it! thanks for the help!!


Okay, bit of a script noob here. I’ve downloaded and it works perfectly in kanji previews, but doesn’t appear at all in lessons. Anyone know what’s going on?

1 Like

I’ll look into it when I get some kanji lessons on my test account

@Wanimekani remindme 115m “Review on test account”


Sorry to be a pain, I’ve only just realised it’s for kanji only! I think I was looking at vocabulary and getting confused, as it worked great in my kanji lessons today. Sorry for the confusion!


Thank you so much! I was wondering why the script was not working anymore.


The script was not loading today so I checked all the GM/FF compatibility options in Edit script > Externals … and now it works but I have no clue what these options do… Just posting this in case someone has a similar issue.


So, I have kind of a weird error.
I am using userscripts through violentmonkey on android kiwi browser and most userscripts work fine.

This one was working fine as well if used directly in the browser, but if I add Wanikani to my home screen as a shortcut and open it there, the diagrams don’t load, but no error message is thrown. It just stays white where the diagrams are supposed to be.

Anyone has an idea what could cause this?


Do you have access to a developer console to see if there are any errors?

1 Like

Thanks for maintaining this Kumirei. I only noticed today after getting 勇 radical in lessons and wanting to know the stroke order but the kanji page didn’t have it. Now it shows up.


If someone writes using brush pen (like me) or simply wants to see how kanji looks like in different fonts here is how I did it.
Maybe someone will find it useful.
And if there’s a problem with jisho.org you can temporarily add font with inline stroke numbers.

Add those lines to script (start from bottom so numbers don’t shift) and choose local fonts (previewFonts) you want to display.

// line 131
let previewFonts = ["EPSON 教科書体M", "nagayama_kai"];
let previewSize = "200px";
let kanjiCur = getKanji();
let previewItems = previewFonts.reduce(
  (acc, cur) =>
    acc +
    `<div style="
  display: inline-block;
  border:1px solid black;
let preview = `<div id="preview" style="margin-bottom:5px;">${previewItems}</div>`;
// line 132

// line 133 (substitute)
`<section><h2>Stroke Order</h2>${preview}<div style="width:100%;overflow-x: auto; overflow-y: hidden"><svg id="stroke_order"></svg></div></section>`;
// line 133

// line 156
let previewNode = $("#preview")[0];
if (previewNode)
  previewNode.childNodes.forEach((e) => {
    e.textContent = getKanji();
// line 157

Just wanted to say thank you for this script! I don’t have to go back and forth between sources now. :smile:


I noticed today that I’m having an issue where the stroke order diagram isn’t showing up on kanji lessons at all. Everything seems normal on my end though, so I thought I should bring it up here.

EDIT: Aaaaand now of course after making a post, it magically came back. :joy: Sometimes all you need to do is complain, it seems.

1 Like

@Kumirei This script seems to be affected by the change of lessons to React. As you have already seen (since you replied to my post last week), I have recently created a library script that simplifies injecting additional item info. Sadly, I didn’t have time yet to write a documentation for it. Should I try to modify this script to use WK Item Info Injector?

Ah, I haven’t had time to look into which scripts would be affected yet!

That would be great!

1 Like

I didn’t anticipate that someone wants to insert elements above all other sections when I designed WK Item Info Injector, so I had to add a small workaround for the review and lessonQuiz. :sweat_smile:

// ==UserScript==
// @name        WaniKani Stroke Order
// @namespace   japanese
// @version     1.1.8
// @description Shows a kanji's stroke order on its page and during lessons and reviews.
// @license     GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
// @include     http*://*wanikani.com/kanji/*
// @include     http*://*wanikani.com/level/*/kanji/*
// @include     http*://*wanikani.com/review/session
// @include     http*://*wanikani.com/lesson/session
// @author      Looki, maintained by Kumirei
// @grant       GM_xmlhttpRequest
// @connect     jisho.org
// @connect     cloudfront.net
// @require     https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.5.1/snap.svg-min.js
// @require      https://greasyfork.org/scripts/430565-wanikani-item-info-injector/code/WaniKani%20Item%20Info%20Injector.user.js?version=962341
// ==/UserScript==

 * Thanks a lot to ...
 * Wanikani Phonetic-Semantic Composition - Userscript
 * by ruipgpinheiro (LordGravewish)
 * ... for code showing me how to insert sections during kanji reviews.
 * The code heavily borrows from that script!
 * Also thanks to Halo for a loading bug fix!

;(function () {
    /* global Snap */

     * Helper Functions/Variables
    let wkItemInfo = unsafeWindow.wkItemInfo

     * Global Variables/Objects/Classes
    const JISHO = 'https://jisho.org'
    const strokeOrderCss =
        '.stroke_order_diagram--bounding_box {fill: none; stroke: #ddd; stroke-width: 2; stroke-linecap: square; stroke-linejoin: square;}' +
        '.stroke_order_diagram--bounding_box {fill: none; stroke: #ddd; stroke-width: 2; stroke-linecap: square; stroke-linejoin: square;}' +
        '.stroke_order_diagram--existing_path {fill: none; stroke: #aaa; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;}' +
        '.stroke_order_diagram--current_path {fill: none; stroke: #000; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;}' +
        '.stroke_order_diagram--path_start {fill: rgba(255,0,0,0.7); stroke: none;}' +
        '.stroke_order_diagram--guide_line {fill: none; stroke: #ddd; stroke-width: 2; stroke-linecap: square; stroke-linejoin: square; stroke-dasharray: 5, 5;}'


     * Main
    function init() {
        wkItemInfo.on('lesson').forType('kanji').under('composition').append('Stroke Order', loadDiagram)
        wkItemInfo.on('lessonQuiz, review,itemPage').forType('kanji').under('composition').appendAtTop('Stroke Order', loadDiagram)

        let style = document.createElement('style')
        style.textContent = strokeOrderCss

    function xmlHttpRequest(urlText) {
        return new Promise((resolve, reject) => GM_xmlhttpRequest({
            method: 'GET',
            url: new URL(urlText),
            onload : xhr => { xhr.status === 200 ? resolve(xhr) : reject(xhr.responseText) },
            onerror: xhr => { reject(xhr.responseText) }

     * Adds the diagram section element to the appropriate location
    async function loadDiagram(injectorState) {
        let xhr = await xmlHttpRequest(JISHO + '/search/' + injectorState.characters + '%20%23kanji')

        let strokeOrderSvg = xhr.responseText.match(/var url = '\/\/(.+)';/)
        if (!strokeOrderSvg) return null

        xhr = await xmlHttpRequest('https://' + strokeOrderSvg[1])

        let namespace = 'http://www.w3.org/2000/svg'
        let div = document.createElement('div')
        let svg = document.createElementNS(namespace, 'svg')
        svg.id = 'stroke_order'
        div.style = 'width: 100%; overflow: auto hidden;'
        new strokeOrderDiagram(svg, xhr.responseXML || new DOMParser().parseFromString(xhr.responseText, "application/xml"))
        return div

     * Lifted from jisho.org
    var strokeOrderDiagram = function (element, svgDocument) {
        var s = Snap(element)
        var diagramSize = 200
        var coordRe = '(?:\\d+(?:\\.\\d+)?)'
        var strokeRe = new RegExp('^[LMT]\\s*(' + coordRe + ')[,\\s](' + coordRe + ')', 'i')
        var f = Snap(svgDocument.getElementsByTagName('svg')[0])
        var allPaths = f.selectAll('path')
        var drawnPaths = []
        var canvasWidth = (allPaths.length * diagramSize) / 2
        var canvasHeight = diagramSize / 2
        var frameSize = diagramSize / 2
        var frameOffsetMatrix = new Snap.Matrix()
        frameOffsetMatrix.translate(-frameSize / 16 + 2, -frameSize / 16 + 2)

        // Set drawing area
        s.node.style.width = canvasWidth + 'px'
        s.node.style.height = canvasHeight + 'px'
        s.node.setAttribute('viewBox', '0 0 ' + canvasWidth + ' ' + canvasHeight)

        // Draw global guides
        var boundingBoxTop = s.line(1, 1, canvasWidth - 1, 1)
        var boundingBoxLeft = s.line(1, 1, 1, canvasHeight - 1)
        var boundingBoxBottom = s.line(1, canvasHeight - 1, canvasWidth - 1, canvasHeight - 1)
        var horizontalGuide = s.line(0, canvasHeight / 2, canvasWidth, canvasHeight / 2)
        boundingBoxTop.attr({ class: 'stroke_order_diagram--bounding_box' })
        boundingBoxLeft.attr({ class: 'stroke_order_diagram--bounding_box' })
        boundingBoxBottom.attr({ class: 'stroke_order_diagram--bounding_box' })
        horizontalGuide.attr({ class: 'stroke_order_diagram--guide_line' })

        // Draw strokes
        var pathNumber = 1
        allPaths.forEach(function (currentPath) {
            var moveFrameMatrix = new Snap.Matrix()
            moveFrameMatrix.translate(frameSize * (pathNumber - 1) - 4, -4)

            // Draw frame guides
            var verticalGuide = s.line(
                frameSize * pathNumber - frameSize / 2,
                frameSize * pathNumber - frameSize / 2,
                canvasHeight - 1,
            var frameBoxRight = s.line(frameSize * pathNumber - 1, 1, frameSize * pathNumber - 1, canvasHeight - 1)
            verticalGuide.attr({ class: 'stroke_order_diagram--guide_line' })
            frameBoxRight.attr({ class: 'stroke_order_diagram--bounding_box' })

            // Draw previous strokes
            drawnPaths.forEach(function (existingPath) {
                var localPath = existingPath.clone()
                localPath.attr({ class: 'stroke_order_diagram--existing_path' })

            // Draw current stroke
            currentPath.attr({ class: 'stroke_order_diagram--current_path' })

            // Draw stroke start point
            var match = strokeRe.exec(currentPath.node.getAttribute('d'))
            var pathStartX = match[1]
            var pathStartY = match[2]
            var strokeStart = s.circle(pathStartX, pathStartY, 4)
            strokeStart.attr({ class: 'stroke_order_diagram--path_start' })


(the version number is not bumped yet)

I have tested this version with Edge (Tampermonkey, Violentmonkey) and Firefox (Tampermonkey).

EDIT: Coincidentally, the newest version of WK Item Info Injector supports inserting elements at the top, so I updated the code to use this functionality.


I just noticed that you also match http*://*wanikani.com/level/*/kanji/* – I don’t remember encountering such an URL before, but it seems to still work: https://www.wanikani.com/level/1/kanji/工. Is it possible to reach such an URL through the current WK interface? Because WK Item Info Injector cannot handle it if the URL also contains /level/1 (it doesn’t do anything in that case).

Now that I’m thinking about it, WK Item Info Injector can be tricked into thinking it is on an item page by appending a / after radicals/kanji/vocabulary (https://www.wanikani.com/kanji/), causing it to throws errors. I don’t think this URL is reachable through the UI – that only leads to https://www.wanikani.com/kanji?difficulty=pleasant. But maybe I should address this issue anyway? However, it’s not even sufficient to check if there is something after the /, because https://www.wanikani.com/kanji/# or https://www.wanikani.com/kanji/? would still cause problems.

I feel like I opened Pandora’s box.

I am not able to find any way to get there, so it’s probably safe to remove

I don’t think it’s really that important to account for. While someone might delete the kanji from the URL in an attempt to get back to the kanji page, the probability of someone doing so is low enough that I think it would be more effort for you to cover that possibility than it would be for those people to deal with whatever it caused. Although I assume it would just throw an error and not be noticeable at all?

1 Like

Yes, the only effect is that it throws a bunch of errors in the console. I will probably put it really low on my to-do list.

1 Like

Hi there!

The stroke order isn’t showing up in kanji lessons like @EvilScheme mentioned. I can see you’re discussing it but I have no idea what your discussions mean, but looks like it’s not an easy fix?

Does it mean it won’t come back soon? :confused:

1 Like

Their issue resolved itself. Did yours?