atcoder-standings-difficulty-analyzer

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

As of 2025-05-05. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

  1. // ==UserScript==
  2. // @name atcoder-standings-difficulty-analyzer
  3. // @namespace iilj
  4. // @version 2025.5.5
  5. // @description 順位表の得点情報を集計し,推定 difficulty やその推移を表示します.
  6. // @author iilj
  7. // @license MIT
  8. // @supportURL https://github.com/iilj/atcoder-standings-difficulty-analyzer/issues
  9. // @match https://atcoder.jp/*standings*
  10. // @exclude https://atcoder.jp/*standings/json
  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. var css = "#acssa-contents .table.acssa-table {\n width: 100%;\n table-layout: fixed;\n margin-bottom: 1.5rem;\n}\n#acssa-contents .table.acssa-table .acssa-thead {\n font-weight: bold;\n}\n#acssa-contents .table.acssa-table > tbody > tr > td.success.acssa-task-success.acssa-task-success-suppress {\n background-color: transparent;\n}\n#acssa-contents #acssa-tab-wrapper {\n display: none;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-chart-tab {\n margin-bottom: 0.5rem;\n display: inline-block;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-chart-tab a {\n cursor: pointer;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-chart-tab a span.glyphicon {\n margin-right: 0.5rem;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-checkbox-tab {\n margin-bottom: 0.5rem;\n display: inline-block;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-checkbox-tab li a {\n color: black;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-checkbox-tab li a:hover {\n background-color: transparent;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-checkbox-tab li a label {\n cursor: pointer;\n margin: 0;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-checkbox-tab li a label input {\n cursor: pointer;\n margin: 0;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-checkbox-tab #acssa-checkbox-toggle-log-plot-parent {\n display: none;\n}\n#acssa-contents .acssa-loader-wrapper {\n background-color: #337ab7;\n display: flex;\n justify-content: center;\n align-items: center;\n padding: 1rem;\n margin-bottom: 1.5rem;\n border-radius: 3px;\n}\n#acssa-contents .acssa-chart-wrapper {\n display: none;\n}\n#acssa-contents .acssa-chart-wrapper.acssa-chart-wrapper-active {\n display: block;\n}";
  16.  
  17. var teamalert = "<div class=\"alert alert-warning\">\n チーム戦順位表が提供されています.個人単位の順位表ページでは,difficulty 推定値が不正確になります.\n</div>";
  18.  
  19. const arrayLowerBound = (arr, n) => {
  20. let first = 0, last = arr.length - 1, middle;
  21. while (first <= last) {
  22. middle = 0 | ((first + last) / 2);
  23. if (arr[middle] < n)
  24. first = middle + 1;
  25. else
  26. last = middle - 1;
  27. }
  28. return first;
  29. };
  30. const getColor = (rating) => {
  31. if (rating < 400)
  32. return '#808080';
  33. // gray
  34. else if (rating < 800)
  35. return '#804000';
  36. // brown
  37. else if (rating < 1200)
  38. return '#008000';
  39. // green
  40. else if (rating < 1600)
  41. return '#00C0C0';
  42. // cyan
  43. else if (rating < 2000)
  44. return '#0000FF';
  45. // blue
  46. else if (rating < 2400)
  47. return '#C0C000';
  48. // yellow
  49. else if (rating < 2800)
  50. return '#FF8000';
  51. // orange
  52. else if (rating == 9999)
  53. return '#000000';
  54. return '#FF0000'; // red
  55. };
  56. const formatTimespan = (sec) => {
  57. let sign;
  58. if (sec >= 0) {
  59. sign = '';
  60. }
  61. else {
  62. sign = '-';
  63. sec *= -1;
  64. }
  65. return `${sign}${Math.floor(sec / 60)}:${`0${sec % 60}`.slice(-2)}`;
  66. };
  67. /** 現在のページから,コンテストの開始から終了までの秒数を抽出する */
  68. const getContestDurationSec = () => {
  69. if (contestScreenName.startsWith('past')) {
  70. return 300 * 60;
  71. }
  72. // toDate.diff(fromDate) でミリ秒が返ってくる
  73. return endTime.diff(startTime) / 1000;
  74. };
  75. const getCenterOfInnerRatingFromRange = (contestRatedRange) => {
  76. if (contestScreenName.startsWith('abc')) {
  77. return 800;
  78. }
  79. if (contestScreenName.startsWith('arc')) {
  80. const contestNumber = Number(contestScreenName.substring(3, 6));
  81. return contestNumber >= 104 ? 1000 : 1600;
  82. }
  83. if (contestScreenName.startsWith('agc')) {
  84. const contestNumber = Number(contestScreenName.substring(3, 6));
  85. return contestNumber >= 34 ? 1200 : 1600;
  86. }
  87. if (contestRatedRange[1] === 1999) {
  88. return 800;
  89. }
  90. else if (contestRatedRange[1] === 2799) {
  91. return 1000;
  92. }
  93. else if (contestRatedRange[1] === Infinity) {
  94. return 1200;
  95. }
  96. return 800;
  97. };
  98. // ContestRatedRange
  99. /*
  100. function getContestInformationAsync(contestScreenName) {
  101. return __awaiter(this, void 0, void 0, function* () {
  102. const html = yield fetchTextDataAsync(`https://atcoder.jp/contests/${contestScreenName}`);
  103. const topPageDom = new DOMParser().parseFromString(html, "text/html");
  104. const dataParagraph = topPageDom.getElementsByClassName("small")[0];
  105. const data = Array.from(dataParagraph.children).map((x) => x.innerHTML.split(":")[1].trim());
  106. return new ContestInformation(parseRangeString(data[0]), parseRangeString(data[1]), parseDurationString(data[2]));
  107. });
  108. }
  109. */
  110. function parseRangeString(s) {
  111. s = s.trim();
  112. if (s === '-')
  113. return [0, -1];
  114. if (s === 'All')
  115. return [0, Infinity];
  116. if (!/[-~]/.test(s))
  117. return [0, -1];
  118. const res = s.split(/[-~]/).map((x) => parseInt(x.trim()));
  119. if (res.length !== 2) {
  120. throw new Error('res is not [number, number]');
  121. }
  122. if (isNaN(res[0]))
  123. res[0] = 0;
  124. if (isNaN(res[1]))
  125. res[1] = Infinity;
  126. return res;
  127. }
  128. const getContestRatedRangeAsync = async (contestScreenName) => {
  129. const html = await fetch(`https://atcoder.jp/contests/${contestScreenName}`);
  130. const topPageDom = new DOMParser().parseFromString(await html.text(), 'text/html');
  131. const dataParagraph = topPageDom.getElementsByClassName('small')[0];
  132. const data = Array.from(dataParagraph.children).map((x) => x.innerHTML.split(':')[1].trim());
  133. // console.log("data", data);
  134. return parseRangeString(data[1]);
  135. // return new ContestInformation(parseRangeString(data[0]), parseRangeString(data[1]), parseDurationString(data[2]));
  136. };
  137. const rangeLen = (len) => Array.from({ length: len }, (v, k) => k);
  138.  
  139. const BASE_URL = 'https://raw.githubusercontent.com/iilj/atcoder-standings-difficulty-analyzer/main/json/standings';
  140. const fetchJson = async (url) => {
  141. const res = await fetch(url);
  142. if (!res.ok) {
  143. throw new Error(res.statusText);
  144. }
  145. const obj = (await res.json());
  146. return obj;
  147. };
  148. const fetchContestAcRatioModel = async (contestScreenName, contestDurationMinutes) => {
  149. // https://raw.githubusercontent.com/iilj/atcoder-standings-difficulty-analyzer/main/json/standings/abc_100m.json
  150. let modelLocation = undefined;
  151. if (/^agc(\d{3,})$/.exec(contestScreenName)) {
  152. if ([110, 120, 130, 140, 150, 160, 180, 200, 210, 240, 270, 300].includes(contestDurationMinutes)) {
  153. modelLocation = `${BASE_URL}/agc_${contestDurationMinutes}m.json`;
  154. }
  155. }
  156. else if (/^arc(\d{3,})$/.exec(contestScreenName)) {
  157. if ([100, 120, 150].includes(contestDurationMinutes)) {
  158. modelLocation = `${BASE_URL}/arc_${contestDurationMinutes}m.json`;
  159. }
  160. }
  161. else if (/^abc(\d{3,})$/.exec(contestScreenName)) {
  162. if ([100, 120].includes(contestDurationMinutes)) {
  163. modelLocation = `${BASE_URL}/abc_${contestDurationMinutes}m.json`;
  164. }
  165. }
  166. if (modelLocation !== undefined) {
  167. return await fetchJson(modelLocation);
  168. }
  169. return undefined;
  170. };
  171. const fetchInnerRatingsFromPredictor = async (contestScreenName) => {
  172. const url = `https://data.ac-predictor.com/aperfs/${contestScreenName}.json`;
  173. try {
  174. return await fetchJson(url);
  175. }
  176. catch (e) {
  177. return {};
  178. }
  179. };
  180.  
  181. class RatingConverter {
  182. }
  183. /** 表示用の低レート帯補正レート → 低レート帯補正前のレート */
  184. RatingConverter.toRealRating = (correctedRating) => {
  185. if (correctedRating >= 400)
  186. return correctedRating;
  187. else
  188. return 400 * (1 - Math.log(400 / correctedRating));
  189. };
  190. /** 低レート帯補正前のレート → 内部レート推定値 */
  191. RatingConverter.toInnerRating = (realRating, comp) => {
  192. return (realRating +
  193. (1200 * (Math.sqrt(1 - Math.pow(0.81, comp)) / (1 - Math.pow(0.9, comp)) - 1)) / (Math.sqrt(19) - 1));
  194. };
  195. /** 低レート帯補正前のレート → 表示用の低レート帯補正レート */
  196. RatingConverter.toCorrectedRating = (realRating) => {
  197. if (realRating >= 400)
  198. return realRating;
  199. else
  200. return Math.floor(400 / Math.exp((400 - realRating) / 400));
  201. };
  202.  
  203. class DifficultyCalculator {
  204. constructor(sortedInnerRatings) {
  205. this.innerRatings = sortedInnerRatings;
  206. this.prepared = new Map();
  207. this.memo = new Map();
  208. }
  209. perf2ExpectedAcceptedCount(m) {
  210. let expectedAcceptedCount;
  211. if (this.prepared.has(m)) {
  212. expectedAcceptedCount = this.prepared.get(m);
  213. }
  214. else {
  215. expectedAcceptedCount = this.innerRatings.reduce((prev_expected_accepts, innerRating) => (prev_expected_accepts += 1 / (1 + Math.pow(6, (m - innerRating) / 400))), 0);
  216. this.prepared.set(m, expectedAcceptedCount);
  217. }
  218. return expectedAcceptedCount;
  219. }
  220. perf2Ranking(x) {
  221. return this.perf2ExpectedAcceptedCount(x) + 0.5;
  222. }
  223. rank2InnerPerf(rank) {
  224. let upper = 9999;
  225. let lower = -9999;
  226. while (upper - lower > 0.1) {
  227. const mid = (upper + lower) / 2;
  228. if (rank > this.perf2Ranking(mid))
  229. upper = mid;
  230. else
  231. lower = mid;
  232. }
  233. return Math.round((upper + lower) / 2);
  234. }
  235. /** Difficulty 推定値を算出する */
  236. binarySearchCorrectedDifficulty(acceptedCount) {
  237. if (this.memo.has(acceptedCount)) {
  238. return this.memo.get(acceptedCount);
  239. }
  240. let lb = -10000;
  241. let ub = 10000;
  242. while (ub - lb > 1) {
  243. const m = Math.floor((ub + lb) / 2);
  244. const expectedAcceptedCount = this.perf2ExpectedAcceptedCount(m);
  245. if (expectedAcceptedCount < acceptedCount)
  246. ub = m;
  247. else
  248. lb = m;
  249. }
  250. const difficulty = lb;
  251. const correctedDifficulty = RatingConverter.toCorrectedRating(difficulty);
  252. this.memo.set(acceptedCount, correctedDifficulty);
  253. return correctedDifficulty;
  254. }
  255. }
  256.  
  257. var html$1 = "<div id=\"acssa-loader\" class=\"loader acssa-loader-wrapper\">\n <div class=\"loader-inner ball-pulse\">\n <div></div>\n <div></div>\n <div></div>\n </div>\n</div>\n<div id=\"acssa-chart-block\">\n <div class=\"acssa-chart-wrapper acssa-chart-wrapper-active\" id=\"acssa-mydiv-difficulty-wrapper\">\n <div id=\"acssa-mydiv-difficulty\" style=\"width:100%;\"></div>\n </div>\n <div class=\"acssa-chart-wrapper\" id=\"acssa-mydiv-accepted-count-wrapper\">\n <div id=\"acssa-mydiv-accepted-count\" style=\"width:100%;\"></div>\n </div>\n <div class=\"acssa-chart-wrapper\" id=\"acssa-mydiv-accepted-time-wrapper\">\n <div id=\"acssa-mydiv-accepted-time\" style=\"width:100%;\"></div>\n </div>\n</div>";
  258.  
  259. const LOADER_ID = 'acssa-loader';
  260. const plotlyDifficultyChartId = 'acssa-mydiv-difficulty';
  261. const plotlyAcceptedCountChartId = 'acssa-mydiv-accepted-count';
  262. const plotlyLastAcceptedTimeChartId = 'acssa-mydiv-accepted-time';
  263. const yourMarker = {
  264. size: 10,
  265. symbol: 'cross',
  266. color: 'red',
  267. line: {
  268. color: 'white',
  269. width: 1,
  270. },
  271. };
  272. const config = { autosize: true };
  273. // 背景用設定
  274. const alpha = 0.3;
  275. const colors = [
  276. [0, 400, `rgba(128,128,128,${alpha})`],
  277. [400, 800, `rgba(128,0,0,${alpha})`],
  278. [800, 1200, `rgba(0,128,0,${alpha})`],
  279. [1200, 1600, `rgba(0,255,255,${alpha})`],
  280. [1600, 2000, `rgba(0,0,255,${alpha})`],
  281. [2000, 2400, `rgba(255,255,0,${alpha})`],
  282. [2400, 2800, `rgba(255,165,0,${alpha})`],
  283. [2800, 10000, `rgba(255,0,0,${alpha})`],
  284. ];
  285. class Charts {
  286. constructor(parent, tasks, scoreLastAcceptedTimeMap, taskAcceptedCounts, taskAcceptedElapsedTimes, yourTaskAcceptedElapsedTimes, yourScore, yourLastAcceptedTime, participants, dcForDifficulty, dcForPerformance, ratedRank2EntireRank, tabs) {
  287. this.tasks = tasks;
  288. this.scoreLastAcceptedTimeMap = scoreLastAcceptedTimeMap;
  289. this.taskAcceptedCounts = taskAcceptedCounts;
  290. this.taskAcceptedElapsedTimes = taskAcceptedElapsedTimes;
  291. this.yourTaskAcceptedElapsedTimes = yourTaskAcceptedElapsedTimes;
  292. this.yourScore = yourScore;
  293. this.yourLastAcceptedTime = yourLastAcceptedTime;
  294. this.participants = participants;
  295. this.dcForDifficulty = dcForDifficulty;
  296. this.dcForPerformance = dcForPerformance;
  297. this.ratedRank2EntireRank = ratedRank2EntireRank;
  298. this.tabs = tabs;
  299. parent.insertAdjacentHTML('beforeend', html$1);
  300. this.duration = getContestDurationSec();
  301. this.xtick = 60 * 10 * Math.max(1, Math.ceil(this.duration / (60 * 10 * 20))); // 10 分を最小単位にする
  302. }
  303. async plotAsync() {
  304. // 以降の計算は時間がかかる
  305. this.taskAcceptedElapsedTimes.forEach((ar) => {
  306. ar.sort((a, b) => a - b);
  307. });
  308. // 時系列データの準備
  309. const [difficultyChartData, acceptedCountChartData] = await this.getTimeSeriesChartData();
  310. // 得点と提出時間データの準備
  311. const [lastAcceptedTimeChartData, maxAcceptedTime] = this.getLastAcceptedTimeChartData();
  312. // 軸フォーマットをカスタムする
  313. this.overrideAxisFormat();
  314. // Difficulty Chart 描画
  315. await this.plotDifficultyChartData(difficultyChartData);
  316. // Accepted Count Chart 描画
  317. await this.plotAcceptedCountChartData(acceptedCountChartData);
  318. // LastAcceptedTime Chart 描画
  319. await this.plotLastAcceptedTimeChartData(lastAcceptedTimeChartData, maxAcceptedTime);
  320. }
  321. /** 時系列データの準備 */
  322. async getTimeSeriesChartData() {
  323. /** Difficulty Chart のデータ */
  324. const difficultyChartData = [];
  325. /** AC Count Chart のデータ */
  326. const acceptedCountChartData = [];
  327. const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  328. for (let j = 0; j < this.tasks.length; ++j) {
  329. //
  330. const interval = Math.ceil(this.taskAcceptedCounts[j] / 140);
  331. const [taskAcceptedElapsedTimesForChart, taskAcceptedCountsForChart] = this.taskAcceptedElapsedTimes[j].reduce(([ar, arr], tm, idx) => {
  332. const tmpInterval = Math.max(1, Math.min(Math.ceil(idx / 10), interval));
  333. if (idx % tmpInterval == 0 || idx == this.taskAcceptedCounts[j] - 1) {
  334. ar.push(tm);
  335. arr.push(idx + 1);
  336. }
  337. return [ar, arr];
  338. }, [[], []]);
  339. const correctedDifficulties = [];
  340. let counter = 0;
  341. for (const taskAcceptedCountForChart of taskAcceptedCountsForChart) {
  342. correctedDifficulties.push(this.dcForDifficulty.binarySearchCorrectedDifficulty(taskAcceptedCountForChart));
  343. counter += 1;
  344. // 20回に1回setTimeout(0)でeventループに処理を移す
  345. if (counter % 20 == 0) {
  346. await sleep(0);
  347. }
  348. }
  349. difficultyChartData.push({
  350. x: taskAcceptedElapsedTimesForChart,
  351. y: correctedDifficulties,
  352. type: 'scattergl',
  353. name: `${this.tasks[j].Assignment}`,
  354. });
  355. acceptedCountChartData.push({
  356. x: taskAcceptedElapsedTimesForChart,
  357. y: taskAcceptedCountsForChart,
  358. type: 'scattergl',
  359. name: `${this.tasks[j].Assignment}`,
  360. });
  361. }
  362. // 現在のユーザのデータを追加
  363. if (this.yourScore !== -1) {
  364. const yourAcceptedTimes = [];
  365. const yourAcceptedDifficulties = [];
  366. const yourAcceptedCounts = [];
  367. for (let j = 0; j < this.tasks.length; ++j) {
  368. if (this.yourTaskAcceptedElapsedTimes[j] !== -1) {
  369. yourAcceptedTimes.push(this.yourTaskAcceptedElapsedTimes[j]);
  370. const yourAcceptedCount = arrayLowerBound(this.taskAcceptedElapsedTimes[j], this.yourTaskAcceptedElapsedTimes[j]) + 1;
  371. yourAcceptedCounts.push(yourAcceptedCount);
  372. yourAcceptedDifficulties.push(this.dcForDifficulty.binarySearchCorrectedDifficulty(yourAcceptedCount));
  373. }
  374. }
  375. this.tabs.yourDifficultyChartData = {
  376. x: yourAcceptedTimes,
  377. y: yourAcceptedDifficulties,
  378. mode: 'markers',
  379. type: 'scattergl',
  380. name: `${userScreenName}`,
  381. marker: yourMarker,
  382. };
  383. this.tabs.yourAcceptedCountChartData = {
  384. x: yourAcceptedTimes,
  385. y: yourAcceptedCounts,
  386. mode: 'markers',
  387. type: 'scattergl',
  388. name: `${userScreenName}`,
  389. marker: yourMarker,
  390. };
  391. difficultyChartData.push(this.tabs.yourDifficultyChartData);
  392. acceptedCountChartData.push(this.tabs.yourAcceptedCountChartData);
  393. }
  394. return [difficultyChartData, acceptedCountChartData];
  395. }
  396. /** 得点と提出時間データの準備 */
  397. getLastAcceptedTimeChartData() {
  398. const lastAcceptedTimeChartData = [];
  399. const scores = [...this.scoreLastAcceptedTimeMap.keys()];
  400. scores.sort((a, b) => b - a);
  401. let acc = 0;
  402. let maxAcceptedTime = 0;
  403. scores.forEach((score) => {
  404. const lastAcceptedTimes = this.scoreLastAcceptedTimeMap.get(score);
  405. lastAcceptedTimes.sort((a, b) => a - b);
  406. const interval = Math.ceil(lastAcceptedTimes.length / 100);
  407. const lastAcceptedTimesForChart = lastAcceptedTimes.reduce((ar, tm, idx) => {
  408. if (idx % interval == 0 || idx == lastAcceptedTimes.length - 1)
  409. ar.push(tm);
  410. return ar;
  411. }, []);
  412. const lastAcceptedTimesRanks = lastAcceptedTimes.reduce((ar, tm, idx) => {
  413. if (idx % interval == 0 || idx == lastAcceptedTimes.length - 1)
  414. ar.push(acc + idx + 1);
  415. return ar;
  416. }, []);
  417. lastAcceptedTimeChartData.push({
  418. x: lastAcceptedTimesRanks,
  419. y: lastAcceptedTimesForChart,
  420. type: 'scattergl',
  421. name: `${score}`,
  422. });
  423. if (score === this.yourScore) {
  424. const lastAcceptedTimesRank = arrayLowerBound(lastAcceptedTimes, this.yourLastAcceptedTime);
  425. this.tabs.yourLastAcceptedTimeChartData = {
  426. x: [acc + lastAcceptedTimesRank + 1],
  427. y: [this.yourLastAcceptedTime],
  428. mode: 'markers',
  429. type: 'scattergl',
  430. name: `${userScreenName}`,
  431. marker: yourMarker,
  432. };
  433. this.tabs.yourLastAcceptedTimeChartDataIndex = lastAcceptedTimeChartData.length + 0;
  434. lastAcceptedTimeChartData.push(this.tabs.yourLastAcceptedTimeChartData);
  435. }
  436. acc += lastAcceptedTimes.length;
  437. if (lastAcceptedTimes[lastAcceptedTimes.length - 1] > maxAcceptedTime) {
  438. maxAcceptedTime = lastAcceptedTimes[lastAcceptedTimes.length - 1];
  439. }
  440. });
  441. return [lastAcceptedTimeChartData, maxAcceptedTime];
  442. }
  443. /**
  444. * 軸フォーマットをカスタムする
  445. * Support specifying a function for tickformat · Issue #1464 · plotly/plotly.js
  446. * https://github.com/plotly/plotly.js/issues/1464#issuecomment-498050894
  447. */
  448. overrideAxisFormat() {
  449. const org_locale = Plotly.d3.locale;
  450. Plotly.d3.locale = (locale) => {
  451. const result = org_locale(locale);
  452. // eslint-disable-next-line @typescript-eslint/unbound-method
  453. const org_number_format = result.numberFormat;
  454. result.numberFormat = (format) => {
  455. if (format != 'TIME') {
  456. return org_number_format(format);
  457. }
  458. return (x) => formatTimespan(x).toString();
  459. };
  460. return result;
  461. };
  462. }
  463. /** Difficulty Chart 描画 */
  464. async plotDifficultyChartData(difficultyChartData) {
  465. const maxAcceptedCount = this.taskAcceptedCounts.reduce((a, b) => Math.max(a, b));
  466. const yMax = RatingConverter.toCorrectedRating(this.dcForDifficulty.binarySearchCorrectedDifficulty(1));
  467. const yMin = RatingConverter.toCorrectedRating(this.dcForDifficulty.binarySearchCorrectedDifficulty(Math.max(2, maxAcceptedCount)));
  468. // 描画
  469. const layout = {
  470. title: 'Difficulty',
  471. xaxis: {
  472. dtick: this.xtick,
  473. tickformat: 'TIME',
  474. range: [0, this.duration],
  475. // title: { text: 'Elapsed' }
  476. },
  477. yaxis: {
  478. dtick: 400,
  479. tickformat: 'd',
  480. range: [
  481. Math.max(0, Math.floor((yMin - 100) / 400) * 400),
  482. Math.max(0, Math.ceil((yMax + 100) / 400) * 400),
  483. ],
  484. // title: { text: 'Difficulty' }
  485. },
  486. shapes: colors.map((c) => {
  487. return {
  488. type: 'rect',
  489. layer: 'below',
  490. xref: 'x',
  491. yref: 'y',
  492. x0: 0,
  493. x1: this.duration,
  494. y0: c[0],
  495. y1: c[1],
  496. line: { width: 0 },
  497. fillcolor: c[2],
  498. };
  499. }),
  500. margin: {
  501. b: 60,
  502. t: 30,
  503. },
  504. };
  505. await Plotly.newPlot(plotlyDifficultyChartId, difficultyChartData, layout, config);
  506. window.addEventListener('resize', () => {
  507. if (this.tabs.activeTab == 0)
  508. void Plotly.relayout(plotlyDifficultyChartId, {
  509. width: document.getElementById(plotlyDifficultyChartId).clientWidth,
  510. });
  511. });
  512. }
  513. /** Accepted Count Chart 描画 */
  514. async plotAcceptedCountChartData(acceptedCountChartData) {
  515. this.tabs.acceptedCountYMax = this.participants;
  516. const rectSpans = colors.reduce((ar, cur) => {
  517. const bottom = this.dcForDifficulty.perf2ExpectedAcceptedCount(cur[1]);
  518. if (bottom > this.tabs.acceptedCountYMax)
  519. return ar;
  520. const top = cur[0] == 0 ? this.tabs.acceptedCountYMax : this.dcForDifficulty.perf2ExpectedAcceptedCount(cur[0]);
  521. if (top < 0.5)
  522. return ar;
  523. ar.push([Math.max(0.5, bottom), Math.min(this.tabs.acceptedCountYMax, top), cur[2]]);
  524. return ar;
  525. }, []);
  526. // 描画
  527. const layout = {
  528. title: 'Accepted Count',
  529. xaxis: {
  530. dtick: this.xtick,
  531. tickformat: 'TIME',
  532. range: [0, this.duration],
  533. // title: { text: 'Elapsed' }
  534. },
  535. yaxis: {
  536. // type: 'log',
  537. // dtick: 100,
  538. tickformat: 'd',
  539. range: [0, this.tabs.acceptedCountYMax],
  540. // range: [
  541. // Math.log10(0.5),
  542. // Math.log10(acceptedCountYMax)
  543. // ],
  544. // title: { text: 'Difficulty' }
  545. },
  546. shapes: rectSpans.map((span) => {
  547. return {
  548. type: 'rect',
  549. layer: 'below',
  550. xref: 'x',
  551. yref: 'y',
  552. x0: 0,
  553. x1: this.duration,
  554. y0: span[0],
  555. y1: span[1],
  556. line: { width: 0 },
  557. fillcolor: span[2],
  558. };
  559. }),
  560. margin: {
  561. b: 60,
  562. t: 30,
  563. },
  564. };
  565. await Plotly.newPlot(plotlyAcceptedCountChartId, acceptedCountChartData, layout, config);
  566. window.addEventListener('resize', () => {
  567. if (this.tabs.activeTab == 1)
  568. void Plotly.relayout(plotlyAcceptedCountChartId, {
  569. width: document.getElementById(plotlyAcceptedCountChartId).clientWidth,
  570. });
  571. });
  572. }
  573. /** LastAcceptedTime Chart 描画 */
  574. async plotLastAcceptedTimeChartData(lastAcceptedTimeChartData, maxAcceptedTime) {
  575. const xMax = this.participants;
  576. // Rated 内のランクから,全体のランクへ変換する
  577. const convRatedRank2EntireRank = (ratedRank) => {
  578. const intRatedRank = Math.floor(ratedRank);
  579. if (intRatedRank >= this.ratedRank2EntireRank.length)
  580. return xMax;
  581. return this.ratedRank2EntireRank[intRatedRank];
  582. };
  583. const yMax = Math.ceil((maxAcceptedTime + this.xtick / 2) / this.xtick) * this.xtick;
  584. const rectSpans = colors.reduce((ar, cur) => {
  585. const right = cur[0] == 0 ? xMax : convRatedRank2EntireRank(this.dcForPerformance.perf2Ranking(cur[0]));
  586. if (right < 1)
  587. return ar;
  588. const left = cur[1] === 10000 ? 0 : convRatedRank2EntireRank(this.dcForPerformance.perf2Ranking(cur[1]));
  589. if (left > xMax)
  590. return ar;
  591. ar.push([Math.max(0, left), Math.min(xMax, right), cur[2]]);
  592. return ar;
  593. }, []);
  594. // console.log(colors);
  595. // console.log(rectSpans);
  596. const layout = {
  597. title: 'LastAcceptedTime v.s. Rank',
  598. xaxis: {
  599. // dtick: 100,
  600. tickformat: 'd',
  601. range: [0, xMax],
  602. // title: { text: 'Elapsed' }
  603. },
  604. yaxis: {
  605. dtick: this.xtick,
  606. tickformat: 'TIME',
  607. range: [0, yMax],
  608. // range: [
  609. // Math.max(0, Math.floor((yMin - 100) / 400) * 400),
  610. // Math.max(0, Math.ceil((yMax + 100) / 400) * 400)
  611. // ],
  612. // title: { text: 'Difficulty' }
  613. },
  614. shapes: rectSpans.map((span) => {
  615. return {
  616. type: 'rect',
  617. layer: 'below',
  618. xref: 'x',
  619. yref: 'y',
  620. x0: span[0],
  621. x1: span[1],
  622. y0: 0,
  623. y1: yMax,
  624. line: { width: 0 },
  625. fillcolor: span[2],
  626. };
  627. }),
  628. margin: {
  629. b: 60,
  630. t: 30,
  631. },
  632. };
  633. await Plotly.newPlot(plotlyLastAcceptedTimeChartId, lastAcceptedTimeChartData, layout, config);
  634. window.addEventListener('resize', () => {
  635. if (this.tabs.activeTab == 2)
  636. void Plotly.relayout(plotlyLastAcceptedTimeChartId, {
  637. width: document.getElementById(plotlyLastAcceptedTimeChartId).clientWidth,
  638. });
  639. });
  640. }
  641. hideLoader() {
  642. document.getElementById(LOADER_ID).style.display = 'none';
  643. }
  644. }
  645.  
  646. /** レートを表す難易度円(◒)の HTML 文字列を生成 */
  647. const generateDifficultyCircle = (rating, isSmall = true) => {
  648. const size = isSmall ? 12 : 36;
  649. const borderWidth = isSmall ? 1 : 3;
  650. const style = `display:inline-block;border-radius:50%;border-style:solid;border-width:${borderWidth}px;` +
  651. `margin-right:5px;vertical-align:initial;height:${size}px;width:${size}px;`;
  652. if (rating < 3200) {
  653. // 色と円がどのぐらい満ちているかを計算
  654. const color = getColor(rating);
  655. const percentFull = ((rating % 400) / 400) * 100;
  656. // ◒を生成
  657. return (`
  658. <span style='${style}border-color:${color};background:` +
  659. `linear-gradient(to top, ${color} 0%, ${color} ${percentFull}%, ` +
  660. `rgba(0, 0, 0, 0) ${percentFull}%, rgba(0, 0, 0, 0) 100%); '>
  661. </span>`);
  662. }
  663. // 金銀銅は例外処理
  664. else if (rating < 3600) {
  665. return (`<span style="${style}border-color: rgb(150, 92, 44);` +
  666. 'background: linear-gradient(to right, rgb(150, 92, 44), rgb(255, 218, 189), rgb(150, 92, 44));"></span>');
  667. }
  668. else if (rating < 4000) {
  669. return (`<span style="${style}border-color: rgb(128, 128, 128);` +
  670. 'background: linear-gradient(to right, rgb(128, 128, 128), white, rgb(128, 128, 128));"></span>');
  671. }
  672. else {
  673. return (`<span style="${style}border-color: rgb(255, 215, 0);` +
  674. 'background: linear-gradient(to right, rgb(255, 215, 0), white, rgb(255, 215, 0));"></span>');
  675. }
  676. };
  677.  
  678. const COL_PER_ROW = 20;
  679. class DifficyltyTable {
  680. constructor(parent, tasks, isEstimationEnabled, dc, taskAcceptedCounts, yourTaskAcceptedElapsedTimes, acCountPredicted) {
  681. // insert
  682. parent.insertAdjacentHTML('beforeend', `
  683. <p><span class="h2">Difficulty</span></p>
  684. <div id="acssa-table-wrapper">
  685. ${rangeLen(Math.ceil(tasks.length / COL_PER_ROW))
  686. .map((tableIdx) => `
  687. <table id="acssa-table-${tableIdx}" class="table table-bordered table-hover th-center td-center td-middle acssa-table">
  688. <tbody>
  689. <tr id="acssa-thead-${tableIdx}" class="acssa-thead"></tr>
  690. </tbody>
  691. <tbody>
  692. <tr id="acssa-tbody-${tableIdx}" class="acssa-tbody"></tr>
  693. ${isEstimationEnabled
  694. ? `<tr id="acssa-tbody-predicted-${tableIdx}" class="acssa-tbody"></tr>`
  695. : ''}
  696. </tbody>
  697. </table>
  698. `)
  699. .join('')}
  700. </div>
  701. `);
  702. if (isEstimationEnabled) {
  703. for (let tableIdx = 0; tableIdx < Math.ceil(tasks.length / COL_PER_ROW); ++tableIdx) {
  704. document.getElementById(`acssa-thead-${tableIdx}`).insertAdjacentHTML('beforeend', `<th></th>`);
  705. document.getElementById(`acssa-tbody-${tableIdx}`).insertAdjacentHTML('beforeend', `<th>Current</td>`);
  706. document.getElementById(`acssa-tbody-predicted-${tableIdx}`).insertAdjacentHTML('beforeend', `<th>Predicted</td>`);
  707. }
  708. }
  709. // build
  710. for (let j = 0; j < tasks.length; ++j) {
  711. const tableIdx = Math.floor(j / COL_PER_ROW);
  712. const correctedDifficulty = dc.binarySearchCorrectedDifficulty(taskAcceptedCounts[j]);
  713. const tdClass = yourTaskAcceptedElapsedTimes[j] === -1 ? '' : 'class="success acssa-task-success"';
  714. document.getElementById(`acssa-thead-${tableIdx}`).insertAdjacentHTML('beforeend', `
  715. <td ${tdClass}>
  716. ${tasks[j].Assignment}
  717. </td>
  718. `);
  719. const id = `td-assa-difficulty-${j}`;
  720. document.getElementById(`acssa-tbody-${tableIdx}`).insertAdjacentHTML('beforeend', `
  721. <td ${tdClass} id="${id}" style="color:${getColor(correctedDifficulty)};">
  722. ${correctedDifficulty === 9999 ? '-' : correctedDifficulty}</td>
  723. `);
  724. if (correctedDifficulty !== 9999) {
  725. document.getElementById(id).insertAdjacentHTML('afterbegin', generateDifficultyCircle(correctedDifficulty));
  726. }
  727. if (isEstimationEnabled) {
  728. const correctedPredictedDifficulty = dc.binarySearchCorrectedDifficulty(acCountPredicted[j]);
  729. const idPredicted = `td-assa-difficulty-predicted-${j}`;
  730. document.getElementById(`acssa-tbody-predicted-${tableIdx}`).insertAdjacentHTML('beforeend', `
  731. <td ${tdClass} id="${idPredicted}" style="color:${getColor(correctedPredictedDifficulty)};">
  732. ${correctedPredictedDifficulty === 9999 ? '-' : correctedPredictedDifficulty}</td>
  733. `);
  734. if (correctedPredictedDifficulty !== 9999) {
  735. document.getElementById(idPredicted).insertAdjacentHTML('afterbegin', generateDifficultyCircle(correctedPredictedDifficulty));
  736. }
  737. }
  738. }
  739. }
  740. }
  741.  
  742. var html = "<p><span class=\"h2\">Chart</span></p>\n<div id=\"acssa-tab-wrapper\">\n <ul class=\"nav nav-pills small\" id=\"acssa-chart-tab\">\n <li class=\"active\">\n <a class=\"acssa-chart-tab-button\"><span class=\"glyphicon glyphicon-stats\" aria-hidden=\"true\"></span>Difficulty</a>\n </li>\n <li>\n <a class=\"acssa-chart-tab-button\"><span class=\"glyphicon glyphicon-stats\" aria-hidden=\"true\"></span>AC\n Count</a>\n </li>\n <li>\n <a class=\"acssa-chart-tab-button\"><span class=\"glyphicon glyphicon-stats\" aria-hidden=\"true\"></span>LastAcceptedTime</a>\n </li>\n </ul>\n <ul class=\"nav nav-pills\" id=\"acssa-checkbox-tab\">\n <li id=\"acssa-checkbox-toggle-your-result-visibility-parent\">\n <a><label><input type=\"checkbox\" id=\"acssa-checkbox-toggle-your-result-visibility\" checked=\"checked\"> Plot your\n result</label></a>\n </li>\n <li id=\"acssa-checkbox-toggle-log-plot-parent\">\n <a><label><input type=\"checkbox\" id=\"acssa-checkbox-toggle-log-plot\">Log plot</label></a>\n </li>\n <li>\n <a><label><input type=\"checkbox\" id=\"acssa-checkbox-toggle-onload-plot\">Onload plot</label></a>\n </li>\n </ul>\n</div>";
  743.  
  744. const TABS_WRAPPER_ID = 'acssa-tab-wrapper';
  745. const CHART_TAB_ID = 'acssa-chart-tab';
  746. const CHART_TAB_BUTTON_CLASS = 'acssa-chart-tab-button';
  747. const CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY = 'acssa-checkbox-toggle-your-result-visibility';
  748. const PARENT_CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY = `${CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY}-parent`;
  749. const CHECKBOX_TOGGLE_LOG_PLOT = 'acssa-checkbox-toggle-log-plot';
  750. const CHECKBOX_TOGGLE_ONLOAD_PLOT = 'acssa-checkbox-toggle-onload-plot';
  751. const CONFIG_CNLOAD_PLOT_KEY = 'acssa-config-onload-plot';
  752. const PARENT_CHECKBOX_TOGGLE_LOG_PLOT = `${CHECKBOX_TOGGLE_LOG_PLOT}-parent`;
  753. class Tabs {
  754. constructor(parent, yourScore, participants) {
  755. var _a;
  756. this.yourScore = yourScore;
  757. this.participants = participants;
  758. // insert
  759. parent.insertAdjacentHTML('beforeend', html);
  760. this.showYourResultCheckbox = document.getElementById(CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY);
  761. this.logPlotCheckbox = document.getElementById(CHECKBOX_TOGGLE_LOG_PLOT);
  762. this.logPlotCheckboxParent = document.getElementById(PARENT_CHECKBOX_TOGGLE_LOG_PLOT);
  763. this.onloadPlotCheckbox = document.getElementById(CHECKBOX_TOGGLE_ONLOAD_PLOT);
  764. this.onloadPlot = JSON.parse((_a = localStorage.getItem(CONFIG_CNLOAD_PLOT_KEY)) !== null && _a !== void 0 ? _a : 'true');
  765. this.onloadPlotCheckbox.checked = this.onloadPlot;
  766. // チェックボックス操作時のイベントを登録する */
  767. this.showYourResultCheckbox.addEventListener('change', () => {
  768. if (this.showYourResultCheckbox.checked) {
  769. document.querySelectorAll('.acssa-task-success.acssa-task-success-suppress').forEach((elm) => {
  770. elm.classList.remove('acssa-task-success-suppress');
  771. });
  772. }
  773. else {
  774. document.querySelectorAll('.acssa-task-success').forEach((elm) => {
  775. elm.classList.add('acssa-task-success-suppress');
  776. });
  777. }
  778. });
  779. this.showYourResultCheckbox.addEventListener('change', () => {
  780. void this.onShowYourResultCheckboxChangedAsync();
  781. });
  782. this.logPlotCheckbox.addEventListener('change', () => {
  783. void this.onLogPlotCheckboxChangedAsync();
  784. });
  785. this.onloadPlotCheckbox.addEventListener('change', () => {
  786. this.onloadPlot = this.onloadPlotCheckbox.checked;
  787. localStorage.setItem(CONFIG_CNLOAD_PLOT_KEY, JSON.stringify(this.onloadPlot));
  788. });
  789. this.activeTab = 0;
  790. this.showYourResult = [true, true, true];
  791. this.acceptedCountYMax = -1;
  792. this.useLogPlot = [false, false, false];
  793. this.yourDifficultyChartData = null;
  794. this.yourAcceptedCountChartData = null;
  795. this.yourLastAcceptedTimeChartData = null;
  796. this.yourLastAcceptedTimeChartDataIndex = -1;
  797. document
  798. .querySelectorAll(`.${CHART_TAB_BUTTON_CLASS}`)
  799. .forEach((btn, key) => {
  800. btn.addEventListener('click', () => void this.onTabButtonClicked(btn, key));
  801. });
  802. if (this.yourScore == -1) {
  803. // disable checkbox
  804. this.showYourResultCheckbox.checked = false;
  805. this.showYourResultCheckbox.disabled = true;
  806. const checkboxParent = this.showYourResultCheckbox.parentElement;
  807. checkboxParent.style.cursor = 'default';
  808. checkboxParent.style.textDecoration = 'line-through';
  809. }
  810. }
  811. async onShowYourResultCheckboxChangedAsync() {
  812. this.showYourResult[this.activeTab] = this.showYourResultCheckbox.checked;
  813. if (this.showYourResultCheckbox.checked) {
  814. // show
  815. switch (this.activeTab) {
  816. case 0:
  817. if (this.yourScore > 0 && this.yourDifficultyChartData !== null)
  818. await Plotly.addTraces(plotlyDifficultyChartId, this.yourDifficultyChartData);
  819. break;
  820. case 1:
  821. if (this.yourScore > 0 && this.yourAcceptedCountChartData !== null)
  822. await Plotly.addTraces(plotlyAcceptedCountChartId, this.yourAcceptedCountChartData);
  823. break;
  824. case 2:
  825. if (this.yourLastAcceptedTimeChartData !== null && this.yourLastAcceptedTimeChartDataIndex != -1) {
  826. await Plotly.addTraces(plotlyLastAcceptedTimeChartId, this.yourLastAcceptedTimeChartData, this.yourLastAcceptedTimeChartDataIndex);
  827. }
  828. break;
  829. }
  830. }
  831. else {
  832. // hide
  833. switch (this.activeTab) {
  834. case 0:
  835. if (this.yourScore > 0)
  836. await Plotly.deleteTraces(plotlyDifficultyChartId, -1);
  837. break;
  838. case 1:
  839. if (this.yourScore > 0)
  840. await Plotly.deleteTraces(plotlyAcceptedCountChartId, -1);
  841. break;
  842. case 2:
  843. if (this.yourLastAcceptedTimeChartDataIndex != -1) {
  844. await Plotly.deleteTraces(plotlyLastAcceptedTimeChartId, this.yourLastAcceptedTimeChartDataIndex);
  845. }
  846. break;
  847. }
  848. }
  849. } // end async onShowYourResultCheckboxChangedAsync()
  850. async onLogPlotCheckboxChangedAsync() {
  851. if (this.acceptedCountYMax == -1)
  852. return;
  853. this.useLogPlot[this.activeTab] = this.logPlotCheckbox.checked;
  854. if (this.activeTab == 1) {
  855. if (this.logPlotCheckbox.checked) {
  856. // log plot
  857. const layout = {
  858. yaxis: {
  859. type: 'log',
  860. range: [Math.log10(0.5), Math.log10(this.acceptedCountYMax)],
  861. },
  862. };
  863. await Plotly.relayout(plotlyAcceptedCountChartId, layout);
  864. }
  865. else {
  866. // linear plot
  867. const layout = {
  868. yaxis: {
  869. type: 'linear',
  870. range: [0, this.acceptedCountYMax],
  871. },
  872. };
  873. await Plotly.relayout(plotlyAcceptedCountChartId, layout);
  874. }
  875. }
  876. else if (this.activeTab == 2) {
  877. if (this.logPlotCheckbox.checked) {
  878. // log plot
  879. const layout = {
  880. xaxis: {
  881. type: 'log',
  882. range: [Math.log10(0.5), Math.log10(this.participants)],
  883. },
  884. };
  885. await Plotly.relayout(plotlyLastAcceptedTimeChartId, layout);
  886. }
  887. else {
  888. // linear plot
  889. const layout = {
  890. xaxis: {
  891. type: 'linear',
  892. range: [0, this.participants],
  893. },
  894. };
  895. await Plotly.relayout(plotlyLastAcceptedTimeChartId, layout);
  896. }
  897. }
  898. } // end async onLogPlotCheckboxChangedAsync
  899. async onTabButtonClicked(btn, key) {
  900. // check whether active or not
  901. const buttonParent = btn.parentElement;
  902. if (buttonParent.className == 'active')
  903. return;
  904. // modify visibility
  905. this.activeTab = key;
  906. document.querySelector(`#${CHART_TAB_ID} li.active`).classList.remove('active');
  907. document.querySelector(`#${CHART_TAB_ID} li:nth-child(${key + 1})`).classList.add('active');
  908. document.querySelector('#acssa-chart-block div.acssa-chart-wrapper-active').classList.remove('acssa-chart-wrapper-active');
  909. document.querySelector(`#acssa-chart-block div.acssa-chart-wrapper:nth-child(${key + 1})`).classList.add('acssa-chart-wrapper-active');
  910. // resize charts
  911. switch (key) {
  912. case 0:
  913. await Plotly.relayout(plotlyDifficultyChartId, {
  914. width: document.getElementById(plotlyDifficultyChartId).clientWidth,
  915. });
  916. this.logPlotCheckboxParent.style.display = 'none';
  917. break;
  918. case 1:
  919. await Plotly.relayout(plotlyAcceptedCountChartId, {
  920. width: document.getElementById(plotlyAcceptedCountChartId).clientWidth,
  921. });
  922. this.logPlotCheckboxParent.style.display = 'block';
  923. break;
  924. case 2:
  925. await Plotly.relayout(plotlyLastAcceptedTimeChartId, {
  926. width: document.getElementById(plotlyLastAcceptedTimeChartId).clientWidth,
  927. });
  928. this.logPlotCheckboxParent.style.display = 'block';
  929. break;
  930. }
  931. if (this.showYourResult[this.activeTab] !== this.showYourResultCheckbox.checked) {
  932. await this.onShowYourResultCheckboxChangedAsync();
  933. }
  934. if (this.activeTab !== 0 && this.useLogPlot[this.activeTab] !== this.logPlotCheckbox.checked) {
  935. await this.onLogPlotCheckboxChangedAsync();
  936. }
  937. }
  938. showTabsControl() {
  939. document.getElementById(TABS_WRAPPER_ID).style.display = 'block';
  940. if (!this.onloadPlot) {
  941. document.getElementById(CHART_TAB_ID).style.display = 'none';
  942. document.getElementById(PARENT_CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY).style.display =
  943. 'none';
  944. }
  945. }
  946. }
  947.  
  948. const finf = bigf(400);
  949. function bigf(n) {
  950. let pow1 = 1;
  951. let pow2 = 1;
  952. let numerator = 0;
  953. let denominator = 0;
  954. for (let i = 0; i < n; ++i) {
  955. pow1 *= 0.81;
  956. pow2 *= 0.9;
  957. numerator += pow1;
  958. denominator += pow2;
  959. }
  960. return Math.sqrt(numerator) / denominator;
  961. }
  962. function f(n) {
  963. return ((bigf(n) - finf) / (bigf(1) - finf)) * 1200.0;
  964. }
  965. /**
  966. * calculate unpositivized rating from last state
  967. * @param {Number} [last] last unpositivized rating
  968. * @param {Number} [perf] performance
  969. * @param {Number} [ratedMatches] count of participated rated contest
  970. * @returns {number} estimated unpositivized rating
  971. */
  972. function calcRatingFromLast(last, perf, ratedMatches) {
  973. if (ratedMatches === 0)
  974. return perf - 1200;
  975. last += f(ratedMatches);
  976. const weight = 9 - 9 * Math.pow(0.9, ratedMatches);
  977. const numerator = weight * Math.pow(2, last / 800.0) + Math.pow(2, perf / 800.0);
  978. const denominator = 1 + weight;
  979. return Math.log2(numerator / denominator) * 800.0 - f(ratedMatches + 1);
  980. }
  981. // class Random {
  982. // x: number
  983. // y: number
  984. // z: number
  985. // w: number
  986. // constructor(seed = 88675123) {
  987. // this.x = 123456789;
  988. // this.y = 362436069;
  989. // this.z = 521288629;
  990. // this.w = seed;
  991. // }
  992. // // XorShift
  993. // next(): number {
  994. // let t;
  995. // t = this.x ^ (this.x << 11);
  996. // this.x = this.y; this.y = this.z; this.z = this.w;
  997. // return this.w = (this.w ^ (this.w >>> 19)) ^ (t ^ (t >>> 8));
  998. // }
  999. // // min以上max以下の乱数を生成する
  1000. // nextInt(min: number, max: number): number {
  1001. // const r = Math.abs(this.next());
  1002. // return min + (r % (max + 1 - min));
  1003. // }
  1004. // };
  1005. class PerformanceTable {
  1006. constructor(parent, tasks, isEstimationEnabled, yourStandingsEntry, taskAcceptedCounts, acCountPredicted, standingsData, innerRatingsFromPredictor, dcForPerformance, centerOfInnerRating, useRating) {
  1007. this.centerOfInnerRating = centerOfInnerRating;
  1008. if (yourStandingsEntry === undefined)
  1009. return;
  1010. // コンテスト終了時点での順位表を予測する
  1011. const len = acCountPredicted.length;
  1012. const rems = [];
  1013. for (let i = 0; i < len; ++i) {
  1014. rems.push(Math.ceil(acCountPredicted[i] - taskAcceptedCounts[i])); //
  1015. }
  1016. const scores = []; // (現レート,スコア合計,時間,問題ごとのスコア,rated)
  1017. const highestScores = tasks.map(() => 0);
  1018. let rowPtr = undefined;
  1019. // const ratedInnerRatings: Rating[] = [];
  1020. const ratedUserRanks = [];
  1021. // console.log(standingsData);
  1022. const threthold = moment('2021-12-03T21:00:00+09:00');
  1023. const isAfterABC230 = startTime >= threthold;
  1024. // OldRating が全員 0 なら,強制的に Rating を使用する(コンテスト終了後,レート更新前)
  1025. standingsData.forEach((standingsEntry) => {
  1026. const userScores = [];
  1027. let penalty = 0;
  1028. for (let j = 0; j < tasks.length; ++j) {
  1029. const taskResultEntry = standingsEntry.TaskResults[tasks[j].TaskScreenName];
  1030. if (!taskResultEntry) {
  1031. // 未提出
  1032. userScores.push(0);
  1033. }
  1034. else {
  1035. userScores.push(taskResultEntry.Score / 100);
  1036. highestScores[j] = Math.max(highestScores[j], taskResultEntry.Score / 100);
  1037. penalty += taskResultEntry.Score === 0 ? taskResultEntry.Failure : taskResultEntry.Penalty;
  1038. }
  1039. }
  1040. // const isRated = standingsEntry.IsRated && standingsEntry.TotalResult.Count > 0;
  1041. const isRated = standingsEntry.IsRated && (isAfterABC230 || standingsEntry.TotalResult.Count > 0);
  1042. if (!isRated) {
  1043. if (standingsEntry.TotalResult.Score === 0 && penalty === 0 && standingsEntry.TotalResult.Count == 0) {
  1044. return; // NoSub を飛ばす
  1045. }
  1046. }
  1047. standingsEntry.Rating;
  1048. // const innerRating: Rating = isTeamOrBeginner
  1049. // ? correctedRating
  1050. // : standingsEntry.UserScreenName in innerRatingsFromPredictor
  1051. // ? innerRatingsFromPredictor[standingsEntry.UserScreenName]
  1052. // : RatingConverter.toInnerRating(
  1053. // Math.max(RatingConverter.toRealRating(correctedRating), 1),
  1054. // standingsEntry.Competitions
  1055. // );
  1056. const innerRating = standingsEntry.UserScreenName in innerRatingsFromPredictor
  1057. ? innerRatingsFromPredictor[standingsEntry.UserScreenName]
  1058. : this.centerOfInnerRating;
  1059. if (isRated) {
  1060. // ratedInnerRatings.push(innerRating);
  1061. ratedUserRanks.push(standingsEntry.EntireRank);
  1062. // if (innerRating || true) {
  1063. const row = [
  1064. innerRating,
  1065. standingsEntry.TotalResult.Score / 100,
  1066. standingsEntry.TotalResult.Elapsed + 300 * standingsEntry.TotalResult.Penalty,
  1067. userScores,
  1068. isRated,
  1069. ];
  1070. scores.push(row);
  1071. if ((standingsEntry.UserScreenName == userScreenName)) {
  1072. rowPtr = row;
  1073. }
  1074. // }
  1075. }
  1076. });
  1077. const sameRatedRankCount = ratedUserRanks.reduce((prev, cur) => {
  1078. if (cur == yourStandingsEntry.EntireRank)
  1079. prev++;
  1080. return prev;
  1081. }, 0);
  1082. const ratedRank = ratedUserRanks.reduce((prev, cur) => {
  1083. if (cur < yourStandingsEntry.EntireRank)
  1084. prev += 1;
  1085. return prev;
  1086. }, (1 + sameRatedRankCount) / 2);
  1087. // レート順でソート
  1088. scores.sort((a, b) => {
  1089. const [innerRatingA, scoreA, timeElapsedA] = a;
  1090. const [innerRatingB, scoreB, timeElapsedB] = b;
  1091. if (innerRatingA != innerRatingB) {
  1092. return innerRatingB - innerRatingA; // 降順(レートが高い順)
  1093. }
  1094. if (scoreA != scoreB) {
  1095. return scoreB - scoreA; // 降順(順位が高い順)
  1096. }
  1097. return timeElapsedA - timeElapsedB; // 昇順(順位が高い順)
  1098. });
  1099. // const random = new Random(0);
  1100. // スコア変化をシミュレート
  1101. // (現レート,スコア合計,時間,問題ごとのスコア,rated)
  1102. scores.forEach((score) => {
  1103. const [, , , scoresA] = score;
  1104. // 自分は飛ばす
  1105. if (score == rowPtr)
  1106. return;
  1107. for (let j = 0; j < tasks.length; ++j) {
  1108. // if (random.nextInt(0, 9) <= 2) continue;
  1109. // まだ満点ではなく,かつ正解者を増やせるなら
  1110. if (scoresA[j] < highestScores[j] && rems[j] > 0) {
  1111. const dif = highestScores[j] - scoresA[j];
  1112. score[1] += dif;
  1113. score[2] += 1000000000 * 60 * 30; // とりあえず30分で解くと仮定する
  1114. scoresA[j] = highestScores[j];
  1115. rems[j]--;
  1116. }
  1117. if (rems[j] == 0)
  1118. break;
  1119. }
  1120. });
  1121. // 順位でソート
  1122. scores.sort((a, b) => {
  1123. const [innerRatingA, scoreA, timeElapsedA, ,] = a;
  1124. const [innerRatingB, scoreB, timeElapsedB, ,] = b;
  1125. if (scoreA != scoreB) {
  1126. return scoreB - scoreA; // 降順(順位が高い順)
  1127. }
  1128. if (timeElapsedA != timeElapsedB) {
  1129. return timeElapsedA - timeElapsedB; // 昇順(順位が高い順)
  1130. }
  1131. return innerRatingB - innerRatingA; // 降順(レートが高い順)
  1132. });
  1133. // 順位を求める
  1134. let estimatedRank = -1;
  1135. let rank = 0;
  1136. let sameCnt = 0;
  1137. for (let i = 0; i < scores.length; ++i) {
  1138. if (estimatedRank == -1) {
  1139. if (scores[i][4] === true) {
  1140. rank++;
  1141. }
  1142. if (scores[i] === rowPtr) {
  1143. if (rank === 0)
  1144. rank = 1;
  1145. estimatedRank = rank;
  1146. // break;
  1147. }
  1148. }
  1149. else {
  1150. if (rowPtr === undefined)
  1151. break;
  1152. if (scores[i][1] === rowPtr[1] && scores[i][2] === rowPtr[2]) {
  1153. sameCnt++;
  1154. }
  1155. else {
  1156. break;
  1157. }
  1158. }
  1159. } //1246
  1160. estimatedRank += sameCnt / 2;
  1161. // const dc = new DifficultyCalculator(ratedInnerRatings);
  1162. // insert
  1163. parent.insertAdjacentHTML('beforeend', `
  1164. <p><span class="h2">Performance</span></p>
  1165. <div id="acssa-perf-table-wrapper">
  1166. <table id="acssa-perf-table" class="table table-bordered table-hover th-center td-center td-middle acssa-table">
  1167. <tbody>
  1168. <tr class="acssa-thead">
  1169. ${isEstimationEnabled ? '<td></td>' : ''}
  1170. <td id="acssa-thead-perf" class="acssa-thead">perf</td>
  1171. <td id="acssa-thead-perf" class="acssa-thead">レート変化</td>
  1172. </tr>
  1173. </tbody>
  1174. <tbody>
  1175. <tr id="acssa-perf-tbody" class="acssa-tbody"></tr>
  1176. ${isEstimationEnabled
  1177. ? `
  1178. <tr id="acssa-perf-tbody-predicted" class="acssa-tbody"></tr>
  1179. `
  1180. : ''}
  1181. </tbody>
  1182. </table>
  1183. </div>
  1184. `);
  1185. if (isEstimationEnabled) {
  1186. document.getElementById(`acssa-perf-tbody`).insertAdjacentHTML('beforeend', `<th>Current</td>`);
  1187. document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', `<th>Predicted</td>`);
  1188. }
  1189. // build
  1190. const id = `td-assa-perf-current`;
  1191. // TODO: ちゃんと判定する
  1192. // const perf = Math.min(2400, dc.rank2InnerPerf(ratedRank));
  1193. const perf = RatingConverter.toCorrectedRating(dcForPerformance.rank2InnerPerf(ratedRank));
  1194. //
  1195. document.getElementById(`acssa-perf-tbody`).insertAdjacentHTML('beforeend', `
  1196. <td id="${id}" style="color:${getColor(perf)};">
  1197. ${perf === 9999 ? '-' : perf}</td>
  1198. `);
  1199. if (perf !== 9999) {
  1200. document.getElementById(id).insertAdjacentHTML('afterbegin', generateDifficultyCircle(perf));
  1201. const oldRating = useRating ? yourStandingsEntry.Rating : yourStandingsEntry.OldRating;
  1202. // const oldRating = yourStandingsEntry.Rating;
  1203. const nextRating = Math.round(RatingConverter.toCorrectedRating(calcRatingFromLast(RatingConverter.toRealRating(oldRating), perf, yourStandingsEntry.Competitions)));
  1204. const sign = nextRating > oldRating ? '+' : nextRating < oldRating ? '-' : '±';
  1205. document.getElementById(`acssa-perf-tbody`).insertAdjacentHTML('beforeend', `
  1206. <td>
  1207. <span style="font-weight:bold;color:${getColor(oldRating)}">${oldRating}</span>
  1208. <span style="font-weight:bold;color:${getColor(nextRating)}">${nextRating}</span>
  1209. <span style="color:gray">(${sign}${Math.abs(nextRating - oldRating)})</span>
  1210. </td>
  1211. `);
  1212. }
  1213. if (isEstimationEnabled) {
  1214. if (estimatedRank != -1) {
  1215. const perfEstimated = RatingConverter.toCorrectedRating(dcForPerformance.rank2InnerPerf(estimatedRank));
  1216. const id2 = `td-assa-perf-predicted`;
  1217. document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', `
  1218. <td id="${id2}" style="color:${getColor(perfEstimated)};">
  1219. ${perfEstimated === 9999 ? '-' : perfEstimated}</td>
  1220. `);
  1221. if (perfEstimated !== 9999) {
  1222. document.getElementById(id2).insertAdjacentHTML('afterbegin', generateDifficultyCircle(perfEstimated));
  1223. const oldRating = useRating ? yourStandingsEntry.Rating : yourStandingsEntry.OldRating;
  1224. // const oldRating = yourStandingsEntry.Rating;
  1225. const nextRating = Math.round(RatingConverter.toCorrectedRating(calcRatingFromLast(RatingConverter.toRealRating(oldRating), perfEstimated, yourStandingsEntry.Competitions)));
  1226. const sign = nextRating > oldRating ? '+' : nextRating < oldRating ? '-' : '±';
  1227. document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', `
  1228. <td>
  1229. <span style="font-weight:bold;color:${getColor(oldRating)}">${oldRating}</span>
  1230. <span style="font-weight:bold;color:${getColor(nextRating)}">${nextRating}</span>
  1231. <span style="color:gray">(${sign}${Math.abs(nextRating - oldRating)})</span>
  1232. </td>
  1233. `);
  1234. }
  1235. }
  1236. else {
  1237. document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', '<td>?</td>');
  1238. }
  1239. }
  1240. }
  1241. }
  1242.  
  1243. const NS2SEC = 1000000000;
  1244. const CONTENT_DIV_ID = 'acssa-contents';
  1245. class Parent {
  1246. constructor(acRatioModel, centerOfInnerRating) {
  1247. const loaderStyles = GM_getResourceText('loaders.min.css');
  1248. GM_addStyle(loaderStyles + '\n' + css);
  1249. // this.centerOfInnerRating = getCenterOfInnerRating(contestScreenName);
  1250. this.centerOfInnerRating = centerOfInnerRating;
  1251. this.acRatioModel = acRatioModel;
  1252. this.working = false;
  1253. this.oldStandingsData = null;
  1254. this.hasTeamStandings = this.searchTeamStandingsPage();
  1255. this.yourStandingsEntry = undefined;
  1256. }
  1257. searchTeamStandingsPage() {
  1258. const teamStandingsLink = document.querySelector(`a[href*="/contests/${contestScreenName}/standings/team"]`);
  1259. return teamStandingsLink !== null;
  1260. }
  1261. async onStandingsChanged(standings) {
  1262. if (!standings)
  1263. return;
  1264. if (this.working)
  1265. return;
  1266. this.tasks = standings.TaskInfo;
  1267. const standingsData = standings.StandingsData; // vueStandings.filteredStandings;
  1268. if (this.oldStandingsData === standingsData)
  1269. return;
  1270. if (this.tasks.length === 0)
  1271. return;
  1272. this.oldStandingsData = standingsData;
  1273. this.working = true;
  1274. this.removeOldContents();
  1275. const currentTime = moment();
  1276. this.elapsedMinutes = Math.floor(currentTime.diff(startTime) / 60 / 1000);
  1277. this.isDuringContest = startTime <= currentTime && currentTime < endTime;
  1278. this.isEstimationEnabled = this.isDuringContest && this.elapsedMinutes >= 1 && this.tasks.length < 10;
  1279. const useRating = this.isDuringContest || this.areOldRatingsAllZero(standingsData);
  1280. this.innerRatingsFromPredictor = await fetchInnerRatingsFromPredictor(contestScreenName);
  1281. this.scanStandingsData(standingsData);
  1282. this.predictAcCountSeries();
  1283. const standingsElement = document.getElementById('vue-standings');
  1284. const acssaContentDiv = document.createElement('div');
  1285. acssaContentDiv.id = CONTENT_DIV_ID;
  1286. standingsElement.insertAdjacentElement('afterbegin', acssaContentDiv);
  1287. if (this.hasTeamStandings) {
  1288. if (!location.href.includes('/standings/team')) {
  1289. // チーム戦順位表へ誘導
  1290. acssaContentDiv.insertAdjacentHTML('afterbegin', teamalert);
  1291. }
  1292. }
  1293. // difficulty
  1294. new DifficyltyTable(acssaContentDiv, this.tasks, this.isEstimationEnabled, this.dcForDifficulty, this.taskAcceptedCounts, this.yourTaskAcceptedElapsedTimes, this.acCountPredicted);
  1295. new PerformanceTable(acssaContentDiv, this.tasks, this.isEstimationEnabled, this.yourStandingsEntry, this.taskAcceptedCounts, this.acCountPredicted, standingsData, this.innerRatingsFromPredictor, this.dcForPerformance, this.centerOfInnerRating, useRating);
  1296. // console.log(this.yourStandingsEntry);
  1297. // console.log(this.yourStandingsEntry?.EntireRank);
  1298. // console.log(this.dc.rank2InnerPerf((this.yourStandingsEntry?.EntireRank ?? 10000) - 0));
  1299. // tabs
  1300. const tabs = new Tabs(acssaContentDiv, this.yourScore, this.participants);
  1301. const charts = new Charts(acssaContentDiv, this.tasks, this.scoreLastAcceptedTimeMap, this.taskAcceptedCounts, this.taskAcceptedElapsedTimes, this.yourTaskAcceptedElapsedTimes, this.yourScore, this.yourLastAcceptedTime, this.participants, this.dcForDifficulty, this.dcForPerformance, this.ratedRank2EntireRank, tabs);
  1302. if (tabs.onloadPlot) {
  1303. // 順位表のその他の描画を優先するために,プロットは後回しにする
  1304. void charts.plotAsync().then(() => {
  1305. charts.hideLoader();
  1306. tabs.showTabsControl();
  1307. this.working = false;
  1308. });
  1309. }
  1310. else {
  1311. charts.hideLoader();
  1312. tabs.showTabsControl();
  1313. }
  1314. }
  1315. removeOldContents() {
  1316. const oldContents = document.getElementById(CONTENT_DIV_ID);
  1317. if (oldContents) {
  1318. // oldContents.parentNode.removeChild(oldContents);
  1319. oldContents.remove();
  1320. }
  1321. }
  1322. scanStandingsData(standingsData) {
  1323. // init
  1324. this.scoreLastAcceptedTimeMap = new Map();
  1325. this.taskAcceptedCounts = rangeLen(this.tasks.length).fill(0);
  1326. this.taskAcceptedElapsedTimes = rangeLen(this.tasks.length).map(() => []);
  1327. this.innerRatings = [];
  1328. this.ratedInnerRatings = [];
  1329. this.ratedRank2EntireRank = [];
  1330. this.yourTaskAcceptedElapsedTimes = rangeLen(this.tasks.length).fill(-1);
  1331. this.yourScore = -1;
  1332. this.yourLastAcceptedTime = -1;
  1333. this.participants = 0;
  1334. this.yourStandingsEntry = undefined;
  1335. // scan
  1336. const threthold = moment('2021-12-03T21:00:00+09:00');
  1337. const isAfterABC230 = startTime >= threthold;
  1338. for (let i = 0; i < standingsData.length; ++i) {
  1339. const standingsEntry = standingsData[i];
  1340. const isRated = standingsEntry.IsRated && (isAfterABC230 || standingsEntry.TotalResult.Count > 0);
  1341. if (isRated) {
  1342. const ratedInnerRating = standingsEntry.UserScreenName in this.innerRatingsFromPredictor
  1343. ? this.innerRatingsFromPredictor[standingsEntry.UserScreenName]
  1344. : this.centerOfInnerRating;
  1345. this.ratedInnerRatings.push(ratedInnerRating);
  1346. this.ratedRank2EntireRank.push(standingsEntry.EntireRank);
  1347. }
  1348. if (!standingsEntry.TaskResults)
  1349. continue; // 参加登録していない
  1350. if (standingsEntry.UserIsDeleted)
  1351. continue; // アカウント削除
  1352. // let correctedRating = this.isDuringContest ? standingsEntry.Rating : standingsEntry.OldRating;
  1353. let correctedRating = standingsEntry.Rating;
  1354. const isTeamOrBeginner = correctedRating === 0;
  1355. if (isTeamOrBeginner) {
  1356. // continue; // 初参加 or チーム
  1357. correctedRating = this.centerOfInnerRating;
  1358. }
  1359. const innerRating = isTeamOrBeginner
  1360. ? correctedRating
  1361. : standingsEntry.UserScreenName in this.innerRatingsFromPredictor
  1362. ? this.innerRatingsFromPredictor[standingsEntry.UserScreenName]
  1363. : RatingConverter.toInnerRating(Math.max(RatingConverter.toRealRating(correctedRating), 1), standingsEntry.Competitions);
  1364. // これは飛ばしちゃダメ(提出しても 0 AC だと Penalty == 0 なので)
  1365. // if (standingsEntry.TotalResult.Score == 0 && standingsEntry.TotalResult.Penalty == 0) continue;
  1366. let score = 0;
  1367. let penalty = 0;
  1368. for (let j = 0; j < this.tasks.length; ++j) {
  1369. const taskResultEntry = standingsEntry.TaskResults[this.tasks[j].TaskScreenName];
  1370. if (!taskResultEntry)
  1371. continue; // 未提出
  1372. score += taskResultEntry.Score;
  1373. penalty += taskResultEntry.Score === 0 ? taskResultEntry.Failure : taskResultEntry.Penalty;
  1374. }
  1375. if (score === 0 && penalty === 0 && standingsEntry.TotalResult.Count == 0)
  1376. continue; // NoSub を飛ばす
  1377. this.participants++;
  1378. // console.log(i + 1, score, penalty);
  1379. score /= 100;
  1380. if (this.scoreLastAcceptedTimeMap.has(score)) {
  1381. this.scoreLastAcceptedTimeMap.get(score).push(standingsEntry.TotalResult.Elapsed / NS2SEC);
  1382. }
  1383. else {
  1384. this.scoreLastAcceptedTimeMap.set(score, [standingsEntry.TotalResult.Elapsed / NS2SEC]);
  1385. }
  1386. // console.log(this.isDuringContest, standingsEntry.Rating, standingsEntry.OldRating, innerRating);
  1387. // if (standingsEntry.IsRated && innerRating) {
  1388. // if (innerRating) {
  1389. // this.innerRatings.push(innerRating);
  1390. // } else {
  1391. // console.log(i, innerRating, correctedRating, standingsEntry.Competitions, standingsEntry, this.innerRatingsFromPredictor[standingsEntry.UserScreenName]);
  1392. // continue;
  1393. // }
  1394. this.innerRatings.push(innerRating);
  1395. for (let j = 0; j < this.tasks.length; ++j) {
  1396. const taskResultEntry = standingsEntry.TaskResults[this.tasks[j].TaskScreenName];
  1397. const isAccepted = (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Score) > 0 && (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Status) == 1;
  1398. if (isAccepted) {
  1399. ++this.taskAcceptedCounts[j];
  1400. this.taskAcceptedElapsedTimes[j].push(taskResultEntry.Elapsed / NS2SEC);
  1401. }
  1402. }
  1403. if ((standingsEntry.UserScreenName == userScreenName)) {
  1404. this.yourScore = score;
  1405. this.yourLastAcceptedTime = standingsEntry.TotalResult.Elapsed / NS2SEC;
  1406. this.yourStandingsEntry = standingsEntry;
  1407. for (let j = 0; j < this.tasks.length; ++j) {
  1408. const taskResultEntry = standingsEntry.TaskResults[this.tasks[j].TaskScreenName];
  1409. const isAccepted = (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Score) > 0 && (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Status) == 1;
  1410. if (isAccepted) {
  1411. this.yourTaskAcceptedElapsedTimes[j] = taskResultEntry.Elapsed / NS2SEC;
  1412. }
  1413. }
  1414. }
  1415. } // end for
  1416. this.innerRatings.sort((a, b) => a - b);
  1417. this.ratedInnerRatings.sort((a, b) => a - b);
  1418. this.ratedRank2EntireRank.sort((a, b) => a - b);
  1419. this.dcForDifficulty = new DifficultyCalculator(this.innerRatings);
  1420. this.dcForPerformance = new DifficultyCalculator(this.ratedInnerRatings);
  1421. } // end async scanStandingsData
  1422. predictAcCountSeries() {
  1423. if (!this.isEstimationEnabled) {
  1424. this.acCountPredicted = [];
  1425. return;
  1426. }
  1427. // 時間ごとの AC 数推移を計算する
  1428. const taskAcceptedCountImos = rangeLen(this.tasks.length).map(() => rangeLen(this.elapsedMinutes).map(() => 0));
  1429. this.taskAcceptedElapsedTimes.forEach((ar, index) => {
  1430. ar.forEach((seconds) => {
  1431. const minutes = Math.floor(seconds / 60);
  1432. if (minutes >= this.elapsedMinutes)
  1433. return;
  1434. taskAcceptedCountImos[index][minutes] += 1;
  1435. });
  1436. });
  1437. const taskAcceptedRatio = rangeLen(this.tasks.length).map(() => []);
  1438. taskAcceptedCountImos.forEach((ar, index) => {
  1439. let cum = 0;
  1440. ar.forEach((imos) => {
  1441. cum += imos;
  1442. taskAcceptedRatio[index].push(cum / this.participants);
  1443. });
  1444. });
  1445. // 差の自乗和が最小になるシーケンスを探す
  1446. this.acCountPredicted = taskAcceptedRatio.map((ar) => {
  1447. if (this.acRatioModel === undefined)
  1448. return 0;
  1449. if (ar[this.elapsedMinutes - 1] === 0)
  1450. return 0;
  1451. let minerror = 1.0 * this.elapsedMinutes;
  1452. // let argmin = '';
  1453. let last_ratio = 0;
  1454. Object.keys(this.acRatioModel).forEach((key) => {
  1455. if (this.acRatioModel === undefined)
  1456. return;
  1457. const ar2 = this.acRatioModel[key];
  1458. let error = 0;
  1459. for (let i = 0; i < this.elapsedMinutes; ++i) {
  1460. error += Math.pow(ar[i] - ar2[i], 2);
  1461. }
  1462. if (error < minerror) {
  1463. minerror = error;
  1464. // argmin = key;
  1465. if (ar2[this.elapsedMinutes - 1] > 0) {
  1466. last_ratio = ar2[ar2.length - 1] * (ar[this.elapsedMinutes - 1] / ar2[this.elapsedMinutes - 1]);
  1467. }
  1468. else {
  1469. last_ratio = ar2[ar2.length - 1];
  1470. }
  1471. }
  1472. });
  1473. // console.log(argmin, minerror, last_ratio);
  1474. if (last_ratio > 1)
  1475. last_ratio = 1;
  1476. return this.participants * last_ratio;
  1477. });
  1478. } // end predictAcCountSeries();
  1479. areOldRatingsAllZero(standingsData) {
  1480. return standingsData.every((standingsEntry) => standingsEntry.OldRating == 0);
  1481. }
  1482. }
  1483. Parent.init = async () => {
  1484. const contestRatedRange = await getContestRatedRangeAsync(contestScreenName);
  1485. const centerOfInnerRating = getCenterOfInnerRatingFromRange(contestRatedRange);
  1486. const curr = moment();
  1487. if (startTime <= curr && curr < endTime) {
  1488. const contestDurationMinutes = endTime.diff(startTime) / 1000 / 60;
  1489. return new Parent(await fetchContestAcRatioModel(contestScreenName, contestDurationMinutes), centerOfInnerRating);
  1490. }
  1491. else {
  1492. return new Parent(undefined, centerOfInnerRating);
  1493. }
  1494. };
  1495.  
  1496. {
  1497. const script = document.createElement('script');
  1498. script.src = 'https://cdnjs.cloudflare.com/ajax/libs/plotly.js/1.33.1/plotly.min.js';
  1499. script.async = true;
  1500. script.onload = async () => {
  1501. const parent = await Parent.init();
  1502. vueStandings.$watch('standings', (standings) => {
  1503. void parent.onStandingsChanged(standings);
  1504. }, { deep: true, immediate: true });
  1505. };
  1506. script.onerror = () => {
  1507. console.error('plotly load failed');
  1508. };
  1509. document.head.appendChild(script);
  1510. }