atcoder-standings-difficulty-analyzer

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

当前为 2021-09-12 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name atcoder-standings-difficulty-analyzer
  3. // @namespace iilj
  4. // @version 2021.9.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] = 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. getTimeSeriesChartData() {
  259. /** Difficulty Chart のデータ */
  260. const difficultyChartData = [];
  261. /** AC Count Chart のデータ */
  262. const acceptedCountChartData = [];
  263. for (let j = 0; j < this.tasks.length; ++j) {
  264. //
  265. const interval = Math.ceil(this.taskAcceptedCounts[j] / 140);
  266. const [taskAcceptedElapsedTimesForChart, taskAcceptedCountsForChart] = this.taskAcceptedElapsedTimes[j].reduce(([ar, arr], tm, idx) => {
  267. const tmpInterval = Math.max(1, Math.min(Math.ceil(idx / 10), interval));
  268. if (idx % tmpInterval == 0 || idx == this.taskAcceptedCounts[j] - 1) {
  269. ar.push(tm);
  270. arr.push(idx + 1);
  271. }
  272. return [ar, arr];
  273. }, [[], []]);
  274. difficultyChartData.push({
  275. x: taskAcceptedElapsedTimesForChart,
  276. y: taskAcceptedCountsForChart.map((taskAcceptedCountForChart) => this.dc.binarySearchCorrectedDifficulty(taskAcceptedCountForChart)),
  277. type: 'scatter',
  278. name: `${this.tasks[j].Assignment}`,
  279. });
  280. acceptedCountChartData.push({
  281. x: taskAcceptedElapsedTimesForChart,
  282. y: taskAcceptedCountsForChart,
  283. type: 'scatter',
  284. name: `${this.tasks[j].Assignment}`,
  285. });
  286. }
  287. // 現在のユーザのデータを追加
  288. if (this.yourScore !== -1) {
  289. const yourAcceptedTimes = [];
  290. const yourAcceptedDifficulties = [];
  291. const yourAcceptedCounts = [];
  292. for (let j = 0; j < this.tasks.length; ++j) {
  293. if (this.yourTaskAcceptedElapsedTimes[j] !== -1) {
  294. yourAcceptedTimes.push(this.yourTaskAcceptedElapsedTimes[j]);
  295. const yourAcceptedCount = arrayLowerBound(this.taskAcceptedElapsedTimes[j], this.yourTaskAcceptedElapsedTimes[j]) + 1;
  296. yourAcceptedCounts.push(yourAcceptedCount);
  297. yourAcceptedDifficulties.push(this.dc.binarySearchCorrectedDifficulty(yourAcceptedCount));
  298. }
  299. }
  300. this.tabs.yourDifficultyChartData = {
  301. x: yourAcceptedTimes,
  302. y: yourAcceptedDifficulties,
  303. mode: 'markers',
  304. type: 'scatter',
  305. name: `${userScreenName}`,
  306. marker: yourMarker,
  307. };
  308. this.tabs.yourAcceptedCountChartData = {
  309. x: yourAcceptedTimes,
  310. y: yourAcceptedCounts,
  311. mode: 'markers',
  312. type: 'scatter',
  313. name: `${userScreenName}`,
  314. marker: yourMarker,
  315. };
  316. difficultyChartData.push(this.tabs.yourDifficultyChartData);
  317. acceptedCountChartData.push(this.tabs.yourAcceptedCountChartData);
  318. }
  319. return [difficultyChartData, acceptedCountChartData];
  320. }
  321. /** 得点と提出時間データの準備 */
  322. getLastAcceptedTimeChartData() {
  323. const lastAcceptedTimeChartData = [];
  324. const scores = [...this.scoreLastAcceptedTimeMap.keys()];
  325. scores.sort((a, b) => b - a);
  326. let acc = 0;
  327. let maxAcceptedTime = 0;
  328. scores.forEach((score) => {
  329. const lastAcceptedTimes = this.scoreLastAcceptedTimeMap.get(score);
  330. lastAcceptedTimes.sort((a, b) => a - b);
  331. const interval = Math.ceil(lastAcceptedTimes.length / 100);
  332. const lastAcceptedTimesForChart = lastAcceptedTimes.reduce((ar, tm, idx) => {
  333. if (idx % interval == 0 || idx == lastAcceptedTimes.length - 1)
  334. ar.push(tm);
  335. return ar;
  336. }, []);
  337. const lastAcceptedTimesRanks = lastAcceptedTimes.reduce((ar, tm, idx) => {
  338. if (idx % interval == 0 || idx == lastAcceptedTimes.length - 1)
  339. ar.push(acc + idx + 1);
  340. return ar;
  341. }, []);
  342. lastAcceptedTimeChartData.push({
  343. x: lastAcceptedTimesRanks,
  344. y: lastAcceptedTimesForChart,
  345. type: 'scatter',
  346. name: `${score}`,
  347. });
  348. if (score === this.yourScore) {
  349. const lastAcceptedTimesRank = arrayLowerBound(lastAcceptedTimes, this.yourLastAcceptedTime);
  350. this.tabs.yourLastAcceptedTimeChartData = {
  351. x: [acc + lastAcceptedTimesRank + 1],
  352. y: [this.yourLastAcceptedTime],
  353. mode: 'markers',
  354. type: 'scatter',
  355. name: `${userScreenName}`,
  356. marker: yourMarker,
  357. };
  358. this.tabs.yourLastAcceptedTimeChartDataIndex = lastAcceptedTimeChartData.length + 0;
  359. lastAcceptedTimeChartData.push(this.tabs.yourLastAcceptedTimeChartData);
  360. }
  361. acc += lastAcceptedTimes.length;
  362. if (lastAcceptedTimes[lastAcceptedTimes.length - 1] > maxAcceptedTime) {
  363. maxAcceptedTime = lastAcceptedTimes[lastAcceptedTimes.length - 1];
  364. }
  365. });
  366. return [lastAcceptedTimeChartData, maxAcceptedTime];
  367. }
  368. /**
  369. * 軸フォーマットをカスタムする
  370. * Support specifying a function for tickformat · Issue #1464 · plotly/plotly.js
  371. * https://github.com/plotly/plotly.js/issues/1464#issuecomment-498050894
  372. */
  373. overrideAxisFormat() {
  374. const org_locale = Plotly.d3.locale;
  375. Plotly.d3.locale = (locale) => {
  376. const result = org_locale(locale);
  377. // eslint-disable-next-line @typescript-eslint/unbound-method
  378. const org_number_format = result.numberFormat;
  379. result.numberFormat = (format) => {
  380. if (format != 'TIME') {
  381. return org_number_format(format);
  382. }
  383. return (x) => formatTimespan(x).toString();
  384. };
  385. return result;
  386. };
  387. }
  388. /** Difficulty Chart 描画 */
  389. async plotDifficultyChartData(difficultyChartData) {
  390. const maxAcceptedCount = this.taskAcceptedCounts.reduce((a, b) => Math.max(a, b));
  391. const yMax = RatingConverter.toCorrectedRating(this.dc.binarySearchCorrectedDifficulty(1));
  392. const yMin = RatingConverter.toCorrectedRating(this.dc.binarySearchCorrectedDifficulty(Math.max(2, maxAcceptedCount)));
  393. // 描画
  394. const layout = {
  395. title: 'Difficulty',
  396. xaxis: {
  397. dtick: this.xtick,
  398. tickformat: 'TIME',
  399. range: [0, this.duration],
  400. // title: { text: 'Elapsed' }
  401. },
  402. yaxis: {
  403. dtick: 400,
  404. tickformat: 'd',
  405. range: [
  406. Math.max(0, Math.floor((yMin - 100) / 400) * 400),
  407. Math.max(0, Math.ceil((yMax + 100) / 400) * 400),
  408. ],
  409. // title: { text: 'Difficulty' }
  410. },
  411. shapes: colors.map((c) => {
  412. return {
  413. type: 'rect',
  414. layer: 'below',
  415. xref: 'x',
  416. yref: 'y',
  417. x0: 0,
  418. x1: this.duration,
  419. y0: c[0],
  420. y1: c[1],
  421. line: { width: 0 },
  422. fillcolor: c[2],
  423. };
  424. }),
  425. margin: {
  426. b: 60,
  427. t: 30,
  428. },
  429. };
  430. await Plotly.newPlot(plotlyDifficultyChartId, difficultyChartData, layout, config);
  431. window.addEventListener('resize', () => {
  432. if (this.tabs.activeTab == 0)
  433. void Plotly.relayout(plotlyDifficultyChartId, {
  434. width: document.getElementById(plotlyDifficultyChartId).clientWidth,
  435. });
  436. });
  437. }
  438. /** Accepted Count Chart 描画 */
  439. async plotAcceptedCountChartData(acceptedCountChartData) {
  440. this.tabs.acceptedCountYMax = this.participants;
  441. const rectSpans = colors.reduce((ar, cur) => {
  442. const bottom = this.dc.perf2ExpectedAcceptedCount(cur[1]);
  443. if (bottom > this.tabs.acceptedCountYMax)
  444. return ar;
  445. const top = cur[0] == 0 ? this.tabs.acceptedCountYMax : this.dc.perf2ExpectedAcceptedCount(cur[0]);
  446. if (top < 0.5)
  447. return ar;
  448. ar.push([Math.max(0.5, bottom), Math.min(this.tabs.acceptedCountYMax, top), cur[2]]);
  449. return ar;
  450. }, []);
  451. // 描画
  452. const layout = {
  453. title: 'Accepted Count',
  454. xaxis: {
  455. dtick: this.xtick,
  456. tickformat: 'TIME',
  457. range: [0, this.duration],
  458. // title: { text: 'Elapsed' }
  459. },
  460. yaxis: {
  461. // type: 'log',
  462. // dtick: 100,
  463. tickformat: 'd',
  464. range: [0, this.tabs.acceptedCountYMax],
  465. // range: [
  466. // Math.log10(0.5),
  467. // Math.log10(acceptedCountYMax)
  468. // ],
  469. // title: { text: 'Difficulty' }
  470. },
  471. shapes: rectSpans.map((span) => {
  472. return {
  473. type: 'rect',
  474. layer: 'below',
  475. xref: 'x',
  476. yref: 'y',
  477. x0: 0,
  478. x1: this.duration,
  479. y0: span[0],
  480. y1: span[1],
  481. line: { width: 0 },
  482. fillcolor: span[2],
  483. };
  484. }),
  485. margin: {
  486. b: 60,
  487. t: 30,
  488. },
  489. };
  490. await Plotly.newPlot(plotlyAcceptedCountChartId, acceptedCountChartData, layout, config);
  491. window.addEventListener('resize', () => {
  492. if (this.tabs.activeTab == 1)
  493. void Plotly.relayout(plotlyAcceptedCountChartId, {
  494. width: document.getElementById(plotlyAcceptedCountChartId).clientWidth,
  495. });
  496. });
  497. }
  498. /** LastAcceptedTime Chart 描画 */
  499. async plotLastAcceptedTimeChartData(lastAcceptedTimeChartData, maxAcceptedTime) {
  500. const xMax = this.participants;
  501. const yMax = Math.ceil((maxAcceptedTime + this.xtick / 2) / this.xtick) * this.xtick;
  502. const rectSpans = colors.reduce((ar, cur) => {
  503. const right = cur[0] == 0 ? xMax : this.dc.perf2Ranking(cur[0]);
  504. if (right < 1)
  505. return ar;
  506. const left = this.dc.perf2Ranking(cur[1]);
  507. if (left > xMax)
  508. return ar;
  509. ar.push([Math.max(0, left), Math.min(xMax, right), cur[2]]);
  510. return ar;
  511. }, []);
  512. // console.log(colors);
  513. // console.log(rectSpans);
  514. const layout = {
  515. title: 'LastAcceptedTime v.s. Rank',
  516. xaxis: {
  517. // dtick: 100,
  518. tickformat: 'd',
  519. range: [0, xMax],
  520. // title: { text: 'Elapsed' }
  521. },
  522. yaxis: {
  523. dtick: this.xtick,
  524. tickformat: 'TIME',
  525. range: [0, yMax],
  526. // range: [
  527. // Math.max(0, Math.floor((yMin - 100) / 400) * 400),
  528. // Math.max(0, Math.ceil((yMax + 100) / 400) * 400)
  529. // ],
  530. // title: { text: 'Difficulty' }
  531. },
  532. shapes: rectSpans.map((span) => {
  533. return {
  534. type: 'rect',
  535. layer: 'below',
  536. xref: 'x',
  537. yref: 'y',
  538. x0: span[0],
  539. x1: span[1],
  540. y0: 0,
  541. y1: yMax,
  542. line: { width: 0 },
  543. fillcolor: span[2],
  544. };
  545. }),
  546. margin: {
  547. b: 60,
  548. t: 30,
  549. },
  550. };
  551. await Plotly.newPlot(plotlyLastAcceptedTimeChartId, lastAcceptedTimeChartData, layout, config);
  552. window.addEventListener('resize', () => {
  553. if (this.tabs.activeTab == 2)
  554. void Plotly.relayout(plotlyLastAcceptedTimeChartId, {
  555. width: document.getElementById(plotlyLastAcceptedTimeChartId).clientWidth,
  556. });
  557. });
  558. }
  559. hideLoader() {
  560. document.getElementById(LOADER_ID).style.display = 'none';
  561. }
  562. }
  563.  
  564. /** レートを表す難易度円(◒)の HTML 文字列を生成 */
  565. const generateDifficultyCircle = (rating, isSmall = true) => {
  566. const size = isSmall ? 12 : 36;
  567. const borderWidth = isSmall ? 1 : 3;
  568. const style = `display:inline-block;border-radius:50%;border-style:solid;border-width:${borderWidth}px;` +
  569. `margin-right:5px;vertical-align:initial;height:${size}px;width:${size}px;`;
  570. if (rating < 3200) {
  571. // 色と円がどのぐらい満ちているかを計算
  572. const color = getColor(rating);
  573. const percentFull = ((rating % 400) / 400) * 100;
  574. // ◒を生成
  575. return (`
  576. <span style='${style}border-color:${color};background:` +
  577. `linear-gradient(to top, ${color} 0%, ${color} ${percentFull}%, ` +
  578. `rgba(0, 0, 0, 0) ${percentFull}%, rgba(0, 0, 0, 0) 100%); '>
  579. </span>`);
  580. }
  581. // 金銀銅は例外処理
  582. else if (rating < 3600) {
  583. return (`<span style="${style}border-color: rgb(150, 92, 44);` +
  584. 'background: linear-gradient(to right, rgb(150, 92, 44), rgb(255, 218, 189), rgb(150, 92, 44));"></span>');
  585. }
  586. else if (rating < 4000) {
  587. return (`<span style="${style}border-color: rgb(128, 128, 128);` +
  588. 'background: linear-gradient(to right, rgb(128, 128, 128), white, rgb(128, 128, 128));"></span>');
  589. }
  590. else {
  591. return (`<span style="${style}border-color: rgb(255, 215, 0);` +
  592. 'background: linear-gradient(to right, rgb(255, 215, 0), white, rgb(255, 215, 0));"></span>');
  593. }
  594. };
  595.  
  596. const COL_PER_ROW = 20;
  597. class DifficyltyTable {
  598. constructor(parent, tasks, isEstimationEnabled, dc, taskAcceptedCounts, yourTaskAcceptedElapsedTimes, acCountPredicted) {
  599. // insert
  600. parent.insertAdjacentHTML('beforeend', `
  601. <div id="acssa-table-wrapper">
  602. ${rangeLen(Math.ceil(tasks.length / COL_PER_ROW))
  603. .map((tableIdx) => `
  604. <table id="acssa-table-${tableIdx}" class="table table-bordered table-hover th-center td-center td-middle acssa-table">
  605. <tbody>
  606. <tr id="acssa-thead-${tableIdx}" class="acssa-thead"></tr>
  607. </tbody>
  608. <tbody>
  609. <tr id="acssa-tbody-${tableIdx}" class="acssa-tbody"></tr>
  610. ${isEstimationEnabled
  611. ? `<tr id="acssa-tbody-predicted-${tableIdx}" class="acssa-tbody"></tr>`
  612. : ''}
  613. </tbody>
  614. </table>
  615. `)
  616. .join('')}
  617. </div>
  618. `);
  619. if (isEstimationEnabled) {
  620. for (let tableIdx = 0; tableIdx < Math.ceil(tasks.length / COL_PER_ROW); ++tableIdx) {
  621. document.getElementById(`acssa-thead-${tableIdx}`).insertAdjacentHTML('beforeend', `<th></th>`);
  622. document.getElementById(`acssa-tbody-${tableIdx}`).insertAdjacentHTML('beforeend', `<th>Current</td>`);
  623. document.getElementById(`acssa-tbody-predicted-${tableIdx}`).insertAdjacentHTML('beforeend', `<th>Predicted</td>`);
  624. }
  625. }
  626. // build
  627. for (let j = 0; j < tasks.length; ++j) {
  628. const tableIdx = Math.floor(j / COL_PER_ROW);
  629. const correctedDifficulty = dc.binarySearchCorrectedDifficulty(taskAcceptedCounts[j]);
  630. const tdClass = yourTaskAcceptedElapsedTimes[j] === -1 ? '' : 'class="success acssa-task-success"';
  631. document.getElementById(`acssa-thead-${tableIdx}`).insertAdjacentHTML('beforeend', `
  632. <td ${tdClass}>
  633. ${tasks[j].Assignment}
  634. </td>
  635. `);
  636. const id = `td-assa-difficulty-${j}`;
  637. document.getElementById(`acssa-tbody-${tableIdx}`).insertAdjacentHTML('beforeend', `
  638. <td ${tdClass} id="${id}" style="color:${getColor(correctedDifficulty)};">
  639. ${correctedDifficulty === 9999 ? '-' : correctedDifficulty}</td>
  640. `);
  641. if (correctedDifficulty !== 9999) {
  642. document.getElementById(id).insertAdjacentHTML('afterbegin', generateDifficultyCircle(correctedDifficulty));
  643. }
  644. if (isEstimationEnabled) {
  645. const correctedPredictedDifficulty = dc.binarySearchCorrectedDifficulty(acCountPredicted[j]);
  646. const idPredicted = `td-assa-difficulty-predicted-${j}`;
  647. document.getElementById(`acssa-tbody-predicted-${tableIdx}`).insertAdjacentHTML('beforeend', `
  648. <td ${tdClass} id="${idPredicted}" style="color:${getColor(correctedPredictedDifficulty)};">
  649. ${correctedPredictedDifficulty === 9999 ? '-' : correctedPredictedDifficulty}</td>
  650. `);
  651. if (correctedPredictedDifficulty !== 9999) {
  652. document.getElementById(idPredicted).insertAdjacentHTML('afterbegin', generateDifficultyCircle(correctedPredictedDifficulty));
  653. }
  654. }
  655. }
  656. }
  657. }
  658.  
  659. 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>";
  660.  
  661. const TABS_WRAPPER_ID = 'acssa-tab-wrapper';
  662. const CHART_TAB_ID = 'acssa-chart-tab';
  663. const CHART_TAB_BUTTON_CLASS = 'acssa-chart-tab-button';
  664. const CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY = 'acssa-checkbox-toggle-your-result-visibility';
  665. const CHECKBOX_TOGGLE_LOG_PLOT = 'acssa-checkbox-toggle-log-plot';
  666. const PARENT_CHECKBOX_TOGGLE_LOG_PLOT = `${CHECKBOX_TOGGLE_LOG_PLOT}-parent`;
  667. class Tabs {
  668. constructor(parent, yourScore, participants) {
  669. this.yourScore = yourScore;
  670. this.participants = participants;
  671. // insert
  672. parent.insertAdjacentHTML('beforeend', html);
  673. this.showYourResultCheckbox = document.getElementById(CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY);
  674. this.logPlotCheckbox = document.getElementById(CHECKBOX_TOGGLE_LOG_PLOT);
  675. this.logPlotCheckboxParent = document.getElementById(PARENT_CHECKBOX_TOGGLE_LOG_PLOT);
  676. // チェックボックス操作時のイベントを登録する */
  677. this.showYourResultCheckbox.addEventListener('change', () => {
  678. if (this.showYourResultCheckbox.checked) {
  679. document.querySelectorAll('.acssa-task-success.acssa-task-success-suppress').forEach((elm) => {
  680. elm.classList.remove('acssa-task-success-suppress');
  681. });
  682. }
  683. else {
  684. document.querySelectorAll('.acssa-task-success').forEach((elm) => {
  685. elm.classList.add('acssa-task-success-suppress');
  686. });
  687. }
  688. });
  689. this.showYourResultCheckbox.addEventListener('change', () => {
  690. void this.onShowYourResultCheckboxChangedAsync();
  691. });
  692. this.logPlotCheckbox.addEventListener('change', () => {
  693. void this.onLogPlotCheckboxChangedAsync();
  694. });
  695. this.activeTab = 0;
  696. this.showYourResult = [true, true, true];
  697. this.acceptedCountYMax = -1;
  698. this.useLogPlot = [false, false, false];
  699. this.yourDifficultyChartData = null;
  700. this.yourAcceptedCountChartData = null;
  701. this.yourLastAcceptedTimeChartData = null;
  702. this.yourLastAcceptedTimeChartDataIndex = -1;
  703. document
  704. .querySelectorAll(`.${CHART_TAB_BUTTON_CLASS}`)
  705. .forEach((btn, key) => {
  706. btn.addEventListener('click', () => void this.onTabButtonClicked(btn, key));
  707. });
  708. if (this.yourScore == -1) {
  709. // disable checkbox
  710. this.showYourResultCheckbox.checked = false;
  711. this.showYourResultCheckbox.disabled = true;
  712. const checkboxParent = this.showYourResultCheckbox.parentElement;
  713. checkboxParent.style.cursor = 'default';
  714. checkboxParent.style.textDecoration = 'line-through';
  715. }
  716. }
  717. async onShowYourResultCheckboxChangedAsync() {
  718. this.showYourResult[this.activeTab] = this.showYourResultCheckbox.checked;
  719. if (this.showYourResultCheckbox.checked) {
  720. // show
  721. switch (this.activeTab) {
  722. case 0:
  723. if (this.yourScore > 0 && this.yourDifficultyChartData !== null)
  724. await Plotly.addTraces(plotlyDifficultyChartId, this.yourDifficultyChartData);
  725. break;
  726. case 1:
  727. if (this.yourScore > 0 && this.yourAcceptedCountChartData !== null)
  728. await Plotly.addTraces(plotlyAcceptedCountChartId, this.yourAcceptedCountChartData);
  729. break;
  730. case 2:
  731. if (this.yourLastAcceptedTimeChartData !== null && this.yourLastAcceptedTimeChartDataIndex != -1) {
  732. await Plotly.addTraces(plotlyLastAcceptedTimeChartId, this.yourLastAcceptedTimeChartData, this.yourLastAcceptedTimeChartDataIndex);
  733. }
  734. break;
  735. }
  736. }
  737. else {
  738. // hide
  739. switch (this.activeTab) {
  740. case 0:
  741. if (this.yourScore > 0)
  742. await Plotly.deleteTraces(plotlyDifficultyChartId, -1);
  743. break;
  744. case 1:
  745. if (this.yourScore > 0)
  746. await Plotly.deleteTraces(plotlyAcceptedCountChartId, -1);
  747. break;
  748. case 2:
  749. if (this.yourLastAcceptedTimeChartDataIndex != -1) {
  750. await Plotly.deleteTraces(plotlyLastAcceptedTimeChartId, this.yourLastAcceptedTimeChartDataIndex);
  751. }
  752. break;
  753. }
  754. }
  755. } // end async onShowYourResultCheckboxChangedAsync()
  756. async onLogPlotCheckboxChangedAsync() {
  757. if (this.acceptedCountYMax == -1)
  758. return;
  759. this.useLogPlot[this.activeTab] = this.logPlotCheckbox.checked;
  760. if (this.activeTab == 1) {
  761. if (this.logPlotCheckbox.checked) {
  762. // log plot
  763. const layout = {
  764. yaxis: {
  765. type: 'log',
  766. range: [Math.log10(0.5), Math.log10(this.acceptedCountYMax)],
  767. },
  768. };
  769. await Plotly.relayout(plotlyAcceptedCountChartId, layout);
  770. }
  771. else {
  772. // linear plot
  773. const layout = {
  774. yaxis: {
  775. type: 'linear',
  776. range: [0, this.acceptedCountYMax],
  777. },
  778. };
  779. await Plotly.relayout(plotlyAcceptedCountChartId, layout);
  780. }
  781. }
  782. else if (this.activeTab == 2) {
  783. if (this.logPlotCheckbox.checked) {
  784. // log plot
  785. const layout = {
  786. xaxis: {
  787. type: 'log',
  788. range: [Math.log10(0.5), Math.log10(this.participants)],
  789. },
  790. };
  791. await Plotly.relayout(plotlyLastAcceptedTimeChartId, layout);
  792. }
  793. else {
  794. // linear plot
  795. const layout = {
  796. xaxis: {
  797. type: 'linear',
  798. range: [0, this.participants],
  799. },
  800. };
  801. await Plotly.relayout(plotlyLastAcceptedTimeChartId, layout);
  802. }
  803. }
  804. } // end async onLogPlotCheckboxChangedAsync
  805. async onTabButtonClicked(btn, key) {
  806. // check whether active or not
  807. const buttonParent = btn.parentElement;
  808. if (buttonParent.className == 'active')
  809. return;
  810. // modify visibility
  811. this.activeTab = key;
  812. document.querySelector(`#${CHART_TAB_ID} li.active`).classList.remove('active');
  813. document.querySelector(`#${CHART_TAB_ID} li:nth-child(${key + 1})`).classList.add('active');
  814. document.querySelector('#acssa-chart-block div.acssa-chart-wrapper-active').classList.remove('acssa-chart-wrapper-active');
  815. document.querySelector(`#acssa-chart-block div.acssa-chart-wrapper:nth-child(${key + 1})`).classList.add('acssa-chart-wrapper-active');
  816. // resize charts
  817. switch (key) {
  818. case 0:
  819. await Plotly.relayout(plotlyDifficultyChartId, {
  820. width: document.getElementById(plotlyDifficultyChartId).clientWidth,
  821. });
  822. this.logPlotCheckboxParent.style.display = 'none';
  823. break;
  824. case 1:
  825. await Plotly.relayout(plotlyAcceptedCountChartId, {
  826. width: document.getElementById(plotlyAcceptedCountChartId).clientWidth,
  827. });
  828. this.logPlotCheckboxParent.style.display = 'block';
  829. break;
  830. case 2:
  831. await Plotly.relayout(plotlyLastAcceptedTimeChartId, {
  832. width: document.getElementById(plotlyLastAcceptedTimeChartId).clientWidth,
  833. });
  834. this.logPlotCheckboxParent.style.display = 'block';
  835. break;
  836. }
  837. if (this.showYourResult[this.activeTab] !== this.showYourResultCheckbox.checked) {
  838. await this.onShowYourResultCheckboxChangedAsync();
  839. }
  840. if (this.activeTab !== 0 && this.useLogPlot[this.activeTab] !== this.logPlotCheckbox.checked) {
  841. await this.onLogPlotCheckboxChangedAsync();
  842. }
  843. }
  844. showTabsControl() {
  845. document.getElementById(TABS_WRAPPER_ID).style.display = 'block';
  846. }
  847. }
  848.  
  849. const NS2SEC = 1000000000;
  850. const CONTENT_DIV_ID = 'acssa-contents';
  851. class Parent {
  852. constructor(acRatioModel) {
  853. const loaderStyles = GM_getResourceText('loaders.min.css');
  854. GM_addStyle(loaderStyles + '\n' + css);
  855. this.centerOfInnerRating = getCenterOfInnerRating(contestScreenName);
  856. this.acRatioModel = acRatioModel;
  857. this.working = false;
  858. this.oldStandingsData = null;
  859. this.hasTeamStandings = this.searchTeamStandingsPage();
  860. }
  861. searchTeamStandingsPage() {
  862. const teamStandingsLink = document.querySelector(`a[href*="/contests/${contestScreenName}/standings/team"]`);
  863. return teamStandingsLink !== null;
  864. }
  865. async onStandingsChanged(standings) {
  866. if (!standings)
  867. return;
  868. if (this.working)
  869. return;
  870. this.tasks = standings.TaskInfo;
  871. const standingsData = standings.StandingsData; // vueStandings.filteredStandings;
  872. if (this.oldStandingsData === standingsData)
  873. return;
  874. if (this.tasks.length === 0)
  875. return;
  876. this.oldStandingsData = standingsData;
  877. this.working = true;
  878. this.removeOldContents();
  879. const currentTime = moment();
  880. this.elapsedMinutes = Math.floor(currentTime.diff(startTime) / 60 / 1000);
  881. this.isDuringContest = startTime <= currentTime && currentTime < endTime;
  882. this.isEstimationEnabled = this.isDuringContest && this.elapsedMinutes >= 1 && this.tasks.length < 10;
  883. this.innerRatingsFromPredictor = await fetchInnerRatingsFromPredictor(contestScreenName);
  884. this.scanStandingsData(standingsData);
  885. this.predictAcCountSeries();
  886. const standingsElement = document.getElementById('vue-standings');
  887. const acssaContentDiv = document.createElement('div');
  888. acssaContentDiv.id = CONTENT_DIV_ID;
  889. standingsElement.insertAdjacentElement('afterbegin', acssaContentDiv);
  890. if (this.hasTeamStandings) {
  891. if (!location.href.includes('/standings/team')) {
  892. // チーム戦順位表へ誘導
  893. acssaContentDiv.insertAdjacentHTML('afterbegin', teamalert);
  894. }
  895. }
  896. // difficulty
  897. new DifficyltyTable(acssaContentDiv, this.tasks, this.isEstimationEnabled, this.dc, this.taskAcceptedCounts, this.yourTaskAcceptedElapsedTimes, this.acCountPredicted);
  898. // tabs
  899. const tabs = new Tabs(acssaContentDiv, this.yourScore, this.participants);
  900. const charts = new Charts(acssaContentDiv, this.tasks, this.scoreLastAcceptedTimeMap, this.taskAcceptedCounts, this.taskAcceptedElapsedTimes, this.yourTaskAcceptedElapsedTimes, this.yourScore, this.yourLastAcceptedTime, this.participants, this.dc, tabs);
  901. // 順位表のその他の描画を優先するために,プロットは後回しにする
  902. window.setTimeout(() => {
  903. void charts.plotAsync().then(() => {
  904. charts.hideLoader();
  905. tabs.showTabsControl();
  906. this.working = false;
  907. });
  908. }, 100);
  909. }
  910. removeOldContents() {
  911. const oldContents = document.getElementById(CONTENT_DIV_ID);
  912. if (oldContents) {
  913. // oldContents.parentNode.removeChild(oldContents);
  914. oldContents.remove();
  915. }
  916. }
  917. scanStandingsData(standingsData) {
  918. // init
  919. this.scoreLastAcceptedTimeMap = new Map();
  920. this.taskAcceptedCounts = rangeLen(this.tasks.length).fill(0);
  921. this.taskAcceptedElapsedTimes = rangeLen(this.tasks.length).map(() => []);
  922. this.innerRatings = [];
  923. this.yourTaskAcceptedElapsedTimes = rangeLen(this.tasks.length).fill(-1);
  924. this.yourScore = -1;
  925. this.yourLastAcceptedTime = -1;
  926. this.participants = 0;
  927. // scan
  928. for (let i = 0; i < standingsData.length; ++i) {
  929. const standingsEntry = standingsData[i];
  930. if (!standingsEntry.TaskResults)
  931. continue; // 参加登録していない
  932. if (standingsEntry.UserIsDeleted)
  933. continue; // アカウント削除
  934. // let correctedRating = this.isDuringContest ? standingsEntry.Rating : standingsEntry.OldRating;
  935. let correctedRating = standingsEntry.Rating;
  936. const isTeamOrBeginner = correctedRating === 0;
  937. if (isTeamOrBeginner) {
  938. // continue; // 初参加 or チーム
  939. correctedRating = this.centerOfInnerRating;
  940. }
  941. // これは飛ばしちゃダメ(提出しても 0 AC だと Penalty == 0 なので)
  942. // if (standingsEntry.TotalResult.Score == 0 && standingsEntry.TotalResult.Penalty == 0) continue;
  943. let score = 0;
  944. let penalty = 0;
  945. for (let j = 0; j < this.tasks.length; ++j) {
  946. const taskResultEntry = standingsEntry.TaskResults[this.tasks[j].TaskScreenName];
  947. if (!taskResultEntry)
  948. continue; // 未提出
  949. score += taskResultEntry.Score;
  950. penalty += taskResultEntry.Score === 0 ? taskResultEntry.Failure : taskResultEntry.Penalty;
  951. }
  952. if (score === 0 && penalty === 0)
  953. continue; // NoSub を飛ばす
  954. this.participants++;
  955. // console.log(i + 1, score, penalty);
  956. score /= 100;
  957. if (this.scoreLastAcceptedTimeMap.has(score)) {
  958. this.scoreLastAcceptedTimeMap.get(score).push(standingsEntry.TotalResult.Elapsed / NS2SEC);
  959. }
  960. else {
  961. this.scoreLastAcceptedTimeMap.set(score, [standingsEntry.TotalResult.Elapsed / NS2SEC]);
  962. }
  963. const innerRating = isTeamOrBeginner
  964. ? correctedRating
  965. : standingsEntry.UserScreenName in this.innerRatingsFromPredictor
  966. ? this.innerRatingsFromPredictor[standingsEntry.UserScreenName]
  967. : RatingConverter.toInnerRating(Math.max(RatingConverter.toRealRating(correctedRating), 1), standingsEntry.Competitions);
  968. // console.log(this.isDuringContest, standingsEntry.Rating, standingsEntry.OldRating, innerRating);
  969. if (innerRating)
  970. this.innerRatings.push(innerRating);
  971. else {
  972. console.log(i, innerRating, correctedRating, standingsEntry.Competitions);
  973. continue;
  974. }
  975. for (let j = 0; j < this.tasks.length; ++j) {
  976. const taskResultEntry = standingsEntry.TaskResults[this.tasks[j].TaskScreenName];
  977. const isAccepted = (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Score) > 0 && (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Status) == 1;
  978. if (isAccepted) {
  979. ++this.taskAcceptedCounts[j];
  980. this.taskAcceptedElapsedTimes[j].push(taskResultEntry.Elapsed / NS2SEC);
  981. }
  982. }
  983. if (standingsEntry.UserScreenName == userScreenName) {
  984. this.yourScore = score;
  985. this.yourLastAcceptedTime = standingsEntry.TotalResult.Elapsed / NS2SEC;
  986. for (let j = 0; j < this.tasks.length; ++j) {
  987. const taskResultEntry = standingsEntry.TaskResults[this.tasks[j].TaskScreenName];
  988. const isAccepted = (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Score) > 0 && (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Status) == 1;
  989. if (isAccepted) {
  990. this.yourTaskAcceptedElapsedTimes[j] = taskResultEntry.Elapsed / NS2SEC;
  991. }
  992. }
  993. }
  994. } // end for
  995. this.innerRatings.sort((a, b) => a - b);
  996. this.dc = new DifficultyCalculator(this.innerRatings);
  997. } // end async scanStandingsData
  998. predictAcCountSeries() {
  999. if (!this.isEstimationEnabled) {
  1000. this.acCountPredicted = [];
  1001. return;
  1002. }
  1003. // 時間ごとの AC 数推移を計算する
  1004. const taskAcceptedCountImos = rangeLen(this.tasks.length).map(() => rangeLen(this.elapsedMinutes).map(() => 0));
  1005. this.taskAcceptedElapsedTimes.forEach((ar, index) => {
  1006. ar.forEach((seconds) => {
  1007. const minutes = Math.floor(seconds / 60);
  1008. if (minutes >= this.elapsedMinutes)
  1009. return;
  1010. taskAcceptedCountImos[index][minutes] += 1;
  1011. });
  1012. });
  1013. const taskAcceptedRatio = rangeLen(this.tasks.length).map(() => []);
  1014. taskAcceptedCountImos.forEach((ar, index) => {
  1015. let cum = 0;
  1016. ar.forEach((imos) => {
  1017. cum += imos;
  1018. taskAcceptedRatio[index].push(cum / this.participants);
  1019. });
  1020. });
  1021. // 差の自乗和が最小になるシーケンスを探す
  1022. this.acCountPredicted = taskAcceptedRatio.map((ar) => {
  1023. if (this.acRatioModel === undefined)
  1024. return 0;
  1025. if (ar[this.elapsedMinutes - 1] === 0)
  1026. return 0;
  1027. let minerror = 1.0 * this.elapsedMinutes;
  1028. // let argmin = '';
  1029. let last_ratio = 0;
  1030. Object.keys(this.acRatioModel).forEach((key) => {
  1031. if (this.acRatioModel === undefined)
  1032. return;
  1033. const ar2 = this.acRatioModel[key];
  1034. let error = 0;
  1035. for (let i = 0; i < this.elapsedMinutes; ++i) {
  1036. error += Math.pow(ar[i] - ar2[i], 2);
  1037. }
  1038. if (error < minerror) {
  1039. minerror = error;
  1040. // argmin = key;
  1041. if (ar2[this.elapsedMinutes - 1] > 0) {
  1042. last_ratio = ar2[ar2.length - 1] * (ar[this.elapsedMinutes - 1] / ar2[this.elapsedMinutes - 1]);
  1043. }
  1044. else {
  1045. last_ratio = ar2[ar2.length - 1];
  1046. }
  1047. }
  1048. });
  1049. // console.log(argmin, minerror, last_ratio);
  1050. if (last_ratio > 1)
  1051. last_ratio = 1;
  1052. return this.participants * last_ratio;
  1053. });
  1054. } // end predictAcCountSeries();
  1055. }
  1056. Parent.init = async () => {
  1057. const curr = moment();
  1058. if (startTime <= curr && curr < endTime) {
  1059. const contestDurationMinutes = endTime.diff(startTime) / 1000 / 60;
  1060. return new Parent(await fetchContestAcRatioModel(contestScreenName, contestDurationMinutes));
  1061. }
  1062. else {
  1063. return new Parent(undefined);
  1064. }
  1065. };
  1066.  
  1067. void (async () => {
  1068. const parent = await Parent.init();
  1069. vueStandings.$watch('standings', (standings) => {
  1070. void parent.onStandingsChanged(standings);
  1071. }, { deep: true, immediate: true });
  1072. })();