Multi rating graph for Codeforces by im0qianqian (http & https)

Enable to show rating history graph with other accounts on profile pages at Codeforces

// ==UserScript==
// @name           Multi rating graph for Codeforces by im0qianqian (http & https)
// @namespace      https://www.dreamwings.cn/
// @description    Enable to show rating history graph with other accounts on profile pages at Codeforces
// @license        http://creativecommons.org/publicdomain/zero/1.0/
// @copyright      im0qianqian
// @version        1.5
// @match        *://www.codeforces.com/profile/*
// @match        *://codeforces.com/profile/*
// @match        *://www.codeforces.ru/profile/*
// @match        *://codeforces.ru/profile/*
// ==/UserScript==

// v1.5  2021/08/06 (by HamletPetrosyan, yak_ex)Fix failures related to codeforces rating graph update
//                  Add setting variables
// v1.4  2019/01/03 (by im0qianqian) Add support for https.
// v1.3  2015/05/05 Sync with site changes.
// v1.2  2014/06/05 Autocomplete account names
//                  Keep the color of the highest dots
// v1.1  2013/03/15 Fix failure to get log-in account
//                  Version jump because Chrome recognizes 0.0x as 1.0
// v0.03 2011/04/17 Show log-in account always
// v0.02 2011/04/16 Adjust yaxis scale
//                  Warn if data can't be obtained
// v0.01 2011/04/16 Initial version

///////////////////////////////////////////////////////////////////////
//
// The following part is executed in content page scope
//

// ------------ Setting variables ------------

var highest_rating = false;         // always shows the highest user ratings 
var my_plot_by_default = false;     // always shows your rating graph in other people's profile pages

//

function extract_data(cont) {
    var re1 = new RegExp('data\\.push\\(([\\S\\s]*?)\\);\\s*data\\.push\\(([\\S\\s]*?)\\);', 'm');
    return re1.test(cont) ? [RegExp.$1, RegExp.$2] : undefined;
}

function extract_scale(cont) {
    var re2 = new RegExp('yaxis: \\{(\r\n|\r|\n)            min: (\\d+),(\r\n|\r|\n)            max: (\\d+),');
    return re2.test(cont) ? [RegExp.$2, RegExp.$4] : undefined;
}

function extract_scaleX(cont) {
    var re3 = new RegExp('zoomRange: \\[172800000, null\\],(\r\n|\r|\n)            panRange: \\[(\\d+), (\\d+)\\]');
    return re3.test(cont) ? [RegExp.$2, RegExp.$3] : undefined;
}

function get_account_data(id) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://' + window.location.host + '/profile/' + id, false);
    xhr.send(null);
    if (xhr.status == 200) {
        return [extract_data(xhr.responseText), extract_scale(xhr.responseText), extract_scaleX(xhr.responseText)];
    }
    return undefined;
}

function update_graph(input) {
    if (input == null) return;
    var handle = window.location.href.match(/[^/]*$/);
    input = handle + ' ' + input;
    accounts = input.trim().split(' ');
    var check = {};
    data = new Array();
    datas = [];
    var mymin = 10000, mymax = -1, myXmin = 10000000000000, myXmax = -1;
    var idx = 0;
    for (var i = 0; i < accounts.length; ++i) {
        if (accounts[i] != '' && check[accounts[i]] == undefined) {
            check[accounts[i]] = 1;
            var d = get_account_data(accounts[i]);
            if (d != undefined && d[0] != undefined) {
                data.push(eval(d[0][0]));

                if (highest_rating || accounts.length == 1) {
                    data.push(eval(d[0][1]));
                    datas[2 * idx] = { label: accounts[i], data: data[2 * idx] };
                    datas[2 * idx + 1] = { clickable: false, hoverable: false, color: "red", data: data[2 * idx + 1] };
                }
                else {
                    datas[idx] = { label: accounts[i], data: data[idx] };
                }

                ++idx;
                if (d[1] != undefined) {
                    if (d[1][0] < mymin) mymin = d[1][0];
                    if (d[1][1] > mymax) mymax = d[1][1];
                }
                if (d[2] != undefined) {
                    if (d[2][0] < myXmin) myXmin = d[2][0];
                    if (d[2][1] > myXmax) myXmax = d[2][1];
                }
            } else {
                alert("Can't get information for account: " + accounts[i] + ".");
            }
        }
    }
    if (idx == 1) {
        options.legend.position = "ne";
    } else {
        options.legend.position = "se";
    }
    if (myXmin != 10000000000000 && myXmax != -1) {
        options.xaxis.panRange = [myXmin, myXmax];
    }
    if (mymin != 10000 && mymax != -1) {
        options.yaxis.min = mymin;
        options.yaxis.max = mymax;
        options.yaxis.panRange = [mymin, mymax];
    }
    plot = $.plot($("#usersRatingGraphPlaceholder"), datas, options);
    $("#usersRatingGraphPlaceholder .legend").unbind("click");
    $("#usersRatingGraphPlaceholder .legend").bind("click", account_manage);
}

