Greasy Fork is available in English.

atcoder-standings-difficulty-analyzer

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

Verze ze dne 07. 08. 2021. Zobrazit nejnovější verzi.

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