レーティンググラフにパフォーマンスのグラフを重ねて表示します AHCにも対応しています
// ==UserScript==
// @name New AtCoder Perf Graph
// @name:en New AtCoder Perf Graph
// @namespace https://kirameku.f5.si/
// @version 1.1.6
// @description レーティンググラフにパフォーマンスのグラフを重ねて表示します AHCにも対応しています
// @description:en Displays a performance graph overlaid on the rating graph. It also supports AHC.
// @author kirameku
// @icon https://atcoder.jp/favicon.ico
// @match *://atcoder.jp/users*
// @exclude *://atcoder.jp/users/*?graph=rank
// @exclude *://atcoder.jp/users/*?graph=dist
// @exclude *://atcoder.jp/users/*/history*
// @exclude *://atcoder.jp/users/*/json*
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
'use strict';
console.log("[PerfGraph] スクリプトが起動しました。");
// 公式のリスナー登録を横取りして無効化するフック
const originalAddEventListener = window.addEventListener;
window.addEventListener = function(type, listener, options) {
if (type === 'DOMContentLoaded' && listener && listener.toString().includes('init()')) {
console.log("[PerfGraph] 公式の自動描画イベントリスナーをブロックしました。");
return;
}
return originalAddEventListener.apply(this, arguments);
};
// グローバル変数 'init' のフック(公式の init を完全に無効化し、カスタム init に差し替える)
Object.defineProperty(window, 'init', {
configurable: true,
enumerable: true,
get: () => {
// 公式が呼び出そうとした場合はカスタム関数を返し、それ以外は空関数でガードする
return window.__customInit || function() {};
},
set: (val) => {
// 公式側が init を上書きしようとしても無視する
console.log("[PerfGraph] 公式による init の上書きを防止しました。");
}
});
// ページ埋め込みの rating_history が有効な配列になるまで待つ
function waitOfRatingHistory() {
return new Promise((resolve) => {
let attempts = 0;
const timer = setInterval(() => {
attempts++;
let history = window.rating_history || (typeof unsafeWindow !== 'undefined' ? unsafeWindow.rating_history : undefined);
if (typeof history !== 'undefined' && Array.isArray(history) && history.length > 0) {
clearInterval(timer);
console.log(`[PerfGraph] rating_history を検出しました。 (試行回数: ${attempts})`);
resolve(history);
}
if (attempts > 400) {
clearInterval(timer);
console.warn("[PerfGraph] rating_history の待機がタイムアウトしました。");
resolve(history || []);
}
}, 10);
});
}
originalAddEventListener.call(window, 'DOMContentLoaded', async () => {
// 定数群(公式コードベース)
const MARGIN_VAL_X = 86400 * 30;
const MARGIN_VAL_Y_LOW = 100;
const MARGIN_VAL_Y_HIGH = 300;
const OFFSET_X = 50;
const OFFSET_Y = 5;
let canvas_status = document.getElementById("ratingStatus");
let canvas_graph = document.getElementById("ratingGraph");
if (!canvas_status || !canvas_graph) {
console.error("[PerfGraph] キャンバス要素が見つかりません。");
return;
}
const BASE_WIDTH_GRAPH = parseInt(canvas_graph.getAttribute('width'), 10) || 640;
const BASE_HEIGHT_GRAPH = parseInt(canvas_graph.getAttribute('height'), 10) || 360;
const BASE_WIDTH_STATUS = parseInt(canvas_status.getAttribute('width'), 10) || 640;
const BASE_HEIGHT_STATUS = parseInt(canvas_status.getAttribute('height'), 10) || 80;
const PANEL_WIDTH = BASE_WIDTH_GRAPH - OFFSET_X - 10;
const PANEL_HEIGHT = BASE_HEIGHT_GRAPH - OFFSET_Y - 30;
const STATUS_WIDTH = BASE_WIDTH_STATUS - OFFSET_X - 10;
const STATUS_HEIGHT = BASE_HEIGHT_STATUS - OFFSET_Y - 5;
const HIGHEST_WIDTH = 115;
const HIGHEST_HEIGHT = 20;
const LABEL_FONT = "12px Lato";
const START_YEAR = 2010;
const MONTH_NAMES = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
const YEAR_SEC = 86400*365;
const STEP_SIZE = 400;
const COLORS = [
[0, "#808080", 0.15],
[400, "#804000", 0.15],
[800, "#008000", 0.15],
[1200, "#00C0C0", 0.2],
[1600, "#0000FF", 0.1],
[2000, "#C0C000", 0.25],
[2400, "#FF8000", 0.2],
[2800, "#FF0000", 0.1]
];
const STAR_MIN = 3200;
const PARTICLE_MIN = 3;
const PARTICLE_MAX = 20;
const LIFE_MAX = 30;
var cj = createjs;
var stage_graph, stage_status;
var panel_shape, border_shape;
var chart_container, line_shape, vertex_shapes, highest_shape;
var n, x_min, x_max, y_min, y_max;
var perf_line_shape, perf_vertex_shapes, perf_highest_shape;
var perf_n, perf_y_min, perf_y_max;
var perf_rating_history = [];
var raw_json_data = [];
var html_perf_map = new Map();
var border_status_shape;
var rating_text, place_text, diff_text, date_text, contest_name_text, perf_text;
var particles;
var standings_url;
const username = document.getElementsByClassName("username")[0]?.textContent.trim() || "";
let showPerformance = GM_getValue("showPerformance", true);
let isExpanded = false;
/**
* AtCoderの現在の表示言語を取得する
* @returns {string} "JA" (日本語) または "EN" (英語)
*/
function getCurrentLanguage() {
const elems = document.querySelectorAll("#navbar-collapse .dropdown > a");
if (elems.length === 0) return "JA";
for (let i = 0; i < elems.length; i++) {
const text = elems[i].textContent;
if (text?.includes("English")) return "EN";
if (text?.includes("日本語")) return "JA";
}
console.warn("[PerfGraph] Language detection failed. Fallback to English.");
return "EN";
}
// 多言語表示用テキスト
const currentLang = getCurrentLanguage();
const LOCALIZED_TEXT = {
perf: "perf: ",
btnOn: currentLang === "EN" ? "Performance: ON" : "パフォーマンス: ON",
btnOff: currentLang === "EN" ? "Performance: OFF" : "パフォーマンス: OFF",
btnDownload: currentLang === "EN" ? "Download Image" : "画像をダウンロード"
};
function initStage(stage, canvas, baseWidth, baseHeight) {
var dpr = window.devicePixelRatio || 1;
canvas.setAttribute('width', Math.round(baseWidth * dpr));
canvas.setAttribute('height', Math.round(baseHeight * dpr));
stage.scaleX = stage.scaleY = dpr;
if (isExpanded) {
canvas.style.maxWidth = canvas.style.maxHeight = "";
} else {
canvas.style.maxWidth = baseWidth + "px";
canvas.style.maxHeight = baseHeight + "px";
}
canvas.style.width = canvas.style.height = "100%";
stage.enableMouseOver();
}
function newShape(parent) { var s = new cj.Shape(); parent.addChild(s); return s; }
function newText(parent, x, y, font) { var t = new cj.Text("", font, "#000"); t.x = x; t.y = y; t.textAlign = "center"; t.textBaseline = "middle"; parent.addChild(t); return t; }
function getPer(x, l, r) { return (x - l) / (r - l); }
function getColor(x) {
for (var i = COLORS.length - 1; i >= 0; i--) { if (x >= COLORS[i][0]) return COLORS[i]; }
return [-1, "#000000", 0.1];
}
function getRatingPer(x) { var pre = COLORS[COLORS.length-1][0] + STEP_SIZE; for (var i = COLORS.length - 1; i >= 0; i--) { if (x >= COLORS[i][0]) return (x - COLORS[i][0]) / (pre - COLORS[i][0]); pre = COLORS[i][0]; } return 0; }
function getOrdinal(x) { var s = ["th", "st", "nd", "rd"], v = x % 100; return x + (s[(v - 20) % 10] || s[v] || s[0]); }
function getDiff(x) { var sign = x==0?'±':(x<0?'-':'+'); return sign + Math.abs(x); }
function setStatus(data, data2, particle_flag) {
if (!data) return;
var date = new Date(data.EndTime * 1000); var rating = data.NewRating, old_rating = data.OldRating;
var place = data.Place; var contest_name = data.ContestName; var perf = data2 ? data2.Performance : 0;
var tmp = getColor(rating); var color = tmp[1], alpha = tmp[2];
var perf_tmp = getColor(perf); var perf_color = perf_tmp[1];
if (border_status_shape && border_status_shape.graphics) {
border_status_shape.graphics.c().s(color).ss(1).rr(OFFSET_X, OFFSET_Y, STATUS_WIDTH, STATUS_HEIGHT, 2);
}
if (rating_text) { rating_text.text = rating; rating_text.color = color; }
if (place_text) place_text.text = getOrdinal(place);
if (diff_text) diff_text.text = getDiff(rating-old_rating);
if (date_text) date_text.text = date.toLocaleDateString();
if (contest_name_text) contest_name_text.text = contest_name;
if (particle_flag && rating_text) {
var particle_num = parseInt(Math.pow(getRatingPer(rating), 2) * (PARTICLE_MAX - PARTICLE_MIN) + PARTICLE_MIN);
setParticles(particle_num, color, alpha, rating);
}
standings_url = data.StandingsUrl;
if (perf_text) {
perf_text.text = (showPerformance && data2 && perf > 0) ? LOCALIZED_TEXT.perf + perf : "";
perf_text.color = perf_color;
}
}
// 演出用パーティクル処理
function setParticle(particle, x, y, color, alpha, star_flag) { particle.x = x; particle.y = y; var ang = Math.random() * Math.PI * 2; var speed = Math.random() * 4 + 4; particle.vx = Math.cos(ang) * speed; particle.vy = Math.sin(ang) * speed; particle.rot_speed = Math.random()*20+10; particle.life = LIFE_MAX; particle.visible = true; particle.color = color; particle.text = star_flag ? "★" : "@"; particle.alpha = alpha; }
function setParticles(num, color, alpha, rating) { for (var i = 0; i < PARTICLE_MAX; i++) { if (i < num && rating_text) { setParticle(particles[i], rating_text.x, rating_text.y, color, alpha, rating >= STAR_MIN); } else { particles[i].life = 0; particles[i].visible = false; } } }
function updateParticle(particle) { if (particle.life <= 0) { particle.visible = false; return; } particle.x += particle.vx; particle.vx *= 0.9; particle.y += particle.vy; particle.vy *= 0.9; particle.life--; particle.scaleX = particle.scaleY = particle.life / LIFE_MAX; particle.rotation += particle.rot_speed; }
function updateParticles() { for (var i = 0; i < PARTICLE_MAX; i++) { if (particles[i].life > 0) { updateParticle(particles[i]); } } }
function remapPerformanceData(currentHistory) {
if (!currentHistory || !Array.isArray(currentHistory) || raw_json_data.length === 0) return;
let activeContests = new Set();
for (let i = 0; i < currentHistory.length; i++) {
if (currentHistory[i] && currentHistory[i].ContestName) {
activeContests.add(currentHistory[i].ContestName.trim());
}
}
let validPerfMap = new Map();
for (let i = 0; i < raw_json_data.length; i++) {
if (raw_json_data[i].IsRated !== true) continue;
// 英語かつ英語名データが存在するときだけ使用し、なければ通常の日本語名を使う
let jsonContestName = (currentLang === "EN" && raw_json_data[i].ContestNameEn)
? raw_json_data[i].ContestNameEn.trim()
: raw_json_data[i].ContestName?.trim();
if (jsonContestName && activeContests.has(jsonContestName)) {
let perfVal = html_perf_map.has(jsonContestName) ? html_perf_map.get(jsonContestName) : 0;
validPerfMap.set(jsonContestName, { ...raw_json_data[i], Performance: perfVal });
}
}
perf_rating_history = [];
for (let i = 0; i < currentHistory.length; i++) {
let cName = currentHistory[i].ContestName?.trim();
if (cName && validPerfMap.has(cName)) {
perf_rating_history.push(validPerfMap.get(cName));
} else {
perf_rating_history.push({ Performance: 0 });
}
}
}
function initBackground() {
panel_shape = newShape(stage_graph); panel_shape.x = OFFSET_X; panel_shape.y = OFFSET_Y; panel_shape.alpha = 0.3;
border_shape = newShape(stage_graph); border_shape.x = OFFSET_X; border_shape.y = OFFSET_Y;
panel_shape.graphics.clear();
border_shape.graphics.clear();
function newLabelY(s, y) { var t = new cj.Text(s, LABEL_FONT, "#000"); t.x = OFFSET_X - 10; t.y = OFFSET_Y + y; t.textAlign = "right"; t.textBaseline = "middle"; stage_graph.addChild(t); }
function newLabelX(s, x, y) { var t = new cj.Text(s, LABEL_FONT, "#000"); t.x = OFFSET_X + x; t.y = OFFSET_Y + PANEL_HEIGHT + 2 + y; t.textAlign = "center"; t.textBaseline = "top"; stage_graph.addChild(t); }
var y1 = 0;
for (var i = COLORS.length - 1; i >= 0; i--) {
var y2 = PANEL_HEIGHT - PANEL_HEIGHT * getPer(COLORS[i][0], y_min, y_max);
if (y2 > 0 && y1 < PANEL_HEIGHT) {
y1 = Math.max(y1, 0);
panel_shape.graphics.f(COLORS[i][1]).r(0, y1, PANEL_WIDTH, Math.min(y2, PANEL_HEIGHT) - y1);
}
y1 = y2;
}
for (var i = 0; i <= y_max; i += STEP_SIZE) {
if (i >= y_min) {
var y = PANEL_HEIGHT - PANEL_HEIGHT * getPer(i, y_min, y_max);
newLabelY(String(i), y);
border_shape.graphics.s("#FFF").ss(0.5);
if (i == 2000) border_shape.graphics.s("#000");
border_shape.graphics.mt(0, y).lt(PANEL_WIDTH, y);
}
}
var month_step = 6;
for (var i = 3; i >= 1; i--) { if (x_max-x_min <= YEAR_SEC*i + MARGIN_VAL_X*2) month_step = i; }
var first_flag = true;
for (var i = START_YEAR; i < 3000; i++) {
var break_flag = false;
for (var j = 0; j < 12; j += month_step) {
var month = ('00' + (j+1)).slice(-2); var unix = Date.parse(String(i)+"-"+month+"-01T00:00:00")/1000;
if (x_min < unix && unix < x_max) {
var x = PANEL_WIDTH * getPer(unix, x_min, x_max);
if (j == 0 || first_flag) { newLabelX(MONTH_NAMES[j], x, 0); newLabelX(String(i), x, 13); first_flag = false; } else { newLabelX(MONTH_NAMES[j], x, 0); }
border_shape.graphics.s("#FFF").ss(0.5);
border_shape.graphics.mt(x, 0).lt(x, PANEL_HEIGHT);
}
if (unix > x_max) { break_flag = true; break;}
}
if (break_flag) break;
}
border_shape.graphics.s("#888").ss(1.5).rr(0, 0, PANEL_WIDTH, PANEL_HEIGHT, 2);
}
function initUnifiedChart(currentHistory) {
var highest_tooltip_container = new cj.Container();
stage_graph.addChild(highest_tooltip_container);
chart_container = new cj.Container();
stage_graph.addChild(chart_container);
chart_container.shadow = new cj.Shadow("rgba(0,0,0,0.3)", 1, 2, 3);
line_shape = newShape(chart_container);
perf_line_shape = newShape(chart_container);
vertex_shapes = new Array();
perf_vertex_shapes = new Array();
// レーティング側の頂点を拡大・縮小するヘルパー関数
function toggleVertexScale(index, isHover) {
let v = vertex_shapes[index];
if (v) {
v.scaleX = v.scaleY = isHover ? 1.2 : 1.0;
}
}
// パフォーマンス側の頂点を拡大・縮小するヘルパー関数
function togglePerfVertexScale(index, isHover) {
// perf_vertex_shapes は Performance が 0 の要素が間引かれている可能性があるため、
// .i プロパティが一致するオブジェクトを探索して制御します
if (perf_vertex_shapes && perf_vertex_shapes.length > 0) {
let pv = perf_vertex_shapes.find(shape => shape && shape.i === index);
if (pv) {
pv.scaleX = pv.scaleY = isHover ? 1.2 : 1.0;
}
}
}
function mouseoverVertex(e) {
const targetIdx = e.currentTarget.i;
if (e.currentTarget.is_vertex) {
e.currentTarget.scaleX = e.currentTarget.scaleY = 1.2;
} else {
// 吹き出し(Highest)にホバーしたときは、対応する頂点を大きくする
toggleVertexScale(targetIdx, true);
}
stage_graph.update();
setStatus(currentHistory[targetIdx], perf_rating_history[targetIdx], true);
}
function mouseoutVertex(e) {
const targetIdx = e.currentTarget.i;
if (e.currentTarget.is_vertex) {
e.currentTarget.scaleX = e.currentTarget.scaleY = 1;
} else {
// 吹き出しから外れたときは、対応する頂点を元に戻す
toggleVertexScale(targetIdx, false);
}
stage_graph.update();
}
function mouseoverPerfVertex(e) {
const targetIdx = e.currentTarget.i;
if (e.currentTarget.is_vertex) {
e.currentTarget.scaleX = e.currentTarget.scaleY = 1.2;
} else {
// 吹き出し(Highest(Perf))にホバーしたときは、対応するパフォーマンス頂点を大きくする
togglePerfVertexScale(targetIdx, true);
}
stage_graph.update();
setStatus(currentHistory[targetIdx], perf_rating_history[targetIdx], true);
}
function mouseoutPerfVertex(e) {
const targetIdx = e.currentTarget.i;
if (e.currentTarget.is_vertex) {
e.currentTarget.scaleX = e.currentTarget.scaleY = 1;
} else {
// 吹き出しから外れたときは、対応するパフォーマンス頂点を元に戻す
togglePerfVertexScale(targetIdx, false);
}
stage_graph.update();
}
if (n > 0) {
line_shape.graphics.s("#AAA").ss(2);
line_shape.graphics.mt(OFFSET_X + PANEL_WIDTH * getPer(currentHistory[0].EndTime, x_min, x_max), OFFSET_Y + (PANEL_HEIGHT - PANEL_HEIGHT * getPer(currentHistory[0].NewRating, y_min, y_max)));
for (var i = 0; i < n; i++) {
line_shape.graphics.lt(OFFSET_X + PANEL_WIDTH * getPer(currentHistory[i].EndTime, x_min, x_max), OFFSET_Y + (PANEL_HEIGHT - PANEL_HEIGHT * getPer(currentHistory[i].NewRating, y_min, y_max)));
}
}
if (showPerformance && perf_n > 0) {
perf_line_shape.graphics.s("#AAA").ss(2);
let first_drawn = false;
for (var i = 0; i < perf_n; i++) {
if (!perf_rating_history[i] || perf_rating_history[i].Performance === 0) continue;
let tx = OFFSET_X + PANEL_WIDTH * getPer(currentHistory[i].EndTime, x_min, x_max);
let ty = OFFSET_Y + (PANEL_HEIGHT - PANEL_HEIGHT * getPer(perf_rating_history[i].Performance, y_min, y_max));
if (!first_drawn) {
perf_line_shape.graphics.mt(tx, ty);
first_drawn = true;
} else {
perf_line_shape.graphics.lt(tx, ty);
}
}
}
if (n > 0) {
line_shape.graphics.s("#FFF").ss(0.5);
line_shape.graphics.mt(OFFSET_X + PANEL_WIDTH * getPer(currentHistory[0].EndTime, x_min, x_max), OFFSET_Y + (PANEL_HEIGHT - PANEL_HEIGHT * getPer(currentHistory[0].NewRating, y_min, y_max)));
for (var i = 0; i < n; i++) {
line_shape.graphics.lt(OFFSET_X + PANEL_WIDTH * getPer(currentHistory[i].EndTime, x_min, x_max), OFFSET_Y + (PANEL_HEIGHT - PANEL_HEIGHT * getPer(currentHistory[i].NewRating, y_min, y_max)));
}
}
if (showPerformance && perf_n > 0) {
perf_line_shape.graphics.s("#FFF").ss(0.5);
let first_drawn = false;
for (var i = 0; i < perf_n; i++) {
if (!perf_rating_history[i] || perf_rating_history[i].Performance === 0) continue;
let tx = OFFSET_X + PANEL_WIDTH * getPer(currentHistory[i].EndTime, x_min, x_max);
let ty = OFFSET_Y + (PANEL_HEIGHT - PANEL_HEIGHT * getPer(perf_rating_history[i].Performance, y_min, y_max));
if (!first_drawn) {
perf_line_shape.graphics.mt(tx, ty);
first_drawn = true;
} else {
perf_line_shape.graphics.lt(tx, ty);
}
}
}
var highest_i = 0;
for (var i = 0; i < n; i++) { if (currentHistory[highest_i].NewRating < currentHistory[i].NewRating) highest_i = i; }
var highest_i_perf = -1;
if (showPerformance && perf_n > 0) {
for (var i = 0; i < perf_n; i++) {
if (!perf_rating_history[i] || perf_rating_history[i].Performance === 0) continue;
if (highest_i_perf === -1 || perf_rating_history[highest_i_perf].Performance < perf_rating_history[i].Performance) {
highest_i_perf = i;
}
}
}
if (n > 0) {
let hx = OFFSET_X + PANEL_WIDTH * getPer(currentHistory[highest_i].EndTime, x_min, x_max);
let hy = OFFSET_Y + (PANEL_HEIGHT - PANEL_HEIGHT * getPer(currentHistory[highest_i].NewRating, y_min, y_max));
highest_shape = newShape(highest_tooltip_container);
highest_shape.shadow = new cj.Shadow("rgba(0,0,0,0.3)", 1, 2, 3);
var dx = ((x_min + x_max)/2 < currentHistory[highest_i].EndTime) ? -80 : 80; var x = hx + dx; var y = hy - 16;
highest_shape.graphics.s("#FFF").mt(hx, hy).lt(x, y);
highest_shape.graphics.s("#888").f("#FFF").rr(x - HIGHEST_WIDTH/2, y - HIGHEST_HEIGHT/2, HIGHEST_WIDTH, HIGHEST_HEIGHT, 2); highest_shape.i = highest_i;
highest_shape.is_vertex = false;
var highest_text = newText(highest_tooltip_container, x, y, "12px Lato");
highest_text.text = "Highest(Rate): " + currentHistory[highest_i].NewRating;
highest_shape.addEventListener("mouseover", mouseoverVertex); highest_shape.addEventListener("mouseout", mouseoutVertex);
}
// Highest(Perf) 吹き出し描画の安全ガード
if (showPerformance && highest_i_perf !== -1 &&
currentHistory[highest_i_perf] &&
perf_rating_history[highest_i_perf] &&
perf_rating_history[highest_i_perf].Performance > 0) {
let highestHistory = currentHistory[highest_i_perf];
let hx_perf = OFFSET_X + PANEL_WIDTH * getPer(highestHistory.EndTime, x_min, x_max);
let hy_perf = OFFSET_Y + (PANEL_HEIGHT - PANEL_HEIGHT * getPer(perf_rating_history[highest_i_perf].Performance, y_min, y_max));
if (!isNaN(hx_perf) && !isNaN(hy_perf)) {
perf_highest_shape = newShape(highest_tooltip_container);
if (perf_highest_shape && perf_highest_shape.graphics) {
perf_highest_shape.shadow = new cj.Shadow("rgba(0,0,0,0.3)", 1, 2, 3);
var dx_perf = ((x_min + x_max) / 2 < highestHistory.EndTime) ? -80 : 80; var x_perf = hx_perf + dx_perf; var y_perf = hy_perf - 16;
try {
perf_highest_shape.graphics.s("#FFF").mt(hx_perf, hy_perf).lt(x_perf, y_perf);
perf_highest_shape.graphics.s("#888").f("#FFF").rr(x_perf - HIGHEST_WIDTH / 2, y_perf - HIGHEST_HEIGHT / 2, HIGHEST_WIDTH, HIGHEST_HEIGHT, 2); perf_highest_shape.i = highest_i_perf;
perf_highest_shape.is_vertex = false;
var highest_perf_text = newText(highest_tooltip_container, x_perf, y_perf, "12px Lato");
highest_perf_text.text = "Highest(Perf): " + perf_rating_history[highest_i_perf].Performance;
perf_highest_shape.addEventListener("mouseover", mouseoverPerfVertex); perf_highest_shape.addEventListener("mouseout", mouseoutPerfVertex);
} catch (graphicsError) {
console.warn("[PerfGraph] perf_highest_shape graphics error:", graphicsError);
}
}
}
}
for (var i = 0; i < n; i++) {
vertex_shapes.push(newShape(chart_container)); vertex_shapes[i].graphics.s("#FFF");
if (i == highest_i) vertex_shapes[i].graphics.s("#000");
vertex_shapes[i].graphics.ss(0.5).f(getColor(currentHistory[i].NewRating)[1]).dc(0, 0, 3.5);
vertex_shapes[i].x = OFFSET_X + PANEL_WIDTH * getPer(currentHistory[i].EndTime, x_min, x_max);
vertex_shapes[i].y = OFFSET_Y + (PANEL_HEIGHT - PANEL_HEIGHT * getPer(currentHistory[i].NewRating, y_min, y_max)); vertex_shapes[i].i = i;
vertex_shapes[i].is_vertex = true;
var hitArea = new cj.Shape(); hitArea.graphics.f("#000").dc(1.5, 1.5, 6); vertex_shapes[i].hitArea = hitArea;
vertex_shapes[i].addEventListener("mouseover", mouseoverVertex); vertex_shapes[i].addEventListener("mouseout", mouseoutVertex);
}
// パフォーマンス頂点描画の安全ガード
if (showPerformance && perf_n > 0) {
for (var i = 0; i < perf_n; i++) {
if (!perf_rating_history[i] || perf_rating_history[i].Performance === 0) continue;
let vShape = newShape(chart_container);
if (!vShape || !vShape.graphics) continue;
perf_vertex_shapes.push(vShape);
vShape.graphics.s("#FFF");
if (i == highest_i_perf) vShape.graphics.s("#000");
let size = (i == highest_i_perf) ? 3.5 : 3.0;
vShape.graphics.ss(0.5).f(getColor(perf_rating_history[i].Performance)[1]).dc(0, 0, size);
let targetHistory = currentHistory[i];
if (targetHistory) {
vShape.x = OFFSET_X + PANEL_WIDTH * getPer(targetHistory.EndTime, x_min, x_max);
vShape.y = OFFSET_Y + (PANEL_HEIGHT - PANEL_HEIGHT * getPer(perf_rating_history[i].Performance, y_min, y_max));
vShape.i = i;
vShape.is_vertex = true;
var hitAreaPerf = new cj.Shape();
if (hitAreaPerf && hitAreaPerf.graphics) {
hitAreaPerf.graphics.f("#000").dc(1.5, 1.5, 6);
vShape.hitArea = hitAreaPerf;
}
vShape.addEventListener("mouseover", mouseoverPerfVertex); vShape.addEventListener("mouseout", mouseoutPerfVertex);
}
}
}
}
function initStatus(currentHistory) {
border_status_shape = newShape(stage_status);
rating_text = newText(stage_status, OFFSET_X + 75, OFFSET_Y + STATUS_HEIGHT / 2, "48px 'Squada One'");
perf_text = newText(stage_status, OFFSET_X + 75, OFFSET_Y + STATUS_HEIGHT / 2 + 25, "16px 'Squada One'");
place_text = newText(stage_status, OFFSET_X + 160, OFFSET_Y + STATUS_HEIGHT / 2.7, "16px Lato");
diff_text = newText(stage_status, OFFSET_X + 160, OFFSET_Y + STATUS_HEIGHT / 1.5, "11px Lato"); diff_text.color = '#888';
date_text = newText(stage_status, OFFSET_X + 200, OFFSET_Y + STATUS_HEIGHT / 4, "14px Lato"); contest_name_text = newText(stage_status, OFFSET_X + 200, OFFSET_Y + STATUS_HEIGHT / 1.6, "20px Lato");
date_text.textAlign = contest_name_text.textAlign = "left"; contest_name_text.maxWidth = STATUS_WIDTH - 200 - 10;
{ var hitArea = new cj.Shape(); hitArea.graphics.f("#000").r(0,-12,contest_name_text.maxWidth,24); contest_name_text.hitArea = hitArea; contest_name_text.cursor = "pointer"; contest_name_text.addEventListener("click", function(){ location.href = standings_url; }); }
particles = new Array();
for (var i = 0; i < PARTICLE_MAX; i++) { particles.push(newText(stage_status, 0, 0, "64px Lato")); particles[i].visible = false; }
if (currentHistory && currentHistory.length > 0) {
setStatus(currentHistory[currentHistory.length-1], perf_rating_history[perf_rating_history.length-1], false);
}
}
function myCustomInit() {
let currentHistory = window.rating_history || (typeof unsafeWindow !== 'undefined' ? unsafeWindow.rating_history : undefined);
n = (currentHistory && Array.isArray(currentHistory)) ? currentHistory.length : 0;
if (n == 0) {
console.warn("[PerfGraph] 表示データ数が0件のため描画を保留します。");
return;
}
const oldGraph = document.getElementById("ratingGraph");
const oldStatus = document.getElementById("ratingStatus");
if (oldGraph && oldStatus) {
// 【対策】古い Canvas に紐づく CreateJS のステージが存在する場合、
// マウスオーバー監視を完全に無効化し、すべてのイベントを無効にする
if (window.stage_graph) {
try {
window.stage_graph.enableMouseOver(0);
window.stage_graph.removeAllChildren();
window.stage_graph.clear();
} catch(e) {}
}
if (window.stage_status) {
try {
window.stage_status.enableMouseOver(0);
window.stage_status.removeAllChildren();
window.stage_status.clear();
} catch(e) {}
}
// Canvas要素をクローンすることで、古いCanvasに公式スクリプトが直接登録した
// DOMレベルのイベントリスナー(clickやmouseover等)を物理的にすべて破棄します
const newGraph = oldGraph.cloneNode(true);
const newStatus = oldStatus.cloneNode(true);
oldGraph.parentNode.replaceChild(newGraph, oldGraph);
oldStatus.parentNode.replaceChild(newStatus, oldStatus);
canvas_graph = newGraph;
canvas_status = newStatus;
}
// 【超強固対策1】描画前に Canvas 自体のグラフィックスコンテキストを真っ白にクリアする
const graphCtx = canvas_graph.getContext("2d");
if (graphCtx) graphCtx.clearRect(0, 0, canvas_graph.width, canvas_graph.height);
const statusCtx = canvas_status.getContext("2d");
if (statusCtx) statusCtx.clearRect(0, 0, canvas_status.width, canvas_status.height);
remapPerformanceData(currentHistory);
perf_n = perf_rating_history.length;
stage_graph = new cj.Stage("ratingGraph");
stage_status = new cj.Stage("ratingStatus");
// 【超強固対策2】以前のステージの登録内容が存在すればクリアし、メモリ上からも一掃
if (window.stage_graph) { window.stage_graph.clear(); window.stage_graph.removeAllChildren(); }
if (window.stage_status) { window.stage_status.clear(); window.stage_status.removeAllChildren(); }
window.stage_graph = stage_graph;
window.stage_status = stage_status;
// 【超強固対策2】以前のステージの登録内容が存在すればクリアし、メモリ上からも一掃
if (window.stage_graph) {
// 【追加】マウスオーバーイベントの監視を明示的に無効化
window.stage_graph.enableMouseOver(0);
// 【追加】古い頂点シェイプのイベントリスナーとhitAreaを明示的に解放してゴースト当たり判定を消滅させる
if (vertex_shapes && vertex_shapes.length > 0) {
vertex_shapes.forEach(v => {
if (v) {
v.removeAllEventListeners();
if (v.hitArea) v.hitArea = null;
}
});
}
if (perf_vertex_shapes && perf_vertex_shapes.length > 0) {
perf_vertex_shapes.forEach(v => {
if (v) {
v.removeAllEventListeners();
if (v.hitArea) v.hitArea = null;
}
});
}
window.stage_graph.clear();
window.stage_graph.removeAllChildren();
}
if (window.stage_status) {
window.stage_status.enableMouseOver(0); // 【追加】
window.stage_status.clear();
window.stage_status.removeAllChildren();
}
// 新しいステージの作成と代入
stage_graph = new cj.Stage("ratingGraph");
stage_status = new cj.Stage("ratingStatus");
window.stage_graph = stage_graph;
window.stage_status = stage_status;
initStage(stage_graph, canvas_graph, BASE_WIDTH_GRAPH, BASE_HEIGHT_GRAPH);
initStage(stage_status, canvas_status, BASE_WIDTH_STATUS, BASE_HEIGHT_STATUS);
x_min = 100000000000; x_max = 0; y_min = 10000; y_max = 0;
for (var i = 0; i < n; i++) {
if (!currentHistory[i] || isNaN(currentHistory[i].EndTime)) continue;
x_min = Math.min(x_min, currentHistory[i].EndTime);
x_max = Math.max(x_max, currentHistory[i].EndTime);
y_min = Math.min(y_min, currentHistory[i].NewRating);
y_max = Math.max(y_max, currentHistory[i].NewRating);
}
x_min -= MARGIN_VAL_X; x_max += MARGIN_VAL_X;
y_min = Math.min(1500, Math.max(0, y_min - MARGIN_VAL_Y_LOW));
y_max += MARGIN_VAL_Y_HIGH;
perf_y_min = 10000; perf_y_max = 0;
for (var i = 0; i < perf_n; i++) {
if (!perf_rating_history[i] || isNaN(perf_rating_history[i].Performance)) continue;
perf_y_min = Math.min(perf_y_min, perf_rating_history[i].Performance);
perf_y_max = Math.max(perf_y_max, perf_rating_history[i].Performance);
}
perf_y_min = Math.min(1500, Math.max(0, perf_y_min - MARGIN_VAL_Y_LOW));
perf_y_max += MARGIN_VAL_Y_HIGH;
if (showPerformance && perf_n > 0) {
y_min = Math.min(y_min, perf_y_min);
y_max = Math.max(y_max, perf_y_max);
}
initBackground();
initUnifiedChart(currentHistory);
stage_graph.update();
initStatus(currentHistory);
stage_status.update();
cj.Ticker.setFPS(60);
cj.Ticker.removeAllEventListeners("tick");
cj.Ticker.addEventListener("tick", handleTick);
function handleTick(event) { updateParticles(); stage_status.update(); }
console.log("[PerfGraph] グラフの描画に成功しました。");
}
window.__customInit = myCustomInit;
$('#rating-graph-expand').click(function() {
isExpanded = true;
canvas_status.style.maxWidth = canvas_status.style.maxHeight = "";
canvas_graph.style.maxWidth = canvas_graph.style.maxHeight = "";
$(this).css('cssText', 'display: none !important;');
});
function injectOnoffButton() {
function updateButtonState(button) {
if (showPerformance) {
button.textContent = LOCALIZED_TEXT.btnOn;
button.className = 'btn btn-primary btn-sm';
} else {
button.textContent = LOCALIZED_TEXT.btnOff;
button.className = 'btn btn-default btn-sm';
}
}
let insertButton = document.getElementById('onoffButton');
let downloadButton = document.getElementById('downloadGraphButton');
const btnGroup = document.querySelector('.btn-group.btn-group-sm');
if (btnGroup && btnGroup.parentNode) {
const targetParent = btnGroup.parentNode;
if (!insertButton) {
const newButton = document.createElement('button');
newButton.id = 'onoffButton';
newButton.style.marginLeft = '12px';
newButton.style.width = '140px'; // ボタンの幅を140pxに固定
newButton.style.textAlign = 'center'; // 【追加】文字を中央揃えにする
newButton.style.paddingLeft = '0'; // 【追加】左パディングをリセット
newButton.style.paddingRight = '0'; // 【追加】右パディングをリセット
newButton.addEventListener('click', function () {
showPerformance = !showPerformance;
GM_setValue("showPerformance", showPerformance);
updateButtonState(newButton);
newButton.blur();
myCustomInit();
});
updateButtonState(newButton);
targetParent.appendChild(newButton);
} else {
updateButtonState(insertButton);
}
if (!downloadButton) {
const newDownloadBtn = document.createElement('button');
newDownloadBtn.id = 'downloadGraphButton';
newDownloadBtn.className = 'btn btn-default btn-sm';
newDownloadBtn.style.marginLeft = '6px';
newDownloadBtn.textContent = LOCALIZED_TEXT.btnDownload;
newDownloadBtn.addEventListener('click', function() {
const canvasStatus = document.getElementById("ratingStatus");
const canvasGraph = document.getElementById("ratingGraph");
if (canvasStatus && canvasGraph) {
const combinedCanvas = document.createElement('canvas');
const ctx = combinedCanvas.getContext('2d');
const w = Math.max(canvasStatus.width, canvasGraph.width);
const h1 = canvasStatus.height;
const h2 = canvasGraph.height;
combinedCanvas.width = w;
combinedCanvas.height = h1 + h2;
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(0, 0, combinedCanvas.width, combinedCanvas.height);
ctx.drawImage(canvasStatus, 0, 0);
ctx.drawImage(canvasGraph, 0, h1);
const imageURI = combinedCanvas.toDataURL("image/png");
const triggerLink = document.createElement('a');
triggerLink.download = `${username}_atcoder_full_graph.png`;
triggerLink.href = imageURI;
document.body.appendChild(triggerLink);
triggerLink.click();
document.body.removeChild(triggerLink);
}
newDownloadBtn.blur();
});
targetParent.appendChild(newDownloadBtn);
}
}
}
/**
* 429エラー対策:フェッチ処理を行い、429を検知した場合は指数バックオフで再試行する
*/
async function fetchWithRetry(url, maxRetries = 4, baseDelay = 300) {
for (let i = 0; i <= maxRetries; i++) {
try {
const response = await fetch(url);
if (response.status === 429) {
if (i === maxRetries) {
throw new Error(`429: 最大再試行回数に達しました (${url})`);
}
// 指数バックオフ: 1秒 -> 2秒 -> 4秒 -> 8秒 と待ち時間を倍増させる
const delay = baseDelay * Math.pow(2, i);
console.warn(`[PerfGraph] 429 Too Many Requestsを検知しました。${delay}ms後に再試行します。 (試行: ${i + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return response;
} catch (error) {
if (i === maxRetries) throw error;
// ネットワークエラー等の場合も1秒待機して再試行
console.warn(`[PerfGraph] 通信エラーが発生しました。1秒後に再試行します。`, error);
await new Promise(resolve => setTimeout(resolve, baseDelay));
}
}
}
async function main() {
if (!username) return;
injectOnoffButton();
let currentHistory = await waitOfRatingHistory();
try {
const urlParams = new URLSearchParams(window.location.search);
let fetchParam = "";
if (urlParams.get('contestType') === 'heuristic') {
fetchParam = "?contestType=heuristic";
}
console.log("[PerfGraph] バックグラウンド情報のフェッチを開始します...");
let parser = new DOMParser();
// 通常の fetch を fetchWithRetry に置き換え
const jsonResponse = await fetchWithRetry(`https://atcoder.jp/users/${username}/history/json${fetchParam}`);
let json = await jsonResponse.json();
const textResponse = await fetchWithRetry(`https://atcoder.jp/users/${username}/history${fetchParam}`);
let text = await textResponse.text();
let historyEl = parser.parseFromString(text, "text/html").getElementById("history");
if (!historyEl) {
myCustomInit();
return;
}
let rows = historyEl.querySelectorAll("tbody tr");
html_perf_map.clear();
rows.forEach(row => {
let tds = row.querySelectorAll("td");
if (tds.length >= 4) {
let contestLinkEl = tds[1]?.querySelector('a');
let perfCell = tds[3];
if (contestLinkEl && perfCell) {
let contestName = contestLinkEl.innerText.trim();
let perfText = perfCell.innerText.trim();
if (perfText !== "ー" && perfText !== "-" && perfText !== "") {
let perfNum = Number(perfText);
if (!isNaN(perfNum)) html_perf_map.set(contestName, perfNum);
}
}
}
});
raw_json_data = json;
console.log(`[PerfGraph] パフォーマンスデータのバックグラウンド取得が完了しました。 (JSON: ${json.length}件)`);
} catch (e) {
console.error('[PerfGraph] フェッチエラー', e);
}
myCustomInit();
let lastLength = (window.rating_history || (typeof unsafeWindow !== 'undefined' ? unsafeWindow.rating_history : []) || []).length;
setInterval(() => {
let checkHistory = window.rating_history || (typeof unsafeWindow !== 'undefined' ? unsafeWindow.rating_history : []) || [];
if (checkHistory.length !== lastLength) {
console.log(`[PerfGraph] データ数変更を検知しました (${lastLength} -> ${checkHistory.length})。再描画します。`);
lastLength = checkHistory.length;
myCustomInit();
}
}, 300);
}
main();
});
})();