function account_manage() {
    var handle = window.location.href.match(/[^/]*$/);
    var dialog = $('<div id="account-dialog"/>').css({
        position: 'fixed', padding: '5px', width: '30em', zIndex: 2000, left: '50%', top: '50%', marginTop: '-3.5em', marginLeft: '-15em',
        border: '1px solid', borderRadius: '5px', background: 'rgb(255,255,255)', boxShadow: 'rgb(64,64,64) 5px 5px 5px'
    }).html(
        '<p>Input space-separated accounts without this account.</p>' +
        '<form id="account-form"><p><input type="text" id="accounts" size="40" value="' + ((handle != login_account && my_plot_by_default == true) ? login_account : '') + '"></p>' +
        '<p><input type="submit" id="ok" value="OK"> <input type="button" id="cancel" value="cancel"></p></form>'
    );
    $('p', dialog).css({ margin: '1em' });
    $('#cancel', dialog).click(function () {
        $('#account-dialog').remove();
        $('#account-dialog-blocker').remove();
    });
    $('#account-form', dialog).submit(function () {
        var input = $('#accounts').val();
        $('#account-dialog').remove();
        $('#account-dialog-blocker').remove();
        update_graph(input);
        return false;
    }).keydown(function (e) {
        if (e.keyCode == 27) {
            $('#cancel').click();
        }
    });
    var blocker = $('<div id="account-dialog-blocker"/>').css({
        position: 'fixed', top: 0, left: 0, bottom: 0, right: 0, width: '100%', height: '100%', zIndex: 15,
        background: 'rgb(64,64,64)', opacity: 0.75
    });
    $('body').append(blocker);
    $('body').append(dialog);
    $('#accounts').autocomplete("/data/handles", {
        delay: 200,
        width: 200,
        selectFirst: false,
        matchContains: true,
        multiple: true,
        multipleSeparator: ' ',
        minChars: 3,
        scroll: true,
    });
    $('#accounts').focus();
}

///////////////////////////////////////////////////////////////////////
//
// The following part is executed in userjs scope.
//

function add_unbind(cont) {
    var marker = '$("#usersRatingGraphPlaceholder").bind("plothover"';
    return cont.replace(marker, '$("#usersRatingGraphPlaceholder").unbind("plothover");\n' + marker);
}

function get_login_account() {
    var e = document.getElementById('header');
    var re3 = new RegExp('<a href="/profile/([^"]*)">[^<]*</a>[^<]*<a href="[^"]*/logout">');
    return re3.test(e.innerHTML) ? RegExp.$1 : undefined;
}

function disable_default_plot(cont) {
    return cont.replace('var plot = $.plot($("#usersRatingGraphPlaceholder"), datas, options);', '').replace('var ctx = plot.getCanvas().getContext("2d");', '');
}

function add_account_manage(cont) {
    var marker = 'var prev = -1;';
    var target = '';
    target += 'var highest_rating = ' + highest_rating + ';\n';
    target += 'var my_plot_by_default = ' + my_plot_by_default + ';\n';
    target += 'var extract_data = ' + extract_data + ';\n';
    target += 'var extract_scale = ' + extract_scale + ';\n';
    target += 'var extract_scaleX = ' + extract_scaleX + ';\n';
    target += 'var get_account_data = ' + get_account_data + ';\n';
    var login_account = get_login_account();
    if (login_account != undefined) {
        target += 'var login_account = "' + get_login_account() + '";\n';
    } else {
        target += 'var login_account = "";\n';
    }
    target += 'options.legend = {};\n';
    target += 'var account_manage;\n';
    target += 'var update_graph = ' + update_graph + ';\n';
    target += 'account_manage = ' + account_manage + ';\n';
    target += 'if(my_plot_by_default) update_graph(login_account);\n';
    target += '$("#usersRatingGraphPlaceholder .legend").unbind("click");\n';
    target += '$("#usersRatingGraphPlaceholder .legend").bind("click", account_manage);\n';
    // CAUTION FRAGILE: monkey patch for Autocompleter to handle multiple words correctly
    target += '$(function() {\n';
    target += 'var old = $.Autocompleter;\n';
    target += 'eval("$.Autocompleter = " + (""+$.Autocompleter).replace("currentValue == q", "lastWord(currentValue) == q"));\n';
    target += '$.Autocompleter.defaults = old.defaults;$.Autocompleter.Cache = old.Cache;$.Autocompleter.Select = old.Select;\n';
    target += '});\n';

    return cont.replace(marker, target + marker);
}

function get_target_script() {
    var e = document.getElementById('pageContent').getElementsByTagName('script');
    for (var i = 0; i < e.length; ++i) {
        if (e[i].textContent.match(/data\.push/) != null) {
            return e[i];
        }
    }
}

script = document.createElement('script');
script.setAttribute("type", "application/javascript");
script.textContent = disable_default_plot(add_account_manage(add_unbind(get_target_script().textContent)));

document.body.appendChild(script);
document.body.removeChild(script);