AtCoder Recent Rating Graph

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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();
}

})();