AtCoderStandingsAnalysis

順位表のjsonを集計し、上部にテーブルを追加します。

As of 2020-03-28. See the latest version.

  1. // ==UserScript==
  2. // @name AtCoderStandingsAnalysis
  3. // @namespace https://github.com/RTnF/AtCoderStandingsAnalysis
  4. // @version 0.1.3
  5. // @description 順位表のjsonを集計し、上部にテーブルを追加します。
  6. // @author RTnF
  7. // @match https://atcoder.jp/*standings*
  8. // @exclude https://atcoder.jp/*standings/json
  9. // @grant none
  10. // @license CC0-1.0
  11. // ==/UserScript==
  12.  
  13. // ソート済み配列のうちval未満が何個あるか求める
  14. function countLower(arr, val) {
  15. var lo = -1;
  16. var hi = arr.length;
  17. while (hi - lo > 1) {
  18. var mid = Math.floor((hi + lo) / 2);
  19. if (arr[mid] < val) {
  20. lo = mid;
  21. } else {
  22. hi = mid;
  23. }
  24. }
  25. return hi;
  26. }
  27.  
  28. // 換算: Rating -> innerRating
  29. function innerRating(rate, comp) {
  30. var ret = rate;
  31. if (rate <= 0) {
  32. throw "rate <= 0";
  33. }
  34. if (ret < 400) {
  35. ret = 400 * (1 - Math.log(400 / rate));
  36. }
  37. ret += 1200 * (Math.sqrt(1 - Math.pow(0.81, comp)) / (1 - Math.pow(0.9, comp)) - 1) / (Math.sqrt(19) - 1);
  38. return ret;
  39. }
  40.  
  41. $(function () {
  42. 'use strict';
  43.  
  44. const cols = ["#808080", "#804000", "#008000", "#00C0C0", "#0000FF", "#C0C000", "#FF8000", "#FF0000"];
  45. const threshold = [-10000, 400, 800, 1200, 1600, 2000, 2400, 2800];
  46. const canvasWidth = 250;
  47. const canvasHeight = 25;
  48.  
  49. // 表を先頭に追加
  50. $('#vue-standings').prepend(`
  51. <div>
  52. <table id="acsa-table" class="table table-bordered table-hover th-center td-center td-middle">
  53. <thead>
  54. </thead>
  55. <tbody>
  56. </tbody>
  57. </table>
  58. </div>
  59. `);
  60.  
  61. // 表の更新
  62. vueStandings.$watch('standings', function (newVal, oldVal) {
  63. if (!newVal) {
  64. return;
  65. }
  66. var data;
  67. var task = newVal.TaskInfo;
  68. if (vueStandings.filtered) {
  69. data = vueStandings.filteredStandings;
  70. } else {
  71. data = newVal.StandingsData;
  72. }
  73.  
  74. $('#acsa-table > tbody').empty();
  75. $('#acsa-table > tbody').append(`
  76. <tr style="font-weight: bold;">
  77. <td>問題</td>
  78. <td>得点</td>
  79. <td>人数</td>
  80. <td>正解率</td>
  81. <td>平均ペナ</td>
  82. <td>ペナ率</td>
  83. <td>内部レート</td>
  84. </tr>
  85. `);
  86. for (let i = 0; i < task.length; i++) {
  87. var isTried = vueStandings.tries[i] > 0;
  88. $('#acsa-table > tbody').append(`
  89. <tr>
  90. <td style="padding: 4px;">` + task[i].Assignment + `</td>
  91. <td style="padding: 4px;">-</td>
  92. <td style="padding: 4px;">` + vueStandings.ac[i] + ` / ` + vueStandings.tries[i] + `</td>
  93. <td style="padding: 4px;">` + (isTried ? (vueStandings.ac[i] / vueStandings.tries[i] * 100).toFixed(2) + "%" : "-") + `</td>
  94. <td style="padding: 4px;">-</td>
  95. <td style="padding: 4px;">-</td>
  96. <td style="padding: 4px; width: ` + canvasWidth + `px;"><canvas style="vertical-align: middle;" width="` + canvasWidth + `px" height="` + canvasHeight +`px"></canvas></td>
  97. </tr>
  98. `);
  99. if (!isTried) {
  100. continue;
  101. }
  102.  
  103. // トップの得点を満点とみなす
  104. var maxScore = -1;
  105. var myScore = -1;
  106. // 不正解数 / 提出者数
  107. var avePenalty = 0;
  108. // ペナルティ >= 1 の人数 / 提出者数
  109. var ratioPenalty = 0;
  110. var rates = [];
  111. for (let j = 0; j < data.length; j++) {
  112. // 参加登録していない
  113. if (!data[j].TaskResults) {
  114. continue;
  115. }
  116. // アカウント削除
  117. if (data[j].UserIsDeleted) {
  118. continue;
  119. }
  120. var result = data[j].TaskResults[task[i].TaskScreenName];
  121. // 未提出のときresult === undefined
  122. if (result) {
  123. if (data[j].UserScreenName === vueStandings.userScreenName) {
  124. myScore = result.Score;
  125. }
  126. // 赤い括弧内の数字
  127. var penalty = result.Score === 0 ? result.Failure : result.Penalty;
  128. avePenalty += penalty;
  129. if (penalty > 0) {
  130. ratioPenalty++;
  131. }
  132. if (maxScore < result.Score) {
  133. maxScore = result.Score;
  134. }
  135. }
  136. }
  137. // 正解者の内部レート配列を作成する
  138. // 初出場はカウントしない
  139. if (maxScore > 0) {
  140. for (let j = 0; j < data.length; j++) {
  141. if (data[j].Competitions > 0
  142. && data[j].TaskResults[task[i].TaskScreenName]
  143. && data[j].TaskResults[task[i].TaskScreenName].Score === maxScore) {
  144. rates.push(innerRating(Math.max(data[j].Rating, 1), data[j].Competitions));
  145. }
  146. }
  147. rates.sort(function (a, b) { return a - b; });
  148. }
  149.  
  150. myScore /= 100;
  151. maxScore /= 100;
  152. avePenalty /= vueStandings.tries[i];
  153. ratioPenalty /= vueStandings.tries[i];
  154. ratioPenalty *= 100;
  155.  
  156. $('#acsa-table > tbody > tr:eq(' + (i+1) + ') > td:eq(1)').text(myScore >= 0 ? myScore.toFixed() : "-");
  157. $('#acsa-table > tbody > tr:eq(' + (i+1) + ') > td:eq(4)').text(avePenalty.toFixed(2));
  158. $('#acsa-table > tbody > tr:eq(' + (i+1) + ') > td:eq(5)').text(ratioPenalty.toFixed(2) + "%");
  159. if (maxScore > 0) {
  160. var canvas = $('#acsa-table > tbody > tr:eq(' + (i+1) + ') > td:eq(6) > canvas')[0];
  161. if (canvas.getContext) {
  162. var context = canvas.getContext('2d');
  163. for (let k = 0; k < 8; k++) {
  164. context.fillStyle = cols[k];
  165. // 色の境界から右端までの矩形描画
  166. var x = Math.round(countLower(rates, threshold[k]) / rates.length * canvasWidth);
  167. context.fillRect(x, 0, canvasWidth - x, canvasHeight);
  168. }
  169. }
  170. }
  171. }
  172. }, {deep: true, immediate: true})
  173. });
  174.