atcoder-standings-difficulty-analyzer

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

  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. }