Greasy Fork 还支持 简体中文。

atcoder-standings-difficulty-analyzer

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

目前為 2022-05-05 提交的版本,檢視 最新版本

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