Greasy Fork is available in English.

AtCoderScoreHistogram

AtCoderの順位表ページにヒストグラムを追加します。

2023-03-19 기준 버전입니다. 최신 버전을 확인하세요.

  1. // ==UserScript==
  2. // @name AtCoderScoreHistogram
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.0
  5. // @description AtCoderの順位表ページにヒストグラムを追加します。
  6. // @author hyyk
  7. // @match https://atcoder.jp/contests/*/standings*
  8. // @require https://cdnjs.cloudflare.com/ajax/libs/plotly.js/2.20.0/plotly-cartesian.min.js
  9. // @exclude https://atcoder.jp/contests/*/standings/json
  10. // @grant none
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. $(function () {
  15. 'use strict';
  16.  
  17. window.addEventListener('load', function () {
  18. vueStandings.$watch('standings', function (newStandings, old) {
  19. if (!newStandings) {
  20. return;
  21. }
  22.  
  23. if (old === undefined || old == null) {
  24. setupInterface();
  25. } else {
  26. const activeTask = document.querySelector('li.tab.active');
  27. if (activeTask !== null) {
  28. const taskScreenName = activeTask.getAttribute('value');
  29. const drawHistogramHandler = createDrawHistogramHandler(taskScreenName);
  30. drawHistogramHandler.handleEvent();
  31. }
  32. }
  33.  
  34. }, { deep: true, immediate: true });
  35. });
  36. });
  37.  
  38. function processStandings(standings) {
  39. let processedStandings = { TaskInfo: {} };
  40.  
  41. const taskInfo = standings['TaskInfo'];
  42. const standingsData = standings['StandingsData'];
  43.  
  44. for (const task of taskInfo) {
  45. const taskScreenName = task['TaskScreenName'];
  46. processedStandings['TaskInfo'][taskScreenName] = task;
  47. processedStandings['TaskInfo'][taskScreenName]['ResultInfo'] = {};
  48. }
  49.  
  50. for (const user of standingsData) {
  51. const userName = user['UserScreenName'];
  52. for (const [taskScreenName, results] of Object.entries(user['TaskResults'])) {
  53. const score = results['Score'];
  54. processedStandings['TaskInfo'][taskScreenName]['ResultInfo'][userName] = {
  55. 'Score': score
  56. };
  57. }
  58. }
  59.  
  60. return processedStandings;
  61. }
  62.  
  63. function setupInterface() {
  64. document.getElementById('vue-standings').insertAdjacentHTML('beforebegin', `
  65. <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>
  66. <div id="hist-container" style="margin-bottom: 1em;padding: 1em;background-color: #fff;border: 1px solid #ddd;" hidden>
  67. <ul id="tab-bar" style="border-bottom: 1px solid #ddd;display: flex;flex-wrap: wrap;padding-left: 0px;"></ul>
  68. <div id="hist-heading" style="display: flex;">
  69. <div style="flex: 0.7;">
  70. <label for="usernames-input">Usernames</label>
  71. <input type="text" name="usernames-input" class="form-control" id="user-names" placeholder="username1, username2, ..." style="margin-bottom: 1em;width: 100%">
  72. <button id="hist-update-btn" class="btn btn-primary" style="display: inline;margin-right: 1em;">Update</button>
  73. <p id="invalid-users" style="display: inline; color: red"></p>
  74. </div>
  75. </div>
  76. <div id="hist-area"></div>
  77. </div>
  78. `);
  79.  
  80. const processedStandings = processStandings(vueStandings.standings);
  81. const tabBar = document.getElementById('tab-bar');
  82.  
  83. for (const [taskScreenName, info] of Object.entries(processedStandings['TaskInfo'])) {
  84. const assignment = info['Assignment'];
  85. tabBar.insertAdjacentHTML('beforeend', `
  86. <li class="tab"
  87. style="border: 1px solid transparent;list-style: none;margin-bottom: -1px;padding: 0.4em 0.7em;cursor: pointer;color: #337ab7;"
  88. value="${taskScreenName}">${assignment}
  89. </li>
  90. `);
  91. }
  92.  
  93. document.querySelectorAll('li.tab').forEach((tab) => {
  94. const taskScreenName = tab.getAttribute('value');
  95. const drawHistogramHandler = createDrawHistogramHandler(taskScreenName);
  96.  
  97. tab.addEventListener('click', tabSwitch);
  98. tab.addEventListener('click', drawHistogramHandler);
  99. });
  100.  
  101.  
  102. document.getElementById('hist-update-btn').addEventListener('click', function () {
  103. const activeTab = document.querySelector('li.tab.active')
  104. if (activeTab !== null) {
  105. const taskScreenName = activeTab.getAttribute('value');
  106. const drawHistogramHandler = createDrawHistogramHandler(taskScreenName);
  107. drawHistogramHandler.handleEvent();
  108. }
  109. });
  110.  
  111. const showOrHideHistogramButton = document.getElementById('show-or-hide-hist-btn');
  112. const histogramContainer = document.getElementById('hist-container');
  113.  
  114. showOrHideHistogramButton.addEventListener('click', function () {
  115. if (histogramContainer.hasAttribute('hidden') === true) {
  116. const refreshButton = document.getElementById('refresh');
  117. if (refreshButton !== null) {
  118. const histogramHeading = document.getElementById('hist-heading');
  119. let refreshButtonArea = refreshButton.parentNode;
  120. refreshButtonArea.setAttribute('style', 'flex: 0.3; text-align: right;');
  121. refreshButtonArea.querySelector('#last-refresh').setAttribute('style', 'display: block;');
  122.  
  123. histogramHeading.insertAdjacentElement('beforeend', refreshButtonArea.parentNode.removeChild(refreshButtonArea));
  124. }
  125.  
  126. histogramContainer.removeAttribute('hidden');
  127.  
  128. if (tabBar.firstChild !== null) {
  129. tabBar.firstElementChild.click();
  130. }
  131. } else {
  132. histogramContainer.setAttribute('hidden', "");
  133. }
  134. });
  135. }
  136.  
  137. function createDrawHistogramHandler(taskScreenName) {
  138. return {
  139. taskScreenName: taskScreenName,
  140. histogramArea: 'hist-area',
  141. processedStandings: processStandings(vueStandings.standings),
  142. handleEvent: drawHistogram
  143. };
  144. }
  145.  
  146. function tabSwitch() {
  147. const activeTab = document.querySelector('li.tab.active')
  148. if (activeTab !== null) {
  149. activeTab.classList.remove('active');
  150. activeTab.setAttribute('style', 'border: 1px solid transparent;list-style: none;margin-bottom: -1px;padding: 0.4em 0.7em;cursor: pointer;color: #337ab7;')
  151. }
  152.  
  153. this.classList.add('active');
  154. 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;');
  155. }
  156.  
  157. function drawHistogram() {
  158. const userNames = document.getElementById('user-names').value;
  159.  
  160. const taskInfo = this.processedStandings['TaskInfo'][this.taskScreenName];
  161. const taskAssignment = taskInfo['Assignment'];
  162.  
  163. let scores = [];
  164. for (const userName in taskInfo['ResultInfo']) {
  165. scores.push(taskInfo['ResultInfo'][userName]['Score']);
  166. }
  167.  
  168. const layout = {
  169. font: { size: 14 },
  170. plot_bgcolor: "rgb(250, 250, 250)",
  171. xaxis: {
  172. title: {
  173. text: "Score"
  174. }
  175. },
  176. yaxis: {
  177. title: {
  178. text: "Frequency"
  179. }
  180. },
  181. title: this.histogramArea === 'hist-for-png' ? 'Problem ' + taskAssignment : '',
  182. shapes: [],
  183. autosize: true,
  184. margin: {
  185. t: 30,
  186. },
  187. };
  188.  
  189. const traceOfScore = {
  190. x: scores.map(x => x / 100),
  191. type: "histogram",
  192. name: "cumulative",
  193. marker: {
  194. color: "#c4ecec",
  195. line: {
  196. color: "#000000",
  197. width: 0.03
  198. }
  199. },
  200. nbinsx: 30
  201. };
  202.  
  203. const data = [traceOfScore];
  204. const config = { responsive: true };
  205.  
  206. layout.annotations = [];
  207.  
  208. if (userNames !== '') {
  209. const colors = [
  210. "#ff1111",
  211. "#0011a3",
  212. "#bfbf00",
  213. "#a55111",
  214. "#000fff",
  215. "#999999",
  216. "#ff00ff",
  217. "#dd0000",
  218. "#ff7777",
  219. "#000000",
  220. ];
  221. const userNameList = Array.from(new Set(userNames.split(',').map(s => s.trim())));
  222. const resultInfo = taskInfo['ResultInfo'];
  223.  
  224. let userNameListLength = userNameList.length;
  225. let loop = 0;
  226. let invalidUsers = [];
  227.  
  228. for (const userName of userNameList) {
  229. if (!(userName in resultInfo)) {
  230. invalidUsers.push(userName);
  231. userNameListLength--;
  232. }
  233. }
  234.  
  235. for (const userName of userNameList) {
  236. if (!(userName in resultInfo)) {
  237. continue;
  238. }
  239.  
  240. const userLine = {
  241. type: "line",
  242. x0: resultInfo[userName]['Score'] / 100,
  243. y0: 0,
  244. x1: resultInfo[userName]['Score'] / 100,
  245. y1: 1,
  246. yref: "paper",
  247. line: {
  248. color: colors[loop],
  249. width: 1.8
  250. }
  251. };
  252.  
  253. const annotation = {
  254. showarrow: true,
  255. text: userName + '<br>Score:' + resultInfo[userName]['Score'] / 100,
  256. x: resultInfo[userName]['Score'] / 100,
  257. yref: "paper",
  258. y: loop / userNameListLength,
  259. ax: 5,
  260. align: "left",
  261. xanchor: "left",
  262. yanchor: "bottom",
  263. font: {
  264. color: colors[loop],
  265. size: 12
  266. }
  267. };
  268.  
  269. layout["shapes"].push(userLine);
  270. layout.annotations.push(annotation);
  271. loop++;
  272. }
  273.  
  274. if (invalidUsers.length !== 0) {
  275. document.getElementById('invalid-users').textContent = 'Invalid usernames: ' + invalidUsers.join(', ');
  276. } else {
  277. document.getElementById('invalid-users').textContent = ''
  278. }
  279. }
  280.  
  281. Plotly.newPlot(this.histogramArea, data, layout, config);
  282. }