From bc0a541034452ff9698d54758d6cc5b565451277 Mon Sep 17 00:00:00 2001 From: Miguel Astor Date: Wed, 25 Feb 2026 18:42:59 -0400 Subject: [PATCH] Add categories chart and expandable Others row to categories table - Add second doughnut chart showing playtime by category alongside games chart - Charts display side-by-side on large screens, stacked on small screens (<700px) - Group categories beyond top 10 into expandable "Others" row in table - Add color boxes to category rows matching chart colors Co-Authored-By: Claude Opus 4.5 --- generate_report.py | 152 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 145 insertions(+), 7 deletions(-) diff --git a/generate_report.py b/generate_report.py index 839a4b3..cb63956 100644 --- a/generate_report.py +++ b/generate_report.py @@ -225,17 +225,41 @@ HTML_TEMPLATE = """ margin-top: 4px; } - /* Chart container */ + /* Chart containers */ + .charts-wrapper { + display: flex; + gap: 20px; + justify-content: center; + flex-wrap: wrap; + } .chart-container { position: relative; - max-width: 500px; - margin: 0 auto; + flex: 1; + min-width: 280px; + max-width: 450px; padding: 10px; background: #FFFFFF; border: 1px solid; border-color: var(--mac-border-dark) var(--mac-border-light) var(--mac-border-light) var(--mac-border-dark); box-shadow: inset 1px 1px 0 var(--mac-border-dark); } + .chart-title { + font-family: 'Charcoal', 'Chicago', Geneva, sans-serif; + font-size: 11px; + font-weight: bold; + text-align: center; + margin-bottom: 8px; + color: var(--mac-text); + } + @media (max-width: 700px) { + .charts-wrapper { + flex-direction: column; + align-items: center; + } + .chart-container { + max-width: 100%; + } + } /* Tables styled like Mac OS 9 lists */ table { @@ -499,8 +523,15 @@ HTML_TEMPLATE = """
-
- +
+
+
Top Games
+ +
+
+
By Category
+ +
@@ -598,7 +629,9 @@ HTML_TEMPLATE = """ }); let chart = null; + let categoriesChart = null; const ctx = document.getElementById('playtime-chart').getContext('2d'); + const ctxCategories = document.getElementById('categories-chart').getContext('2d'); function getSelectedServices() { const checkboxes = filtersDiv.querySelectorAll('input[type="checkbox"]'); @@ -671,6 +704,9 @@ HTML_TEMPLATE = """ if (chart) { chart.destroy(); } + if (categoriesChart) { + categoriesChart.destroy(); + } if (chartData.length === 0) { document.getElementById('games-table').innerHTML = @@ -718,6 +754,61 @@ HTML_TEMPLATE = """ } }); + // Categories chart data + const topCategoriesChart = categoriesData.slice(0, topN); + const otherCategoriesChart = categoriesData.slice(topN); + const categoriesChartData = topCategoriesChart.map(c => ({ + name: c.name, + playtime: c.playtime + })); + if (otherCategoriesChart.length > 0) { + const othersPlaytime = otherCategoriesChart.reduce((sum, c) => sum + c.playtime, 0); + categoriesChartData.push({ + name: `Others (${otherCategoriesChart.length} categories)`, + playtime: othersPlaytime + }); + } + + if (categoriesChartData.length > 0) { + categoriesChart = new Chart(ctxCategories, { + type: 'doughnut', + data: { + labels: categoriesChartData.map(c => c.name), + datasets: [{ + data: categoriesChartData.map(c => c.playtime), + backgroundColor: colors.slice(0, categoriesChartData.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) => { @@ -772,17 +863,64 @@ HTML_TEMPLATE = """ if (categoriesData.length === 0) { catTbody.innerHTML = 'No categories found'; } else { - categoriesData.forEach((cat, index) => { + const topCategories = categoriesData.slice(0, topN); + const otherCategories = categoriesData.slice(topN); + + topCategories.forEach((cat, index) => { const percent = ((cat.playtime / totalPlaytime) * 100).toFixed(1); const row = document.createElement('tr'); row.innerHTML = ` ${index + 1} - ${cat.name} ${cat.gameCount} games + + + ${cat.name} ${cat.gameCount} games + ${formatTime(cat.playtime)} ${percent}% `; catTbody.appendChild(row); }); + + if (otherCategories.length > 0) { + const othersPlaytime = otherCategories.reduce((sum, c) => sum + c.playtime, 0); + const othersPercent = ((othersPlaytime / totalPlaytime) * 100).toFixed(1); + const othersIndex = topCategories.length; + + const othersRow = document.createElement('tr'); + othersRow.className = 'others-row'; + othersRow.innerHTML = ` + ${othersIndex + 1} + + + Others (${otherCategories.length} categories) + + ${formatTime(othersPlaytime)} + ${othersPercent}% + `; + catTbody.appendChild(othersRow); + + const detailRows = []; + otherCategories.forEach((otherCat, otherIndex) => { + const otherPercent = ((otherCat.playtime / totalPlaytime) * 100).toFixed(1); + const detailRow = document.createElement('tr'); + detailRow.className = 'others-detail'; + detailRow.innerHTML = ` + ${othersIndex + 1}.${otherIndex + 1} + + ${otherCat.name} ${otherCat.gameCount} games + + ${formatTime(otherCat.playtime)} + ${otherPercent}% + `; + catTbody.appendChild(detailRow); + detailRows.push(detailRow); + }); + + othersRow.addEventListener('click', () => { + othersRow.classList.toggle('expanded'); + detailRows.forEach(dr => dr.classList.toggle('visible')); + }); + } } }