您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
AtCoderの順位表ページに得点分布を表すヒストグラムを追加します。
// ==UserScript== // @name AtCoderScoreHistogram // @namespace http://tampermonkey.net/ // @version 1.0.1 // @description AtCoderの順位表ページに得点分布を表すヒストグラムを追加します。 // @author hyyk // @match https://atcoder.jp/contests/*/standings* // @require https://cdnjs.cloudflare.com/ajax/libs/plotly.js/2.20.0/plotly-cartesian.min.js // @exclude https://atcoder.jp/contests/*/standings/json // @grant none // @license MIT // ==/UserScript== $(function () { 'use strict'; window.addEventListener('load', function () { vueStandings.$watch('standings', function (newStandings, old) { if (!newStandings) { return; } if (old === undefined || old == null) { setupInterface(); } else { const activeTask = document.querySelector('li.tab.active'); if (activeTask !== null) { const taskScreenName = activeTask.getAttribute('value'); const drawHistogramHandler = createDrawHistogramHandler(taskScreenName); drawHistogramHandler.handleEvent(); } } }, { deep: true, immediate: true }); }); }); function processStandings(standings) { let processedStandings = { TaskInfo: {} }; const taskInfo = standings['TaskInfo']; const standingsData = standings['StandingsData']; for (const task of taskInfo) { const taskScreenName = task['TaskScreenName']; processedStandings['TaskInfo'][taskScreenName] = task; processedStandings['TaskInfo'][taskScreenName]['ResultInfo'] = {}; } for (const user of standingsData) { const userName = user['UserScreenName']; for (const [taskScreenName, results] of Object.entries(user['TaskResults'])) { const score = results['Score']; processedStandings['TaskInfo'][taskScreenName]['ResultInfo'][userName] = { 'Score': score }; } } return processedStandings; } function setupInterface() { document.getElementById('vue-standings').insertAdjacentHTML('beforebegin', ` <button type="button" id="show-or-hide-hist-btn" class="btn btn-default" style="margin-bottom: 1em;margin-top: 1em;">Show/Hide a histogram</button> <div id="hist-container" style="margin-bottom: 1em;padding: 1em;background-color: #fff;border: 1px solid #ddd;" hidden> <ul id="tab-bar" style="border-bottom: 1px solid #ddd;display: flex;flex-wrap: wrap;padding-left: 0px;"></ul> <div id="hist-heading" style="display: flex;"> <div style="flex: 0.7;"> <label for="usernames-input">Usernames</label> <input type="text" name="usernames-input" class="form-control" id="user-names" placeholder="username1, username2, ..." style="margin-bottom: 1em;width: 100%"> <button id="hist-update-btn" class="btn btn-primary" style="display: inline;margin-right: 1em;">Update</button> <p id="invalid-users" style="display: inline; color: red"></p> </div> </div> <div id="hist-area"></div> </div> `); const processedStandings = processStandings(vueStandings.standings); const tabBar = document.getElementById('tab-bar'); for (const [taskScreenName, info] of Object.entries(processedStandings['TaskInfo'])) { const assignment = info['Assignment']; tabBar.insertAdjacentHTML('beforeend', ` <li class="tab" style="border: 1px solid transparent;list-style: none;margin-bottom: -1px;padding: 0.4em 0.7em;cursor: pointer;color: #337ab7;" value="${taskScreenName}">${assignment} </li> `); } document.querySelectorAll('li.tab').forEach((tab) => { const taskScreenName = tab.getAttribute('value'); const drawHistogramHandler = createDrawHistogramHandler(taskScreenName); tab.addEventListener('click', tabSwitch); tab.addEventListener('click', drawHistogramHandler); }); document.getElementById('hist-update-btn').addEventListener('click', function () { const activeTab = document.querySelector('li.tab.active') if (activeTab !== null) { const taskScreenName = activeTab.getAttribute('value'); const drawHistogramHandler = createDrawHistogramHandler(taskScreenName); drawHistogramHandler.handleEvent(); } }); const showOrHideHistogramButton = document.getElementById('show-or-hide-hist-btn'); const histogramContainer = document.getElementById('hist-container'); showOrHideHistogramButton.addEventListener('click', function () { if (histogramContainer.hasAttribute('hidden') === true) { const refreshButton = document.getElementById('refresh'); if (refreshButton !== null) { const histogramHeading = document.getElementById('hist-heading'); let refreshButtonArea = refreshButton.parentNode; refreshButtonArea.setAttribute('style', 'flex: 0.3; text-align: right;'); refreshButtonArea.querySelector('#last-refresh').setAttribute('style', 'display: block;'); histogramHeading.insertAdjacentElement('beforeend', refreshButtonArea.parentNode.removeChild(refreshButtonArea)); } histogramContainer.removeAttribute('hidden'); if (tabBar.firstChild !== null) { tabBar.firstElementChild.click(); } } else { histogramContainer.setAttribute('hidden', ""); } }); } function createDrawHistogramHandler(taskScreenName) { return { taskScreenName: taskScreenName, histogramArea: 'hist-area', processedStandings: processStandings(vueStandings.standings), handleEvent: drawHistogram }; } function tabSwitch() { const activeTab = document.querySelector('li.tab.active') if (activeTab !== null) { activeTab.classList.remove('active'); activeTab.setAttribute('style', 'border: 1px solid transparent;list-style: none;margin-bottom: -1px;padding: 0.4em 0.7em;cursor: pointer;color: #337ab7;') } this.classList.add('active'); this.setAttribute('style', 'border: 1px solid transparent;list-style: none;margin-bottom: -1px;padding: 0.4em 0.7em;cursor: pointer;color: #337ab7;border-left: 1px solid #ccc;border-right: 1px solid #ccc;border-top: 1px solid #ccc;border-bottom: 1px solid #fff;background-color: #fff;'); } function drawHistogram() { const userNames = document.getElementById('user-names').value; const taskInfo = this.processedStandings['TaskInfo'][this.taskScreenName]; const taskAssignment = taskInfo['Assignment']; let scores = []; for (const userName in taskInfo['ResultInfo']) { scores.push(taskInfo['ResultInfo'][userName]['Score']); } const layout = { font: { size: 14 }, plot_bgcolor: "rgb(250, 250, 250)", xaxis: { title: { text: "Score" } }, yaxis: { title: { text: "Frequency" } }, title: this.histogramArea === 'hist-for-png' ? 'Problem ' + taskAssignment : '', shapes: [], autosize: true, margin: { t: 30, }, }; const traceOfScore = { x: scores.map(x => x / 100), type: "histogram", name: "cumulative", marker: { color: "#c4ecec", line: { color: "#000000", width: 0.03 } }, nbinsx: 30 }; const data = [traceOfScore]; const config = { responsive: true }; layout.annotations = []; if (userNames !== '') { const colors = [ "#ff1111", "#0011a3", "#bfbf00", "#a55111", "#000fff", "#999999", "#ff00ff", "#dd0000", "#ff7777", "#000000", ]; const userNameList = Array.from(new Set(userNames.split(',').map(s => s.trim()))); const resultInfo = taskInfo['ResultInfo']; let userNameListLength = userNameList.length; let loop = 0; let invalidUsers = []; for (const userName of userNameList) { if (!(userName in resultInfo)) { invalidUsers.push(userName); userNameListLength--; } } for (const userName of userNameList) { if (!(userName in resultInfo)) { continue; } const userLine = { type: "line", x0: resultInfo[userName]['Score'] / 100, y0: 0, x1: resultInfo[userName]['Score'] / 100, y1: 1, yref: "paper", line: { color: colors[loop], width: 1.8 } }; const annotation = { showarrow: true, text: userName + '<br>Score:' + resultInfo[userName]['Score'] / 100, x: resultInfo[userName]['Score'] / 100, yref: "paper", y: loop / userNameListLength, ax: 5, align: "left", xanchor: "left", yanchor: "bottom", font: { color: colors[loop], size: 12 } }; layout["shapes"].push(userLine); layout.annotations.push(annotation); loop++; } if (invalidUsers.length !== 0) { document.getElementById('invalid-users').textContent = 'Invalid usernames: ' + invalidUsers.join(', '); } else { document.getElementById('invalid-users').textContent = '' } } Plotly.newPlot(this.histogramArea, data, layout, config); }