- // ==UserScript==
- // @name atcoder-standings-difficulty-analyzer
- // @namespace iilj
- // @version 2025.5.5
- // @description 順位表の得点情報を集計し,推定 difficulty やその推移を表示します.
- // @author iilj
- // @license MIT
- // @supportURL https://github.com/iilj/atcoder-standings-difficulty-analyzer/issues
- // @match https://atcoder.jp/*standings*
- // @exclude https://atcoder.jp/*standings/json
- // @resource loaders.min.css https://cdnjs.cloudflare.com/ajax/libs/loaders.css/0.1.2/loaders.min.css
- // @grant GM_getResourceText
- // @grant GM_addStyle
- // ==/UserScript==
- 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}";
-
- var teamalert = "<div class=\"alert alert-warning\">\n チーム戦順位表が提供されています.個人単位の順位表ページでは,difficulty 推定値が不正確になります.\n</div>";
-
- const arrayLowerBound = (arr, n) => {
- let first = 0, last = arr.length - 1, middle;
- while (first <= last) {
- middle = 0 | ((first + last) / 2);
- if (arr[middle] < n)
- first = middle + 1;
- else
- last = middle - 1;
- }
- return first;
- };
- const getColor = (rating) => {
- if (rating < 400)
- return '#808080';
- // gray
- else if (rating < 800)
- return '#804000';
- // brown
- else if (rating < 1200)
- return '#008000';
- // green
- else if (rating < 1600)
- return '#00C0C0';
- // cyan
- else if (rating < 2000)
- return '#0000FF';
- // blue
- else if (rating < 2400)
- return '#C0C000';
- // yellow
- else if (rating < 2800)
- return '#FF8000';
- // orange
- else if (rating == 9999)
- return '#000000';
- return '#FF0000'; // red
- };
- const formatTimespan = (sec) => {
- let sign;
- if (sec >= 0) {
- sign = '';
- }
- else {
- sign = '-';
- sec *= -1;
- }
- return `${sign}${Math.floor(sec / 60)}:${`0${sec % 60}`.slice(-2)}`;
- };
- /** 現在のページから,コンテストの開始から終了までの秒数を抽出する */
- const getContestDurationSec = () => {
- if (contestScreenName.startsWith('past')) {
- return 300 * 60;
- }
- // toDate.diff(fromDate) でミリ秒が返ってくる
- return endTime.diff(startTime) / 1000;
- };
- const getCenterOfInnerRatingFromRange = (contestRatedRange) => {
- if (contestScreenName.startsWith('abc')) {
- return 800;
- }
- if (contestScreenName.startsWith('arc')) {
- const contestNumber = Number(contestScreenName.substring(3, 6));
- return contestNumber >= 104 ? 1000 : 1600;
- }
- if (contestScreenName.startsWith('agc')) {
- const contestNumber = Number(contestScreenName.substring(3, 6));
- return contestNumber >= 34 ? 1200 : 1600;
- }
- if (contestRatedRange[1] === 1999) {
- return 800;
- }
- else if (contestRatedRange[1] === 2799) {
- return 1000;
- }
- else if (contestRatedRange[1] === Infinity) {
- return 1200;
- }
- return 800;
- };
- // ContestRatedRange
- /*
- function getContestInformationAsync(contestScreenName) {
- return __awaiter(this, void 0, void 0, function* () {
- const html = yield fetchTextDataAsync(`https://atcoder.jp/contests/${contestScreenName}`);
- const topPageDom = new DOMParser().parseFromString(html, "text/html");
- const dataParagraph = topPageDom.getElementsByClassName("small")[0];
- const data = Array.from(dataParagraph.children).map((x) => x.innerHTML.split(":")[1].trim());
- return new ContestInformation(parseRangeString(data[0]), parseRangeString(data[1]), parseDurationString(data[2]));
- });
- }
- */
- function parseRangeString(s) {
- s = s.trim();
- if (s === '-')
- return [0, -1];
- if (s === 'All')
- return [0, Infinity];
- if (!/[-~]/.test(s))
- return [0, -1];
- const res = s.split(/[-~]/).map((x) => parseInt(x.trim()));
- if (res.length !== 2) {
- throw new Error('res is not [number, number]');
- }
- if (isNaN(res[0]))
- res[0] = 0;
- if (isNaN(res[1]))
- res[1] = Infinity;
- return res;
- }
- const getContestRatedRangeAsync = async (contestScreenName) => {
- const html = await fetch(`https://atcoder.jp/contests/${contestScreenName}`);
- const topPageDom = new DOMParser().parseFromString(await html.text(), 'text/html');
- const dataParagraph = topPageDom.getElementsByClassName('small')[0];
- const data = Array.from(dataParagraph.children).map((x) => x.innerHTML.split(':')[1].trim());
- // console.log("data", data);
- return parseRangeString(data[1]);
- // return new ContestInformation(parseRangeString(data[0]), parseRangeString(data[1]), parseDurationString(data[2]));
- };
- const rangeLen = (len) => Array.from({ length: len }, (v, k) => k);
-
- const BASE_URL = 'https://raw.githubusercontent.com/iilj/atcoder-standings-difficulty-analyzer/main/json/standings';
- const fetchJson = async (url) => {
- const res = await fetch(url);
- if (!res.ok) {
- throw new Error(res.statusText);
- }
- const obj = (await res.json());
- return obj;
- };
- const fetchContestAcRatioModel = async (contestScreenName, contestDurationMinutes) => {
- // https://raw.githubusercontent.com/iilj/atcoder-standings-difficulty-analyzer/main/json/standings/abc_100m.json
- let modelLocation = undefined;
- if (/^agc(\d{3,})$/.exec(contestScreenName)) {
- if ([110, 120, 130, 140, 150, 160, 180, 200, 210, 240, 270, 300].includes(contestDurationMinutes)) {
- modelLocation = `${BASE_URL}/agc_${contestDurationMinutes}m.json`;
- }
- }
- else if (/^arc(\d{3,})$/.exec(contestScreenName)) {
- if ([100, 120, 150].includes(contestDurationMinutes)) {
- modelLocation = `${BASE_URL}/arc_${contestDurationMinutes}m.json`;
- }
- }
- else if (/^abc(\d{3,})$/.exec(contestScreenName)) {
- if ([100, 120].includes(contestDurationMinutes)) {
- modelLocation = `${BASE_URL}/abc_${contestDurationMinutes}m.json`;
- }
- }
- if (modelLocation !== undefined) {
- return await fetchJson(modelLocation);
- }
- return undefined;
- };
- const fetchInnerRatingsFromPredictor = async (contestScreenName) => {
- const url = `https://data.ac-predictor.com/aperfs/${contestScreenName}.json`;
- try {
- return await fetchJson(url);
- }
- catch (e) {
- return {};
- }
- };
-
- class RatingConverter {
- }
- /** 表示用の低レート帯補正レート → 低レート帯補正前のレート */
- RatingConverter.toRealRating = (correctedRating) => {
- if (correctedRating >= 400)
- return correctedRating;
- else
- return 400 * (1 - Math.log(400 / correctedRating));
- };
- /** 低レート帯補正前のレート → 内部レート推定値 */
- RatingConverter.toInnerRating = (realRating, comp) => {
- return (realRating +
- (1200 * (Math.sqrt(1 - Math.pow(0.81, comp)) / (1 - Math.pow(0.9, comp)) - 1)) / (Math.sqrt(19) - 1));
- };
- /** 低レート帯補正前のレート → 表示用の低レート帯補正レート */
- RatingConverter.toCorrectedRating = (realRating) => {
- if (realRating >= 400)
- return realRating;
- else
- return Math.floor(400 / Math.exp((400 - realRating) / 400));
- };
-
- class DifficultyCalculator {
- constructor(sortedInnerRatings) {
- this.innerRatings = sortedInnerRatings;
- this.prepared = new Map();
- this.memo = new Map();
- }
- perf2ExpectedAcceptedCount(m) {
- let expectedAcceptedCount;
- if (this.prepared.has(m)) {
- expectedAcceptedCount = this.prepared.get(m);
- }
- else {
- expectedAcceptedCount = this.innerRatings.reduce((prev_expected_accepts, innerRating) => (prev_expected_accepts += 1 / (1 + Math.pow(6, (m - innerRating) / 400))), 0);
- this.prepared.set(m, expectedAcceptedCount);
- }
- return expectedAcceptedCount;
- }
- perf2Ranking(x) {
- return this.perf2ExpectedAcceptedCount(x) + 0.5;
- }
- rank2InnerPerf(rank) {
- let upper = 9999;
- let lower = -9999;
- while (upper - lower > 0.1) {
- const mid = (upper + lower) / 2;
- if (rank > this.perf2Ranking(mid))
- upper = mid;
- else
- lower = mid;
- }
- return Math.round((upper + lower) / 2);
- }
- /** Difficulty 推定値を算出する */
- binarySearchCorrectedDifficulty(acceptedCount) {
- if (this.memo.has(acceptedCount)) {
- return this.memo.get(acceptedCount);
- }
- let lb = -10000;
- let ub = 10000;
- while (ub - lb > 1) {
- const m = Math.floor((ub + lb) / 2);
- const expectedAcceptedCount = this.perf2ExpectedAcceptedCount(m);
- if (expectedAcceptedCount < acceptedCount)
- ub = m;
- else
- lb = m;
- }
- const difficulty = lb;
- const correctedDifficulty = RatingConverter.toCorrectedRating(difficulty);
- this.memo.set(acceptedCount, correctedDifficulty);
- return correctedDifficulty;
- }
- }
-
- 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>";
-
- const LOADER_ID = 'acssa-loader';
- const plotlyDifficultyChartId = 'acssa-mydiv-difficulty';
- const plotlyAcceptedCountChartId = 'acssa-mydiv-accepted-count';
- const plotlyLastAcceptedTimeChartId = 'acssa-mydiv-accepted-time';
- const yourMarker = {
- size: 10,
- symbol: 'cross',
- color: 'red',
- line: {
- color: 'white',
- width: 1,
- },
- };
- const config = { autosize: true };
- // 背景用設定
- const alpha = 0.3;
- const colors = [
- [0, 400, `rgba(128,128,128,${alpha})`],
- [400, 800, `rgba(128,0,0,${alpha})`],
- [800, 1200, `rgba(0,128,0,${alpha})`],
- [1200, 1600, `rgba(0,255,255,${alpha})`],
- [1600, 2000, `rgba(0,0,255,${alpha})`],
- [2000, 2400, `rgba(255,255,0,${alpha})`],
- [2400, 2800, `rgba(255,165,0,${alpha})`],
- [2800, 10000, `rgba(255,0,0,${alpha})`],
- ];
- class Charts {
- constructor(parent, tasks, scoreLastAcceptedTimeMap, taskAcceptedCounts, taskAcceptedElapsedTimes, yourTaskAcceptedElapsedTimes, yourScore, yourLastAcceptedTime, participants, dcForDifficulty, dcForPerformance, ratedRank2EntireRank, tabs) {
- this.tasks = tasks;
- this.scoreLastAcceptedTimeMap = scoreLastAcceptedTimeMap;
- this.taskAcceptedCounts = taskAcceptedCounts;
- this.taskAcceptedElapsedTimes = taskAcceptedElapsedTimes;
- this.yourTaskAcceptedElapsedTimes = yourTaskAcceptedElapsedTimes;
- this.yourScore = yourScore;
- this.yourLastAcceptedTime = yourLastAcceptedTime;
- this.participants = participants;
- this.dcForDifficulty = dcForDifficulty;
- this.dcForPerformance = dcForPerformance;
- this.ratedRank2EntireRank = ratedRank2EntireRank;
- this.tabs = tabs;
- parent.insertAdjacentHTML('beforeend', html$1);
- this.duration = getContestDurationSec();
- this.xtick = 60 * 10 * Math.max(1, Math.ceil(this.duration / (60 * 10 * 20))); // 10 分を最小単位にする
- }
- async plotAsync() {
- // 以降の計算は時間がかかる
- this.taskAcceptedElapsedTimes.forEach((ar) => {
- ar.sort((a, b) => a - b);
- });
- // 時系列データの準備
- const [difficultyChartData, acceptedCountChartData] = await this.getTimeSeriesChartData();
- // 得点と提出時間データの準備
- const [lastAcceptedTimeChartData, maxAcceptedTime] = this.getLastAcceptedTimeChartData();
- // 軸フォーマットをカスタムする
- this.overrideAxisFormat();
- // Difficulty Chart 描画
- await this.plotDifficultyChartData(difficultyChartData);
- // Accepted Count Chart 描画
- await this.plotAcceptedCountChartData(acceptedCountChartData);
- // LastAcceptedTime Chart 描画
- await this.plotLastAcceptedTimeChartData(lastAcceptedTimeChartData, maxAcceptedTime);
- }
- /** 時系列データの準備 */
- async getTimeSeriesChartData() {
- /** Difficulty Chart のデータ */
- const difficultyChartData = [];
- /** AC Count Chart のデータ */
- const acceptedCountChartData = [];
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
- for (let j = 0; j < this.tasks.length; ++j) {
- //
- const interval = Math.ceil(this.taskAcceptedCounts[j] / 140);
- const [taskAcceptedElapsedTimesForChart, taskAcceptedCountsForChart] = this.taskAcceptedElapsedTimes[j].reduce(([ar, arr], tm, idx) => {
- const tmpInterval = Math.max(1, Math.min(Math.ceil(idx / 10), interval));
- if (idx % tmpInterval == 0 || idx == this.taskAcceptedCounts[j] - 1) {
- ar.push(tm);
- arr.push(idx + 1);
- }
- return [ar, arr];
- }, [[], []]);
- const correctedDifficulties = [];
- let counter = 0;
- for (const taskAcceptedCountForChart of taskAcceptedCountsForChart) {
- correctedDifficulties.push(this.dcForDifficulty.binarySearchCorrectedDifficulty(taskAcceptedCountForChart));
- counter += 1;
- // 20回に1回setTimeout(0)でeventループに処理を移す
- if (counter % 20 == 0) {
- await sleep(0);
- }
- }
- difficultyChartData.push({
- x: taskAcceptedElapsedTimesForChart,
- y: correctedDifficulties,
- type: 'scattergl',
- name: `${this.tasks[j].Assignment}`,
- });
- acceptedCountChartData.push({
- x: taskAcceptedElapsedTimesForChart,
- y: taskAcceptedCountsForChart,
- type: 'scattergl',
- name: `${this.tasks[j].Assignment}`,
- });
- }
- // 現在のユーザのデータを追加
- if (this.yourScore !== -1) {
- const yourAcceptedTimes = [];
- const yourAcceptedDifficulties = [];
- const yourAcceptedCounts = [];
- for (let j = 0; j < this.tasks.length; ++j) {
- if (this.yourTaskAcceptedElapsedTimes[j] !== -1) {
- yourAcceptedTimes.push(this.yourTaskAcceptedElapsedTimes[j]);
- const yourAcceptedCount = arrayLowerBound(this.taskAcceptedElapsedTimes[j], this.yourTaskAcceptedElapsedTimes[j]) + 1;
- yourAcceptedCounts.push(yourAcceptedCount);
- yourAcceptedDifficulties.push(this.dcForDifficulty.binarySearchCorrectedDifficulty(yourAcceptedCount));
- }
- }
- this.tabs.yourDifficultyChartData = {
- x: yourAcceptedTimes,
- y: yourAcceptedDifficulties,
- mode: 'markers',
- type: 'scattergl',
- name: `${userScreenName}`,
- marker: yourMarker,
- };
- this.tabs.yourAcceptedCountChartData = {
- x: yourAcceptedTimes,
- y: yourAcceptedCounts,
- mode: 'markers',
- type: 'scattergl',
- name: `${userScreenName}`,
- marker: yourMarker,
- };
- difficultyChartData.push(this.tabs.yourDifficultyChartData);
- acceptedCountChartData.push(this.tabs.yourAcceptedCountChartData);
- }
- return [difficultyChartData, acceptedCountChartData];
- }
- /** 得点と提出時間データの準備 */
- getLastAcceptedTimeChartData() {
- const lastAcceptedTimeChartData = [];
- const scores = [...this.scoreLastAcceptedTimeMap.keys()];
- scores.sort((a, b) => b - a);
- let acc = 0;
- let maxAcceptedTime = 0;
- scores.forEach((score) => {
- const lastAcceptedTimes = this.scoreLastAcceptedTimeMap.get(score);
- lastAcceptedTimes.sort((a, b) => a - b);
- const interval = Math.ceil(lastAcceptedTimes.length / 100);
- const lastAcceptedTimesForChart = lastAcceptedTimes.reduce((ar, tm, idx) => {
- if (idx % interval == 0 || idx == lastAcceptedTimes.length - 1)
- ar.push(tm);
- return ar;
- }, []);
- const lastAcceptedTimesRanks = lastAcceptedTimes.reduce((ar, tm, idx) => {
- if (idx % interval == 0 || idx == lastAcceptedTimes.length - 1)
- ar.push(acc + idx + 1);
- return ar;
- }, []);
- lastAcceptedTimeChartData.push({
- x: lastAcceptedTimesRanks,
- y: lastAcceptedTimesForChart,
- type: 'scattergl',
- name: `${score}`,
- });
- if (score === this.yourScore) {
- const lastAcceptedTimesRank = arrayLowerBound(lastAcceptedTimes, this.yourLastAcceptedTime);
- this.tabs.yourLastAcceptedTimeChartData = {
- x: [acc + lastAcceptedTimesRank + 1],
- y: [this.yourLastAcceptedTime],
- mode: 'markers',
- type: 'scattergl',
- name: `${userScreenName}`,
- marker: yourMarker,
- };
- this.tabs.yourLastAcceptedTimeChartDataIndex = lastAcceptedTimeChartData.length + 0;
- lastAcceptedTimeChartData.push(this.tabs.yourLastAcceptedTimeChartData);
- }
- acc += lastAcceptedTimes.length;
- if (lastAcceptedTimes[lastAcceptedTimes.length - 1] > maxAcceptedTime) {
- maxAcceptedTime = lastAcceptedTimes[lastAcceptedTimes.length - 1];
- }
- });
- return [lastAcceptedTimeChartData, maxAcceptedTime];
- }
- /**
- * 軸フォーマットをカスタムする
- * Support specifying a function for tickformat · Issue #1464 · plotly/plotly.js
- * https://github.com/plotly/plotly.js/issues/1464#issuecomment-498050894
- */
- overrideAxisFormat() {
- const org_locale = Plotly.d3.locale;
- Plotly.d3.locale = (locale) => {
- const result = org_locale(locale);
- // eslint-disable-next-line @typescript-eslint/unbound-method
- const org_number_format = result.numberFormat;
- result.numberFormat = (format) => {
- if (format != 'TIME') {
- return org_number_format(format);
- }
- return (x) => formatTimespan(x).toString();
- };
- return result;
- };
- }
- /** Difficulty Chart 描画 */
- async plotDifficultyChartData(difficultyChartData) {
- const maxAcceptedCount = this.taskAcceptedCounts.reduce((a, b) => Math.max(a, b));
- const yMax = RatingConverter.toCorrectedRating(this.dcForDifficulty.binarySearchCorrectedDifficulty(1));
- const yMin = RatingConverter.toCorrectedRating(this.dcForDifficulty.binarySearchCorrectedDifficulty(Math.max(2, maxAcceptedCount)));
- // 描画
- const layout = {
- title: 'Difficulty',
- xaxis: {
- dtick: this.xtick,
- tickformat: 'TIME',
- range: [0, this.duration],
- // title: { text: 'Elapsed' }
- },
- yaxis: {
- dtick: 400,
- tickformat: 'd',
- range: [
- Math.max(0, Math.floor((yMin - 100) / 400) * 400),
- Math.max(0, Math.ceil((yMax + 100) / 400) * 400),
- ],
- // title: { text: 'Difficulty' }
- },
- shapes: colors.map((c) => {
- return {
- type: 'rect',
- layer: 'below',
- xref: 'x',
- yref: 'y',
- x0: 0,
- x1: this.duration,
- y0: c[0],
- y1: c[1],
- line: { width: 0 },
- fillcolor: c[2],
- };
- }),
- margin: {
- b: 60,
- t: 30,
- },
- };
- await Plotly.newPlot(plotlyDifficultyChartId, difficultyChartData, layout, config);
- window.addEventListener('resize', () => {
- if (this.tabs.activeTab == 0)
- void Plotly.relayout(plotlyDifficultyChartId, {
- width: document.getElementById(plotlyDifficultyChartId).clientWidth,
- });
- });
- }
- /** Accepted Count Chart 描画 */
- async plotAcceptedCountChartData(acceptedCountChartData) {
- this.tabs.acceptedCountYMax = this.participants;
- const rectSpans = colors.reduce((ar, cur) => {
- const bottom = this.dcForDifficulty.perf2ExpectedAcceptedCount(cur[1]);
- if (bottom > this.tabs.acceptedCountYMax)
- return ar;
- const top = cur[0] == 0 ? this.tabs.acceptedCountYMax : this.dcForDifficulty.perf2ExpectedAcceptedCount(cur[0]);
- if (top < 0.5)
- return ar;
- ar.push([Math.max(0.5, bottom), Math.min(this.tabs.acceptedCountYMax, top), cur[2]]);
- return ar;
- }, []);
- // 描画
- const layout = {
- title: 'Accepted Count',
- xaxis: {
- dtick: this.xtick,
- tickformat: 'TIME',
- range: [0, this.duration],
- // title: { text: 'Elapsed' }
- },
- yaxis: {
- // type: 'log',
- // dtick: 100,
- tickformat: 'd',
- range: [0, this.tabs.acceptedCountYMax],
- // range: [
- // Math.log10(0.5),
- // Math.log10(acceptedCountYMax)
- // ],
- // title: { text: 'Difficulty' }
- },
- shapes: rectSpans.map((span) => {
- return {
- type: 'rect',
- layer: 'below',
- xref: 'x',
- yref: 'y',
- x0: 0,
- x1: this.duration,
- y0: span[0],
- y1: span[1],
- line: { width: 0 },
- fillcolor: span[2],
- };
- }),
- margin: {
- b: 60,
- t: 30,
- },
- };
- await Plotly.newPlot(plotlyAcceptedCountChartId, acceptedCountChartData, layout, config);
- window.addEventListener('resize', () => {
- if (this.tabs.activeTab == 1)
- void Plotly.relayout(plotlyAcceptedCountChartId, {
- width: document.getElementById(plotlyAcceptedCountChartId).clientWidth,
- });
- });
- }
- /** LastAcceptedTime Chart 描画 */
- async plotLastAcceptedTimeChartData(lastAcceptedTimeChartData, maxAcceptedTime) {
- const xMax = this.participants;
- // Rated 内のランクから,全体のランクへ変換する
- const convRatedRank2EntireRank = (ratedRank) => {
- const intRatedRank = Math.floor(ratedRank);
- if (intRatedRank >= this.ratedRank2EntireRank.length)
- return xMax;
- return this.ratedRank2EntireRank[intRatedRank];
- };
- const yMax = Math.ceil((maxAcceptedTime + this.xtick / 2) / this.xtick) * this.xtick;
- const rectSpans = colors.reduce((ar, cur) => {
- const right = cur[0] == 0 ? xMax : convRatedRank2EntireRank(this.dcForPerformance.perf2Ranking(cur[0]));
- if (right < 1)
- return ar;
- const left = cur[1] === 10000 ? 0 : convRatedRank2EntireRank(this.dcForPerformance.perf2Ranking(cur[1]));
- if (left > xMax)
- return ar;
- ar.push([Math.max(0, left), Math.min(xMax, right), cur[2]]);
- return ar;
- }, []);
- // console.log(colors);
- // console.log(rectSpans);
- const layout = {
- title: 'LastAcceptedTime v.s. Rank',
- xaxis: {
- // dtick: 100,
- tickformat: 'd',
- range: [0, xMax],
- // title: { text: 'Elapsed' }
- },
- yaxis: {
- dtick: this.xtick,
- tickformat: 'TIME',
- range: [0, yMax],
- // range: [
- // Math.max(0, Math.floor((yMin - 100) / 400) * 400),
- // Math.max(0, Math.ceil((yMax + 100) / 400) * 400)
- // ],
- // title: { text: 'Difficulty' }
- },
- shapes: rectSpans.map((span) => {
- return {
- type: 'rect',
- layer: 'below',
- xref: 'x',
- yref: 'y',
- x0: span[0],
- x1: span[1],
- y0: 0,
- y1: yMax,
- line: { width: 0 },
- fillcolor: span[2],
- };
- }),
- margin: {
- b: 60,
- t: 30,
- },
- };
- await Plotly.newPlot(plotlyLastAcceptedTimeChartId, lastAcceptedTimeChartData, layout, config);
- window.addEventListener('resize', () => {
- if (this.tabs.activeTab == 2)
- void Plotly.relayout(plotlyLastAcceptedTimeChartId, {
- width: document.getElementById(plotlyLastAcceptedTimeChartId).clientWidth,
- });
- });
- }
- hideLoader() {
- document.getElementById(LOADER_ID).style.display = 'none';
- }
- }
-
- /** レートを表す難易度円(◒)の HTML 文字列を生成 */
- const generateDifficultyCircle = (rating, isSmall = true) => {
- const size = isSmall ? 12 : 36;
- const borderWidth = isSmall ? 1 : 3;
- const style = `display:inline-block;border-radius:50%;border-style:solid;border-width:${borderWidth}px;` +
- `margin-right:5px;vertical-align:initial;height:${size}px;width:${size}px;`;
- if (rating < 3200) {
- // 色と円がどのぐらい満ちているかを計算
- const color = getColor(rating);
- const percentFull = ((rating % 400) / 400) * 100;
- // ◒を生成
- return (`
- <span style='${style}border-color:${color};background:` +
- `linear-gradient(to top, ${color} 0%, ${color} ${percentFull}%, ` +
- `rgba(0, 0, 0, 0) ${percentFull}%, rgba(0, 0, 0, 0) 100%); '>
- </span>`);
- }
- // 金銀銅は例外処理
- else if (rating < 3600) {
- return (`<span style="${style}border-color: rgb(150, 92, 44);` +
- 'background: linear-gradient(to right, rgb(150, 92, 44), rgb(255, 218, 189), rgb(150, 92, 44));"></span>');
- }
- else if (rating < 4000) {
- return (`<span style="${style}border-color: rgb(128, 128, 128);` +
- 'background: linear-gradient(to right, rgb(128, 128, 128), white, rgb(128, 128, 128));"></span>');
- }
- else {
- return (`<span style="${style}border-color: rgb(255, 215, 0);` +
- 'background: linear-gradient(to right, rgb(255, 215, 0), white, rgb(255, 215, 0));"></span>');
- }
- };
-
- const COL_PER_ROW = 20;
- class DifficyltyTable {
- constructor(parent, tasks, isEstimationEnabled, dc, taskAcceptedCounts, yourTaskAcceptedElapsedTimes, acCountPredicted) {
- // insert
- parent.insertAdjacentHTML('beforeend', `
- <p><span class="h2">Difficulty</span></p>
- <div id="acssa-table-wrapper">
- ${rangeLen(Math.ceil(tasks.length / COL_PER_ROW))
- .map((tableIdx) => `
- <table id="acssa-table-${tableIdx}" class="table table-bordered table-hover th-center td-center td-middle acssa-table">
- <tbody>
- <tr id="acssa-thead-${tableIdx}" class="acssa-thead"></tr>
- </tbody>
- <tbody>
- <tr id="acssa-tbody-${tableIdx}" class="acssa-tbody"></tr>
- ${isEstimationEnabled
- ? `<tr id="acssa-tbody-predicted-${tableIdx}" class="acssa-tbody"></tr>`
- : ''}
- </tbody>
- </table>
- `)
- .join('')}
- </div>
- `);
- if (isEstimationEnabled) {
- for (let tableIdx = 0; tableIdx < Math.ceil(tasks.length / COL_PER_ROW); ++tableIdx) {
- document.getElementById(`acssa-thead-${tableIdx}`).insertAdjacentHTML('beforeend', `<th></th>`);
- document.getElementById(`acssa-tbody-${tableIdx}`).insertAdjacentHTML('beforeend', `<th>Current</td>`);
- document.getElementById(`acssa-tbody-predicted-${tableIdx}`).insertAdjacentHTML('beforeend', `<th>Predicted</td>`);
- }
- }
- // build
- for (let j = 0; j < tasks.length; ++j) {
- const tableIdx = Math.floor(j / COL_PER_ROW);
- const correctedDifficulty = dc.binarySearchCorrectedDifficulty(taskAcceptedCounts[j]);
- const tdClass = yourTaskAcceptedElapsedTimes[j] === -1 ? '' : 'class="success acssa-task-success"';
- document.getElementById(`acssa-thead-${tableIdx}`).insertAdjacentHTML('beforeend', `
- <td ${tdClass}>
- ${tasks[j].Assignment}
- </td>
- `);
- const id = `td-assa-difficulty-${j}`;
- document.getElementById(`acssa-tbody-${tableIdx}`).insertAdjacentHTML('beforeend', `
- <td ${tdClass} id="${id}" style="color:${getColor(correctedDifficulty)};">
- ${correctedDifficulty === 9999 ? '-' : correctedDifficulty}</td>
- `);
- if (correctedDifficulty !== 9999) {
- document.getElementById(id).insertAdjacentHTML('afterbegin', generateDifficultyCircle(correctedDifficulty));
- }
- if (isEstimationEnabled) {
- const correctedPredictedDifficulty = dc.binarySearchCorrectedDifficulty(acCountPredicted[j]);
- const idPredicted = `td-assa-difficulty-predicted-${j}`;
- document.getElementById(`acssa-tbody-predicted-${tableIdx}`).insertAdjacentHTML('beforeend', `
- <td ${tdClass} id="${idPredicted}" style="color:${getColor(correctedPredictedDifficulty)};">
- ${correctedPredictedDifficulty === 9999 ? '-' : correctedPredictedDifficulty}</td>
- `);
- if (correctedPredictedDifficulty !== 9999) {
- document.getElementById(idPredicted).insertAdjacentHTML('afterbegin', generateDifficultyCircle(correctedPredictedDifficulty));
- }
- }
- }
- }
- }
-
- var html = "<p><span class=\"h2\">Chart</span></p>\n<div id=\"acssa-tab-wrapper\">\n <ul class=\"nav nav-pills small\" id=\"acssa-chart-tab\">\n <li class=\"active\">\n <a class=\"acssa-chart-tab-button\"><span class=\"glyphicon glyphicon-stats\" aria-hidden=\"true\"></span>Difficulty</a>\n </li>\n <li>\n <a class=\"acssa-chart-tab-button\"><span class=\"glyphicon glyphicon-stats\" aria-hidden=\"true\"></span>AC\n Count</a>\n </li>\n <li>\n <a class=\"acssa-chart-tab-button\"><span class=\"glyphicon glyphicon-stats\" aria-hidden=\"true\"></span>LastAcceptedTime</a>\n </li>\n </ul>\n <ul class=\"nav nav-pills\" id=\"acssa-checkbox-tab\">\n <li id=\"acssa-checkbox-toggle-your-result-visibility-parent\">\n <a><label><input type=\"checkbox\" id=\"acssa-checkbox-toggle-your-result-visibility\" checked=\"checked\"> Plot your\n result</label></a>\n </li>\n <li id=\"acssa-checkbox-toggle-log-plot-parent\">\n <a><label><input type=\"checkbox\" id=\"acssa-checkbox-toggle-log-plot\">Log plot</label></a>\n </li>\n <li>\n <a><label><input type=\"checkbox\" id=\"acssa-checkbox-toggle-onload-plot\">Onload plot</label></a>\n </li>\n </ul>\n</div>";
-
- const TABS_WRAPPER_ID = 'acssa-tab-wrapper';
- const CHART_TAB_ID = 'acssa-chart-tab';
- const CHART_TAB_BUTTON_CLASS = 'acssa-chart-tab-button';
- const CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY = 'acssa-checkbox-toggle-your-result-visibility';
- const PARENT_CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY = `${CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY}-parent`;
- const CHECKBOX_TOGGLE_LOG_PLOT = 'acssa-checkbox-toggle-log-plot';
- const CHECKBOX_TOGGLE_ONLOAD_PLOT = 'acssa-checkbox-toggle-onload-plot';
- const CONFIG_CNLOAD_PLOT_KEY = 'acssa-config-onload-plot';
- const PARENT_CHECKBOX_TOGGLE_LOG_PLOT = `${CHECKBOX_TOGGLE_LOG_PLOT}-parent`;
- class Tabs {
- constructor(parent, yourScore, participants) {
- var _a;
- this.yourScore = yourScore;
- this.participants = participants;
- // insert
- parent.insertAdjacentHTML('beforeend', html);
- this.showYourResultCheckbox = document.getElementById(CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY);
- this.logPlotCheckbox = document.getElementById(CHECKBOX_TOGGLE_LOG_PLOT);
- this.logPlotCheckboxParent = document.getElementById(PARENT_CHECKBOX_TOGGLE_LOG_PLOT);
- this.onloadPlotCheckbox = document.getElementById(CHECKBOX_TOGGLE_ONLOAD_PLOT);
- this.onloadPlot = JSON.parse((_a = localStorage.getItem(CONFIG_CNLOAD_PLOT_KEY)) !== null && _a !== void 0 ? _a : 'true');
- this.onloadPlotCheckbox.checked = this.onloadPlot;
- // チェックボックス操作時のイベントを登録する */
- this.showYourResultCheckbox.addEventListener('change', () => {
- if (this.showYourResultCheckbox.checked) {
- document.querySelectorAll('.acssa-task-success.acssa-task-success-suppress').forEach((elm) => {
- elm.classList.remove('acssa-task-success-suppress');
- });
- }
- else {
- document.querySelectorAll('.acssa-task-success').forEach((elm) => {
- elm.classList.add('acssa-task-success-suppress');
- });
- }
- });
- this.showYourResultCheckbox.addEventListener('change', () => {
- void this.onShowYourResultCheckboxChangedAsync();
- });
- this.logPlotCheckbox.addEventListener('change', () => {
- void this.onLogPlotCheckboxChangedAsync();
- });
- this.onloadPlotCheckbox.addEventListener('change', () => {
- this.onloadPlot = this.onloadPlotCheckbox.checked;
- localStorage.setItem(CONFIG_CNLOAD_PLOT_KEY, JSON.stringify(this.onloadPlot));
- });
- this.activeTab = 0;
- this.showYourResult = [true, true, true];
- this.acceptedCountYMax = -1;
- this.useLogPlot = [false, false, false];
- this.yourDifficultyChartData = null;
- this.yourAcceptedCountChartData = null;
- this.yourLastAcceptedTimeChartData = null;
- this.yourLastAcceptedTimeChartDataIndex = -1;
- document
- .querySelectorAll(`.${CHART_TAB_BUTTON_CLASS}`)
- .forEach((btn, key) => {
- btn.addEventListener('click', () => void this.onTabButtonClicked(btn, key));
- });
- if (this.yourScore == -1) {
- // disable checkbox
- this.showYourResultCheckbox.checked = false;
- this.showYourResultCheckbox.disabled = true;
- const checkboxParent = this.showYourResultCheckbox.parentElement;
- checkboxParent.style.cursor = 'default';
- checkboxParent.style.textDecoration = 'line-through';
- }
- }
- async onShowYourResultCheckboxChangedAsync() {
- this.showYourResult[this.activeTab] = this.showYourResultCheckbox.checked;
- if (this.showYourResultCheckbox.checked) {
- // show
- switch (this.activeTab) {
- case 0:
- if (this.yourScore > 0 && this.yourDifficultyChartData !== null)
- await Plotly.addTraces(plotlyDifficultyChartId, this.yourDifficultyChartData);
- break;
- case 1:
- if (this.yourScore > 0 && this.yourAcceptedCountChartData !== null)
- await Plotly.addTraces(plotlyAcceptedCountChartId, this.yourAcceptedCountChartData);
- break;
- case 2:
- if (this.yourLastAcceptedTimeChartData !== null && this.yourLastAcceptedTimeChartDataIndex != -1) {
- await Plotly.addTraces(plotlyLastAcceptedTimeChartId, this.yourLastAcceptedTimeChartData, this.yourLastAcceptedTimeChartDataIndex);
- }
- break;
- }
- }
- else {
- // hide
- switch (this.activeTab) {
- case 0:
- if (this.yourScore > 0)
- await Plotly.deleteTraces(plotlyDifficultyChartId, -1);
- break;
- case 1:
- if (this.yourScore > 0)
- await Plotly.deleteTraces(plotlyAcceptedCountChartId, -1);
- break;
- case 2:
- if (this.yourLastAcceptedTimeChartDataIndex != -1) {
- await Plotly.deleteTraces(plotlyLastAcceptedTimeChartId, this.yourLastAcceptedTimeChartDataIndex);
- }
- break;
- }
- }
- } // end async onShowYourResultCheckboxChangedAsync()
- async onLogPlotCheckboxChangedAsync() {
- if (this.acceptedCountYMax == -1)
- return;
- this.useLogPlot[this.activeTab] = this.logPlotCheckbox.checked;
- if (this.activeTab == 1) {
- if (this.logPlotCheckbox.checked) {
- // log plot
- const layout = {
- yaxis: {
- type: 'log',
- range: [Math.log10(0.5), Math.log10(this.acceptedCountYMax)],
- },
- };
- await Plotly.relayout(plotlyAcceptedCountChartId, layout);
- }
- else {
- // linear plot
- const layout = {
- yaxis: {
- type: 'linear',
- range: [0, this.acceptedCountYMax],
- },
- };
- await Plotly.relayout(plotlyAcceptedCountChartId, layout);
- }
- }
- else if (this.activeTab == 2) {
- if (this.logPlotCheckbox.checked) {
- // log plot
- const layout = {
- xaxis: {
- type: 'log',
- range: [Math.log10(0.5), Math.log10(this.participants)],
- },
- };
- await Plotly.relayout(plotlyLastAcceptedTimeChartId, layout);
- }
- else {
- // linear plot
- const layout = {
- xaxis: {
- type: 'linear',
- range: [0, this.participants],
- },
- };
- await Plotly.relayout(plotlyLastAcceptedTimeChartId, layout);
- }
- }
- } // end async onLogPlotCheckboxChangedAsync
- async onTabButtonClicked(btn, key) {
- // check whether active or not
- const buttonParent = btn.parentElement;
- if (buttonParent.className == 'active')
- return;
- // modify visibility
- this.activeTab = key;
- document.querySelector(`#${CHART_TAB_ID} li.active`).classList.remove('active');
- document.querySelector(`#${CHART_TAB_ID} li:nth-child(${key + 1})`).classList.add('active');
- document.querySelector('#acssa-chart-block div.acssa-chart-wrapper-active').classList.remove('acssa-chart-wrapper-active');
- document.querySelector(`#acssa-chart-block div.acssa-chart-wrapper:nth-child(${key + 1})`).classList.add('acssa-chart-wrapper-active');
- // resize charts
- switch (key) {
- case 0:
- await Plotly.relayout(plotlyDifficultyChartId, {
- width: document.getElementById(plotlyDifficultyChartId).clientWidth,
- });
- this.logPlotCheckboxParent.style.display = 'none';
- break;
- case 1:
- await Plotly.relayout(plotlyAcceptedCountChartId, {
- width: document.getElementById(plotlyAcceptedCountChartId).clientWidth,
- });
- this.logPlotCheckboxParent.style.display = 'block';
- break;
- case 2:
- await Plotly.relayout(plotlyLastAcceptedTimeChartId, {
- width: document.getElementById(plotlyLastAcceptedTimeChartId).clientWidth,
- });
- this.logPlotCheckboxParent.style.display = 'block';
- break;
- }
- if (this.showYourResult[this.activeTab] !== this.showYourResultCheckbox.checked) {
- await this.onShowYourResultCheckboxChangedAsync();
- }
- if (this.activeTab !== 0 && this.useLogPlot[this.activeTab] !== this.logPlotCheckbox.checked) {
- await this.onLogPlotCheckboxChangedAsync();
- }
- }
- showTabsControl() {
- document.getElementById(TABS_WRAPPER_ID).style.display = 'block';
- if (!this.onloadPlot) {
- document.getElementById(CHART_TAB_ID).style.display = 'none';
- document.getElementById(PARENT_CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY).style.display =
- 'none';
- }
- }
- }
-
- const finf = bigf(400);
- function bigf(n) {
- let pow1 = 1;
- let pow2 = 1;
- let numerator = 0;
- let denominator = 0;
- for (let i = 0; i < n; ++i) {
- pow1 *= 0.81;
- pow2 *= 0.9;
- numerator += pow1;
- denominator += pow2;
- }
- return Math.sqrt(numerator) / denominator;
- }
- function f(n) {
- return ((bigf(n) - finf) / (bigf(1) - finf)) * 1200.0;
- }
- /**
- * calculate unpositivized rating from last state
- * @param {Number} [last] last unpositivized rating
- * @param {Number} [perf] performance
- * @param {Number} [ratedMatches] count of participated rated contest
- * @returns {number} estimated unpositivized rating
- */
- function calcRatingFromLast(last, perf, ratedMatches) {
- if (ratedMatches === 0)
- return perf - 1200;
- last += f(ratedMatches);
- const weight = 9 - 9 * Math.pow(0.9, ratedMatches);
- const numerator = weight * Math.pow(2, last / 800.0) + Math.pow(2, perf / 800.0);
- const denominator = 1 + weight;
- return Math.log2(numerator / denominator) * 800.0 - f(ratedMatches + 1);
- }
- // class Random {
- // x: number
- // y: number
- // z: number
- // w: number
- // constructor(seed = 88675123) {
- // this.x = 123456789;
- // this.y = 362436069;
- // this.z = 521288629;
- // this.w = seed;
- // }
- // // XorShift
- // next(): number {
- // let t;
- // t = this.x ^ (this.x << 11);
- // this.x = this.y; this.y = this.z; this.z = this.w;
- // return this.w = (this.w ^ (this.w >>> 19)) ^ (t ^ (t >>> 8));
- // }
- // // min以上max以下の乱数を生成する
- // nextInt(min: number, max: number): number {
- // const r = Math.abs(this.next());
- // return min + (r % (max + 1 - min));
- // }
- // };
- class PerformanceTable {
- constructor(parent, tasks, isEstimationEnabled, yourStandingsEntry, taskAcceptedCounts, acCountPredicted, standingsData, innerRatingsFromPredictor, dcForPerformance, centerOfInnerRating, useRating) {
- this.centerOfInnerRating = centerOfInnerRating;
- if (yourStandingsEntry === undefined)
- return;
- // コンテスト終了時点での順位表を予測する
- const len = acCountPredicted.length;
- const rems = [];
- for (let i = 0; i < len; ++i) {
- rems.push(Math.ceil(acCountPredicted[i] - taskAcceptedCounts[i])); //
- }
- const scores = []; // (現レート,スコア合計,時間,問題ごとのスコア,rated)
- const highestScores = tasks.map(() => 0);
- let rowPtr = undefined;
- // const ratedInnerRatings: Rating[] = [];
- const ratedUserRanks = [];
- // console.log(standingsData);
- const threthold = moment('2021-12-03T21:00:00+09:00');
- const isAfterABC230 = startTime >= threthold;
- // OldRating が全員 0 なら,強制的に Rating を使用する(コンテスト終了後,レート更新前)
- standingsData.forEach((standingsEntry) => {
- const userScores = [];
- let penalty = 0;
- for (let j = 0; j < tasks.length; ++j) {
- const taskResultEntry = standingsEntry.TaskResults[tasks[j].TaskScreenName];
- if (!taskResultEntry) {
- // 未提出
- userScores.push(0);
- }
- else {
- userScores.push(taskResultEntry.Score / 100);
- highestScores[j] = Math.max(highestScores[j], taskResultEntry.Score / 100);
- penalty += taskResultEntry.Score === 0 ? taskResultEntry.Failure : taskResultEntry.Penalty;
- }
- }
- // const isRated = standingsEntry.IsRated && standingsEntry.TotalResult.Count > 0;
- const isRated = standingsEntry.IsRated && (isAfterABC230 || standingsEntry.TotalResult.Count > 0);
- if (!isRated) {
- if (standingsEntry.TotalResult.Score === 0 && penalty === 0 && standingsEntry.TotalResult.Count == 0) {
- return; // NoSub を飛ばす
- }
- }
- standingsEntry.Rating;
- // const innerRating: Rating = isTeamOrBeginner
- // ? correctedRating
- // : standingsEntry.UserScreenName in innerRatingsFromPredictor
- // ? innerRatingsFromPredictor[standingsEntry.UserScreenName]
- // : RatingConverter.toInnerRating(
- // Math.max(RatingConverter.toRealRating(correctedRating), 1),
- // standingsEntry.Competitions
- // );
- const innerRating = standingsEntry.UserScreenName in innerRatingsFromPredictor
- ? innerRatingsFromPredictor[standingsEntry.UserScreenName]
- : this.centerOfInnerRating;
- if (isRated) {
- // ratedInnerRatings.push(innerRating);
- ratedUserRanks.push(standingsEntry.EntireRank);
- // if (innerRating || true) {
- const row = [
- innerRating,
- standingsEntry.TotalResult.Score / 100,
- standingsEntry.TotalResult.Elapsed + 300 * standingsEntry.TotalResult.Penalty,
- userScores,
- isRated,
- ];
- scores.push(row);
- if ((standingsEntry.UserScreenName == userScreenName)) {
- rowPtr = row;
- }
- // }
- }
- });
- const sameRatedRankCount = ratedUserRanks.reduce((prev, cur) => {
- if (cur == yourStandingsEntry.EntireRank)
- prev++;
- return prev;
- }, 0);
- const ratedRank = ratedUserRanks.reduce((prev, cur) => {
- if (cur < yourStandingsEntry.EntireRank)
- prev += 1;
- return prev;
- }, (1 + sameRatedRankCount) / 2);
- // レート順でソート
- scores.sort((a, b) => {
- const [innerRatingA, scoreA, timeElapsedA] = a;
- const [innerRatingB, scoreB, timeElapsedB] = b;
- if (innerRatingA != innerRatingB) {
- return innerRatingB - innerRatingA; // 降順(レートが高い順)
- }
- if (scoreA != scoreB) {
- return scoreB - scoreA; // 降順(順位が高い順)
- }
- return timeElapsedA - timeElapsedB; // 昇順(順位が高い順)
- });
- // const random = new Random(0);
- // スコア変化をシミュレート
- // (現レート,スコア合計,時間,問題ごとのスコア,rated)
- scores.forEach((score) => {
- const [, , , scoresA] = score;
- // 自分は飛ばす
- if (score == rowPtr)
- return;
- for (let j = 0; j < tasks.length; ++j) {
- // if (random.nextInt(0, 9) <= 2) continue;
- // まだ満点ではなく,かつ正解者を増やせるなら
- if (scoresA[j] < highestScores[j] && rems[j] > 0) {
- const dif = highestScores[j] - scoresA[j];
- score[1] += dif;
- score[2] += 1000000000 * 60 * 30; // とりあえず30分で解くと仮定する
- scoresA[j] = highestScores[j];
- rems[j]--;
- }
- if (rems[j] == 0)
- break;
- }
- });
- // 順位でソート
- scores.sort((a, b) => {
- const [innerRatingA, scoreA, timeElapsedA, ,] = a;
- const [innerRatingB, scoreB, timeElapsedB, ,] = b;
- if (scoreA != scoreB) {
- return scoreB - scoreA; // 降順(順位が高い順)
- }
- if (timeElapsedA != timeElapsedB) {
- return timeElapsedA - timeElapsedB; // 昇順(順位が高い順)
- }
- return innerRatingB - innerRatingA; // 降順(レートが高い順)
- });
- // 順位を求める
- let estimatedRank = -1;
- let rank = 0;
- let sameCnt = 0;
- for (let i = 0; i < scores.length; ++i) {
- if (estimatedRank == -1) {
- if (scores[i][4] === true) {
- rank++;
- }
- if (scores[i] === rowPtr) {
- if (rank === 0)
- rank = 1;
- estimatedRank = rank;
- // break;
- }
- }
- else {
- if (rowPtr === undefined)
- break;
- if (scores[i][1] === rowPtr[1] && scores[i][2] === rowPtr[2]) {
- sameCnt++;
- }
- else {
- break;
- }
- }
- } //1246
- estimatedRank += sameCnt / 2;
- // const dc = new DifficultyCalculator(ratedInnerRatings);
- // insert
- parent.insertAdjacentHTML('beforeend', `
- <p><span class="h2">Performance</span></p>
- <div id="acssa-perf-table-wrapper">
- <table id="acssa-perf-table" class="table table-bordered table-hover th-center td-center td-middle acssa-table">
- <tbody>
- <tr class="acssa-thead">
- ${isEstimationEnabled ? '<td></td>' : ''}
- <td id="acssa-thead-perf" class="acssa-thead">perf</td>
- <td id="acssa-thead-perf" class="acssa-thead">レート変化</td>
- </tr>
- </tbody>
- <tbody>
- <tr id="acssa-perf-tbody" class="acssa-tbody"></tr>
- ${isEstimationEnabled
- ? `
- <tr id="acssa-perf-tbody-predicted" class="acssa-tbody"></tr>
- `
- : ''}
- </tbody>
- </table>
- </div>
- `);
- if (isEstimationEnabled) {
- document.getElementById(`acssa-perf-tbody`).insertAdjacentHTML('beforeend', `<th>Current</td>`);
- document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', `<th>Predicted</td>`);
- }
- // build
- const id = `td-assa-perf-current`;
- // TODO: ちゃんと判定する
- // const perf = Math.min(2400, dc.rank2InnerPerf(ratedRank));
- const perf = RatingConverter.toCorrectedRating(dcForPerformance.rank2InnerPerf(ratedRank));
- //
- document.getElementById(`acssa-perf-tbody`).insertAdjacentHTML('beforeend', `
- <td id="${id}" style="color:${getColor(perf)};">
- ${perf === 9999 ? '-' : perf}</td>
- `);
- if (perf !== 9999) {
- document.getElementById(id).insertAdjacentHTML('afterbegin', generateDifficultyCircle(perf));
- const oldRating = useRating ? yourStandingsEntry.Rating : yourStandingsEntry.OldRating;
- // const oldRating = yourStandingsEntry.Rating;
- const nextRating = Math.round(RatingConverter.toCorrectedRating(calcRatingFromLast(RatingConverter.toRealRating(oldRating), perf, yourStandingsEntry.Competitions)));
- const sign = nextRating > oldRating ? '+' : nextRating < oldRating ? '-' : '±';
- document.getElementById(`acssa-perf-tbody`).insertAdjacentHTML('beforeend', `
- <td>
- <span style="font-weight:bold;color:${getColor(oldRating)}">${oldRating}</span> →
- <span style="font-weight:bold;color:${getColor(nextRating)}">${nextRating}</span>
- <span style="color:gray">(${sign}${Math.abs(nextRating - oldRating)})</span>
- </td>
- `);
- }
- if (isEstimationEnabled) {
- if (estimatedRank != -1) {
- const perfEstimated = RatingConverter.toCorrectedRating(dcForPerformance.rank2InnerPerf(estimatedRank));
- const id2 = `td-assa-perf-predicted`;
- document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', `
- <td id="${id2}" style="color:${getColor(perfEstimated)};">
- ${perfEstimated === 9999 ? '-' : perfEstimated}</td>
- `);
- if (perfEstimated !== 9999) {
- document.getElementById(id2).insertAdjacentHTML('afterbegin', generateDifficultyCircle(perfEstimated));
- const oldRating = useRating ? yourStandingsEntry.Rating : yourStandingsEntry.OldRating;
- // const oldRating = yourStandingsEntry.Rating;
- const nextRating = Math.round(RatingConverter.toCorrectedRating(calcRatingFromLast(RatingConverter.toRealRating(oldRating), perfEstimated, yourStandingsEntry.Competitions)));
- const sign = nextRating > oldRating ? '+' : nextRating < oldRating ? '-' : '±';
- document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', `
- <td>
- <span style="font-weight:bold;color:${getColor(oldRating)}">${oldRating}</span> →
- <span style="font-weight:bold;color:${getColor(nextRating)}">${nextRating}</span>
- <span style="color:gray">(${sign}${Math.abs(nextRating - oldRating)})</span>
- </td>
- `);
- }
- }
- else {
- document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', '<td>?</td>');
- }
- }
- }
- }
-
- const NS2SEC = 1000000000;
- const CONTENT_DIV_ID = 'acssa-contents';
- class Parent {
- constructor(acRatioModel, centerOfInnerRating) {
- const loaderStyles = GM_getResourceText('loaders.min.css');
- GM_addStyle(loaderStyles + '\n' + css);
- // this.centerOfInnerRating = getCenterOfInnerRating(contestScreenName);
- this.centerOfInnerRating = centerOfInnerRating;
- this.acRatioModel = acRatioModel;
- this.working = false;
- this.oldStandingsData = null;
- this.hasTeamStandings = this.searchTeamStandingsPage();
- this.yourStandingsEntry = undefined;
- }
- searchTeamStandingsPage() {
- const teamStandingsLink = document.querySelector(`a[href*="/contests/${contestScreenName}/standings/team"]`);
- return teamStandingsLink !== null;
- }
- async onStandingsChanged(standings) {
- if (!standings)
- return;
- if (this.working)
- return;
- this.tasks = standings.TaskInfo;
- const standingsData = standings.StandingsData; // vueStandings.filteredStandings;
- if (this.oldStandingsData === standingsData)
- return;
- if (this.tasks.length === 0)
- return;
- this.oldStandingsData = standingsData;
- this.working = true;
- this.removeOldContents();
- const currentTime = moment();
- this.elapsedMinutes = Math.floor(currentTime.diff(startTime) / 60 / 1000);
- this.isDuringContest = startTime <= currentTime && currentTime < endTime;
- this.isEstimationEnabled = this.isDuringContest && this.elapsedMinutes >= 1 && this.tasks.length < 10;
- const useRating = this.isDuringContest || this.areOldRatingsAllZero(standingsData);
- this.innerRatingsFromPredictor = await fetchInnerRatingsFromPredictor(contestScreenName);
- this.scanStandingsData(standingsData);
- this.predictAcCountSeries();
- const standingsElement = document.getElementById('vue-standings');
- const acssaContentDiv = document.createElement('div');
- acssaContentDiv.id = CONTENT_DIV_ID;
- standingsElement.insertAdjacentElement('afterbegin', acssaContentDiv);
- if (this.hasTeamStandings) {
- if (!location.href.includes('/standings/team')) {
- // チーム戦順位表へ誘導
- acssaContentDiv.insertAdjacentHTML('afterbegin', teamalert);
- }
- }
- // difficulty
- new DifficyltyTable(acssaContentDiv, this.tasks, this.isEstimationEnabled, this.dcForDifficulty, this.taskAcceptedCounts, this.yourTaskAcceptedElapsedTimes, this.acCountPredicted);
- new PerformanceTable(acssaContentDiv, this.tasks, this.isEstimationEnabled, this.yourStandingsEntry, this.taskAcceptedCounts, this.acCountPredicted, standingsData, this.innerRatingsFromPredictor, this.dcForPerformance, this.centerOfInnerRating, useRating);
- // console.log(this.yourStandingsEntry);
- // console.log(this.yourStandingsEntry?.EntireRank);
- // console.log(this.dc.rank2InnerPerf((this.yourStandingsEntry?.EntireRank ?? 10000) - 0));
- // tabs
- const tabs = new Tabs(acssaContentDiv, this.yourScore, this.participants);
- const charts = new Charts(acssaContentDiv, this.tasks, this.scoreLastAcceptedTimeMap, this.taskAcceptedCounts, this.taskAcceptedElapsedTimes, this.yourTaskAcceptedElapsedTimes, this.yourScore, this.yourLastAcceptedTime, this.participants, this.dcForDifficulty, this.dcForPerformance, this.ratedRank2EntireRank, tabs);
- if (tabs.onloadPlot) {
- // 順位表のその他の描画を優先するために,プロットは後回しにする
- void charts.plotAsync().then(() => {
- charts.hideLoader();
- tabs.showTabsControl();
- this.working = false;
- });
- }
- else {
- charts.hideLoader();
- tabs.showTabsControl();
- }
- }
- removeOldContents() {
- const oldContents = document.getElementById(CONTENT_DIV_ID);
- if (oldContents) {
- // oldContents.parentNode.removeChild(oldContents);
- oldContents.remove();
- }
- }
- scanStandingsData(standingsData) {
- // init
- this.scoreLastAcceptedTimeMap = new Map();
- this.taskAcceptedCounts = rangeLen(this.tasks.length).fill(0);
- this.taskAcceptedElapsedTimes = rangeLen(this.tasks.length).map(() => []);
- this.innerRatings = [];
- this.ratedInnerRatings = [];
- this.ratedRank2EntireRank = [];
- this.yourTaskAcceptedElapsedTimes = rangeLen(this.tasks.length).fill(-1);
- this.yourScore = -1;
- this.yourLastAcceptedTime = -1;
- this.participants = 0;
- this.yourStandingsEntry = undefined;
- // scan
- const threthold = moment('2021-12-03T21:00:00+09:00');
- const isAfterABC230 = startTime >= threthold;
- for (let i = 0; i < standingsData.length; ++i) {
- const standingsEntry = standingsData[i];
- const isRated = standingsEntry.IsRated && (isAfterABC230 || standingsEntry.TotalResult.Count > 0);
- if (isRated) {
- const ratedInnerRating = standingsEntry.UserScreenName in this.innerRatingsFromPredictor
- ? this.innerRatingsFromPredictor[standingsEntry.UserScreenName]
- : this.centerOfInnerRating;
- this.ratedInnerRatings.push(ratedInnerRating);
- this.ratedRank2EntireRank.push(standingsEntry.EntireRank);
- }
- if (!standingsEntry.TaskResults)
- continue; // 参加登録していない
- if (standingsEntry.UserIsDeleted)
- continue; // アカウント削除
- // let correctedRating = this.isDuringContest ? standingsEntry.Rating : standingsEntry.OldRating;
- let correctedRating = standingsEntry.Rating;
- const isTeamOrBeginner = correctedRating === 0;
- if (isTeamOrBeginner) {
- // continue; // 初参加 or チーム
- correctedRating = this.centerOfInnerRating;
- }
- const innerRating = isTeamOrBeginner
- ? correctedRating
- : standingsEntry.UserScreenName in this.innerRatingsFromPredictor
- ? this.innerRatingsFromPredictor[standingsEntry.UserScreenName]
- : RatingConverter.toInnerRating(Math.max(RatingConverter.toRealRating(correctedRating), 1), standingsEntry.Competitions);
- // これは飛ばしちゃダメ(提出しても 0 AC だと Penalty == 0 なので)
- // if (standingsEntry.TotalResult.Score == 0 && standingsEntry.TotalResult.Penalty == 0) continue;
- let score = 0;
- let penalty = 0;
- for (let j = 0; j < this.tasks.length; ++j) {
- const taskResultEntry = standingsEntry.TaskResults[this.tasks[j].TaskScreenName];
- if (!taskResultEntry)
- continue; // 未提出
- score += taskResultEntry.Score;
- penalty += taskResultEntry.Score === 0 ? taskResultEntry.Failure : taskResultEntry.Penalty;
- }
- if (score === 0 && penalty === 0 && standingsEntry.TotalResult.Count == 0)
- continue; // NoSub を飛ばす
- this.participants++;
- // console.log(i + 1, score, penalty);
- score /= 100;
- if (this.scoreLastAcceptedTimeMap.has(score)) {
- this.scoreLastAcceptedTimeMap.get(score).push(standingsEntry.TotalResult.Elapsed / NS2SEC);
- }
- else {
- this.scoreLastAcceptedTimeMap.set(score, [standingsEntry.TotalResult.Elapsed / NS2SEC]);
- }
- // console.log(this.isDuringContest, standingsEntry.Rating, standingsEntry.OldRating, innerRating);
- // if (standingsEntry.IsRated && innerRating) {
- // if (innerRating) {
- // this.innerRatings.push(innerRating);
- // } else {
- // console.log(i, innerRating, correctedRating, standingsEntry.Competitions, standingsEntry, this.innerRatingsFromPredictor[standingsEntry.UserScreenName]);
- // continue;
- // }
- this.innerRatings.push(innerRating);
- for (let j = 0; j < this.tasks.length; ++j) {
- const taskResultEntry = standingsEntry.TaskResults[this.tasks[j].TaskScreenName];
- const isAccepted = (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Score) > 0 && (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Status) == 1;
- if (isAccepted) {
- ++this.taskAcceptedCounts[j];
- this.taskAcceptedElapsedTimes[j].push(taskResultEntry.Elapsed / NS2SEC);
- }
- }
- if ((standingsEntry.UserScreenName == userScreenName)) {
- this.yourScore = score;
- this.yourLastAcceptedTime = standingsEntry.TotalResult.Elapsed / NS2SEC;
- this.yourStandingsEntry = standingsEntry;
- for (let j = 0; j < this.tasks.length; ++j) {
- const taskResultEntry = standingsEntry.TaskResults[this.tasks[j].TaskScreenName];
- const isAccepted = (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Score) > 0 && (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Status) == 1;
- if (isAccepted) {
- this.yourTaskAcceptedElapsedTimes[j] = taskResultEntry.Elapsed / NS2SEC;
- }
- }
- }
- } // end for
- this.innerRatings.sort((a, b) => a - b);
- this.ratedInnerRatings.sort((a, b) => a - b);
- this.ratedRank2EntireRank.sort((a, b) => a - b);
- this.dcForDifficulty = new DifficultyCalculator(this.innerRatings);
- this.dcForPerformance = new DifficultyCalculator(this.ratedInnerRatings);
- } // end async scanStandingsData
- predictAcCountSeries() {
- if (!this.isEstimationEnabled) {
- this.acCountPredicted = [];
- return;
- }
- // 時間ごとの AC 数推移を計算する
- const taskAcceptedCountImos = rangeLen(this.tasks.length).map(() => rangeLen(this.elapsedMinutes).map(() => 0));
- this.taskAcceptedElapsedTimes.forEach((ar, index) => {
- ar.forEach((seconds) => {
- const minutes = Math.floor(seconds / 60);
- if (minutes >= this.elapsedMinutes)
- return;
- taskAcceptedCountImos[index][minutes] += 1;
- });
- });
- const taskAcceptedRatio = rangeLen(this.tasks.length).map(() => []);
- taskAcceptedCountImos.forEach((ar, index) => {
- let cum = 0;
- ar.forEach((imos) => {
- cum += imos;
- taskAcceptedRatio[index].push(cum / this.participants);
- });
- });
- // 差の自乗和が最小になるシーケンスを探す
- this.acCountPredicted = taskAcceptedRatio.map((ar) => {
- if (this.acRatioModel === undefined)
- return 0;
- if (ar[this.elapsedMinutes - 1] === 0)
- return 0;
- let minerror = 1.0 * this.elapsedMinutes;
- // let argmin = '';
- let last_ratio = 0;
- Object.keys(this.acRatioModel).forEach((key) => {
- if (this.acRatioModel === undefined)
- return;
- const ar2 = this.acRatioModel[key];
- let error = 0;
- for (let i = 0; i < this.elapsedMinutes; ++i) {
- error += Math.pow(ar[i] - ar2[i], 2);
- }
- if (error < minerror) {
- minerror = error;
- // argmin = key;
- if (ar2[this.elapsedMinutes - 1] > 0) {
- last_ratio = ar2[ar2.length - 1] * (ar[this.elapsedMinutes - 1] / ar2[this.elapsedMinutes - 1]);
- }
- else {
- last_ratio = ar2[ar2.length - 1];
- }
- }
- });
- // console.log(argmin, minerror, last_ratio);
- if (last_ratio > 1)
- last_ratio = 1;
- return this.participants * last_ratio;
- });
- } // end predictAcCountSeries();
- areOldRatingsAllZero(standingsData) {
- return standingsData.every((standingsEntry) => standingsEntry.OldRating == 0);
- }
- }
- Parent.init = async () => {
- const contestRatedRange = await getContestRatedRangeAsync(contestScreenName);
- const centerOfInnerRating = getCenterOfInnerRatingFromRange(contestRatedRange);
- const curr = moment();
- if (startTime <= curr && curr < endTime) {
- const contestDurationMinutes = endTime.diff(startTime) / 1000 / 60;
- return new Parent(await fetchContestAcRatioModel(contestScreenName, contestDurationMinutes), centerOfInnerRating);
- }
- else {
- return new Parent(undefined, centerOfInnerRating);
- }
- };
-
- {
- const script = document.createElement('script');
- script.src = 'https://cdnjs.cloudflare.com/ajax/libs/plotly.js/1.33.1/plotly.min.js';
- script.async = true;
- script.onload = async () => {
- const parent = await Parent.init();
- vueStandings.$watch('standings', (standings) => {
- void parent.onStandingsChanged(standings);
- }, { deep: true, immediate: true });
- };
- script.onerror = () => {
- console.error('plotly load failed');
- };
- document.head.appendChild(script);
- }