Greasy Fork 还支持 简体中文。

atcoder-standings-difficulty-analyzer

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

目前為 2021-06-27 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name atcoder-standings-difficulty-analyzer
  3. // @namespace iilj
  4. // @version 2021.6.27.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 1 のとき満点? 6 のとき部分点?
  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 所属.IsTeam = true のときは,チームメンバを「, 」で結合した文字列.
  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, moment, userScreenName, Plotly */
  82.  
  83. (async () => {
  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-tab-wrapper {
  107. display: none;
  108. }
  109. #acssa-chart-tab, #acssa-checkbox-tab {
  110. margin-bottom: 0.5rem;
  111. display: inline-block;
  112. }
  113. #acssa-chart-tab a, #acssa-checkbox-tab label, #acssa-checkbox-tab label input {
  114. cursor: pointer;
  115. }
  116. #acssa-chart-tab span.glyphicon {
  117. margin-right: 0.5rem;
  118. }
  119. #acssa-checkbox-tab label, #acssa-checkbox-tab input {
  120. margin: 0;
  121. }
  122. #acssa-checkbox-tab li a {
  123. color: black;
  124. }
  125. #acssa-checkbox-tab li a:hover {
  126. background-color: transparent;
  127. }
  128. .acssa-chart-wrapper {
  129. display: none;
  130. }
  131. .acssa-chart-wrapper.acssa-chart-wrapper-active {
  132. display: block;
  133. }
  134. .table>tbody>tr>td.success.acssa-task-success.acssa-task-success-suppress {
  135. background-color: transparent;
  136. }
  137. #acssa-checkbox-toggle-log-plot-parent {
  138. display: none;
  139. }
  140. `;
  141. GM_addStyle(loaderStyles + loaderWrapperStyles);
  142.  
  143. class RatingConverter {
  144. /** 表示用の低レート帯補正レート → 低レート帯補正前のレート
  145. * @type {(correctedRating: number) => number} */
  146. static toRealRating = (correctedRating) => {
  147. if (correctedRating >= 400) return correctedRating;
  148. else return 400 * (1 - Math.log(400 / correctedRating));
  149. };
  150.  
  151. /** 低レート帯補正前のレート → 内部レート推定値
  152. * @type {(correctedRating: number) => number} */
  153. static toInnerRating = (realRating, comp) => {
  154. return realRating + 1200 * (Math.sqrt(1 - Math.pow(0.81, comp)) / (1 - Math.pow(0.9, comp)) - 1) / (Math.sqrt(19) - 1);
  155. };
  156.  
  157. /** 低レート帯補正前のレート → 表示用の低レート帯補正レート
  158. * @type {(correctedRating: number) => number} */
  159. static toCorrectedRating = (realRating) => {
  160. if (realRating >= 400) return realRating;
  161. else return Math.floor(400 / Math.exp((400 - realRating) / 400));
  162. };
  163. }
  164.  
  165. class DifficultyCalculator {
  166. /** @constructor
  167. * @type {(sortedInnerRatings: number[]) => DifficultyCalculator}
  168. */
  169. constructor(sortedInnerRatings) {
  170. this.innerRatings = sortedInnerRatings;
  171. /** @type {Map<number, number>} */
  172. this.prepared = new Map();
  173. /** @type {Map<number, number>} */
  174. this.memo = new Map();
  175. }
  176.  
  177. perf2ExpectedAcceptedCount = (m) => {
  178. let expectedAcceptedCount;
  179. if (this.prepared.has(m)) {
  180. expectedAcceptedCount = this.prepared.get(m);
  181. } else {
  182. expectedAcceptedCount = this.innerRatings.reduce((prev_expected_accepts, innerRating) =>
  183. prev_expected_accepts += 1 / (1 + Math.pow(6, (m - innerRating) / 400)), 0);
  184. this.prepared.set(m, expectedAcceptedCount);
  185. }
  186. return expectedAcceptedCount;
  187. };
  188.  
  189. perf2Ranking = (x) => this.perf2ExpectedAcceptedCount(x) + 0.5;
  190.  
  191. /** Difficulty 推定値を算出する
  192. * @type {((acceptedCount: number) => number)} */
  193. binarySearch = (acceptedCount) => {
  194. if (this.memo.has(acceptedCount)) {
  195. return this.memo.get(acceptedCount);
  196. }
  197. let lb = -10000;
  198. let ub = 10000;
  199. while (ub - lb > 1) {
  200. const m = Math.floor((ub + lb) / 2);
  201. const expectedAcceptedCount = this.perf2ExpectedAcceptedCount(m);
  202.  
  203. if (expectedAcceptedCount < acceptedCount) ub = m;
  204. else lb = m;
  205. }
  206. const difficulty = lb
  207. const correctedDifficulty = RatingConverter.toCorrectedRating(difficulty);
  208. this.memo.set(acceptedCount, correctedDifficulty);
  209. return correctedDifficulty;
  210. };
  211. }
  212.  
  213. /** @type {(ar: number[], n: number) => number} */
  214. const arrayLowerBound = (arr, n) => {
  215. let first = 0, last = arr.length - 1, middle;
  216. while (first <= last) {
  217. middle = 0 | (first + last) / 2;
  218. if (arr[middle] < n) first = middle + 1;
  219. else last = middle - 1;
  220. }
  221. return first;
  222. };
  223.  
  224. /** @type {(rating: number) => string} */
  225. const getColor = (rating) => {
  226. if (rating < 400) return '#808080'; // gray
  227. else if (rating < 800) return '#804000'; // brown
  228. else if (rating < 1200) return '#008000'; // green
  229. else if (rating < 1600) return '#00C0C0'; // cyan
  230. else if (rating < 2000) return '#0000FF'; // blue
  231. else if (rating < 2400) return '#C0C000'; // yellow
  232. else if (rating < 2800) return '#FF8000'; // orange
  233. else if (rating == 9999) return '#000000';
  234. return '#FF0000'; // red
  235. };
  236.  
  237. /** レートを表す難易度円(◒)の HTML 文字列を生成
  238. * @type {(rating: number, isSmall?: boolean) => string} */
  239. const generateDifficultyCircle = (rating, isSmall = true) => {
  240. const size = (isSmall ? 12 : 36);
  241. const borderWidth = (isSmall ? 1 : 3);
  242.  
  243. const style = `display:inline-block;border-radius:50%;border-style:solid;border-width:${borderWidth}px;`
  244. + `margin-right:5px;vertical-align:initial;height:${size}px;width:${size}px;`;
  245.  
  246. if (rating < 3200) {
  247. // 色と円がどのぐらい満ちているかを計算
  248. const color = getColor(rating);
  249. const percentFull = (rating % 400) / 400 * 100;
  250.  
  251. // ◒を生成
  252. return `
  253. <span style='${style}border-color:${color};background:`
  254. + `linear-gradient(to top, ${color} 0%, ${color} ${percentFull}%, `
  255. + `rgba(0, 0, 0, 0) ${percentFull}%, rgba(0, 0, 0, 0) 100%); '>
  256. </span>`;
  257.  
  258. }
  259. // 金銀銅は例外処理
  260. else if (rating < 3600) {
  261. return `<span style="${style}border-color: rgb(150, 92, 44);`
  262. + 'background: linear-gradient(to right, rgb(150, 92, 44), rgb(255, 218, 189), rgb(150, 92, 44));"></span>';
  263. } else if (rating < 4000) {
  264. return `<span style="${style}border-color: rgb(128, 128, 128);`
  265. + 'background: linear-gradient(to right, rgb(128, 128, 128), white, rgb(128, 128, 128));"></span>';
  266. } else {
  267. return `<span style="${style}border-color: rgb(255, 215, 0);`
  268. + 'background: linear-gradient(to right, rgb(255, 215, 0), white, rgb(255, 215, 0));"></span>';
  269. }
  270. }
  271.  
  272. /** @type {(sec: number) => string} */
  273. const formatTimespan = (sec) => {
  274. let sign;
  275. if (sec >= 0) {
  276. sign = "";
  277. } else {
  278. sign = "-";
  279. sec *= -1;
  280. }
  281. return `${sign}${Math.floor(sec / 60)}:${`0${sec % 60}`.slice(-2)}`;
  282. };
  283.  
  284. /** 現在のページから,コンテストの開始から終了までの秒数を抽出する
  285. * @type {() => number}
  286. */
  287. const getContestDurationSec = () => {
  288. if (contestScreenName.startsWith("past")) {
  289. return 300 * 60;
  290. }
  291. return (endTime - startTime) / 1000;
  292. };
  293.  
  294. /** @type {(contestScreenName: string) => number} */
  295. const getCenterOfInnerRating = (contestScreenName) => {
  296. if (contestScreenName.startsWith("agc")) {
  297. const contestNumber = Number(contestScreenName.substring(3, 6));
  298. return (contestNumber >= 34) ? 1200 : 1600;
  299. }
  300. if (contestScreenName.startsWith("arc")) {
  301. const contestNumber = Number(contestScreenName.substring(3, 6));
  302. return (contestNumber >= 104) ? 1000 : 1600;
  303. }
  304. return 800;
  305. };
  306. const centerOfInnerRating = getCenterOfInnerRating(contestScreenName);
  307.  
  308. // AC 数推移モデルファイルを取得する
  309. let modelLocation = undefined;
  310. /** @type{{[key:string]: number[]}} */
  311. let acRatioModel = undefined;
  312. if (moment() < endTime) {
  313. const contestDurationMinutes = (endTime - startTime) / 1000 / 60;
  314. // https://raw.githubusercontent.com/iilj/atcoder-standings-difficulty-analyzer/main/json/standings/abc_100m.json
  315. const baseUrl = 'https://raw.githubusercontent.com/iilj/atcoder-standings-difficulty-analyzer/main/json/standings';
  316. if (contestScreenName.match(/^agc(\d{3,})$/)) {
  317. if ([110, 120, 130, 140, 150, 160, 180, 200, 210, 240, 270, 300].includes(contestDurationMinutes)) {
  318. modelLocation = `${baseUrl}/agc_${contestDurationMinutes}m.json`;
  319. }
  320. } else if (contestScreenName.match(/^arc(\d{3,})$/)) {
  321. if ([100, 120, 150], includes(contestDurationMinutes)) {
  322. modelLocation = `${baseUrl}/arc_${contestDurationMinutes}m.json`;
  323. }
  324. } else if (contestScreenName.match(/^abc(\d{3,})$/)) {
  325. if ([100, 120].includes(contestDurationMinutes)) {
  326. modelLocation = `${baseUrl}/abc_${contestDurationMinutes}m.json`;
  327. }
  328. }
  329. if (modelLocation !== undefined) {
  330. const acRatioModelFetchResult = await fetch(modelLocation);
  331. acRatioModel = await acRatioModelFetchResult.json();
  332. }
  333. }
  334.  
  335. let working = false;
  336. let oldStandingsData = null;
  337.  
  338. /** 順位表更新時の処理:テーブル追加
  339. * @type {(v: Standings) => void} */
  340. const onStandingsChanged = async (standings) => {
  341. if (!standings) return;
  342. if (working) return;
  343.  
  344. const tasks = standings.TaskInfo;
  345. const standingsData = standings.StandingsData; // vueStandings.filteredStandings;
  346.  
  347. if (oldStandingsData === standingsData) return;
  348. if (tasks.length === 0) return;
  349. oldStandingsData = standingsData;
  350. working = true;
  351. // console.log(standings);
  352.  
  353. { // remove old contents
  354. const oldContents = document.getElementById("acssa-contents");
  355. if (oldContents) {
  356. // oldContents.parentNode.removeChild(oldContents);
  357. oldContents.remove();
  358. }
  359. }
  360.  
  361. /** @type {number} */
  362. const currentTime = moment();
  363. const elapsedMinutes = Math.floor(currentTime.diff(startTime) / 60 / 1000);
  364. const estimationIsEnabled = (startTime < currentTime && currentTime < endTime && elapsedMinutes >= 1 && tasks.length < 10);
  365.  
  366. /** 問題ごとの最終 AC 時刻リスト.
  367. * @type {Map<number, number[]>} */
  368. const scoreLastAcceptedTimeMap = new Map();
  369.  
  370. // コンテスト中かどうか判別する
  371. let isDuringContest = true;
  372. for (let i = 0; i < standingsData.length; ++i) {
  373. const standingsEntry = standingsData[i];
  374. if (standingsEntry.OldRating > 0) {
  375. isDuringContest = false;
  376. break;
  377. }
  378. }
  379.  
  380. /** 各問題の正答者数.
  381. * @type {number[]} */
  382. const taskAcceptedCounts = Array(tasks.length);
  383. taskAcceptedCounts.fill(0);
  384.  
  385. /** 各問題の正答時間リスト.秒単位で格納する.
  386. * @type {number[][]} */
  387. const taskAcceptedElapsedTimes = [...Array(tasks.length)].map((_, i) => []);
  388. // taskAcceptedElapsedTimes.fill([]); // これだと同じインスタンスで埋めてしまう
  389.  
  390. /** 内部レートのリスト.
  391. * @type {number[]} */
  392. const innerRatings = [];
  393.  
  394. const NS2SEC = 1000000000;
  395.  
  396. /** @type {{[key: string]: number}} */
  397. const innerRatingsFromPredictor = await (async () => {
  398. try {
  399. const res = await fetch(`https://data.ac-predictor.com/aperfs/${contestScreenName}.json`);
  400. if (res.ok) return await res.json();
  401. } catch (e) {
  402. console.warn(e);
  403. }
  404. return {};
  405. })();
  406.  
  407. /** 現在のユーザの各問題の AC 時刻.
  408. * @type {number[]} */
  409. const yourTaskAcceptedElapsedTimes = Array(tasks.length);
  410. yourTaskAcceptedElapsedTimes.fill(-1);
  411. /** 現在のユーザのスコア */
  412. let yourScore = -1;
  413. /** 現在のユーザの最終 AC 時刻 */
  414. let yourLastAcceptedTime = -1;
  415.  
  416. // 順位表情報を走査する(内部レートのリストと正答時間リストを構築する)
  417. let participants = 0;
  418. for (let i = 0; i < standingsData.length; ++i) {
  419. const standingsEntry = standingsData[i];
  420.  
  421. if (!standingsEntry.TaskResults) continue; // 参加登録していない
  422. if (standingsEntry.UserIsDeleted) continue; // アカウント削除
  423. let correctedRating = isDuringContest ? standingsEntry.Rating : standingsEntry.OldRating;
  424. const isTeamOrBeginner = (correctedRating === 0);
  425. if (isTeamOrBeginner) {
  426. // continue; // 初参加 or チーム
  427. correctedRating = centerOfInnerRating;
  428. }
  429.  
  430. // これは飛ばしちゃダメ(提出しても 0 AC だと Penalty == 0 なので)
  431. // if (standingsEntry.TotalResult.Score == 0 && standingsEntry.TotalResult.Penalty == 0) continue;
  432.  
  433. let score = 0;
  434. let penalty = 0;
  435. for (let j = 0; j < tasks.length; ++j) {
  436. const taskResultEntry = standingsEntry.TaskResults[tasks[j].TaskScreenName];
  437. if (!taskResultEntry) continue; // 未提出
  438. score += taskResultEntry.Score;
  439. penalty += (taskResultEntry.Score === 0 ? taskResultEntry.Failure : taskResultEntry.Penalty);
  440. }
  441. if (score === 0 && penalty === 0) continue; // NoSub を飛ばす
  442. participants++;
  443. // console.log(i + 1, score, penalty);
  444.  
  445. score /= 100;
  446. if (scoreLastAcceptedTimeMap.has(score)) {
  447. scoreLastAcceptedTimeMap.get(score).push(standingsEntry.TotalResult.Elapsed / NS2SEC)
  448. } else {
  449. scoreLastAcceptedTimeMap.set(score, [standingsEntry.TotalResult.Elapsed / NS2SEC]);
  450. }
  451.  
  452. const innerRating = isTeamOrBeginner
  453. ? correctedRating
  454. : (standingsEntry.UserScreenName in innerRatingsFromPredictor)
  455. ? innerRatingsFromPredictor[standingsEntry.UserScreenName]
  456. : RatingConverter.toInnerRating(
  457. Math.max(RatingConverter.toRealRating(correctedRating), 1), standingsEntry.Competitions);
  458. if (innerRating) innerRatings.push(innerRating);
  459. else {
  460. console.log(i, innerRating, correctedRating, standingsEntry.Competitions);
  461. continue;
  462. }
  463. for (let j = 0; j < tasks.length; ++j) {
  464. const taskResultEntry = standingsEntry.TaskResults[tasks[j].TaskScreenName];
  465. const isAccepted = (taskResultEntry?.Score > 0 && taskResultEntry?.Status == 1);
  466. if (isAccepted) {
  467. ++taskAcceptedCounts[j];
  468. taskAcceptedElapsedTimes[j].push(taskResultEntry.Elapsed / NS2SEC);
  469. }
  470. }
  471. if (standingsEntry.UserScreenName == userScreenName) {
  472. yourScore = score;
  473. yourLastAcceptedTime = standingsEntry.TotalResult.Elapsed / NS2SEC;
  474. for (let j = 0; j < tasks.length; ++j) {
  475. const taskResultEntry = standingsEntry.TaskResults[tasks[j].TaskScreenName];
  476. const isAccepted = (taskResultEntry?.Score > 0 && taskResultEntry?.Status == 1);
  477. if (isAccepted) {
  478. yourTaskAcceptedElapsedTimes[j] = taskResultEntry.Elapsed / NS2SEC;
  479. }
  480. }
  481. }
  482. }
  483. innerRatings.sort((a, b) => a - b);
  484.  
  485. /** @type {number[]} */
  486. const acCountPredicted = (!estimationIsEnabled) ? [] : (() => {
  487. // 時間ごとの AC 数推移を計算する
  488. const taskAcceptedCountImos = [...Array(tasks.length)].map((_, i) => [...Array(elapsedMinutes)].map((_, i) => 0));
  489. taskAcceptedElapsedTimes.forEach((ar, index) => {
  490. ar.forEach(seconds => {
  491. const minutes = Math.floor(seconds / 60);
  492. if (minutes >= elapsedMinutes) return;
  493. taskAcceptedCountImos[index][minutes] += 1
  494. });
  495. });
  496. /** @type {number[][]} */
  497. const taskAcceptedRatio = [...Array(tasks.length)].map((_, i) => []);
  498. taskAcceptedCountImos.forEach((ar, index) => {
  499. let cum = 0;
  500. ar.forEach(imos => {
  501. cum += imos;
  502. taskAcceptedRatio[index].push(cum / participants);
  503. })
  504. });
  505. // 差の自乗和が最小になるシーケンスを探す
  506. return taskAcceptedRatio.map(ar => {
  507. if (acRatioModel === undefined) return 0;
  508. if (ar[elapsedMinutes - 1] === 0) return 0;
  509. let minerror = 1.0 * elapsedMinutes;
  510. let argmin = '';
  511. let last_ratio = 0;
  512. Object.keys(acRatioModel).forEach(key => {
  513. const ar2 = acRatioModel[key];
  514. let error = 0;
  515. for (let i = 0; i < elapsedMinutes; ++i) {
  516. error += Math.pow(ar[i] - ar2[i], 2);
  517. }
  518. if (error < minerror) {
  519. minerror = error;
  520. argmin = key;
  521. if (ar2[elapsedMinutes - 1] > 0) {
  522. last_ratio = ar2[ar2.length - 1] * (ar[elapsedMinutes - 1] / ar2[elapsedMinutes - 1]);
  523. } else {
  524. last_ratio = ar2[ar2.length - 1];
  525. }
  526. }
  527. });
  528. // console.log(argmin, minerror, last_ratio);
  529. if (last_ratio > 1) last_ratio = 1;
  530. return participants * last_ratio;
  531. });
  532. })();
  533.  
  534. const dc = new DifficultyCalculator(innerRatings);
  535.  
  536. const plotlyDifficultyChartId = 'acssa-mydiv-difficulty';
  537. const plotlyAcceptedCountChartId = 'acssa-mydiv-accepted-count';
  538. const plotlyLastAcceptedTimeChartId = 'acssa-mydiv-accepted-time';
  539. const COL_PER_ROW = 20;
  540. $('#vue-standings').prepend(`
  541. <div id="acssa-contents">
  542. ${[...Array(Math.ceil(tasks.length / COL_PER_ROW)).keys()].map(tableIdx => `
  543. <table id="acssa-table-${tableIdx}" class="table table-bordered table-hover th-center td-center td-middle acssa-table">
  544. <tbody>
  545. <tr id="acssa-thead-${tableIdx}" class="acssa-thead"></tr>
  546. </tbody>
  547. <tbody>
  548. <tr id="acssa-tbody-${tableIdx}" class="acssa-tbody"></tr>
  549. ${(estimationIsEnabled) ? `<tr id="acssa-tbody-predicted-${tableIdx}" class="acssa-tbody"></tr>` : ''}
  550. </tbody>
  551. </table>
  552. `).join('')}
  553. <div id="acssa-tab-wrapper">
  554. <ul class="nav nav-pills small" id="acssa-chart-tab">
  555. <li class="active">
  556. <a class="acssa-chart-tab-button"><span class="glyphicon glyphicon-stats" aria-hidden="true"></span>Difficulty</a></li>
  557. <li>
  558. <a class="acssa-chart-tab-button"><span class="glyphicon glyphicon-stats" aria-hidden="true"></span>AC Count</a></li>
  559. <li>
  560. <a class="acssa-chart-tab-button"><span class="glyphicon glyphicon-stats" aria-hidden="true"></span>LastAcceptedTime</a></li>
  561. </ul>
  562. <ul class="nav nav-pills" id="acssa-checkbox-tab">
  563. <li>
  564. <a><label><input type="checkbox" id="acssa-checkbox-toggle-your-result-visibility" checked> Plot your result</label></a></li>
  565. <li id="acssa-checkbox-toggle-log-plot-parent">
  566. <a><label><input type="checkbox" id="acssa-checkbox-toggle-log-plot">Log plot</label></a></li>
  567. </ul>
  568. </div>
  569. <div id="acssa-loader" class="loader acssa-loader-wrapper">
  570. <div class="loader-inner ball-pulse">
  571. <div></div>
  572. <div></div>
  573. <div></div>
  574. </div>
  575. </div>
  576. <div id="acssa-chart-block">
  577. <div class="acssa-chart-wrapper acssa-chart-wrapper-active" id="${plotlyDifficultyChartId}-wrapper">
  578. <div id="${plotlyDifficultyChartId}" style="width:100%;"></div>
  579. </div>
  580. <div class="acssa-chart-wrapper" id="${plotlyAcceptedCountChartId}-wrapper">
  581. <div id="${plotlyAcceptedCountChartId}" style="width:100%;"></div>
  582. </div>
  583. <div class="acssa-chart-wrapper" id="${plotlyLastAcceptedTimeChartId}-wrapper">
  584. <div id="${plotlyLastAcceptedTimeChartId}" style="width:100%;"></div>
  585. </div>
  586. </div>
  587. </div>
  588. `);
  589.  
  590. // チェックボックス操作時のイベントを登録する
  591. /** @type {HTMLInputElement} */
  592. const checkbox = document.getElementById("acssa-checkbox-toggle-your-result-visibility");
  593. checkbox.addEventListener("change", () => {
  594. if (checkbox.checked) {
  595. document.querySelectorAll('.acssa-task-success.acssa-task-success-suppress').forEach(elm => {
  596. elm.classList.remove('acssa-task-success-suppress');
  597. });
  598. } else {
  599. document.querySelectorAll('.acssa-task-success').forEach(elm => {
  600. elm.classList.add('acssa-task-success-suppress');
  601. });
  602. }
  603. });
  604.  
  605. let activeTab = 0;
  606. const showYourResult = [true, true, true];
  607.  
  608. let yourDifficultyChartData = null;
  609. let yourAcceptedCountChartData = null;
  610. let yourLastAcceptedTimeChartData = null;
  611. let yourLastAcceptedTimeChartDataIndex = -1;
  612. const onCheckboxChanged = () => {
  613. showYourResult[activeTab] = checkbox.checked;
  614. if (checkbox.checked) {
  615. // show
  616. switch (activeTab) {
  617. case 0:
  618. if (yourScore > 0) Plotly.addTraces(plotlyDifficultyChartId, yourDifficultyChartData);
  619. break;
  620. case 1:
  621. if (yourScore > 0) Plotly.addTraces(plotlyAcceptedCountChartId, yourAcceptedCountChartData);
  622. break;
  623. case 2:
  624. if (yourLastAcceptedTimeChartDataIndex != -1) {
  625. Plotly.addTraces(plotlyLastAcceptedTimeChartId, yourLastAcceptedTimeChartData, yourLastAcceptedTimeChartDataIndex);
  626. }
  627. break;
  628. default:
  629. break;
  630. }
  631. } else {
  632. // hide
  633. switch (activeTab) {
  634. case 0:
  635. if (yourScore > 0) Plotly.deleteTraces(plotlyDifficultyChartId, -1);
  636. break;
  637. case 1:
  638. if (yourScore > 0) Plotly.deleteTraces(plotlyAcceptedCountChartId, -1);
  639. break;
  640. case 2:
  641. if (yourLastAcceptedTimeChartDataIndex != -1) {
  642. Plotly.deleteTraces(plotlyLastAcceptedTimeChartId, yourLastAcceptedTimeChartDataIndex);
  643. }
  644. break;
  645. default:
  646. break;
  647. }
  648. }
  649. };
  650.  
  651. /** @type {HTMLInputElement} */
  652. const logPlotCheckbox = document.getElementById('acssa-checkbox-toggle-log-plot');
  653. const logPlotCheckboxParent = document.getElementById('acssa-checkbox-toggle-log-plot-parent');
  654.  
  655. let acceptedCountYMax = -1;
  656. const useLogPlot = [false, false, false];
  657. const onLogPlotCheckboxChanged = () => {
  658. if (acceptedCountYMax == -1) return;
  659. useLogPlot[activeTab] = logPlotCheckbox.checked;
  660. if (activeTab == 1) {
  661. if (logPlotCheckbox.checked) {
  662. // log plot
  663. const layout = {
  664. yaxis: {
  665. type: 'log',
  666. range: [
  667. Math.log10(0.5),
  668. Math.log10(acceptedCountYMax)
  669. ],
  670. },
  671. };
  672. Plotly.relayout(plotlyAcceptedCountChartId, layout);
  673. } else {
  674. // linear plot
  675. const layout = {
  676. yaxis: {
  677. type: 'linear',
  678. range: [
  679. 0,
  680. acceptedCountYMax
  681. ],
  682. },
  683. };
  684. Plotly.relayout(plotlyAcceptedCountChartId, layout);
  685. }
  686. } else if (activeTab == 2) {
  687. if (logPlotCheckbox.checked) {
  688. // log plot
  689. const layout = {
  690. xaxis: {
  691. type: 'log',
  692. range: [
  693. Math.log10(0.5),
  694. Math.log10(participants)
  695. ],
  696. },
  697. };
  698. Plotly.relayout(plotlyLastAcceptedTimeChartId, layout);
  699. } else {
  700. // linear plot
  701. const layout = {
  702. xaxis: {
  703. type: 'linear',
  704. range: [
  705. 0,
  706. participants
  707. ],
  708. },
  709. };
  710. Plotly.relayout(plotlyLastAcceptedTimeChartId, layout);
  711. }
  712. }
  713. };
  714.  
  715. document.querySelectorAll(".acssa-chart-tab-button").forEach((btn, key) => {
  716. btn.addEventListener("click", () => {
  717. // check whether active or not
  718. if (btn.parentElement.className == "active") return;
  719. // modify visibility
  720. activeTab = key;
  721. document.querySelector("#acssa-chart-tab li.active").classList.remove("active");
  722. document.querySelector(`#acssa-chart-tab li:nth-child(${key + 1})`).classList.add("active");
  723. document.querySelector("#acssa-chart-block div.acssa-chart-wrapper-active").classList.remove("acssa-chart-wrapper-active");
  724. document.querySelector(`#acssa-chart-block div.acssa-chart-wrapper:nth-child(${key + 1})`).classList.add("acssa-chart-wrapper-active");
  725. // resize charts
  726. switch (key) {
  727. case 0:
  728. Plotly.relayout(plotlyDifficultyChartId, { width: document.getElementById(plotlyDifficultyChartId).clientWidth });
  729. logPlotCheckboxParent.style.display = 'none';
  730. break;
  731. case 1:
  732. Plotly.relayout(plotlyAcceptedCountChartId, { width: document.getElementById(plotlyAcceptedCountChartId).clientWidth });
  733. logPlotCheckboxParent.style.display = 'block';
  734. break;
  735. case 2:
  736. Plotly.relayout(plotlyLastAcceptedTimeChartId, { width: document.getElementById(plotlyLastAcceptedTimeChartId).clientWidth });
  737. logPlotCheckboxParent.style.display = 'block';
  738. break;
  739. default:
  740. break;
  741. }
  742. if (showYourResult[activeTab] !== checkbox.checked) {
  743. onCheckboxChanged();
  744. }
  745. if (activeTab !== 0 && useLogPlot[activeTab] !== logPlotCheckbox.checked) {
  746. onLogPlotCheckboxChanged();
  747. }
  748. });
  749. });
  750.  
  751. logPlotCheckbox.addEventListener('change', onLogPlotCheckboxChanged);
  752.  
  753. // 現在の Difficulty テーブルを構築する
  754. if (estimationIsEnabled) {
  755. for (let tableIdx = 0; tableIdx < Math.ceil(tasks.length / COL_PER_ROW); ++tableIdx) {
  756. document.getElementById(`acssa-thead-${tableIdx}`).insertAdjacentHTML("beforeend", `<th></th>`);
  757. document.getElementById(`acssa-tbody-${tableIdx}`).insertAdjacentHTML("beforeend", `<th>Current</td>`);
  758. document.getElementById(`acssa-tbody-predicted-${tableIdx}`).insertAdjacentHTML("beforeend", `<th>Predicted</td>`);
  759. }
  760. }
  761. for (let j = 0; j < tasks.length; ++j) {
  762. const tableIdx = Math.floor(j / COL_PER_ROW);
  763. const correctedDifficulty = RatingConverter.toCorrectedRating(dc.binarySearch(taskAcceptedCounts[j]));
  764. const tdClass = yourTaskAcceptedElapsedTimes[j] === -1 ? '' : 'class="success acssa-task-success"';
  765. document.getElementById(`acssa-thead-${tableIdx}`).insertAdjacentHTML("beforeend", `
  766. <td ${tdClass}>
  767. ${tasks[j].Assignment}
  768. </td>
  769. `);
  770. const id = `td-assa-difficulty-${j}`;
  771. document.getElementById(`acssa-tbody-${tableIdx}`).insertAdjacentHTML("beforeend", `
  772. <td ${tdClass} id="${id}" style="color:${getColor(correctedDifficulty)};">
  773. ${correctedDifficulty === 9999 ? '-' : correctedDifficulty}</td>
  774. `);
  775. if (correctedDifficulty !== 9999) {
  776. document.getElementById(id).insertAdjacentHTML(
  777. "afterbegin", generateDifficultyCircle(correctedDifficulty));
  778. }
  779. if (estimationIsEnabled) {
  780. const correctedPredictedDifficulty = RatingConverter.toCorrectedRating(dc.binarySearch(acCountPredicted[j]));
  781. const idPredicted = `td-assa-difficulty-predicted-${j}`;
  782. document.getElementById(`acssa-tbody-predicted-${tableIdx}`).insertAdjacentHTML("beforeend", `
  783. <td ${tdClass} id="${idPredicted}" style="color:${getColor(correctedPredictedDifficulty)};">
  784. ${correctedPredictedDifficulty === 9999 ? '-' : correctedPredictedDifficulty}</td>
  785. `);
  786. if (correctedPredictedDifficulty !== 9999) {
  787. document.getElementById(idPredicted).insertAdjacentHTML(
  788. "afterbegin", generateDifficultyCircle(correctedPredictedDifficulty));
  789. }
  790. }
  791. }
  792.  
  793. if (yourScore == -1) {
  794. // disable checkbox
  795. checkbox.checked = false;
  796. checkbox.disabled = true;
  797. checkbox.parentElement.style.cursor = 'default';
  798. checkbox.parentElement.style.textDecoration = 'line-through';
  799. }
  800.  
  801. // 順位表のその他の描画を優先するために,後回しにする
  802. setTimeout(() => {
  803. const maxAcceptedCount = taskAcceptedCounts.reduce((a, b) => Math.max(a, b));
  804. const yMax = RatingConverter.toCorrectedRating(dc.binarySearch(1));
  805. const yMin = RatingConverter.toCorrectedRating(dc.binarySearch(Math.max(2, maxAcceptedCount)));
  806.  
  807. // 以降の計算は時間がかかる
  808.  
  809. taskAcceptedElapsedTimes.forEach(ar => {
  810. ar.sort((a, b) => a - b);
  811. });
  812.  
  813. // 時系列データの準備
  814. /** Difficulty Chart のデータ
  815. * @type {{x: number, y: number, type: string, name: string}[]} */
  816. const difficultyChartData = [];
  817. /** AC Count Chart のデータ
  818. * @type {{x: number, y: number, type: string, name: string}[]} */
  819. const acceptedCountChartData = [];
  820.  
  821. for (let j = 0; j < tasks.length; ++j) { //
  822. const interval = Math.ceil(taskAcceptedCounts[j] / 140);
  823. /** @type {[number[], number[]]} */
  824. const [taskAcceptedElapsedTimesForChart, taskAcceptedCountsForChart] = taskAcceptedElapsedTimes[j].reduce(
  825. ([ar, arr], tm, idx) => {
  826. const tmpInterval = Math.max(1, Math.min(Math.ceil(idx / 10), interval));
  827. if (idx % tmpInterval == 0 || idx == taskAcceptedCounts[j] - 1) {
  828. ar.push(tm);
  829. arr.push(idx + 1);
  830. }
  831. return [ar, arr];
  832. },
  833. [[], []]
  834. );
  835.  
  836. difficultyChartData.push({
  837. x: taskAcceptedElapsedTimesForChart,
  838. y: taskAcceptedCountsForChart.map(taskAcceptedCountForChart => dc.binarySearch(taskAcceptedCountForChart)),
  839. type: 'scatter',
  840. name: `${tasks[j].Assignment}`,
  841. });
  842. acceptedCountChartData.push({
  843. x: taskAcceptedElapsedTimesForChart,
  844. y: taskAcceptedCountsForChart,
  845. type: 'scatter',
  846. name: `${tasks[j].Assignment}`,
  847. });
  848. }
  849.  
  850. // 現在のユーザのデータを追加
  851. const yourMarker = {
  852. size: 10,
  853. symbol: "cross",
  854. color: 'red',
  855. line: {
  856. color: 'white',
  857. width: 1,
  858. },
  859. };
  860. if (yourScore !== -1) {
  861. /** @type {number[]} */
  862. const yourAcceptedTimes = [];
  863. /** @type {number[]} */
  864. const yourAcceptedDifficulties = [];
  865. /** @type {number[]} */
  866. const yourAcceptedCounts = [];
  867.  
  868. for (let j = 0; j < tasks.length; ++j) {
  869. if (yourTaskAcceptedElapsedTimes[j] !== -1) {
  870. yourAcceptedTimes.push(yourTaskAcceptedElapsedTimes[j]);
  871. const yourAcceptedCount = arrayLowerBound(taskAcceptedElapsedTimes[j], yourTaskAcceptedElapsedTimes[j]) + 1;
  872. yourAcceptedCounts.push(yourAcceptedCount);
  873. yourAcceptedDifficulties.push(dc.binarySearch(yourAcceptedCount));
  874. }
  875. }
  876.  
  877. yourDifficultyChartData = {
  878. x: yourAcceptedTimes,
  879. y: yourAcceptedDifficulties,
  880. mode: 'markers',
  881. type: 'scatter',
  882. name: `${userScreenName}`,
  883. marker: yourMarker,
  884. };
  885. yourAcceptedCountChartData = {
  886. x: yourAcceptedTimes,
  887. y: yourAcceptedCounts,
  888. mode: 'markers',
  889. type: 'scatter',
  890. name: `${userScreenName}`,
  891. marker: yourMarker,
  892. };
  893. difficultyChartData.push(yourDifficultyChartData);
  894. acceptedCountChartData.push(yourAcceptedCountChartData);
  895. }
  896.  
  897. // 得点と提出時間データの準備
  898. /** @type {{x: number, y: number, type: string, name: string}[]} */
  899. const lastAcceptedTimeChartData = [];
  900. const scores = [...scoreLastAcceptedTimeMap.keys()];
  901. scores.sort((a, b) => b - a);
  902. let acc = 0;
  903. let maxAcceptedTime = 0;
  904. scores.forEach(score => {
  905. const lastAcceptedTimes = scoreLastAcceptedTimeMap.get(score);
  906. lastAcceptedTimes.sort((a, b) => a - b);
  907. const interval = Math.ceil(lastAcceptedTimes.length / 100);
  908. /** @type {number[]} */
  909. const lastAcceptedTimesForChart = lastAcceptedTimes.reduce((ar, tm, idx) => {
  910. if (idx % interval == 0 || idx == lastAcceptedTimes.length - 1) ar.push(tm);
  911. return ar;
  912. }, []);
  913. const lastAcceptedTimesRanks = lastAcceptedTimes.reduce((ar, tm, idx) => {
  914. if (idx % interval == 0 || idx == lastAcceptedTimes.length - 1) ar.push(acc + idx + 1);
  915. return ar;
  916. }, []);
  917.  
  918. lastAcceptedTimeChartData.push({
  919. x: lastAcceptedTimesRanks,
  920. y: lastAcceptedTimesForChart,
  921. type: 'scatter',
  922. name: `${score}`,
  923. });
  924.  
  925. if (score === yourScore) {
  926. const lastAcceptedTimesRank = arrayLowerBound(lastAcceptedTimes, yourLastAcceptedTime);
  927. yourLastAcceptedTimeChartData = {
  928. x: [acc + lastAcceptedTimesRank + 1],
  929. y: [yourLastAcceptedTime],
  930. mode: 'markers',
  931. type: 'scatter',
  932. name: `${userScreenName}`,
  933. marker: yourMarker,
  934. };
  935. yourLastAcceptedTimeChartDataIndex = lastAcceptedTimeChartData.length + 0;
  936. lastAcceptedTimeChartData.push(yourLastAcceptedTimeChartData);
  937. }
  938.  
  939. acc += lastAcceptedTimes.length;
  940. if (lastAcceptedTimes[lastAcceptedTimes.length - 1] > maxAcceptedTime) {
  941. maxAcceptedTime = lastAcceptedTimes[lastAcceptedTimes.length - 1];
  942. }
  943. });
  944.  
  945. const duration = getContestDurationSec();
  946. const xtick = (60 * 10) * Math.max(1, Math.ceil(duration / (60 * 10 * 20))); // 10 分を最小単位にする
  947.  
  948. // 軸フォーマットをカスタムする
  949. // Support specifying a function for tickformat · Issue #1464 · plotly/plotly.js
  950. // https://github.com/plotly/plotly.js/issues/1464#issuecomment-498050894
  951. {
  952. const org_locale = Plotly.d3.locale;
  953. Plotly.d3.locale = (locale) => {
  954. const result = org_locale(locale);
  955. const org_number_format = result.numberFormat;
  956. result.numberFormat = (format) => {
  957. if (format != 'TIME') {
  958. return org_number_format(format)
  959. }
  960. return (x) => formatTimespan(x).toString();
  961. }
  962. return result;
  963. };
  964. }
  965.  
  966. // 背景用設定
  967. const alpha = 0.3;
  968. /** @type {[number, number, string][]} */
  969. const colors = [
  970. [0, 400, `rgba(128,128,128,${alpha})`],
  971. [400, 800, `rgba(128,0,0,${alpha})`],
  972. [800, 1200, `rgba(0,128,0,${alpha})`],
  973. [1200, 1600, `rgba(0,255,255,${alpha})`],
  974. [1600, 2000, `rgba(0,0,255,${alpha})`],
  975. [2000, 2400, `rgba(255,255,0,${alpha})`],
  976. [2400, 2800, `rgba(255,165,0,${alpha})`],
  977. [2800, 10000, `rgba(255,0,0,${alpha})`],
  978. ];
  979.  
  980. // Difficulty Chart 描画
  981. {
  982. // 描画
  983. const layout = {
  984. title: 'Difficulty',
  985. xaxis: {
  986. dtick: xtick,
  987. tickformat: 'TIME',
  988. range: [0, duration],
  989. // title: { text: 'Elapsed' }
  990. },
  991. yaxis: {
  992. dtick: 400,
  993. tickformat: 'd',
  994. range: [
  995. Math.max(0, Math.floor((yMin - 100) / 400) * 400),
  996. Math.max(0, Math.ceil((yMax + 100) / 400) * 400)
  997. ],
  998. // title: { text: 'Difficulty' }
  999. },
  1000. shapes: colors.map(c => {
  1001. return {
  1002. type: 'rect',
  1003. layer: 'below',
  1004. xref: 'x',
  1005. yref: 'y',
  1006. x0: 0,
  1007. x1: duration,
  1008. y0: c[0],
  1009. y1: c[1],
  1010. line: { width: 0 },
  1011. fillcolor: c[2]
  1012. };
  1013. }),
  1014. margin: {
  1015. b: 60,
  1016. t: 30,
  1017. }
  1018. };
  1019. const config = { autosize: true };
  1020. Plotly.newPlot(plotlyDifficultyChartId, difficultyChartData, layout, config);
  1021.  
  1022. window.addEventListener('resize', () => {
  1023. if (activeTab == 0)
  1024. Plotly.relayout(plotlyDifficultyChartId, { width: document.getElementById(plotlyDifficultyChartId).clientWidth });
  1025. });
  1026. }
  1027.  
  1028. // Accepted Count Chart 描画
  1029. {
  1030. acceptedCountYMax = participants;
  1031. /** @type {[number, number, string][]} */
  1032. const rectSpans = colors.reduce((ar, cur) => {
  1033. const bottom = dc.perf2ExpectedAcceptedCount(cur[1]);
  1034. if (bottom > acceptedCountYMax) return ar;
  1035. const top = (cur[0] == 0) ? acceptedCountYMax : dc.perf2ExpectedAcceptedCount(cur[0]);
  1036. if (top < 0.5) return ar;
  1037. ar.push([Math.max(0.5, bottom), Math.min(acceptedCountYMax, top), cur[2]]);
  1038. return ar;
  1039. }, []);
  1040. // 描画
  1041. const layout = {
  1042. title: 'Accepted Count',
  1043. xaxis: {
  1044. dtick: xtick,
  1045. tickformat: 'TIME',
  1046. range: [0, duration],
  1047. // title: { text: 'Elapsed' }
  1048. },
  1049. yaxis: {
  1050. // type: 'log',
  1051. // dtick: 100,
  1052. tickformat: 'd',
  1053. range: [
  1054. 0,
  1055. acceptedCountYMax
  1056. ],
  1057. // range: [
  1058. // Math.log10(0.5),
  1059. // Math.log10(acceptedCountYMax)
  1060. // ],
  1061. // title: { text: 'Difficulty' }
  1062. },
  1063. shapes: rectSpans.map(span => {
  1064. return {
  1065. type: 'rect',
  1066. layer: 'below',
  1067. xref: 'x',
  1068. yref: 'y',
  1069. x0: 0,
  1070. x1: duration,
  1071. y0: span[0],
  1072. y1: span[1],
  1073. line: { width: 0 },
  1074. fillcolor: span[2]
  1075. };
  1076. }),
  1077. margin: {
  1078. b: 60,
  1079. t: 30,
  1080. }
  1081. };
  1082. const config = { autosize: true };
  1083. Plotly.newPlot(plotlyAcceptedCountChartId, acceptedCountChartData, layout, config);
  1084.  
  1085. window.addEventListener('resize', () => {
  1086. if (activeTab == 1)
  1087. Plotly.relayout(plotlyAcceptedCountChartId, { width: document.getElementById(plotlyAcceptedCountChartId).clientWidth });
  1088. });
  1089. }
  1090.  
  1091. // LastAcceptedTime Chart 描画
  1092. {
  1093. const xMax = participants;
  1094. const yMax = Math.ceil((maxAcceptedTime + xtick / 2) / xtick) * xtick;
  1095. /** @type {[number, number, string][]} */
  1096. const rectSpans = colors.reduce((ar, cur) => {
  1097. const right = (cur[0] == 0) ? xMax : dc.perf2Ranking(cur[0]);
  1098. if (right < 1) return ar;
  1099. const left = dc.perf2Ranking(cur[1]);
  1100. if (left > xMax) return ar;
  1101. ar.push([Math.max(0, left), Math.min(xMax, right), cur[2]]);
  1102. return ar;
  1103. }, []);
  1104. // console.log(colors);
  1105. // console.log(rectSpans);
  1106. const layout = {
  1107. title: 'LastAcceptedTime v.s. Rank',
  1108. xaxis: {
  1109. // dtick: 100,
  1110. tickformat: 'd',
  1111. range: [0, xMax],
  1112. // title: { text: 'Elapsed' }
  1113. },
  1114. yaxis: {
  1115. dtick: xtick,
  1116. tickformat: 'TIME',
  1117. range: [0, yMax],
  1118. // range: [
  1119. // Math.max(0, Math.floor((yMin - 100) / 400) * 400),
  1120. // Math.max(0, Math.ceil((yMax + 100) / 400) * 400)
  1121. // ],
  1122. // title: { text: 'Difficulty' }
  1123. },
  1124. shapes: rectSpans.map(span => {
  1125. return {
  1126. type: 'rect',
  1127. layer: 'below',
  1128. xref: 'x',
  1129. yref: 'y',
  1130. x0: span[0],
  1131. x1: span[1],
  1132. y0: 0,
  1133. y1: yMax,
  1134. line: { width: 0 },
  1135. fillcolor: span[2]
  1136. };
  1137. }),
  1138. margin: {
  1139. b: 60,
  1140. t: 30,
  1141. }
  1142. };
  1143. const config = { autosize: true };
  1144. Plotly.newPlot(plotlyLastAcceptedTimeChartId, lastAcceptedTimeChartData, layout, config);
  1145.  
  1146. window.addEventListener('resize', () => {
  1147. if (activeTab == 2)
  1148. Plotly.relayout(plotlyLastAcceptedTimeChartId, { width: document.getElementById(plotlyLastAcceptedTimeChartId).clientWidth });
  1149. });
  1150. }
  1151.  
  1152. // 現在のユーザの結果表示・非表示 toggle
  1153. checkbox.addEventListener('change', onCheckboxChanged);
  1154.  
  1155. document.getElementById('acssa-loader').style.display = 'none';
  1156. document.getElementById('acssa-tab-wrapper').style.display = 'block';
  1157. working = false;
  1158. }, 100); // end setTimeout()
  1159. };
  1160.  
  1161. // MAIN
  1162. vueStandings.$watch('standings', onStandingsChanged, { deep: true, immediate: true });
  1163.  
  1164. })();