Last active
October 31, 2025 17:10
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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