AtCoder Recent Rating Graph

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

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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

})();