OMCStandingsStatistics

onlinemathcontestでの /standings ページに補助情報を追加します

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         OMCStandingsStatistics
// @namespace    none
// @version      1.0.2
// @description  onlinemathcontestでの /standings ページに補助情報を追加します
// @match        https://onlinemathcontest.com/contests/*/standings
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
  'use strict';

  // ------------------------------------------
  // /standings ページのURLパターン
  // ------------------------------------------
  const standingsPattern = /^https:\/\/onlinemathcontest\.com\/contests\/[^/]+\/standings$/;

  if (standingsPattern.test(location.href)) {
    // コンテスト名をURLから抜き出す
    const match = location.href.match(/contests\/([^/]+)\/standings/);
    if (!match) return;
    const contestName = match[1];
    const apiUrl = `https://onlinemathcontest.com/api/contests/${contestName}/standings?rated=0`;

    fetch(apiUrl)
      .then(res => res.json())
      .then(json => {
        const standingsDiv = document.getElementById('standings');
        if (!standingsDiv) {
          console.warn('#standingsが見つかりません');
          return;
        }
        const tasks = json.tasks;
        const standings = json.standings;
        const isPast = json.isPast;
        const isPointVisible = json.is_point_visible;

        // isPast かつ is_point_visible のときのみテーブルを表示
        if(isPast && isPointVisible){
          // テーブルを作成し、standingsDivの最上位に挿入
          const table = document.createElement('table');
          table.style.width = '100%';
          table.style.borderCollapse = 'collapse';
          table.innerHTML = `
            <thead>
              <tr>
                <th style="border:1px solid #ccc; padding:4px;">問題</th>
                <th style="border:1px solid #ccc; padding:4px;">得点</th>
                <th style="border:1px solid #ccc; padding:4px;">人数</th>
                <th style="border:1px solid #ccc; padding:4px;">正解率</th>
                <th style="border:1px solid #ccc; padding:4px;">平均ペナ</th>
                <th style="border:1px solid #ccc; padding:4px;">ペナ率</th>
                <th style="border:1px solid #ccc; padding:4px; width:200px;">レート</th>
              </tr>
            </thead>
            <tbody></tbody>
          `;
          standingsDiv.insertBefore(table, standingsDiv.firstChild);

          const tbody = table.querySelector('tbody');

          // レート→色 (ただし r=0 で黒)
          const rateColors = [
            { min: 1,    max: 400,    color: '#808080' },
            { min: 400,  max: 800,    color: '#804000' },
            { min: 800,  max: 1200,   color: '#008000' },
            { min: 1200, max: 1600,   color: '#00c0c0' },
            { min: 1600, max: 2000,   color: '#0000ff' },
            { min: 2000, max: 2400,   color: '#c0c000' },
            { min: 2400, max: 2800,   color: '#ff8000' },
            { min: 2800, max: 999999, color: '#ff0000' },
          ];

          tasks.forEach((task, idx) => {
            const label = String.fromCharCode('A'.charCodeAt(0) + idx);
            // タスクへのリンク
            const taskUrl = `https://onlinemathcontest.com/contests/${contestName}/tasks/${task.id}`;

            // 集計用
            let triedCount = 0;
            let solvedCount = 0;
            let penSum = 0;
            let penNonZero = 0;
            // レート分布のカウント
            const rateCounter = Array(rateColors.length).fill(0);
            // レート0 (None含む) のユーザーをカウントするための変数
            let countBlack = 0;

            standings.forEach(userStand => {
              const t = userStand.tasks?.[idx];
              if (!t) return;

              const tim = t.time;         // 整数 or null
              const pen = t.penalty;      // 整数 or null
              // userStand.user?.rate がnull/undefinedの場合は0と同じ扱い
              const r   = userStand.user?.rate ?? 0;

              // 提出ずみかどうか
              const tried = (tim != null) || (pen != null && pen >= 1);
              if (tried) {
                triedCount++;
              }

              // CA(正解)したかどうか
              if (tim != null) {
                solvedCount++;
                const realPen = pen ?? 0;
                penSum += realPen;
                if (realPen >= 1) {
                  penNonZero++;
                }
                // レートが0なら黒にする
                if (r === 0) {
                  countBlack++;
                } else {
                  // レートごとにカウント
                  for (let i = 0; i < rateColors.length; i++){
                    if (r >= rateColors[i].min && r < rateColors[i].max) {
                      rateCounter[i]++;
                      break;
                    }
                  }
                }
              }
            });

            // 表示用の文字列
            let peopleText = '0/0';
            let accPct = '0.00%';
            let avgPen = '0.00';
            let penPct = '0.00%';

            if (triedCount > 0) {
              peopleText = `${solvedCount}/${triedCount}`;
              const accuracy = (solvedCount / triedCount) * 100;
              accPct = accuracy.toFixed(2) + '%';
            }
            if (solvedCount > 0) {
              avgPen = (penSum / solvedCount).toFixed(2);
              const penRate = (penNonZero / solvedCount) * 100;
              penPct = penRate.toFixed(2) + '%';
            }

            const tr = document.createElement('tr');
            // ここで問題ラベルをリンク化
            tr.innerHTML = `
              <td style="border:1px solid #ccc; padding:4px;">
                <a href="${taskUrl}">
                  ${label}
                </a>
              </td>
              <td style="border:1px solid #ccc; padding:4px;">${task.admin_point ?? '-'}</td>
              <td style="border:1px solid #ccc; padding:4px;">${peopleText}</td>
              <td style="border:1px solid #ccc; padding:4px;">${accPct}</td>
              <td style="border:1px solid #ccc; padding:4px;">${avgPen}</td>
              <td style="border:1px solid #ccc; padding:4px;">${penPct}</td>
            `;

            // レート分布の棒グラフ
            const tdRate = document.createElement('td');
            tdRate.style.border = '1px solid #ccc';
            tdRate.style.padding = '4px';
            tdRate.style.width = '200px';

            const barDiv = document.createElement('div');
            barDiv.style.width = '100%';
            barDiv.style.height = '16px';
            barDiv.style.display = 'flex';
            barDiv.style.border = '1px solid #ccc';

            if (solvedCount > 0) {
              // まずレート0 (None) のユーザーを描画
              if (countBlack > 0) {
                const ratio = (countBlack / solvedCount) * 100;
                const segBlack = document.createElement('div');
                segBlack.style.width = ratio + '%';
                segBlack.style.backgroundColor = '#000000'; // 黒
                barDiv.appendChild(segBlack);
              }

              // それ以外の既存レート分布を描画
              for (let i = 0; i < rateCounter.length; i++){
                const c = rateCounter[i];
                if (!c) continue;
                const ratio = (c / solvedCount) * 100;
                const seg = document.createElement('div');
                seg.style.width = ratio + '%';
                seg.style.backgroundColor = rateColors[i].color;
                barDiv.appendChild(seg);
              }
            }

            tdRate.appendChild(barDiv);
            tr.appendChild(tdRate);
            tbody.appendChild(tr);
          });
        }
      })
      .catch(err => {
        console.error('Standings API取得失敗:', err);
      });
  }
})();