AtCoderStandingsAnalysis

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

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

  1. // ==UserScript==
  2. // @name AtCoderStandingsAnalysis
  3. // @namespace https://github.com/RTnF/AtCoderStandingsAnalysis
  4. // @version 0.1.2
  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. <tr>
  55. <th>問題</th>
  56. <th>得点</th>
  57. <th>人数</th>
  58. <th>正解率</th>
  59. <th>平均ペナ</th>
  60. <th>ペナ率</th>
  61. <th>レート</th>
  62. </tr>
  63. </thead>
  64. <tbody>
  65. </tbody>
  66. </table>
  67. </div>
  68. `);
  69.  
  70. // 表の更新
  71. vueStandings.$watch('standings', function (newVal, oldVal) {
  72. if (!newVal) {
  73. return;
  74. }
  75. var data;
  76. var task = newVal.TaskInfo;
  77. if (vueStandings.filtered) {
  78. data = vueStandings.filteredStandings;
  79. } else {
  80. data = newVal.StandingsData;
  81. }
  82.  
  83. $('#acsa-table > tbody').empty();
  84. for (let i = 0; i < task.length; i++) {
  85. var isTried = vueStandings.tries[i] > 0;
  86. $('#acsa-table > tbody').append(`
  87. <tr>
  88. <td style="padding: 4px;">` + task[i].Assignment + `</td>
  89. <td style="padding: 4px;">-</td>
  90. <td style="padding: 4px;">` + vueStandings.ac[i] + ` / ` + vueStandings.tries[i] + `</td>
  91. <td style="padding: 4px;">` + (isTried ? (vueStandings.ac[i] / vueStandings.tries[i] * 100).toFixed(2) + "%" : "-") + `</td>
  92. <td style="padding: 4px;">-</td>
  93. <td style="padding: 4px;">-</td>
  94. <td style="padding: 4px;"><canvas style="vertical-align: middle;" width="` + canvasWidth + `px" height="` + canvasHeight +`px"></canvas></td>
  95. </tr>
  96. `);
  97. if (!isTried) {
  98. continue;
  99. }
  100.  
  101. // トップの得点を満点とみなす
  102. var maxScore = -1;
  103. var myScore = -1;
  104. // 不正解数 / 提出者数
  105. var avePenalty = 0;
  106. // ペナルティ >= 1 の人数 / 提出者数
  107. var ratioPenalty = 0;
  108. var rates = [];
  109. for (let j = 0; j < data.length; j++) {
  110. // 参加登録していない
  111. if (!data[j].TaskResults) {
  112. continue;
  113. }
  114. // アカウント削除
  115. if (data[j].UserIsDeleted) {
  116. continue;
  117. }
  118. var result = data[j].TaskResults[task[i].TaskScreenName];
  119. // 未提出のときresult === undefined
  120. if (result) {
  121. if (data[j].UserScreenName === vueStandings.userScreenName) {
  122. myScore = result.Score;
  123. }
  124. // 赤い括弧内の数字
  125. var penalty = result.Score === 0 ? result.Failure : result.Penalty;
  126. avePenalty += penalty;
  127. if (penalty > 0) {
  128. ratioPenalty++;
  129. }
  130. if (maxScore < result.Score) {
  131. maxScore = result.Score;
  132. }
  133. }
  134. }
  135. // 正解者の内部レート配列を作成する
  136. // 初出場はカウントしない
  137. if (maxScore > 0) {
  138. for (let j = 0; j < data.length; j++) {
  139. if (data[j].Competitions > 0
  140. && data[j].TaskResults[task[i].TaskScreenName]
  141. && data[j].TaskResults[task[i].TaskScreenName].Score === maxScore) {
  142. rates.push(innerRating(Math.max(data[j].Rating, 1), data[j].Competitions));
  143. }
  144. }
  145. rates.sort(function (a, b) { return a - b; });
  146. }
  147.  
  148. myScore /= 100;
  149. maxScore /= 100;
  150. avePenalty /= vueStandings.tries[i];
  151. ratioPenalty /= vueStandings.tries[i];
  152. ratioPenalty *= 100;
  153.  
  154. $('#acsa-table > tbody > tr:eq(' + i + ') > td:eq(1)').text(myScore >= 0 ? myScore.toFixed() : "-");
  155. $('#acsa-table > tbody > tr:eq(' + i + ') > td:eq(4)').text(avePenalty.toFixed(2));
  156. $('#acsa-table > tbody > tr:eq(' + i + ') > td:eq(5)').text(ratioPenalty.toFixed(2) + "%");
  157. if (maxScore > 0) {
  158. var canvas = $('#acsa-table > tbody > tr:eq(' + i + ') > td:eq(6) > canvas')[0];
  159. if (canvas.getContext) {
  160. var context = canvas.getContext('2d');
  161. for (let k = 0; k < 8; k++) {
  162. context.fillStyle = cols[k];
  163. // 色の境界から右端までの矩形描画
  164. var x = Math.round(countLower(rates, threshold[k]) / rates.length * canvasWidth);
  165. context.fillRect(x, 0, canvasWidth - x, canvasHeight);
  166. }
  167. }
  168. }
  169. }
  170. }, {deep: true, immediate: true})
  171. });
  172.