// ==UserScript==
// @name GeoGuessr Rating History Displayer
// @namespace http://tampermonkey.net/
// @version 1.0
// @match https://www.geoguessr.com/*
// @description Display rating history from georank.io on GeoGuessr profile pages
// @author AaronThug
// @license MIT
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.js
// @icon https://www.google.com/s2/favicons?sz=64&domain=geoguessr.com
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function() {
'use strict';
let isExpanded = false;
let chartInstance = null;
let cachedData = null;
let currentUserId = null;
const styles = `
.rating-history-bar {
background: #2c2c54;
border: 1px solid #3c3c7e;
border-radius: 8px;
margin-bottom: 12px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.rating-history-bar:hover {
background: #3c3c7e;
border-color: #4c4c8e;
}
.rating-history-content {
padding: 8px 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.rating-history-content h4 {
color: #ffffff;
margin: 0;
font-size: 14px;
font-weight: 600;
}
.rating-history-arrow {
color: #ffffff;
font-size: 12px;
transition: transform 0.3s ease;
}
.rating-history-arrow.expanded {
transform: rotate(180deg);
}
.rating-history-chart-container {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
background: #1a1a2e;
border-top: 1px solid #3c3c7e;
}
.rating-history-chart-container.expanded {
max-height: 600px;
}
.rating-history-chart {
padding: 20px;
height: 400px;
}
.rating-history-loading {
text-align: center;
padding: 40px 20px;
color: #ffffff;
}
.rating-history-error {
text-align: center;
padding: 40px 20px;
color: #ff6b6b;
}
`;
function addStyles() {
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
}
function isProfilePage() {
const path = window.location.pathname;
if (path === '/me/profile' || path.startsWith('/user/')) return true;
const pathParts = path.split('/').filter(part => part !== '');
if (pathParts.length >= 2 && pathParts[0].length === 2) {
const remainingPath = '/' + pathParts.slice(1).join('/');
return remainingPath === '/me/profile' || remainingPath.startsWith('/user/');
}
return false;
}
async function getUserId() {
const path = window.location.pathname;
if (path.startsWith('/user/')) return path.split('/user/')[1];
if (path === '/me/profile') return await fetchCurrentUserId();
const pathParts = path.split('/').filter(part => part !== '');
if (pathParts.length >= 2 && pathParts[0].length === 2) {
const remainingPath = '/' + pathParts.slice(1).join('/');
if (remainingPath.startsWith('/user/')) return remainingPath.split('/user/')[1];
if (remainingPath === '/me/profile') return await fetchCurrentUserId();
}
return null;
}
async function fetchCurrentUserId() {
try {
const response = await fetch('https://www.geoguessr.com/api/v3/profiles');
const data = await response.json();
return data.id || data.user?.id || data.profile?.id || null;
} catch (error) {
return null;
}
}
async function fetchRatingHistory(userId) {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: `https://georank.io/api/userhistory/${userId}`,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
},
onload: function(response) {
try {
if (response.status === 200) {
const data = JSON.parse(response.responseText);
resolve(data.rating?.overall || null);
} else {
resolve(null);
}
} catch (error) {
resolve(null);
}
},
onerror: () => resolve(null),
ontimeout: () => resolve(null),
timeout: 10000
});
});
}
function createRatingHistoryBar() {
const bar = document.createElement('div');
bar.className = 'rating-history-bar';
bar.innerHTML = `
<div class="rating-history-content">
<h4>Rating History</h4>
<span class="rating-history-arrow">▼</span>
</div>
<div class="rating-history-chart-container">
<div class="rating-history-chart">
<canvas id="rating-history-canvas"></canvas>
</div>
</div>
`;
const arrow = bar.querySelector('.rating-history-arrow');
const container = bar.querySelector('.rating-history-chart-container');
bar.addEventListener('click', async function() {
if (!isExpanded) {
isExpanded = true;
arrow.classList.add('expanded');
container.classList.add('expanded');
if (!cachedData) {
showLoading();
await loadAndDisplayChart();
}
} else {
isExpanded = false;
arrow.classList.remove('expanded');
container.classList.remove('expanded');
}
});
return bar;
}
function showLoading() {
const chartDiv = document.querySelector('.rating-history-chart');
chartDiv.innerHTML = '<div class="rating-history-loading">Loading rating history...</div>';
}
function showError(message) {
const chartDiv = document.querySelector('.rating-history-chart');
chartDiv.innerHTML = `<div class="rating-history-error">${message}</div>`;
}
async function loadAndDisplayChart() {
try {
if (!currentUserId) {
currentUserId = await getUserId();
if (!currentUserId) {
showError('Failed to get rating history. Try again later.');
return;
}
}
const ratingData = await fetchRatingHistory(currentUserId);
if (!ratingData) {
showError('Failed to get rating history. Try again later.');
return;
}
if (Object.keys(ratingData).length === 0) {
showError('The user has no available rating history.');
return;
}
cachedData = ratingData;
displayChart(ratingData);
} catch (error) {
showError('Failed to get rating history. Try again later.');
}
}
function displayChart(data) {
const chartDiv = document.querySelector('.rating-history-chart');
chartDiv.innerHTML = '<canvas id="rating-history-canvas"></canvas>';
const canvas = document.getElementById('rating-history-canvas');
const ctx = canvas.getContext('2d');
const dates = Object.keys(data).sort();
const ratings = dates.map(date => data[date]);
if (chartInstance) chartInstance.destroy();
chartInstance = new Chart(ctx, {
type: 'line',
data: {
labels: dates,
datasets: [{
label: 'Overall Rating',
data: ratings,
borderColor: '#4CAF50',
backgroundColor: 'rgba(76, 175, 80, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.1,
pointRadius: 3,
pointHoverRadius: 5,
pointBackgroundColor: '#4CAF50',
pointBorderColor: '#ffffff',
pointBorderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { labels: { color: '#ffffff' } },
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: '#ffffff',
bodyColor: '#ffffff',
borderColor: '#4CAF50',
borderWidth: 1
}
},
scales: {
x: {
ticks: { color: '#ffffff', maxTicksLimit: 10 },
grid: { color: 'rgba(255, 255, 255, 0.1)' }
},
y: {
ticks: { color: '#ffffff' },
grid: { color: 'rgba(255, 255, 255, 0.1)' }
}
},
interaction: { intersect: false, mode: 'index' }
}
});
}
function insertRatingHistoryBar() {
if (document.querySelector('.rating-history-bar')) return true;
const statisticsBar = document.querySelector('.bars_root__tryg2');
if (statisticsBar) {
const ratingHistoryBar = createRatingHistoryBar();
statisticsBar.parentNode.insertBefore(ratingHistoryBar, statisticsBar);
return true;
}
return false;
}
function init() {
if (!isProfilePage()) return;
addStyles();
isExpanded = false;
cachedData = null;
currentUserId = null;
if (chartInstance) {
chartInstance.destroy();
chartInstance = null;
}
if (insertRatingHistoryBar()) return;
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
if (insertRatingHistoryBar()) {
setTimeout(() => {
if (!document.querySelector('.rating-history-bar')) {
insertRatingHistoryBar();
}
}, 500);
}
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
if (window.location.pathname.includes('/me/profile')) {
const interval = setInterval(() => {
if (insertRatingHistoryBar()) clearInterval(interval);
}, 1000);
setTimeout(() => clearInterval(interval), 10000);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
let lastUrl = location.href;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
const existingBar = document.querySelector('.rating-history-bar');
if (!isProfilePage()) {
if (existingBar) existingBar.remove();
return;
}
cachedData = null;
currentUserId = null;
isExpanded = false;
if (chartInstance) {
chartInstance.destroy();
chartInstance = null;
}
if (existingBar) existingBar.remove();
setTimeout(init, 1500);
}
}).observe(document, {subtree: true, childList: true});
})();