AtCoder のレーティンググラフを直近Nか月だけ表示し、開始時の色帯を消す(期間はボタンから変更可)
// ==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();
}
})();