NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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'> </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> </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(" ").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(); } })();