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 <noreply@anthropic.com>
This commit is contained in:
@@ -225,17 +225,41 @@ HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
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,9 +523,16 @@ HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
<div class="window-shade"></div>
|
||||
</div>
|
||||
<div class="window-content">
|
||||
<div class="charts-wrapper">
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">Top Games</div>
|
||||
<canvas id="playtime-chart"></canvas>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">By Category</div>
|
||||
<canvas id="categories-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -598,7 +629,9 @@ HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
});
|
||||
|
||||
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 = """<!DOCTYPE html>
|
||||
if (chart) {
|
||||
chart.destroy();
|
||||
}
|
||||
if (categoriesChart) {
|
||||
categoriesChart.destroy();
|
||||
}
|
||||
|
||||
if (chartData.length === 0) {
|
||||
document.getElementById('games-table').innerHTML =
|
||||
@@ -718,6 +754,61 @@ HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
}
|
||||
});
|
||||
|
||||
// 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 = """<!DOCTYPE html>
|
||||
if (categoriesData.length === 0) {
|
||||
catTbody.innerHTML = '<tr><td colspan="4" class="no-data">No categories found</td></tr>';
|
||||
} 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 = `
|
||||
<td>${index + 1}</td>
|
||||
<td>${cat.name} <span class="service-badge">${cat.gameCount} games</span></td>
|
||||
<td>
|
||||
<span class="color-box" style="background: ${colors[index]}"></span>
|
||||
${cat.name} <span class="service-badge">${cat.gameCount} games</span>
|
||||
</td>
|
||||
<td class="time">${formatTime(cat.playtime)}</td>
|
||||
<td class="percent">${percent}%</td>
|
||||
`;
|
||||
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 = `
|
||||
<td>${othersIndex + 1}</td>
|
||||
<td>
|
||||
<span class="color-box" style="background: ${colors[othersIndex]}"></span>
|
||||
Others (${otherCategories.length} categories)
|
||||
</td>
|
||||
<td class="time">${formatTime(othersPlaytime)}</td>
|
||||
<td class="percent">${othersPercent}%</td>
|
||||
`;
|
||||
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 = `
|
||||
<td>${othersIndex + 1}.${otherIndex + 1}</td>
|
||||
<td>
|
||||
${otherCat.name} <span class="service-badge">${otherCat.gameCount} games</span>
|
||||
</td>
|
||||
<td class="time">${formatTime(otherCat.playtime)}</td>
|
||||
<td class="percent">${otherPercent}%</td>
|
||||
`;
|
||||
catTbody.appendChild(detailRow);
|
||||
detailRows.push(detailRow);
|
||||
});
|
||||
|
||||
othersRow.addEventListener('click', () => {
|
||||
othersRow.classList.toggle('expanded');
|
||||
detailRows.forEach(dr => dr.classList.toggle('visible'));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user