AtCoder Recent Rating Graph

AtCoder のレーティンググラフを直近Nか月だけ表示し、開始時の色帯を消す(期間はボタンから変更可)

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         AtCoder Recent Rating Graph
// @namespace    https://greasyfork.org/users/hamath
// @version      0.2.0
// @description  AtCoder のレーティンググラフを直近Nか月だけ表示し、開始時の色帯を消す(期間はボタンから変更可)
// @author       hamath
// @match        https://atcoder.jp/users/*
// @run-at       document-start
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

'use strict';

var SEC_PER_YEAR = 365.25 * 24 * 3600;

// anchorSec から years 年ぶん遡った UNIX 秒を返す(近似カレンダー年)
function computeCutoff(anchorSec, years) {
  return anchorSec - years * SEC_PER_YEAR;
}

// history: [{EndTime, ...}]。opts: {years, anchorMode:'last'|'today', nowSec}
// anchorMode は 'last'(デフォルト)= 最後のコンテスト基準、'today' = nowSec 基準。
// フィルタ結果が 0〜1 点でもそのまま返す(呼び出し側で判断)。
function filterHistory(history, opts) {
  if (!Array.isArray(history) || history.length === 0) return history;
  if (!opts || !opts.years || opts.years <= 0) return history;
  var years = opts.years;
  var anchorMode = (opts && opts.anchorMode) || 'last';
  var anchor;
  if (anchorMode === 'today') {
    anchor = opts.nowSec;
  } else {
    anchor = history.reduce(function (m, e) {
      return e.EndTime > m ? e.EndTime : m;
    }, -Infinity);
  }
  var cutoff = computeCutoff(anchor, years);
  return history.filter(function (e) { return e.EndTime >= cutoff; });
}

if (typeof module !== 'undefined' && module.exports) {
  module.exports = { computeCutoff: computeCutoff, filterHistory: filterHistory };
}

'use strict';

var LS_MONTHS = 'acRatingMonths';
var DEFAULT_MONTHS = 24;

function getMonths() {
  var stored = localStorage.getItem(LS_MONTHS);
  if (stored === null) return DEFAULT_MONTHS;
  var v = parseInt(stored, 10);
  return (v >= 0 && isFinite(v)) ? v : DEFAULT_MONTHS; // 0 = 全期間
}

// 直近 N か月の最小レート(recompute で更新、patchYMin で参照)
var _filteredMinRating = null;

// ----- rating_history への代入をアクセサで横取り(document-start で実行) -----
(function installInterceptor() {
  var raw;
  var current;
  function recompute() {
    if (!Array.isArray(raw)) { current = raw; return; }
    var months = getMonths();
    if (months === 0) {
      current = raw; // 0 = 全期間
      _filteredMinRating = null;
      return;
    }
    current = filterHistory(raw, {
      years: months / 12,
      anchorMode: 'last',
      nowSec: Math.floor(Date.now() / 1000),
    });
    if (Array.isArray(current) && current.length > 0) {
      _filteredMinRating = current.reduce(function (m, e) {
        return e.NewRating < m ? e.NewRating : m;
      }, Infinity);
    }
  }
  try {
    Object.defineProperty(window, 'rating_history', {
      configurable: true,
      get: function () { return current; },
      set: function (v) { raw = v; recompute(); },
    });
  } catch (e) {
    // 既に定義済み等で失敗したら何もしない(生グラフのまま)
  }
})();

// ----- rating-graph.js の y_min 1500 固定上限を除去 -----
// rating-graph.js は $(window).load(init) で描画するため、
// setTimeout で復元を defer し init より後に走るようにする。
(function patchYMin() {
  var _origMin = Math.min;
  Math.min = function () {
    if (arguments.length === 2 && arguments[0] === 1500 && arguments[1] >= 0
        && _filteredMinRating !== null && _filteredMinRating > 1600) {
      return arguments[1];
    }
    return _origMin.apply(Math, arguments);
  };
  window.addEventListener('load', function () {
    setTimeout(function () { Math.min = _origMin; }, 0);
  }, { once: true });
})();

// ----- 期間変更モーダル -----
function showModal() {
  var months = getMonths();

  var overlay = document.createElement('div');
  overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;'
    + 'background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;';

  var box = document.createElement('div');
  box.style.cssText = 'background:#fff;padding:24px 28px;border-radius:6px;'
    + 'min-width:260px;box-shadow:0 4px 16px rgba(0,0,0,0.25);';

  var row = document.createElement('p');
  row.style.cssText = 'margin:0 0 4px;font-size:14px;';
  row.appendChild(document.createTextNode('直近 '));

  var input = document.createElement('input');
  input.type = 'number';
  input.value = String(months);
  input.min = '0';
  input.style.cssText = 'width:56px;padding:2px 6px;border:1px solid #ccc;'
    + 'border-radius:3px;font-size:14px;text-align:right;';
  row.appendChild(input);
  row.appendChild(document.createTextNode(' か月を表示'));

  var hint = document.createElement('p');
  hint.style.cssText = 'color:#888;font-size:11px;margin:4px 0 0;';
  hint.textContent = '0 を入力すると全期間を表示します';

  var errMsg = document.createElement('p');
  errMsg.style.cssText = 'color:#d9534f;font-size:12px;margin:6px 0 0;display:none;';
  errMsg.textContent = '0 以上の整数を入力してください';

  var btns = document.createElement('div');
  btns.style.cssText = 'text-align:right;margin-top:16px;';

  var cancelBtn = document.createElement('button');
  cancelBtn.type = 'button';
  cancelBtn.className = 'btn btn-default btn-sm';
  cancelBtn.textContent = 'キャンセル';

  var okBtn = document.createElement('button');
  okBtn.type = 'button';
  okBtn.className = 'btn btn-primary btn-sm';
  okBtn.textContent = 'OK';
  okBtn.style.marginLeft = '8px';

  btns.appendChild(cancelBtn);
  btns.appendChild(okBtn);
  box.appendChild(row);
  box.appendChild(hint);
  box.appendChild(errMsg);
  box.appendChild(btns);
  overlay.appendChild(box);
  document.body.appendChild(overlay);

  input.focus();
  input.select();

  function close() { document.body.removeChild(overlay); }

  function submit() {
    var v = Number(input.value.trim());
    if (!Number.isInteger(v) || v < 0) {
      errMsg.style.display = 'block';
      input.focus();
      return;
    }
    localStorage.setItem(LS_MONTHS, String(v));
    location.reload();
  }

  cancelBtn.addEventListener('click', close);
  okBtn.addEventListener('click', submit);
  overlay.addEventListener('click', function (e) { if (e.target === overlay) close(); });
  input.addEventListener('keydown', function (e) {
    if (e.key === 'Enter') submit();
    if (e.key === 'Escape') close();
  });
}

// ----- グラフ下部左にボタンを注入 -----
function injectButtons() {
  var graph = document.getElementById('ratingGraph');
  if (!graph) return;

  var graphDiv = graph.closest('div') || graph.parentNode;
  var row = document.createElement('div');
  row.style.cssText = 'text-align:left; line-height:normal; margin-top:4px;';

  var btn = document.createElement('button');
  btn.type = 'button';
  btn.className = 'btn btn-default btn-xs';
  var months = getMonths();
  btn.textContent = months === 0 ? '全期間を表示' : '直近' + months + 'か月を表示';
  btn.addEventListener('click', function (e) {
    e.preventDefault();
    showModal();
  });

  row.appendChild(btn);
  graphDiv.appendChild(row);
}

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', injectButtons);
} else {
  injectButtons();
}

})();