repmastered.app winrate sort

Improves matchup tables with sorting and grouping data


Install this script?
  1. // ==UserScript==// @name repmastered.app winrate sort// @description Improves matchup tables with sorting and grouping data// @namespace https://github.com/T1mL3arn// @version 1.0.3// @match https://repmastered.app/map/*// @grant none// @author T1mL3arn// @run-at document-end// @require https://code.jquery.com/jquery-3.5.1.min.js// @require https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/js/jquery.dataTables.min.js// @license WTFPL 2// @icon https://repmastered.app/static/img/favicon.ico// @homepageURL https://github.com/t1ml3arn-userscript-js/repmastered.app-winrate-sorting// @supportURL https://github.com/t1ml3arn-userscript-js/repmastered.app-winrate-sorting/issues// ==/UserScript==// add DataTables CSSconst link = document.createElement('link')link.rel = "stylesheet"link.type = "text/css"link.href = "https://cdn.datatables.net/v/dt/dt-1.10.21/datatables.min.css"link.onload = _ => run()document.head.appendChild(link)// ----------------------------class query {constructor() {this.r###lt = []}from(data) {this.r###lt = data.slice()return this}where(filter) {this.r###lt = this.r###lt.filter(filter)return this}/*** Group data (by only 1 column !!!)*/groupBy(func) {// get all unique groupsconst groups = new Set(this.r###lt.map(row => func(row)))// collect all values for all groupsthis.r###lt = [...groups].map(gr => [gr, this.r###lt.filter(i => func(i) == gr)])// the r###lt now looks like:/*[[group_1, [ row_1, row_2, ...]],[group_2, [ row_3, row_5, ...]],...]*/return this}aggregate(targetCol, func, colAlias = null) {this.r###lt.forEach(row => {const groupData = row[1]const dateToAggreagate = groupData.map(obj => obj[targetCol])const r###lt = func(dateToAggreagate)row.push({ alias: colAlias || targetCol, value: r###lt })})/* now r###lts look like this[[group_1, [ row_1, row_2, ...], { alias: alias_1, value: gr_1_aggr_r###lt }, { }, ... ],[group_2, [ row_3, row_5, ...], { alias: alias_1, value: gr_2_aggr_r###lt }, { }, ... ],...]*/return this}}/*** Add <thead> if a table misses it*/function addThead($table) {$table.not(":has(thead)") // get tables without <thead>.each((i, t) => {$(t).find("tr:first-child") // lets suppose the first <tr> is <thead>.wrap("<thead>") // wrap it with header.parent() // then get this header.remove() // remove the header.prependTo(t) // and add the header into beginning of original table})return $table}/** Creates textual tag for given elt.* The text then should be passed into jquery* to create DOM element. */function ce(elt) {return `<${elt}></${elt}>`}// ----------------------------// CSS fixes// NOTE: DataTables CSS interferes with repmastered CSS,// so it should be fixedconst STATS_TABLE_CLASS = 'stats-tbl'const CSS_FIX = `.${STATS_TABLE_CLASS} {border-collapse: collapse !important;}.${STATS_TABLE_CLASS} td, .${STATS_TABLE_CLASS} th {padding: 3px !important;}.${STATS_TABLE_CLASS} th {padding-right: 8px !important;position: unset !important;}.${STATS_TABLE_CLASS} td {text-align: center;}.${STATS_TABLE_CLASS} tr:first-child th[colspan='1'][rowspan='1'] {height: 2.25em;}.${STATS_TABLE_CLASS} thead {position: sticky;top: 0;/* this fixes repmastered.app arrows visibilityover a table header */z-index: 1;}/* row striping */.${STATS_TABLE_CLASS} tr:nth-child(even) { background-color: #fff !important }.${STATS_TABLE_CLASS} tr:nth-child(odd) { background-color: #fff3cf !important }.${STATS_TABLE_CLASS} tr:hover { background-color: #ddf !important }.dataTables_wrapper.hidden { display: none; }.text--hint { color: #777; font-style: italic; font-size: 0.9em; }.winrate-tbl-menu { margin-top: 1em; }.matchup-details {border-top: 1px solid #ccc;margin-top: 2em;margin-bottom: 1em;}td.no-after::after {content: '';}`$('<style></style>').attr('id', 'sort-stats-css-fix').text(CSS_FIX).appendTo('head')/*** Fixes css for initialized(!) DataTables.* @param {jQuery} $target jQeury object (list of tables)*/function fixCss($target, width = '80%') {$target.addClass(['display'])$target.parent().css('width', width)$target.parent().find('.dataTables_filter').css('margin-bottom', '.5em')return $target}/** Removes markup from text extracted from matchup coulumn */function getMatchup(txt) {txt = txt.slice(txt.indexOf('>')+1)return txt.slice(0, txt.indexOf('<'))}/*** Fills background of a given cell with linear gradient.* @param {jQuery} cell table cell (jquery object)* @param {Number} fill Percent value for linear-gradient()*/function addProgressBar(cell, fill = 0) {cell.css('background', `linear-gradient(to right,#fd0 ${fill}%,#ccc ${fill}%)`)}/*** Creates menu to control what table to show -* detailed stats or grouped by race composition.* @param {jQuery} srcTable Source detailed table (jquery DOM object)* @param {jQuery} groupTableWrap Grouped table's wrapper (jquery DOM object)*/function createGroupCtrlMenu(srcTable, groupTableWrap) {const check = $(ce('input')).attr({type: "checkbox"}).get(0)check.dataset.srcId = srcTable.parent().attr('id')check.dataset.targetId = groupTableWrap.attr('id')$(check).change(e => {srcTable.parent().toggleClass('hidden')groupTableWrap.toggleClass('hidden')})const div = $(ce('div')).addClass('winrate-tbl-menu')div.insertBefore(srcTable.parent())$(ce('label')).append(check).append($(ce('span')).text('Group by race combination')).appendTo(div)$(ce('p')).text('NOTE: Grouped data exclude mirror matchups').addClass('text--hint').appendTo(div)$(ce('p')).text('HINT: shift-click a column for multiple-column ordering').addClass('text--hint').appendTo(div)}/** Mimics original popup behavior when a user clicks on a matchup cell */function showMatchup2Popup(e) {// save original textconst cell = e.currentTargetconst srcText = cell.textContent// restore full matchup namecell.textContent = $(cell).parent().prev().text() + 'v' + srcText// call method how it should be calledshowPopup('matchup2', cell)// restore original textcell.textContent = srcText}/** Add shared title attribute to both matchup cells* (they were splitted before) */function setMatchupTitle(td, d, row) {$(td).attr('title', `${row[0]}v${getMatchup(row[1])}`)}// ----------------------------function run() {// VM tells me @require scripts are executed before the script itself// and also the script executed on "document-end" event// so it should be safe to just use jquery and the rest.// set ids to matchup tables$('h3').filter((i, elt) => {const match = elt.textContent.match(/(\d)v\d\smatchups/i)if (match) {const num = match[1]// new id for a table// looks like "v11" or "v44" etcconst id = 'v' + num + num// find the <table> (it is sibling with <h3> parrent elt - <summary>)// and set its new id$(elt.parentNode).find('+ table').attr('id', id).addClass(STATS_TABLE_CLASS)}})const TBL_SELECTOR = '.'+STATS_TABLE_CLASS// DataTables lib demands <thead> for <table>addThead($(TBL_SELECTOR))// remove first column with row number$(TBL_SELECTOR).find('th:first-child, td:first-child').remove()// delete DOWN arrow$(TBL_SELECTOR).find('thead').find('th:contains("Games ↓")').text('Games')// for tables all except 1v1$(TBL_SELECTOR).not('#v11').each((i, tbl) => {// split matchup into 2 columns$(tbl).find('tbody tr td:first-child').each((i, td) => {const matchup = $(td).text().split('v')$(ce('td')).text(matchup[0]).insertBefore(td)$(td).find('span').text(matchup[1]).attr('onclick', '').click(showMatchup2Popup)})// extend table headers after matchup spliting// see example for colspan/rowspan there - https://jsfiddle.net/qgk5twdo/$(tbl).find('thead th:first-child').attr('colspan', 2)$(tbl).find('thead th:not(:first-child)').attr('rowspan', 2)$(tbl).find('thead').append('<tr></tr>').find('tr:last-child').append('<th>race</th>').append('<th>race</th>')})// init tables as DataTablesconst initv11 = {paging: false,order: [[4, "desc"]],orderMulti: true,columnDefs: [// disable ordering for some columns{ orderable: false, targets: [3, 8, 9] }],autoWidth: false,}$('#v11').DataTable(initv11)const initArgs = {paging: false,order: [[5, "desc"]],orderMulti: true,columnDefs: [// disable ordering for some columns{ orderable: false, targets: [4, 9, 10] },{ createdCell: setMatchupTitle, targets: [0, 1] },],autoWidth: false,}$('#v22, #v33, #v44').DataTable(initArgs)// ----------------------------// apply CSS fixesfixCss($(TBL_SELECTOR))$(TBL_SELECTOR).each((i, tbl) => {const id = tbl.id$(tbl).parent().parent().addClass('matchup-details').attr('id', `${id}-details`)})$(TBL_SELECTOR).not('#v11').find('tbody tr').find('td:first-child, td:nth-child(2)').addClass('no-after')// ----------------------------// build groupped datafunction split_1v1_race(data) {return data.map(row => {const split = row[0].split('v')return [...split, ...row.slice(1)]})}function duplicateMatchupRows(data) {return data.concat(data.map(row => {const newRow = row.slice()newRow[5] = 100 - parseInt(newRow[5])newRow[0] = row[1]newRow[1] = row[0]return newRow}));}const rawData = [];(function(){let data = $('#v11').DataTable().rows().data().toArray()data.forEach( row => row[0] = getMatchup(row[0]) )data = split_1v1_race(data)data = duplicateMatchupRows(data)rawData.push(data)})();$('#v22, #v33, #v44').each((i, tbl) => {let data = $(tbl).DataTable().rows().data().toArray()data.forEach( row => row[1] = getMatchup(row[1]) )// duplicate data to get all race combinationsdata = duplicateMatchupRows(data)rawData.push(data)})// filter, grouping and aggregatesconst notMirror = row => row[0] != row[1] ;const matchupGroup = row => row[0]const sum = value => value.reduce((acc, cur) => acc + parseInt(cur), 0)const avg = value => sum(value) / value.lengthconst minStr = value => value.reduce((acc, curr) => curr < acc ? curr: acc)const maxStr = value => value.reduce((acc, curr) => curr > acc ? curr: acc)const groupData = rawData.map(rows => {return new query().from(rows).where(notMirror).groupBy(matchupGroup).aggregate(5, avg, 'winrate').aggregate(2, sum, 'num games').aggregate(3, sum, 'num games %').aggregate(7, minStr, 'first game').aggregate(8, maxStr, 'last game').r###lt;})// console.log(groupData);/*** Creates race composition winrate table.* Returns jQuery object*/function createRCWTable(){return $(ce('table')).append(ce('thead')).find('thead').append(ce('tr')).find('tr').append($(ce('th')).text('Race').attr('title', 'Race composition')).append($(ce('th')).text('Winrate %')).append($(ce('th')).text('Games')).append($(ce('th')).text('Games %')).append($(ce('th')).text('First Game')).append($(ce('th')).text('Last Game')).parent() // back to <thead>.parent() // back to <table>.addClass(STATS_TABLE_CLASS)}$('#v11, #v22, #v33, #v44').each((i, srcTable) => {// groupData[i] is an array of rows with aggregate r###ltsconst data = groupData[i].map(row => {return [row[0], // race compositionrow[2].value, // winraterow[3].value, // gamesrow[4].value, // games %row[5].value, // first gamerow[6].value, // last game];})// calc "Games %" properlyconst sumGames = data.reduce((acc, row) => acc + row[2], 0)data.forEach(row => row[3] = (row[2] * 100) / sumGames )const initArgs = {paging: false,data: data,order: [[1, "desc"]],orderMulti: true,autoWidth: false,columnDefs: [ {// render percent symbol in "Games %" columntargets: 3,render: val => String(Math.round(val)) + '%'}, {// render percent symbol in "Winrate %" columntargets: 1,render: val => String(Math.round(val)) + '%'} ],}const tbl = createRCWTable()tbl.DataTable(initArgs)const tblWrap = tbl.DataTable().table().container()// such tables are hidden by default$(tblWrap).addClass('hidden')fixCss(tbl, '60%')// add progress bar bg for "games" and "winrate" columnstbl.find('tbody td:nth-child(2), tbody td:nth-child(4)').each((i, td) => addProgressBar($(td), parseFloat($(td).text())) );// place group table after coresponding initial table$(srcTable).parent().after(tblWrap)createGroupCtrlMenu($(srcTable), $(tblWrap))})}