atcoder-standings-difficulty-analyzer

順位表の得点情報を集計し,推定 difficulty やその推移を表示します.

当前为 2021-01-02 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name atcoder-standings-difficulty-analyzer
  3. // @namespace iilj
  4. // @version 2021.1.2.0
  5. // @description 順位表の得点情報を集計し,推定 difficulty やその推移を表示します.
  6. // @author iilj
  7. // @supportURL https://github.com/iilj/atcoder-standings-difficulty-analyzer/issues
  8. // @match https://atcoder.jp/*standings*
  9. // @exclude https://atcoder.jp/*standings/json
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/plotly.js/1.33.1/plotly.min.js
  11. // @resource loaders.min.css https://cdnjs.cloudflare.com/ajax/libs/loaders.css/0.1.2/loaders.min.css
  12. // @grant GM_getResourceText
  13. // @grant GM_addStyle
  14. // ==/UserScript==
  15.  
  16. /**
  17. * 問題ごとの結果エントリ
  18. * @typedef {Object} TaskResultEntry
  19. * @property {any} Additional 謎
  20. * @property {number} Count 提出回数
  21. * @property {number} Elapsed コンテスト開始からの経過時間 [ns].
  22. * @property {number} Failure 非 AC の提出数(ACするまではペナルティではない).
  23. * @property {boolean} Frozen アカウントが凍結済みかどうか?
  24. * @property {number} Penalty ペナルティ数
  25. * @property {boolean} Pending ジャッジ中かどうか?
  26. * @property {number} Score 得点(×100)
  27. * @property {number} Status 謎
  28. */
  29.  
  30. /**
  31. * 全問題の結果
  32. * @typedef {Object} TotalResultEntry
  33. * @property {number} Accepted 正解した問題数
  34. * @property {any} Additional 謎
  35. * @property {number} Count 提出回数
  36. * @property {number} Elapsed コンテスト開始からの経過時間 [ns].
  37. * @property {boolean} Frozen アカウントが凍結済みかどうか?
  38. * @property {number} Penalty ペナルティ数
  39. * @property {number} Score 得点(×100)
  40. */
  41.  
  42. /**
  43. * 順位表エントリ
  44. * @typedef {Object} StandingsEntry
  45. * @property {any} Additional 謎
  46. * @property {string} Affiliation 所属
  47. * @property {number} AtCoderRank AtCoder 内順位
  48. * @property {number} Competitions Rated コンテスト参加回数
  49. * @property {string} Country 国ラベル."JP" など.
  50. * @property {string} DisplayName 表示名."hitonanode" など.
  51. * @property {number} EntireRank コンテスト順位?
  52. * @property {boolean} IsRated Rated かどうか
  53. * @property {boolean} IsTeam チームかどうか
  54. * @property {number} OldRating コンテスト前のレーティング
  55. * @property {number} Rank コンテスト順位?
  56. * @property {number} Rating コンテスト後のレーティング
  57. * @property {{[key: string]: TaskResultEntry}} TaskResults 問題ごとの結果.参加登録していない人は空.
  58. * @property {TotalResultEntry} TotalResult 全体の結果
  59. * @property {boolean} UserIsDeleted ユーザアカウントが削除済みかどうか
  60. * @property {string} UserName ユーザ名."hitonanode" など.
  61. * @property {string} UserScreenName ユーザの表示名."hitonanode" など.
  62. */
  63.  
  64. /**
  65. * 問題エントリ
  66. * @typedef {Object} TaskInfoEntry
  67. * @property {string} Assignment 問題ラベル."A" など.
  68. * @property {string} TaskName 問題名.
  69. * @property {string} TaskScreenName 問題の slug. "abc185_a" など.
  70. */
  71.  
  72. /**
  73. * 順位表情報
  74. * @typedef {Object} Standings
  75. * @property {any} AdditionalColumns 謎
  76. * @property {boolean} Fixed 謎
  77. * @property {StandingsEntry[]} StandingsData 順位表データ
  78. * @property {TaskInfoEntry[]} TaskInfo 問題データ
  79. */
  80.  
  81. /* globals vueStandings, $, contestScreenName, startTime, endTime, userScreenName, Plotly */
  82.  
  83. (() => {
  84. 'use strict';
  85.  
  86. // loader のスタイル設定
  87. const loaderStyles = GM_getResourceText("loaders.min.css");
  88. const loaderWrapperStyles = `
  89. #acssa-table {
  90. width: 100%;
  91. table-layout: fixed;
  92. margin-bottom: 1.5rem;
  93. }
  94. #acssa-thead {
  95. font-weight: bold;
  96. }
  97. .acssa-loader-wrapper {
  98. background-color: #337ab7;
  99. display: flex;
  100. justify-content: center;
  101. align-items: center;
  102. padding: 1rem;
  103. margin-bottom: 1.5rem;
  104. border-radius: 3px;
  105. }
  106. #acssa-chart-tab {
  107. margin-bottom: 0.5rem;
  108. }
  109. #acssa-chart-tab a {
  110. cursor: pointer;
  111. }
  112. #acssa-chart-tab span.glyphicon {
  113. margin-right: 0.5rem;
  114. }
  115. .acssa-chart-wrapper {
  116. display: none;
  117. }
  118. .acssa-chart-wrapper.acssa-chart-wrapper-active {
  119. display: block;
  120. }
  121. `;
  122. GM_addStyle(loaderStyles + loaderWrapperStyles);
  123.  
  124. class RatingConverter {
  125. /** 表示用の低レート帯補正レート → 低レート帯補正前のレート
  126. * @type {(correctedRating: number) => number} */
  127. static toRealRating = (correctedRating) => {
  128. if (correctedRating >= 400) return correctedRating;
  129. else return 400 * (1 - Math.log(400 / correctedRating));
  130. };
  131.  
  132. /** 低レート帯補正前のレート → 内部レート推定値
  133. * @type {(correctedRating: number) => number} */
  134. static toInnerRating = (realRating, comp) => {
  135. return realRating + 1200 * (Math.sqrt(1 - Math.pow(0.81, comp)) / (1 - Math.pow(0.9, comp)) - 1) / (Math.sqrt(19) - 1);
  136. };
  137.  
  138. /** 低レート帯補正前のレート → 表示用の低レート帯補正レート
  139. * @type {(correctedRating: number) => number} */
  140. static toCorrectedRating = (realRating) => {
  141. if (realRating >= 400) return realRating;
  142. else return Math.floor(400 / Math.exp((400 - realRating) / 400));
  143. };
  144. }
  145.  
  146. class DifficultyCalculator {
  147. /** @constructor
  148. * @type {(sortedInnerRatings: number[]) => DifficultyCalculator}
  149. */
  150. constructor(sortedInnerRatings) {
  151. this.innerRatings = sortedInnerRatings;
  152. /** @type {Map<number, number>} */
  153. this.prepared = new Map();
  154. /** @type {Map<number, number>} */
  155. this.memo = new Map();
  156. }
  157.  
  158. perf2ExpectedAcceptedCount = (m) => {
  159. let expectedAcceptedCount;
  160. if (this.prepared.has(m)) {
  161. expectedAcceptedCount = this.prepared.get(m);
  162. } else {
  163. expectedAcceptedCount = this.innerRatings.reduce((prev_expected_accepts, innerRating) =>
  164. prev_expected_accepts += 1 / (1 + Math.pow(6, (m - innerRating) / 400)), 0);
  165. this.prepared.set(m, expectedAcceptedCount);
  166. }
  167. return expectedAcceptedCount;
  168. };
  169.  
  170. perf2Ranking = (x) => this.perf2ExpectedAcceptedCount(x) + 0.5;
  171.  
  172. /** Difficulty 推定値を算出する
  173. * @type {((acceptedCount: number) => number)} */
  174. binarySearch = (acceptedCount) => {
  175. if (this.memo.has(acceptedCount)) {
  176. return this.memo.get(acceptedCount);
  177. }
  178. let lb = -10000;
  179. let ub = 10000;
  180. while (ub - lb > 1) {
  181. const m = Math.floor((ub + lb) / 2);
  182. const expectedAcceptedCount = this.perf2ExpectedAcceptedCount(m);
  183.  
  184. if (expectedAcceptedCount < acceptedCount) ub = m;
  185. else lb = m;
  186. }
  187. const difficulty = lb
  188. const correctedDifficulty = RatingConverter.toCorrectedRating(difficulty);
  189. this.memo.set(acceptedCount, correctedDifficulty);
  190. return correctedDifficulty;
  191. };
  192. }
  193.  
  194. /** @type {(rating: number) => string} */
  195. const getColor = (rating) => {
  196. if (rating < 400) return '#808080'; // gray
  197. else if (rating < 800) return '#804000'; // brown
  198. else if (rating < 1200) return '#008000'; // green
  199. else if (rating < 1600) return '#00C0C0'; // cyan
  200. else if (rating < 2000) return '#0000FF'; // blue
  201. else if (rating < 2400) return '#C0C000'; // yellow
  202. else if (rating < 2800) return '#FF8000'; // orange
  203. else if (rating == 9999) return '#000000';
  204. return '#FF0000'; // red
  205. };
  206.  
  207. /** レートを表す難易度円(◒)の HTML 文字列を生成
  208. * @type {(rating: number, isSmall?: boolean) => string} */
  209. const generateDifficultyCircle = (rating, isSmall = true) => {
  210. const size = (isSmall ? 12 : 36);
  211. const borderWidth = (isSmall ? 1 : 3);
  212.  
  213. const style = `display:inline-block;border-radius:50%;border-style:solid;border-width:${borderWidth}px;`
  214. + `margin-right:5px;vertical-align:initial;height:${size}px;width:${size}px;`;
  215.  
  216. if (rating < 3200) {
  217. // 色と円がどのぐらい満ちているかを計算
  218. const color = getColor(rating);
  219. const percentFull = (rating % 400) / 400 * 100;
  220.  
  221. // ◒を生成
  222. return `
  223. <span style='${style}border-color:${color};background:`
  224. + `linear-gradient(to top, ${color} 0%, ${color} ${percentFull}%, `
  225. + `rgba(0, 0, 0, 0) ${percentFull}%, rgba(0, 0, 0, 0) 100%); '>
  226. </span>`;
  227.  
  228. }
  229. // 金銀銅は例外処理
  230. else if (rating < 3600) {
  231. return `<span style="${style}border-color: rgb(150, 92, 44);`
  232. + 'background: linear-gradient(to right, rgb(150, 92, 44), rgb(255, 218, 189), rgb(150, 92, 44));"></span>';
  233. } else if (rating < 4000) {
  234. return `<span style="${style}border-color: rgb(128, 128, 128);`
  235. + 'background: linear-gradient(to right, rgb(128, 128, 128), white, rgb(128, 128, 128));"></span>';
  236. } else {
  237. return `<span style="${style}border-color: rgb(255, 215, 0);`
  238. + 'background: linear-gradient(to right, rgb(255, 215, 0), white, rgb(255, 215, 0));"></span>';
  239. }
  240. }
  241.  
  242. /** @type {(sec: number) => string} */
  243. const formatTimespan = (sec) => {
  244. let sign;
  245. if (sec >= 0) {
  246. sign = "";
  247. } else {
  248. sign = "-";
  249. sec *= -1;
  250. }
  251. return `${sign}${Math.floor(sec / 60)}:${`0${sec % 60}`.slice(-2)}`;
  252. };
  253.  
  254. /** 現在のページから,コンテストの開始から終了までの秒数を抽出する
  255. * @type {() => number}
  256. */
  257. const getContestDurationSec = () => (endTime - startTime) / 1000;
  258.  
  259. let working = false;
  260.  
  261. /** 順位表更新時の処理:テーブル追加
  262. * @type {(v: Standings) => void} */
  263. const onStandingsChanged = async (standings) => {
  264. if (!standings) return;
  265. if (working) return;
  266. working = true;
  267.  
  268. { // remove old contents
  269. const oldContents = document.getElementById("acssa-contents");
  270. if (oldContents) {
  271. // oldContents.parentNode.removeChild(oldContents);
  272. oldContents.remove();
  273. }
  274. }
  275.  
  276. const tasks = standings.TaskInfo;
  277. const standingsData = standings.StandingsData; // vueStandings.filteredStandings;
  278. // console.log(tasks, standings);
  279.  
  280. /** @type {Map<number, number[]>} */
  281. const scoreLastAcceptedTimeMap = new Map();
  282.  
  283. // コンテスト中かどうか判別する
  284. let isDuringContest = true;
  285. for (let i = 0; i < standingsData.length; ++i) {
  286. const standingsEntry = standingsData[i];
  287. if (standingsEntry.OldRating > 0) {
  288. isDuringContest = false;
  289. break;
  290. }
  291. }
  292.  
  293. /** 各問題の正答者数.
  294. * @type {number[]} */
  295. const taskAcceptedCounts = Array(tasks.length);
  296. taskAcceptedCounts.fill(0);
  297.  
  298. /** 各問題の正答時間リスト.秒単位で格納する.
  299. * @type {number[][]} */
  300. const taskAcceptedElapsedTimes = [...Array(tasks.length)].map((_, i) => []);
  301. // taskAcceptedElapsedTimes.fill([]); // これだと同じインスタンスで埋めてしまう
  302.  
  303. /** 内部レートのリスト.
  304. * @type {number[]} */
  305. const innerRatings = [];
  306.  
  307. const NS2SEC = 1000000000;
  308.  
  309. /** @type {{[key: string]: number}} */
  310. const innerRatingsFromPredictor = await (await fetch(`https://data.ac-predictor.com/aperfs/${contestScreenName}.json`)).json();
  311.  
  312. // 順位表情報を走査する(内部レートのリストと正答時間リストを構築する)
  313. let participants = 0;
  314. for (let i = 0; i < standingsData.length; ++i) {
  315. const standingsEntry = standingsData[i];
  316.  
  317. if (!standingsEntry.TaskResults) continue; // 参加登録していない
  318. if (standingsEntry.UserIsDeleted) continue; // アカウント削除
  319. const correctedRating = isDuringContest ? standingsEntry.Rating : standingsEntry.OldRating;
  320. if (correctedRating === 0) continue; // 初参加
  321. participants++;
  322.  
  323. // これは飛ばしちゃダメ(提出しても 0 AC だと Penalty == 0 なので)
  324. // if (standingsEntry.TotalResult.Score == 0 && standingsEntry.TotalResult.Penalty == 0) continue;
  325.  
  326. let score = 0;
  327. let penalty = 0;
  328. for (let j = 0; j < tasks.length; ++j) {
  329. const taskResultEntry = standingsEntry.TaskResults[tasks[j].TaskScreenName];
  330. if (!taskResultEntry) continue; // 未提出
  331. score += taskResultEntry.Score;
  332. penalty += (taskResultEntry.Score === 0 ? taskResultEntry.Failure : taskResultEntry.Penalty);
  333. }
  334. if (score === 0 && penalty === 0) continue; // NoSub を飛ばす
  335. // console.log(i + 1, score, penalty);
  336.  
  337. score /= 100;
  338. if (scoreLastAcceptedTimeMap.has(score)) {
  339. scoreLastAcceptedTimeMap.get(score).push(standingsEntry.TotalResult.Elapsed / NS2SEC)
  340. } else {
  341. scoreLastAcceptedTimeMap.set(score, [standingsEntry.TotalResult.Elapsed / NS2SEC]);
  342. }
  343.  
  344. const innerRating = (standingsEntry.UserScreenName in innerRatingsFromPredictor)
  345. ? innerRatingsFromPredictor[standingsEntry.UserScreenName]
  346. : RatingConverter.toInnerRating(
  347. Math.max(RatingConverter.toRealRating(correctedRating), 1), standingsEntry.Competitions);
  348. if (innerRating) innerRatings.push(innerRating);
  349. else {
  350. console.log(i, innerRating, rating, standingsEntry.Competitions);
  351. continue;
  352. }
  353. for (let j = 0; j < tasks.length; ++j) {
  354. const taskResultEntry = standingsEntry.TaskResults[tasks[j].TaskScreenName];
  355. const isAccepted = (taskResultEntry?.Score > 0);
  356. if (isAccepted) {
  357. ++taskAcceptedCounts[j];
  358. taskAcceptedElapsedTimes[j].push(taskResultEntry.Elapsed / NS2SEC);
  359. }
  360. }
  361. }
  362. innerRatings.sort((a, b) => a - b);
  363.  
  364. const dc = new DifficultyCalculator(innerRatings);
  365.  
  366. const plotlyDifficultyChartId = 'acssa-mydiv-difficulty';
  367. const plotlyLastAcceptedCountChartId = 'acssa-mydiv-accepted-count';
  368. const plotlyLastAcceptedTimeChartId = 'acssa-mydiv-accepted-time';
  369. $('#vue-standings').prepend(`
  370. <div id="acssa-contents">
  371. <table id="acssa-table" class="table table-bordered table-hover th-center td-center td-middle">
  372. <tbody>
  373. <tr id="acssa-thead"></tr>
  374. </tbody>
  375. <tbody>
  376. <tr id="acssa-tbody"></tr>
  377. </tbody>
  378. </table>
  379. <ul class="nav nav-pills small" id="acssa-chart-tab">
  380. <li class="active">
  381. <a class="acssa-chart-tab-button"><span class="glyphicon glyphicon-stats" aria-hidden="true"></span>Difficulty</a></li>
  382. <li>
  383. <a class="acssa-chart-tab-button"><span class="glyphicon glyphicon-stats" aria-hidden="true"></span>AC Count</a></li>
  384. <li>
  385. <a class="acssa-chart-tab-button"><span class="glyphicon glyphicon-stats" aria-hidden="true"></span>LastAcceptedTime</a></li>
  386. </ul>
  387. <div id="acssa-loader" class="loader acssa-loader-wrapper">
  388. <div class="loader-inner ball-pulse">
  389. <div></div>
  390. <div></div>
  391. <div></div>
  392. </div>
  393. </div>
  394. <div id="acssa-chart-block">
  395. <div class="acssa-chart-wrapper acssa-chart-wrapper-active" id="${plotlyDifficultyChartId}-wrapper">
  396. <div id="${plotlyDifficultyChartId}" style="width:100%;"></div>
  397. </div>
  398. <div class="acssa-chart-wrapper" id="${plotlyLastAcceptedCountChartId}-wrapper">
  399. <div id="${plotlyLastAcceptedCountChartId}" style="width:100%;"></div>
  400. </div>
  401. <div class="acssa-chart-wrapper" id="${plotlyLastAcceptedTimeChartId}-wrapper">
  402. <div id="${plotlyLastAcceptedTimeChartId}" style="width:100%;"></div>
  403. </div>
  404. </div>
  405. </div>
  406. `);
  407.  
  408. let activeTab = 0;
  409. document.querySelectorAll(".acssa-chart-tab-button").forEach((btn, key) => {
  410. btn.addEventListener("click", () => {
  411. // check whether active or not
  412. if (btn.parentElement.className == "active") return;
  413. // modify visibility
  414. activeTab = key;
  415. document.querySelector("#acssa-chart-tab li.active").classList.remove("active");
  416. document.querySelector(`#acssa-chart-tab li:nth-child(${key + 1})`).classList.add("active");
  417. document.querySelector("#acssa-chart-block div.acssa-chart-wrapper-active").classList.remove("acssa-chart-wrapper-active");
  418. document.querySelector(`#acssa-chart-block div.acssa-chart-wrapper:nth-child(${key + 1})`).classList.add("acssa-chart-wrapper-active");
  419. // resize charts
  420. switch (key) {
  421. case 0:
  422. Plotly.relayout(plotlyDifficultyChartId, { width: document.getElementById(plotlyDifficultyChartId).clientWidth });
  423. break;
  424. case 1:
  425. Plotly.relayout(plotlyLastAcceptedCountChartId, { width: document.getElementById(plotlyLastAcceptedCountChartId).clientWidth });
  426. break;
  427. case 2:
  428. Plotly.relayout(plotlyLastAcceptedTimeChartId, { width: document.getElementById(plotlyLastAcceptedTimeChartId).clientWidth });
  429. break;
  430. default:
  431. break;
  432. }
  433. });
  434. });
  435.  
  436. // 現在の Difficulty テーブルを構築する
  437. for (let j = 0; j < tasks.length; ++j) {
  438. const correctedDifficulty = RatingConverter.toCorrectedRating(dc.binarySearch(taskAcceptedCounts[j]));
  439. document.getElementById("acssa-thead").insertAdjacentHTML("beforeend", `
  440. <td>${tasks[j].Assignment}</td>
  441. `);
  442. const id = `td-assa-difficulty-${j}`;
  443. document.getElementById("acssa-tbody").insertAdjacentHTML("beforeend", `
  444. <td id="${id}" style="color:${getColor(correctedDifficulty)};">
  445. ${correctedDifficulty === 9999 ? '-' : correctedDifficulty}</td>
  446. `);
  447. if (correctedDifficulty !== 9999) {
  448. document.getElementById(id).insertAdjacentHTML(
  449. "afterbegin", generateDifficultyCircle(correctedDifficulty));
  450. }
  451. }
  452.  
  453. // 順位表のその他の描画を優先するために,後回しにする
  454. setTimeout(() => {
  455. const maxAcceptedCount = taskAcceptedCounts.reduce((a, b) => Math.max(a, b));
  456. const yMax = RatingConverter.toCorrectedRating(dc.binarySearch(1));
  457. const yMin = RatingConverter.toCorrectedRating(dc.binarySearch(Math.max(2, maxAcceptedCount)));
  458.  
  459. // 以降の計算は時間がかかる
  460.  
  461. taskAcceptedElapsedTimes.forEach(ar => {
  462. ar.sort((a, b) => a - b);
  463. });
  464.  
  465. // 時系列データの準備
  466. /** @type {{x: number, y: number, type: string, name: string}[]} */
  467. const difficultyChartData = [];
  468. const acceptedCountChartData = [];
  469. for (let j = 0; j < tasks.length; ++j) { //
  470. const interval = Math.ceil(taskAcceptedCounts[j] / 160);
  471. /** @type {number[]} */
  472. const taskAcceptedElapsedTimesForChart = taskAcceptedElapsedTimes[j].reduce((ar, tm, idx) => {
  473. if (idx % interval == 0 || idx == taskAcceptedCounts[j] - 1) ar.push(tm);
  474. return ar;
  475. }, []);
  476.  
  477. difficultyChartData.push({
  478. x: taskAcceptedElapsedTimesForChart,
  479. y: taskAcceptedElapsedTimesForChart.map((_, i) => dc.binarySearch(interval * i + 1)),
  480. type: 'scatter',
  481. name: `${tasks[j].Assignment}`,
  482. });
  483. acceptedCountChartData.push({
  484. x: taskAcceptedElapsedTimesForChart,
  485. y: taskAcceptedElapsedTimesForChart.map((_, i) => (interval * i + 1)),
  486. type: 'scatter',
  487. name: `${tasks[j].Assignment}`,
  488. });
  489. }
  490.  
  491. // 得点と提出時間データの準備
  492. /** @type {{x: number, y: number, type: string, name: string}[]} */
  493. const lastAcceptedTimeChartData = [];
  494. const scores = [...scoreLastAcceptedTimeMap.keys()];
  495. scores.sort((a, b) => b - a);
  496. let acc = 0;
  497. let maxAcceptedTime = 0;
  498. scores.forEach(score => {
  499. const lastAcceptedTimes = scoreLastAcceptedTimeMap.get(score);
  500. lastAcceptedTimes.sort((a, b) => a - b);
  501. const interval = Math.ceil(lastAcceptedTimes.length / 100);
  502. /** @type {number[]} */
  503. const lastAcceptedTimesForChart = lastAcceptedTimes.reduce((ar, tm, idx) => {
  504. if (idx % interval == 0 || idx == lastAcceptedTimes.length - 1) ar.push(tm);
  505. return ar;
  506. }, []);
  507. const lastAcceptedTimesRanks = lastAcceptedTimes.reduce((ar, tm, idx) => {
  508. if (idx % interval == 0 || idx == lastAcceptedTimes.length - 1) ar.push(acc + idx + 1);
  509. return ar;
  510. }, []);
  511.  
  512. lastAcceptedTimeChartData.push({
  513. x: lastAcceptedTimesRanks,
  514. y: lastAcceptedTimesForChart,
  515. type: 'scatter',
  516. name: `${score}`,
  517. });
  518.  
  519. acc += lastAcceptedTimes.length;
  520. if (lastAcceptedTimes[lastAcceptedTimes.length - 1] > maxAcceptedTime) {
  521. maxAcceptedTime = lastAcceptedTimes[lastAcceptedTimes.length - 1];
  522. }
  523. });
  524.  
  525. // 軸フォーマットをカスタムする
  526. // Support specifying a function for tickformat · Issue #1464 · plotly/plotly.js
  527. // https://github.com/plotly/plotly.js/issues/1464#issuecomment-498050894
  528. {
  529. const org_locale = Plotly.d3.locale;
  530. Plotly.d3.locale = (locale) => {
  531. const result = org_locale(locale);
  532. const org_number_format = result.numberFormat;
  533. result.numberFormat = (format) => {
  534. if (format != 'TIME') {
  535. return org_number_format(format)
  536. }
  537. return (x) => formatTimespan(x).toString();
  538. }
  539. return result;
  540. };
  541. }
  542.  
  543. // 背景用設定
  544. const alpha = 0.3;
  545. /** @type {[number, number, string][]} */
  546. const colors = [
  547. [0, 400, `rgba(128,128,128,${alpha})`],
  548. [400, 800, `rgba(128,0,0,${alpha})`],
  549. [800, 1200, `rgba(0,128,0,${alpha})`],
  550. [1200, 1600, `rgba(0,255,255,${alpha})`],
  551. [1600, 2000, `rgba(0,0,255,${alpha})`],
  552. [2000, 2400, `rgba(255,255,0,${alpha})`],
  553. [2400, 2800, `rgba(255,165,0,${alpha})`],
  554. [2800, 10000, `rgba(255,0,0,${alpha})`],
  555. ];
  556.  
  557. // Difficulty Chart 描画
  558. {
  559. // 描画
  560. const duration = getContestDurationSec();
  561. const layout = {
  562. title: 'Difficulty',
  563. xaxis: {
  564. dtick: 60 * 10,
  565. tickformat: 'TIME',
  566. range: [0, duration],
  567. // title: { text: 'Elapsed' }
  568. },
  569. yaxis: {
  570. dtick: 400,
  571. tickformat: 'd',
  572. range: [
  573. Math.max(0, Math.floor((yMin - 100) / 400) * 400),
  574. Math.max(0, Math.ceil((yMax + 100) / 400) * 400)
  575. ],
  576. // title: { text: 'Difficulty' }
  577. },
  578. shapes: colors.map(c => {
  579. return {
  580. type: 'rect',
  581. layer: 'below',
  582. xref: 'x',
  583. yref: 'y',
  584. x0: 0,
  585. x1: duration,
  586. y0: c[0],
  587. y1: c[1],
  588. line: { width: 0 },
  589. fillcolor: c[2]
  590. };
  591. }),
  592. margin: {
  593. b: 60,
  594. t: 30,
  595. }
  596. };
  597. const config = { autosize: true };
  598. Plotly.newPlot(plotlyDifficultyChartId, difficultyChartData, layout, config);
  599.  
  600. window.addEventListener('resize', () => {
  601. if (activeTab == 0)
  602. Plotly.relayout(plotlyDifficultyChartId, { width: document.getElementById(plotlyDifficultyChartId).clientWidth });
  603. });
  604. }
  605.  
  606. // Accepted Count Chart 描画
  607. {
  608. const yMax = participants;
  609. /** @type {[number, number, string][]} */
  610. const rectSpans = colors.reduce((ar, cur) => {
  611. const bottom = dc.perf2ExpectedAcceptedCount(cur[1]);
  612. if (bottom > yMax) return ar;
  613. const top = (cur[0] == 0) ? yMax : dc.perf2ExpectedAcceptedCount(cur[0]);
  614. ar.push([Math.max(0, bottom), Math.min(yMax, top), cur[2]]);
  615. return ar;
  616. }, []);
  617. // 描画
  618. const duration = getContestDurationSec();
  619. const layout = {
  620. title: 'Accepted Count',
  621. xaxis: {
  622. dtick: 60 * 10,
  623. tickformat: 'TIME',
  624. range: [0, duration],
  625. // title: { text: 'Elapsed' }
  626. },
  627. yaxis: {
  628. // dtick: 100,
  629. tickformat: 'd',
  630. range: [
  631. 0,
  632. yMax
  633. ],
  634. // title: { text: 'Difficulty' }
  635. },
  636. shapes: rectSpans.map(span => {
  637. return {
  638. type: 'rect',
  639. layer: 'below',
  640. xref: 'x',
  641. yref: 'y',
  642. x0: 0,
  643. x1: duration,
  644. y0: span[0],
  645. y1: span[1],
  646. line: { width: 0 },
  647. fillcolor: span[2]
  648. };
  649. }),
  650. margin: {
  651. b: 60,
  652. t: 30,
  653. }
  654. };
  655. const config = { autosize: true };
  656. Plotly.newPlot(plotlyLastAcceptedCountChartId, acceptedCountChartData, layout, config);
  657.  
  658. window.addEventListener('resize', () => {
  659. if (activeTab == 1)
  660. Plotly.relayout(plotlyLastAcceptedCountChartId, { width: document.getElementById(plotlyLastAcceptedCountChartId).clientWidth });
  661. });
  662. }
  663.  
  664. // LastAcceptedTime Chart 描画
  665. {
  666. const xMax = participants;
  667. const yMax = Math.ceil((maxAcceptedTime + 60 * 5) / (60 * 10)) * (60 * 10);
  668. /** @type {[number, number, string][]} */
  669. const rectSpans = colors.reduce((ar, cur) => {
  670. const right = (cur[0] == 0) ? xMax : dc.perf2Ranking(cur[0]);
  671. if (right < 1) return ar;
  672. const left = dc.perf2Ranking(cur[1]);
  673. if (left > xMax) return ar;
  674. ar.push([Math.max(0, left), Math.min(xMax, right), cur[2]]);
  675. return ar;
  676. }, []);
  677. // console.log(colors);
  678. // console.log(rectSpans);
  679. const layout = {
  680. title: 'LastAcceptedTime v.s. Rank',
  681. xaxis: {
  682. // dtick: 100,
  683. tickformat: 'd',
  684. range: [0, xMax],
  685. // title: { text: 'Elapsed' }
  686. },
  687. yaxis: {
  688. dtick: 60 * 10,
  689. tickformat: 'TIME',
  690. range: [0, yMax],
  691. // range: [
  692. // Math.max(0, Math.floor((yMin - 100) / 400) * 400),
  693. // Math.max(0, Math.ceil((yMax + 100) / 400) * 400)
  694. // ],
  695. // title: { text: 'Difficulty' }
  696. },
  697. shapes: rectSpans.map(span => {
  698. return {
  699. type: 'rect',
  700. layer: 'below',
  701. xref: 'x',
  702. yref: 'y',
  703. x0: span[0],
  704. x1: span[1],
  705. y0: 0,
  706. y1: yMax,
  707. line: { width: 0 },
  708. fillcolor: span[2]
  709. };
  710. }),
  711. margin: {
  712. b: 60,
  713. t: 30,
  714. }
  715. };
  716. const config = { autosize: true };
  717. Plotly.newPlot(plotlyLastAcceptedTimeChartId, lastAcceptedTimeChartData, layout, config);
  718.  
  719. window.addEventListener('resize', () => {
  720. if (activeTab == 2)
  721. Plotly.relayout(plotlyLastAcceptedTimeChartId, { width: document.getElementById(plotlyLastAcceptedTimeChartId).clientWidth });
  722. });
  723. }
  724.  
  725. document.getElementById('acssa-loader').style.display = 'none';
  726. working = false;
  727. }, 100); // end setTimeout()
  728. };
  729.  
  730. // MAIN
  731. vueStandings.$watch('standings', onStandingsChanged, { deep: true, immediate: true });
  732.  
  733. })();