From c0c25e27194d28566797893e3018cd97f7b275d4 Mon Sep 17 00:00:00 2001 From: Miguel Astor Date: Fri, 6 Mar 2026 06:19:06 -0400 Subject: [PATCH] Add By Runner and By Source charts to playtime reports - Add runners and sources charts to modern and platinum templates - Update responsive layout to use grid for multiple charts - Update .gitignore to exclude pga.db --- .gitignore | 1 + templates/brutalism.css | 7 +- templates/glassmorphism.css | 8 +- templates/modern.html | 8 ++ templates/neumorphism.css | 7 +- templates/platinum.html | 151 ++++++++++++++++++++++++++++++-- templates/script.js | 168 +++++++++++++++++++++++++++++++++++- 7 files changed, 333 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index db8d585..2f60589 100644 --- a/.gitignore +++ b/.gitignore @@ -259,3 +259,4 @@ flycheck_*.el # Built Visual Studio Code Extensions *.vsix +pga.db diff --git a/templates/brutalism.css b/templates/brutalism.css index e64c371..b2039b9 100644 --- a/templates/brutalism.css +++ b/templates/brutalism.css @@ -286,14 +286,15 @@ body { /* Charts */ .charts-wrapper { - display: flex; - gap: 20px; + display: grid; + grid-auto-columns: minmax(280px, 450px); + grid-template-columns: repeat(auto-fill, minmax(280px, 450px)); + grid-gap: 24px; justify-content: center; flex-wrap: wrap; } .chart-container { - flex: 1; min-width: 280px; max-width: 450px; padding: 16px; diff --git a/templates/glassmorphism.css b/templates/glassmorphism.css index b2494db..c76c97d 100644 --- a/templates/glassmorphism.css +++ b/templates/glassmorphism.css @@ -274,14 +274,14 @@ body { /* Charts */ .charts-wrapper { - display: flex; - gap: 24px; + display: grid; + grid-auto-columns: minmax(280px, 450px); + grid-template-columns: repeat(auto-fill, minmax(280px, 450px)); + grid-gap: 24px; justify-content: center; - flex-wrap: wrap; } .chart-container { - flex: 1; min-width: 280px; max-width: 450px; padding: 16px; diff --git a/templates/modern.html b/templates/modern.html index 9165db9..502b458 100644 --- a/templates/modern.html +++ b/templates/modern.html @@ -79,6 +79,14 @@ OF THIS SOFTWARE.
By Category
+
+
By Runner
+ +
+
+
By Source
+ +
diff --git a/templates/neumorphism.css b/templates/neumorphism.css index 3fe93e9..62dcbc7 100644 --- a/templates/neumorphism.css +++ b/templates/neumorphism.css @@ -294,14 +294,15 @@ body { /* Charts */ .charts-wrapper { - display: flex; - gap: 24px; + display: grid; + grid-auto-columns: minmax(280px, 450px); + grid-template-columns: repeat(auto-fill, minmax(280px, 450px)); + grid-gap: 24px; justify-content: center; flex-wrap: wrap; } .chart-container { - flex: 1; min-width: 280px; max-width: 450px; padding: 20px; diff --git a/templates/platinum.html b/templates/platinum.html index d058c99..0d33818 100644 --- a/templates/platinum.html +++ b/templates/platinum.html @@ -217,14 +217,15 @@ OF THIS SOFTWARE. /* Chart containers */ .charts-wrapper { - display: flex; - gap: 20px; + display: grid; + grid-auto-columns: minmax(280px, 450px); + grid-template-columns: repeat(auto-fill, minmax(280px, 450px)); + grid-gap: 20px; justify-content: center; flex-wrap: wrap; } .chart-container { position: relative; - flex: 1; min-width: 280px; max-width: 450px; padding: 10px; @@ -580,6 +581,14 @@ OF THIS SOFTWARE.
By Category
+
+
By Runner
+ +
+
+
By Source
+ +
@@ -694,8 +703,12 @@ OF THIS SOFTWARE. let chart = null; let categoriesChart = null; + let runnersChart = null; + let sourcesChart = null; const ctx = document.getElementById('playtime-chart').getContext('2d'); const ctxCategories = document.getElementById('categories-chart').getContext('2d'); + const ctxRunners = document.getElementById('runners-chart').getContext('2d'); + const ctxSources = document.getElementById('sources-chart').getContext('2d'); function getSelectedServices() { const checkboxes = filtersDiv.querySelectorAll('input[type="checkbox"]'); @@ -769,12 +782,24 @@ OF THIS SOFTWARE. const runnersData = Object.values(runnerMap) .sort((a, b) => b.playtime - a.playtime); - return { chartData: topGames, othersGames, categoriesData, runnersData, totalPlaytime, totalGames }; + const sourceMap = {}; + filtered.forEach(g => { + const source = g.service || 'unknown'; + if (!sourceMap[source]) { + sourceMap[source] = { name: source, playtime: 0, gameCount: 0 }; + } + sourceMap[source].playtime += g.playtime; + sourceMap[source].gameCount++; + }); + const sourcesData = Object.values(sourceMap) + .sort((a, b) => b.playtime - a.playtime); + + return { chartData: topGames, othersGames, categoriesData, runnersData, sourcesData, totalPlaytime, totalGames }; } function updateDisplay() { const selectedServices = getSelectedServices(); - const { chartData, othersGames, categoriesData, runnersData, totalPlaytime, totalGames } = getFilteredData(selectedServices); + const { chartData, othersGames, categoriesData, runnersData, sourcesData, totalPlaytime, totalGames } = getFilteredData(selectedServices); document.getElementById('total-games').textContent = totalGames; document.getElementById('total-time').textContent = formatTime(totalPlaytime); @@ -785,6 +810,12 @@ OF THIS SOFTWARE. if (categoriesChart) { categoriesChart.destroy(); } + if (runnersChart) { + runnersChart.destroy(); + } + if (sourcesChart) { + sourcesChart.destroy(); + } if (chartData.length === 0) { document.getElementById('games-table').innerHTML = @@ -906,6 +937,116 @@ OF THIS SOFTWARE. }); } + // Runners Chart + const topRunnersChart = runnersData.slice(0, topN); + const otherRunnersChart = runnersData.slice(topN); + const runnersChartData = topRunnersChart.map(r => ({ + name: r.name, + playtime: r.playtime + })); + if (otherRunnersChart.length > 0) { + const othersPlaytime = otherRunnersChart.reduce((sum, r) => sum + r.playtime, 0); + runnersChartData.push({ + name: `Others (${otherRunnersChart.length} runners)`, + playtime: othersPlaytime + }); + } + + if (runnersChartData.length > 0) { + runnersChart = new Chart(ctxRunners, { + type: 'doughnut', + data: { + labels: runnersChartData.map(r => r.name), + datasets: [{ + data: runnersChartData.map(r => r.playtime), + backgroundColor: colors.slice(0, runnersChartData.length), + borderColor: '#FFFFFF', + borderWidth: 1 + }] + }, + options: { + responsive: true, + plugins: { + legend: { + position: 'bottom', + labels: { + color: '#000000', + font: { + family: "'Charcoal', 'Chicago', Geneva, sans-serif", + size: 10 + }, + padding: 10 + } + }, + tooltip: { + callbacks: { + label: function(context) { + const value = context.raw; + const percent = ((value / totalPlaytime) * 100).toFixed(1); + return ' ' + formatTime(value) + ' (' + percent + '%)'; + } + } + } + } + } + }); + } + + // Sources Chart + const topSourcesChart = sourcesData.slice(0, topN); + const otherSourcesChart = sourcesData.slice(topN); + const sourcesChartData = topSourcesChart.map(s => ({ + name: s.name, + playtime: s.playtime + })); + if (otherSourcesChart.length > 0) { + const othersPlaytime = otherSourcesChart.reduce((sum, s) => sum + s.playtime, 0); + sourcesChartData.push({ + name: `Others (${otherSourcesChart.length} sources)`, + playtime: othersPlaytime + }); + } + + if (sourcesChartData.length > 0) { + sourcesChart = new Chart(ctxSources, { + type: 'doughnut', + data: { + labels: sourcesChartData.map(s => s.name), + datasets: [{ + data: sourcesChartData.map(s => s.playtime), + backgroundColor: colors.slice(0, sourcesChartData.length), + borderColor: '#FFFFFF', + borderWidth: 1 + }] + }, + options: { + responsive: true, + plugins: { + legend: { + position: 'bottom', + labels: { + color: '#000000', + font: { + family: "'Charcoal', 'Chicago', Geneva, sans-serif", + size: 10 + }, + padding: 10 + } + }, + tooltip: { + callbacks: { + label: function(context) { + const value = context.raw; + const percent = ((value / totalPlaytime) * 100).toFixed(1); + return ' ' + formatTime(value) + ' (' + percent + '%)'; + } + } + } + } + } + }); + } + const tbody = document.getElementById('games-table'); tbody.innerHTML = ''; chartData.forEach((game, index) => { diff --git a/templates/script.js b/templates/script.js index 3c04cb5..b1030a1 100644 --- a/templates/script.js +++ b/templates/script.js @@ -115,8 +115,12 @@ services.forEach(service => { let chart = null; let categoriesChart = null; +let runnersChart = null; +let sourcesChart = null; const ctx = document.getElementById('playtime-chart').getContext('2d'); const ctxCategories = document.getElementById('categories-chart').getContext('2d'); +const ctxRunners = document.getElementById('runners-chart').getContext('2d'); +const ctxSources = document.getElementById('sources-chart').getContext('2d'); // Initialize theme after chart variables are declared loadSavedTheme(); @@ -193,7 +197,19 @@ function getFilteredData(selectedServices) { const runnersData = Object.values(runnerMap) .sort((a, b) => b.playtime - a.playtime); - return { chartData: topGames, othersGames, categoriesData, runnersData, totalPlaytime, totalGames }; + const sourceMap = {}; + filtered.forEach(g => { + const source = g.service || 'unknown'; + if (!sourceMap[source]) { + sourceMap[source] = { name: source, playtime: 0, gameCount: 0 }; + } + sourceMap[source].playtime += g.playtime; + sourceMap[source].gameCount++; + }); + const sourcesData = Object.values(sourceMap) + .sort((a, b) => b.playtime - a.playtime); + + return { chartData: topGames, othersGames, categoriesData, runnersData, sourcesData, totalPlaytime, totalGames }; } function getChartTextColor() { @@ -220,11 +236,19 @@ function updateChartColors() { categoriesChart.options.plugins.legend.labels.color = textColor; categoriesChart.update(); } + if (typeof runnersChart !== 'undefined' && runnersChart) { + runnersChart.options.plugins.legend.labels.color = textColor; + runnersChart.update(); + } + if (typeof sourcesChart !== 'undefined' && sourcesChart) { + sourcesChart.options.plugins.legend.labels.color = textColor; + sourcesChart.update(); + } } function updateDisplay() { const selectedServices = getSelectedServices(); - const { chartData, othersGames, categoriesData, runnersData, totalPlaytime, totalGames } = getFilteredData(selectedServices); + const { chartData, othersGames, categoriesData, runnersData, sourcesData, totalPlaytime, totalGames } = getFilteredData(selectedServices); document.getElementById('total-games').textContent = totalGames; document.getElementById('total-time').textContent = formatTime(totalPlaytime); @@ -235,6 +259,12 @@ function updateDisplay() { if (categoriesChart) { categoriesChart.destroy(); } + if (runnersChart) { + runnersChart.destroy(); + } + if (sourcesChart) { + sourcesChart.destroy(); + } if (chartData.length === 0) { document.getElementById('games-table').innerHTML = @@ -376,6 +406,140 @@ function updateDisplay() { }); } + // Runners Chart + const topRunnersChart = runnersData.slice(0, topN); + const otherRunnersChart = runnersData.slice(topN); + const runnersChartData = topRunnersChart.map(r => ({ + name: r.name, + playtime: r.playtime + })); + if (otherRunnersChart.length > 0) { + const othersPlaytime = otherRunnersChart.reduce((sum, r) => sum + r.playtime, 0); + runnersChartData.push({ + name: `Others (${otherRunnersChart.length} runners)`, + playtime: othersPlaytime + }); + } + + if (runnersChartData.length > 0) { + runnersChart = new Chart(ctxRunners, { + type: 'doughnut', + data: { + labels: runnersChartData.map(r => r.name), + datasets: [{ + data: runnersChartData.map(r => r.playtime), + backgroundColor: themeConfig.colors.slice(0, runnersChartData.length), + borderColor: borderColor, + borderWidth: themeConfig.borderWidth + }] + }, + options: { + responsive: true, + plugins: { + legend: { + position: 'bottom', + labels: { + color: textColor, + font: { + family: themeConfig.fontFamily, + size: 11, + weight: themeConfig.fontWeight + }, + padding: 12, + usePointStyle: true, + pointStyle: themeConfig.pointStyle + } + }, + tooltip: { + backgroundColor: themeConfig.tooltipBg, + titleColor: themeConfig.tooltipTitleColor, + bodyColor: themeConfig.tooltipBodyColor, + borderColor: themeConfig.tooltipBorderColor, + borderWidth: themeConfig.tooltipBorderWidth, + cornerRadius: themeConfig.tooltipCornerRadius, + padding: 12, + titleFont: { family: themeConfig.fontFamily }, + bodyFont: { family: themeConfig.fontFamily }, + callbacks: { + label: function(context) { + const value = context.raw; + const percent = ((value / totalPlaytime) * 100).toFixed(1); + return ' ' + formatTime(value) + ' (' + percent + '%)'; + } + } + } + } + } + }); + } + + // Sources Chart + const topSourcesChart = sourcesData.slice(0, topN); + const otherSourcesChart = sourcesData.slice(topN); + const sourcesChartData = topSourcesChart.map(s => ({ + name: s.name, + playtime: s.playtime + })); + if (otherSourcesChart.length > 0) { + const othersPlaytime = otherSourcesChart.reduce((sum, s) => sum + s.playtime, 0); + sourcesChartData.push({ + name: `Others (${otherSourcesChart.length} sources)`, + playtime: othersPlaytime + }); + } + + if (sourcesChartData.length > 0) { + sourcesChart = new Chart(ctxSources, { + type: 'doughnut', + data: { + labels: sourcesChartData.map(s => s.name), + datasets: [{ + data: sourcesChartData.map(s => s.playtime), + backgroundColor: themeConfig.colors.slice(0, sourcesChartData.length), + borderColor: borderColor, + borderWidth: themeConfig.borderWidth + }] + }, + options: { + responsive: true, + plugins: { + legend: { + position: 'bottom', + labels: { + color: textColor, + font: { + family: themeConfig.fontFamily, + size: 11, + weight: themeConfig.fontWeight + }, + padding: 12, + usePointStyle: true, + pointStyle: themeConfig.pointStyle + } + }, + tooltip: { + backgroundColor: themeConfig.tooltipBg, + titleColor: themeConfig.tooltipTitleColor, + bodyColor: themeConfig.tooltipBodyColor, + borderColor: themeConfig.tooltipBorderColor, + borderWidth: themeConfig.tooltipBorderWidth, + cornerRadius: themeConfig.tooltipCornerRadius, + padding: 12, + titleFont: { family: themeConfig.fontFamily }, + bodyFont: { family: themeConfig.fontFamily }, + callbacks: { + label: function(context) { + const value = context.raw; + const percent = ((value / totalPlaytime) * 100).toFixed(1); + return ' ' + formatTime(value) + ' (' + percent + '%)'; + } + } + } + } + } + }); + } + const tbody = document.getElementById('games-table'); tbody.innerHTML = ''; chartData.forEach((game, index) => {