Skip to content

Instantly share code, notes, and snippets.

@Far-Se
Last active October 31, 2025 17:10
Show Gist options
  • Save Far-Se/e980e06f36734ed342ab94bbc069ff16 to your computer and use it in GitHub Desktop.
Save Far-Se/e980e06f36734ed342ab94bbc069ff16 to your computer and use it in GitHub Desktop.
Gamalytic Tampermonkey script for Steam to show stats on the sidebar of the game page.
// ==UserScript==
// @name Steam Gamalytic Statistics
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Display Gamalytic statistics on Steam game pages
// @author You
// @match https://store.steampowered.com/app/*/*
// @grant GM_xmlhttpRequest
// @connect api.gamalytic.com
// ==/UserScript==
(function() {
'use strict';
// Extract app ID from pathname
const pathMatch = window.location.pathname.match(/^\/app\/(\d+)\//);
if (!pathMatch) return;
const appId = pathMatch[1];
const apiUrl = `https://api.gamalytic.com/game/${appId}`;
// Fetch data from Gamalytic API
GM_xmlhttpRequest({
method: 'GET',
url: apiUrl,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
insertStatsTables(data);
} catch (error) {
console.error('Error parsing Gamalytic data:', error);
}
},
onerror: function(error) {
console.error('Error fetching Gamalytic data:', error);
}
});
function insertStatsTables(data) {
const targetElement = document.querySelector('.rightcol .responsive_apppage_details_left#category_block');
if (!targetElement) return;
// Create container
const container = document.createElement('div');
container.className = 'gamalytics-stats-container';
container.style.cssText = `
margin-bottom: 20px;
background: linear-gradient(135deg, rgba(42, 71, 94, 1) 0%, rgba(24, 42, 56, 1) 100%);
border-radius: 5px;
overflow: hidden;
`;
// Create main stats table
const mainStatsTable = createMainStatsTable(data);
container.appendChild(mainStatsTable);
// Create Gamalytics link
const gamalyticsLink = document.createElement('div');
gamalyticsLink.style.cssText = `
padding: 10px 15px;
background: rgba(0, 0, 0, 0.2);
text-align: center;
border-top: 1px solid rgba(255, 255, 255, 0.1);
`;
gamalyticsLink.innerHTML = `
<a href="https://gamalytic.com/game/${data.steamId}"
target="_blank"
style="color: #66c0f4; text-decoration: none; font-size: 13px;">
View detailed statistics on Gamalytic →
</a>
`;
container.appendChild(gamalyticsLink);
// Create history table
const historyTable = createHistoryTable(data);
container.appendChild(historyTable);
// Insert before target element
targetElement.parentNode.insertBefore(container, targetElement);
// Add CSS styles
addStyles();
}
function createMainStatsTable(data) {
const wrapper = document.createElement('div');
wrapper.className = 'gamalytics-main-stats';
// Format numbers
const formatNumber = (num) => {
return num ? num.toLocaleString('en-US') : 'N/A';
};
const formatCurrency = (num) => {
return num ? '$' + num.toLocaleString('en-US') : 'N/A';
};
const formatPercent = (num) => {
return num ? num.toFixed(1) + '%' : 'N/A';
};
// Get country data
const countryData = data.countryData || {};
const countries = Object.entries(countryData)
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
const countryNames = {
'cn': 'China',
'us': 'United States',
'jp': 'Japan',
'de': 'Germany',
'ru': 'Russia',
'gb': 'United Kingdom',
'kr': 'South Korea',
'fr': 'France',
'br': 'Brazil',
'ca': 'Canada'
};
// Create header
const header = document.createElement('div');
header.style.cssText = `
padding: 15px;
background: rgba(0, 0, 0, 0.3);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
`;
header.innerHTML = `
<h3 style="margin: 0; color: #ffffff; font-size: 16px; font-weight: 500;">
📊 Gamalytic Statistics
</h3>
`;
wrapper.appendChild(header);
// Main stats section
const mainStats = document.createElement('div');
mainStats.className = 'gamalytics-summary';
mainStats.style.cssText = `
padding: 15px;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
background: rgba(0, 0, 0, 0.2);
`;
const stats = [
{ label: 'Copies Sold', value: formatNumber(data.copiesSold) },
{ label: 'Gross Revenue', value: formatCurrency(data.revenue) },
{ label: 'Total Players', value: formatNumber(data.players) },
{ label: 'Total Owners', value: formatNumber(data.owners) }
];
stats.forEach(stat => {
const statDiv = document.createElement('div');
statDiv.style.cssText = `
padding: 10px;
background: rgba(0, 0, 0, 0.3);
border-radius: 3px;
`;
statDiv.innerHTML = `
<div style="color: #8f98a0; font-size: 11px; text-transform: uppercase; margin-bottom: 5px;">
${stat.label}
</div>
<div style="color: #ffffff; font-size: 18px; font-weight: 600;">
${stat.value}
</div>
`;
mainStats.appendChild(statDiv);
});
wrapper.appendChild(mainStats);
// Expandable section
const expandableSection = document.createElement('div');
expandableSection.className = 'gamalytics-expandable';
// Toggle button
const toggleBtn = document.createElement('button');
toggleBtn.className = 'gamalytics-toggle-btn';
toggleBtn.style.cssText = `
width: 100%;
padding: 12px 15px;
background: rgba(0, 0, 0, 0.3);
border: none;
border-top: 1px solid rgba(255, 255, 255, 0.1);
color: #66c0f4;
font-size: 13px;
cursor: pointer;
text-align: left;
display: flex;
justify-content: space-between;
align-items: center;
transition: background 0.2s;
`;
toggleBtn.innerHTML = `
<span>Show Less Statistics</span>
<span class="arrow">▲</span>
`;
// Expandable content
const expandableContent = document.createElement('div');
expandableContent.className = 'gamalytics-expandable-content';
expandableContent.style.cssText = `
display: block;
padding: 15px;
background: rgba(0, 0, 0, 0.2);
border-top: 1px solid rgba(255, 255, 255, 0.1);
`;
// Additional stats table
expandableContent.innerHTML = `
<table style="width: 100%; border-collapse: collapse; color: #c6d4df; font-size: 13px;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
<span style="color: #8f98a0;">Accuracy:</span>
</td>
<td style="padding: 8px 0; text-align: right; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
${formatPercent(data.accuracy * 100)}
</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
<span style="color: #8f98a0;">Sells based on Reviews:</span>
</td>
<td style="padding: 8px 0; text-align: right; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
${parseInt(data.estimateDetails?.reviewBased ?? "0").toLocaleString()}
</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
<span style="color: #8f98a0;">Sells based on Playtime:</span>
</td>
<td style="padding: 8px 0; text-align: right; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
${parseInt(data.estimateDetails?.playtimeBased ?? "0").toLocaleString()}
</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
<span style="color: #8f98a0;">Average Playtime:</span>
</td>
<td style="padding: 8px 0; text-align: right; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
${data.avgPlaytime ? data.avgPlaytime.toFixed(2) + ' hours' : 'N/A'}
</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
<span style="color: #8f98a0;">Review Score:</span>
</td>
<td style="padding: 8px 0; text-align: right; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
${data.reviewScore || 'N/A'}
</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
<span style="color: #8f98a0;">Total Reviews:</span>
</td>
<td style="padding: 8px 0; text-align: right; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
${formatNumber(data.reviews)}
</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
<span style="color: #8f98a0;">Steam Reviews:</span>
</td>
<td style="padding: 8px 0; text-align: right; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
${formatNumber(data.reviewsSteam)}
</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
<span style="color: #8f98a0;">Followers:</span>
</td>
<td style="padding: 8px 0; text-align: right; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
${formatNumber(data.followers)}
</td>
</tr>
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
<span style="color: #8f98a0;">Price:</span>
</td>
<td style="padding: 8px 0; text-align: right; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
${formatCurrency(data.price)}
</td>
</tr>
</table>
`;
// Add country data if available
if (countries.length > 0) {
const countrySection = document.createElement('div');
countrySection.style.marginTop = '15px';
countrySection.innerHTML = `
<h4 style="color: #ffffff; font-size: 14px; margin: 0 0 10px 0; font-weight: 500;">
Players by Country
</h4>
<table style="width: 100%; border-collapse: collapse; color: #c6d4df; font-size: 13px;">
${countries.map(([code, percent]) => `
<tr>
<td style="padding: 6px 0;">
<span style="color: #8f98a0;">${countryNames[code] || code.toUpperCase()}:</span>
</td>
<td style="padding: 6px 0; text-align: right;">
${formatPercent(percent)}
</td>
</tr>
`).join('')}
</table>
`;
expandableContent.appendChild(countrySection);
}
// Toggle functionality
toggleBtn.addEventListener('click', () => {
const isExpanded = expandableContent.style.display === 'block';
expandableContent.style.display = isExpanded ? 'none' : 'block';
toggleBtn.querySelector('.arrow').textContent = isExpanded ? '▼' : '▲';
toggleBtn.querySelector('span:first-child').textContent =
isExpanded ? 'Show More Statistics' : 'Show Less Statistics';
});
toggleBtn.addEventListener('mouseenter', () => {
toggleBtn.style.background = 'rgba(102, 192, 244, 0.1)';
});
toggleBtn.addEventListener('mouseleave', () => {
toggleBtn.style.background = 'rgba(0, 0, 0, 0.3)';
});
expandableSection.appendChild(toggleBtn);
expandableSection.appendChild(expandableContent);
wrapper.appendChild(expandableSection);
return wrapper;
}
function createHistoryTable(data) {
if (!data.history || data.history.length === 0) return document.createElement('div');
const wrapper = document.createElement('div');
wrapper.className = 'gamalytics-history';
wrapper.style.cssText = `
margin-top: 15px;
background: rgba(0, 0, 0, 0.2);
border-top: 1px solid rgba(255, 255, 255, 0.1);
`;
// Header
const header = document.createElement('div');
header.style.cssText = `
padding: 15px;
background: rgba(0, 0, 0, 0.3);
`;
header.innerHTML = `
<h3 style="margin: 0; color: #ffffff; font-size: 16px; font-weight: 500;">
📈 Sales History (Daily)
</h3>
`;
wrapper.appendChild(header);
// Process history data by day
const dailyData = processHistoryByDay(data.history);
// Create scrollable table container
const tableContainer = document.createElement('div');
tableContainer.style.cssText = `
max-height: 400px;
overflow-y: auto;
`;
const table = document.createElement('table');
table.style.cssText = `
width: 100%;
border-collapse: collapse;
color: #c6d4df;
font-size: 13px;
`;
table.innerHTML = `
<thead>
<tr style="background: rgba(0, 0, 0, 0.3); position: sticky; top: 0;">
<th style="padding: 10px 8px; text-align: left; color: #8f98a0; font-weight: 500; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
Date
</th>
<th style="padding: 10px 8px; text-align: right; color: #8f98a0; font-weight: 500; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
Copies
</th>
<th style="padding: 10px 8px; text-align: right; color: #8f98a0; font-weight: 500; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
Revenue
</th>
<th style="padding: 10px 8px; text-align: right; color: #8f98a0; font-weight: 500; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
Players
</th>
</tr>
</thead>
<tbody>
${dailyData.map((entry, index) => `
<tr style="background: ${index % 2 === 0 ? 'rgba(0, 0, 0, 0.1)' : 'transparent'};">
<td style="padding: 10px 8px; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
${entry.date}
</td>
<td style="padding: 10px 8px; text-align: right; border-bottom: 1px solid rgba(255, 255, 255, 0.05);">
${entry.sales.toLocaleString('en-US')}
</td>
<td style="padding: 10px 8px; text-align: right; border-bottom: 1px solid rgba(255, 255, 255, 0.05); color: #a4d007;">
$${entry.revenue.toLocaleString('en-US')}
</td>
<td style="padding: 10px 8px; text-align: right; border-bottom: 1px solid rgba(255, 255, 255, 0.05); color: #a4d007;">
${entry.players}
</td>
</tr>
`).join('')}
</tbody>
`;
tableContainer.appendChild(table);
wrapper.appendChild(tableContainer);
return wrapper;
}
function processHistoryByDay(history) {
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const dailyData = [];
// Filter entries with sales > 0
const salesEntries = history.filter(entry => entry.sales > 0);
const d = new Date();
let year = d.getFullYear();
salesEntries.forEach((entry, index) => {
if (index === 0) {
// First entry shows total accumulated sales
const date = new Date(entry.timeStamp);
const dateLabel = `${date.getDate()} ${monthNames[date.getMonth()]}${year === date.getFullYear() ? "" : `, ${date.getFullYear()}`}`;
dailyData.push({
date: dateLabel,
sales: entry.sales,
players: parseInt(entry.players).toLocaleString(),
revenue: entry.revenue,
timestamp: entry.timeStamp
});
} else {
// Calculate daily difference
const prevEntry = salesEntries[index - 1];
const salesDiff = entry.sales - prevEntry.sales;
const revenueDiff = entry.revenue - prevEntry.revenue;
// Only add if there's a change
if (salesDiff > 0 || revenueDiff > 0) {
const date = new Date(entry.timeStamp);
const dateLabel = `${date.getDate()} ${monthNames[date.getMonth()]}${year === date.getFullYear() ? "" : `, ${date.getFullYear()}`}`;
dailyData.push({
date: dateLabel,
sales: salesDiff,
players: parseInt(entry.players).toLocaleString(),
revenue: revenueDiff,
timestamp: entry.timeStamp
});
}
}
});
// Sort by date (newest first)
return dailyData.sort((a, b) => b.timestamp - a.timestamp);
}
function addStyles() {
const style = document.createElement('style');
style.textContent = `
.gamalytics-stats-container * {
box-sizing: border-box;
}
.gamalytics-history > div:last-child::-webkit-scrollbar {
width: 8px;
}
.gamalytics-history > div:last-child::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
}
.gamalytics-history > div:last-child::-webkit-scrollbar-thumb {
background: rgba(102, 192, 244, 0.3);
border-radius: 4px;
}
.gamalytics-history > div:last-child::-webkit-scrollbar-thumb:hover {
background: rgba(102, 192, 244, 0.5);
}
`;
document.head.appendChild(style);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment