Gaxx / FunTrivia Stats

// ==UserScript==
// @name         FunTrivia Stats
// @namespace    http://www.funtrivia.com/stats
// @version      1.15
// @description  Grab some stats for FunTrivia.com private games
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery-dateFormat/1.0/jquery.dateFormat.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/chartist/0.11.0/chartist.min.js
// @require      https://raw.githubusercontent.com/padolsey-archive/jquery.fn/master/sortElements/jquery.sortElements.js
// @author       Ssieth
// @match        http://www.funtrivia.com/private/main.cfm*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';
    console.log("== FunTrivia Answer Stats v" + GM_info.script.version + " ==");

    var show = {};

    // -----------------------------------------------------------------------------
    // Show options - these can be tweaked to vary what is shown by the stats script
    // -----------------------------------------------------------------------------
    show.dayTotals = false;
    show.dayAverages = true;
    show.statsTable = false;
    show.rankScores = {};
    show.rankScores.days = 7;
    show.rankScores.show = true;
    show.graph = {};
    show.graph.draw = true;
    show.graph.type = 'adjusted';
    show.graph.fill = true;
    show.graph.entries = 7;
    show.hide = {};
    // -----------------------------------------------------------------------------

    var thisUser = "-not-logged-in-";
    var chartdata = {};
    var chartopt = {};

    // Set up chat colours
    var chartCols = ["black", "green", "red", "pink", "purple", "crimson", "gold", "gray", "blue", "maroon", "orange", "yellow", "darkslategray", "greenyellow", "chocolate", "darkseagreen",
                     "darkolivegreen", "darkorange", "darkorchid", "deeppink", "deepskyblue", "cyan", "navy", "lightblue", "saddlebrown", "mediumspringgreen", "mediumvioletred",
                     "burlywood", "cornflowerblue", "darkgoldenrod", "teal", "darkkhaki", "darkgreen", "darkred"
                    ];
    var alphabet = 'abcdefghijklmnopqrstuvwxyz'.split('');
    var aryCSS = [];
    for (var iCol = 0; iCol < chartCols.length; iCol++) {
        aryCSS.push(".ct-series-" + iCol + " .ct-line, .ct-series-" + iCol + " .ct-point { stroke: " + chartCols[iCol % chartCols.length] + " }");
        aryCSS.push(".ct-legend-" + iCol + " {color: " + chartCols[iCol % chartCols.length] + "; }");
    }
    //console.log(aryCSS);
    aryCSS.push("div#chart-legend { text-align: center; font-weight: bold; width: 15em; border: 2px solid black; padding: 5px; position: relative; }");
    aryCSS.push(".ct-legend { cursor: pointer; margin-top: 10px; }");
    aryCSS.push(".ct-controls { position: absolute; bottom: 0px; text-align: center; width: 100%; }");
    aryCSS.push(".ct-control { cursor: pointer; margin-bottom: 5px; text-align: center; width: 100%; color: blue; text-decoration: underline; }");
    aryCSS.push(".ct-control:hover { text-decoration: none; }");
    aryCSS.push(".grid-container { display: grid; grid-column-gap: 5px;  grid-template-columns: auto 16em;}");

    var strCSS = aryCSS.join('\n') + '\n';

    var today = $.format.date(new Date(), "dd MMM yyyy");
    var userStats = {};
    var strUserStats = "";
    var statsByUser = {};
    var lstUsers = [];

    // -----------------------------------------------------------------------------
    // Actual grunt-work starts here
    // -----------------------------------------------------------------------------
    $(document).ready(function () {
        thisUser = $("small:eq(0) b").text();
        // console.log(thisUser);
        // Set up CSS stuff
        $('head').append('<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/chartist/0.11.0/chartist.min.css" type="text/css" />');
        GM_addStyle(strCSS);
        strUserStats = GM_getValue("userStats", "");
        if (strUserStats !== "") {
            userStats = JSON.parse(strUserStats);
        }
        var $t = $("body>table.lighttable table.darktable");
        var fullData = processRows($t);
        var data = fullData.data;
        var dataTot = fullData.totals;

        // Expand the main table
        insertBlankRow($t);
        showAggregates($t,dataTot);
        showUserHistory($t,userStats);

        // Get all user data
        populateAllUsers();

        // Show handicaps
        if (show.rankScores.show) {
            addHandicaps($t);
        }
        sortTable($t);
        addAvgLine($t,dataTot);

        // Graphs are fun
        $("body>table.lighttable").after('<table class="lighttable" style="-moz-border-radius: 12px; border : 2px solid #0066CC;margin-top:3px;" width="95%" align="center"><tbody><tr><td id="chartccontainer" style="background-color: white;">' +
                                         '<div class="grid-container"><div class="grid-item" style="grid-row: 1; grid-column: 1 / 2; text-align: center; font-size: 250%;" id="graph-title">Deviation from Daily Average</div><div class="grid-item" style="grid-row: 2; grid-column: 1; height: 90em;" id="chart"></div><div class="grid-item"  style="grid-row: 2; grid-column: 2; position: relative;" id="chart-legend"></div></div></td><tr></tbody></table>');

        if ($("#chart").length > 0) {
            drawChart();
        }

        GM_setValue("userStats", JSON.stringify(userStats));

        // Tidy stuff up in the UI
        tidy();

        console.log("/== FunTrivia Answer Stats v" + GM_info.script.version + " ==");
    });

    function sortTable($table) {
        $table.find('tr.menubar td')
            .wrapInner('<span title="sort this column"/>')
            .each(function(){

            var th = $(this),
                thIndex = th.index(),
                inverse = false;

            th.click(function(){

                $table.find('tr:not(:first):not(.no-sort) td').filter(function(){
                    return $(this).index() === thIndex;

                }).sortElements(function(a, b){
                    if ($.isNumeric($.text([a])) && $.isNumeric($.text([b]))) {
                        if( parseFloat($.text([a])) == parseFloat($.text([b])) )
                            return 0;

                        return parseFloat($.text([a])) > parseFloat($.text([b])) ?
                            inverse ? -1 : 1
                        : inverse ? 1 : -1;
                    } else {
                        if( $.text([a]) == $.text([b]) )
                            return 0;

                        return $.text([a]) > $.text([b]) ?
                            inverse ? -1 : 1
                        : inverse ? 1 : -1;
                    }

                }, function(){

                    // parentNode is the element we want to move
                    return this.parentNode;

                });

                inverse = !inverse;

            });

        });
    }

    function populateAllUsers() {
        statsByUser = {};
        lstUsers = [];
        for (var key in userStats) {
            var dayd = userStats[key];
            for (var dkey in dayd.data) {
                var row = dayd.data[dkey];
                if (!statsByUser[row.name]) {
                    statsByUser[row.name] = {};
                    lstUsers.push(row.name);
                }
                statsByUser[row.name][key] = {};
                $.extend(statsByUser[row.name][key], row);
            }
        }
        lstUsers.sort(function (a, b) {
            return a.toLowerCase().localeCompare(b.toLowerCase());
        });
        for (var userkey in statsByUser) {
            var user = statsByUser[userkey];
            var scoresCount = 0;
            var dayCount = 0;
            user.dailyScores = [];
            user.dailyAdjusted = [];
            user.handicap = 0;
            user.lastScore = 0;
            for (var daykey in userStats) {
                dayCount++;
                if (user[daykey]) {
                    user.lastScore = dayCount;
                    scoresCount++;
                    var adj = (user[daykey].score - (userStats[daykey].totals.score / userStats[daykey].totals.count));
                    user.handicap += adj;
                    user.dailyScores.push({
                        meta: userkey,
                        value: user[daykey].score
                    });
                    user.dailyAdjusted.push({
                        meta: userkey,
                        value: adj
                    });
                }
                else {
                    user.dailyScores.push({
                        meta: userkey,
                        value: null
                    });
                    user.dailyAdjusted.push({
                        meta: userkey,
                        value: null
                    });
                }
            }
            var handicaps = user.dailyAdjusted.slice(Math.max(user.dailyAdjusted.length - show.rankScores.days, 1));
            user.handicap = avgVals(handicaps);
        }
        //console.log(statsByUser);
    }

    function avgVals(arr) {
        var tot = 0;
        var cnt = 0;
        for (var i=0;i<arr.length;i++) {
            if (arr[i].value != null) {
                cnt++;
                tot+=arr[i].value;
            }
        }
        if (cnt==0) {
            return 0;
        } else {
            return tot/cnt;
        }
    }

    function showUserHistory($table,userStats) {
        if (show.statsTable) {
            // Insert user history to table
            insertBlankRow($table);
            for (var key in userStats) {
                var row = userStats[key];
                $table.find("tbody").append("<tr class='lighttable'><td><b>" + row.name + "</b> on " + row.date + ":<br/>Along with " + (row.totals.count - 1) + " other players.</td><td>" +
                                            row.pts + "<br/><br/>" + (row.totals.pts / row.totals.count).toFixed(2) + " avg</td><td>" +
                                            row.answers + "<br/><br/>" + (row.totals.answers / row.totals.count).toFixed(2) + " avg</td><td>" +
                                            row.secs + "<br/><br/>" + (row.totals.secs / row.totals.count).toFixed(2) + " avg</td><td>" +
                                            row.score + "<br/><br/>" + (row.totals.score / row.totals.count).toFixed(2) + " avg</td></tr>");
            }
        }
    }

    function showAggregates($table,dataTot) {
        if (show.dayTotals) {
            // Insert totals
            $table.find("tbody").append("<tr class='darktable total-row no-sort'><td><b>Total:</b></td><td>" + dataTot.pts + "</td><td>" + dataTot.answers + "</td><td>" + dataTot.secs + "</td><td>" + dataTot.score + "</td></tr>");
        }

        if (show.dayAverages) {
            // Inssert averages
            $table.find("tbody").append("<tr class='darktable avg-row no-sort'><td><b>Average:</b></td><td>" + (dataTot.pts / dataTot.count).toFixed(2) + "</td><td>" + (dataTot.answers / dataTot.count).toFixed(2) + "</td><td>" +
                                        (dataTot.secs / dataTot.count).toFixed(2) + "</td><td>" + (dataTot.score / dataTot.count).toFixed(2) + "</td></tr>");
        }


    }

    function setChartTitle() {
        var title = "";
        if (show.graph.type == "adjusted") {
            title = "Deviation from Daily Average";
        } else {
            title = "Daily Score";
        }
        if (show.graph.entries == 0) {
            title += " (All)";
        } else {
            title += " (" + show.graph.entries + " entries)";
        }
        $("#graph-title").html(title);
    }

    // Function to prep chart data
    function drawChart() {
        setChartTitle();
        var chartType = show.graph.type;
        var fill = show.graph.fill;
        var aryUsers = [];
        // Clear down data
        chartdata = {
            labels: [],
            series: []
        };
        var chartopt = { fullWidth: true };
        if (fill) {
            chartopt.lineSmooth = Chartist.Interpolation.monotoneCubic({ fillHoles: true });
        }
        // Clear chart dom
        $("#chart").html("");
        $("#chart-legend").html("");
        var entKeys = Object.keys(userStats);
        if (show.graph.entries > 0 && show.graph.entries < entKeys.length) {
            for (var iEnt = 0; iEnt < show.graph.entries; iEnt++) {
                chartdata.labels.push(entKeys[entKeys.length - show.graph.entries + iEnt]);
            }
        } else {
            for (var k1 in userStats) {
                chartdata.labels.push(k1);
            }
        }
        var ucount = -1;
        if (show.graph.entries === 0) {
            aryUsers = lstUsers;
        } else {
            aryUsers = lstUsers.filter(function(u) {
                var usr = statsByUser[u];
                return (usr.lastScore >= (entKeys.length - show.graph.entries));
            });
        }
        for (var i2 = 0; i2 < aryUsers.length; i2++) {
            var k2 = aryUsers[i2];
            var user = statsByUser[k2];
            ucount++;
            var $leg = $("<div class='ct-legend ct-legend-" + ucount + "' id='ct-legend-" + ucount+ "'>" + k2 + "</div>");
            $leg.hover(function () {
                //console.log("hover in " + $(this).attr("id"));
                var $ser = $("." + $(this).attr("id").replace("legend", "series"));
                $ser.find(".ct-point").css("stroke-width", "20px").css("z-index", "20");
                $ser.find(".ct-line").css("stroke-width", "6px").css("z-index", "20");
            }, function () {
                //console.log("hover out " + $(this).attr("id"));
                var $ser = $("." + $(this).attr("id").replace("legend", "series"));
                $ser.find(".ct-point").css("stroke-width", "10px").css("z-index", "0");
                $ser.find(".ct-line").css("stroke-width", "4px").css("z-index", "0");
            });
            $leg.click(function () {
                //console.log("clicky " + $(this).attr("id"));
                showHideChartLine($(this));
            });
            $("#chart-legend").append($leg);

            if (k2 == thisUser) {
                GM_addStyle("ct-line, .ct-series-" + ucount + " { stroke-dasharray: 4 }");
            }
            var objSer = {
                "data": [],
                "className": 'ct-series-' + ucount
            };
            if (chartType == "adjusted") {
                objSer.data = user.dailyAdjusted;
            } else {
                objSer.data = user.dailyScores;
            }
            if (show.graph.entries > 0 && show.graph.entries < objSer.data.length) {
                objSer.data = objSer.data.slice(Math.max(objSer.data.length - show.graph.entries, 1))
            }
            chartdata.series.push(objSer);
        }
        var $ctc = $("<div class='ct-controls' id='cd-controls'></div>");
        var $switch = $("<div class='ct-control' id='ct-switch'>Switch Chart Type</div>");
        var $high = $("<div class='ct-control' id='ct-high'>High Half</div>");
        var $low = $("<div class='ct-control' id='ct-high'>Low Half</div>");
        var $hide = $("<div class='ct-control' id='ct-hide'>Hide All</div>");
        var $show = $("<div class='ct-control' id='ct-show'>Show All</div>");
        var $fill = $("<div class='ct-fill' id='ct-fill'><label for='ct-fill-cbx'>Fill: </label><input type='checkbox' id='ct-fill-cbx' /></div>");
        var $entries = $("<div class='ct-entries' id='ct-entries'><label for='ct-entries-sel'>Entries: </label><select id='ct-entries-sel'></select></div>");
        var $entSel = $entries.find("#ct-entries-sel");
        $entSel.append("<option value='3'>3</option>");
        $entSel.append("<option value='5'>5</option>");
        $entSel.append("<option value='7'>7</option>");
        $entSel.append("<option value='10'>10</option>");
        $entSel.append("<option value='0'>All</option>");
        $entSel.find("option").each(function() {
            if ($(this).prop("value") == show.graph.entries) {
                $(this).prop("selected","selected");
            }
        });

        if (fill) {
            $fill.find("#ct-fill-cbx").prop("checked",true);
        }
        $fill.find("#ct-fill-cbx").click(function() {
            show.graph.fill = $fill.find("#ct-fill-cbx").prop("checked");
            drawChart();
        });
        $entries.find("#ct-entries-sel").change(function() {
            show.graph.entries = parseInt($(this).val());
            drawChart();
        });

        $switch.click(function () {
            if (show.graph.type == "adjusted") {
                //$("#graph-title").html("Daily Score");
                show.graph.type = "scores";
            } else {
                //$("#graph-title").html("Deviation from Daily Average");
                show.graph.type = "adjusted";
            }
            drawChart();
        });
        $hide.click(function () {
            $(".ct-legend").each(function () {
                showHideChartLine($(this), 'h');
            });
        });
        $show.click(function () {
            $(".ct-legend").each(function () {
                showHideChartLine($(this), 's');
            });
        });
        $high.click(function() {
            for (var pplHigh in statsByUser) {
                var $high = $('div#chart-legend div.ct-legend').filter(function(){ return $(this).text() === pplHigh;})
                if ($high.length > 0) {
                    if (statsByUser[pplHigh].handicap > 0) {
                        showHideChartLine($high,'show');
                    } else {
                        showHideChartLine($high,'hide');
                    }
                }
            }
        });
        $low.click(function() {
            for (var pplLow in statsByUser) {
                var $low = $('div#chart-legend div.ct-legend').filter(function(){ return $(this).text() === pplLow;})
                if ($low.length > 0) {
                    if (statsByUser[pplLow].handicap < 0) {
                        showHideChartLine($low,'show');
                    } else {
                        showHideChartLine($low,'hide');
                    }
                }
            }
        });
        $ctc.append($fill);
        $ctc.append($entries);
        $ctc.append($high);
        $ctc.append($low);
        $ctc.append($show);
        $ctc.append($hide);
        $ctc.append($switch);
        $("#chart-legend").append($ctc);
        var chart = new Chartist.Line('#chart', chartdata, chartopt );
        chart.on('draw', function(data) {
            // Hide deselected people
            setTimeout(function() {
                for (var ppl in show.hide) {
                    var $toHide = $('div#chart-legend div.ct-legend').filter(function(){ return $(this).text() === ppl;})
                    //console.log("Hiding: " + ppl);
                    showHideChartLine($toHide,'hide');
                }
            },100);
        });

    }

    function insertBlankRow($table) {
        $table.find("tbody").append("<tr class='lighttable blank-row no-sort'><td colspan='5'>&nbsp;</td></tr>");
    }

    function showHideChartLine($legend, $forceTo) {
        var $ser = $("." + $legend.attr("id").replace("legend", "series"));
        //console.log($ser);
        //console.log($legend.attr("id").replace("legend", "series"))
        var op = '';
        if ($forceTo) {
            switch ($forceTo.trim().toLowerCase()) {
                case 'h':
                case 'hide':
                    op = 'h';
                    break;
                default:
                    op = 's';
            }
        }
        else {
            if ($ser.css("display") == "inline") {
                op = 'h';
            }
            else {
                op = 's';
            }
        }

        if (op === "h") {
            $legend.css("color", "rgba(0,0,0,0.2)");
            $ser.css("display", "none");
            show.hide[$legend.text()] = true;
        }
        else {
            $legend.css("color", "");
            $ser.css("display", "inline");
            delete show.hide[$legend.text()];
        }
        //console.log(show.hide);
    }

    function processRows($table) {
        var $trows = $table.find("tr").not(".menubar");
        var data = [];
        var fullData = {};
        var dataTot = {
            "count": 0,
            "pts": 0,
            "answers": 0,
            "secs": 0,
            "score": 0
        };
        $trows.each(function () {
            var $row = $(this);
            var row = {};

            // Build row object
            row.name = $row.find("td:eq(0) a").text();
            row.pts = parseInt($row.find("td:eq(1)").text().replace('+', '').replace('pts', '').trim());
            row.answers = parseInt($row.find("td:eq(2)").text().replace('!', ''));
            row.secs = parseInt($row.find("td:eq(3)").text());
            row.score = parseInt($row.find("td:eq(4)").text());
            row.date = today;
            row.isMe = false;

            // Set historic user stats
            if ($row.attr('class') != "lighttable") {
                row.isMe = true;
                userStats[today] = row;
            }

            // Update totals
            dataTot.count++;
            dataTot.pts += row.pts;
            dataTot.answers += row.answers;
            dataTot.secs += row.secs;
            dataTot.score += row.score;

            // Save row to the data collection
            data.push(row);
        });

        if (userStats[today]) {
            // Add totals in to userStats
            userStats[today].totals = dataTot;
            // Add in full row data in case we decide we want it later.
            var dataCopy = {};
            $.extend(true, dataCopy, data);
            userStats[today].data = dataCopy;
        }
        fullData.data = data;
        fullData.totals = dataTot;
        return fullData;
    }

    function addAvgLine($table,dataTot) {
        var $trows = $table.find("tr").not(".menubar");
        // Mark line of average
        var mkAvg = false;
        $trows.each(function () {
           if (mkAvg) return;
           var $row = $(this);
           if (parseInt($row.find("td:eq(4)").text()) < dataTot.score/dataTot.count) {
               //console.log($row);
               $row.find("td").css("border-top","2px solid black");
               mkAvg = true;
           }
        });
    }

    function addHandicaps($table) {
        var $trows = $table.find("tr").not(".menubar");
        var $trmenu = $table.find("tr.menubar");
        $trmenu.append("<td>RS (" + show.rankScores.days + " days)</td>");
        $trows.each(function () {
            var $row = $(this);
            var name = $row.find("td:eq(0) a").text();
            if (name !== "") {
                $row.append("<td>" + Math.round(statsByUser[name].handicap) + "</td>");
            } else {
                $row.append("<td>&nbsp;</td>");
            }
        });
    }

    function tidy() {
        removeFooter();
        tidyHeader();
    }

    function tidyHeader() {
        var $header = $("body>table.darktable:eq(0)");

        // Reemove quizz info waffle
        $header.find("table.lighttable:eq(0)").remove();
        $header.find("table:eq(0) table:eq(1)").remove();

        // Add play link
        $header.find("table:eq(0) table:eq(0) td:eq(1)").append(" <a href='/private/play.cfm?tid=86650' style='font-size: 180%; float: right; color: red;'>Play</a>"); //css("border","thick solid pink");
        // Move invite form
        $header.find("table:eq(0) table:eq(0) td:eq(1)").append("&nbsp;&nbsp;&nbsp; ").append($header.find("table:eq(2)>tbody>tr>td:eq(0) form"));

        // Remove shoutbox etc
        $header.find("table:eq(2)").remove();

        // Finaly tidy
        $("body>table.darktable:eq(0)>tbody>tr>td>p").remove();

        // Remove old play button etc
        $header.find("table:eq(2)>tbody>tr>td:eq(0) form").css("border","thick solid pink");

        // Move table containing play link and invite form
        var $tabMove = $header.find("table:eq(0)").css({ "width": "80%", "margin-top": "auto", "margin-bottom": "auto"});
        $tabMove.find("tbody:eq(0)>tr>td:eq(1)").remove();
        var $newCell = $("<td></td>").append($tabMove);
        $("body>table:eq(0) td:eq(0)").after($newCell);

        // Remove header title
        $("body>table:eq(2)").remove();
        $("body>table:eq(3)").remove();

        // Tidy up maintitle
        var $main = $("body>table.maintitle");
        $main.find("td:eq(4)").css("width","auto");
        $main.find("td:eq(6)").remove();
    }

    function removeFooter() {
        $("body>center").remove();
    }
})();