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;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chart container */
|
/* Chart containers */
|
||||||
|
.charts-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
.chart-container {
|
.chart-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 500px;
|
flex: 1;
|
||||||
margin: 0 auto;
|
min-width: 280px;
|
||||||
|
max-width: 450px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
border-color: var(--mac-border-dark) var(--mac-border-light) var(--mac-border-light) var(--mac-border-dark);
|
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);
|
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 */
|
/* Tables styled like Mac OS 9 lists */
|
||||||
table {
|
table {
|
||||||
@@ -499,8 +523,15 @@ HTML_TEMPLATE = """<!DOCTYPE html>
|
|||||||
<div class="window-shade"></div>
|
<div class="window-shade"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="window-content">
|
<div class="window-content">
|
||||||
<div class="chart-container">
|
<div class="charts-wrapper">
|
||||||
<canvas id="playtime-chart"></canvas>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -598,7 +629,9 @@ HTML_TEMPLATE = """<!DOCTYPE html>
|
|||||||
});
|
});
|
||||||
|
|
||||||
let chart = null;
|
let chart = null;
|
||||||
|
let categoriesChart = null;
|
||||||
const ctx = document.getElementById('playtime-chart').getContext('2d');
|
const ctx = document.getElementById('playtime-chart').getContext('2d');
|
||||||
|
const ctxCategories = document.getElementById('categories-chart').getContext('2d');
|
||||||
|
|
||||||
function getSelectedServices() {
|
function getSelectedServices() {
|
||||||
const checkboxes = filtersDiv.querySelectorAll('input[type="checkbox"]');
|
const checkboxes = filtersDiv.querySelectorAll('input[type="checkbox"]');
|
||||||
@@ -671,6 +704,9 @@ HTML_TEMPLATE = """<!DOCTYPE html>
|
|||||||
if (chart) {
|
if (chart) {
|
||||||
chart.destroy();
|
chart.destroy();
|
||||||
}
|
}
|
||||||
|
if (categoriesChart) {
|
||||||
|
categoriesChart.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
if (chartData.length === 0) {
|
if (chartData.length === 0) {
|
||||||
document.getElementById('games-table').innerHTML =
|
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');
|
const tbody = document.getElementById('games-table');
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
chartData.forEach((game, index) => {
|
chartData.forEach((game, index) => {
|
||||||
@@ -772,17 +863,64 @@ HTML_TEMPLATE = """<!DOCTYPE html>
|
|||||||
if (categoriesData.length === 0) {
|
if (categoriesData.length === 0) {
|
||||||
catTbody.innerHTML = '<tr><td colspan="4" class="no-data">No categories found</td></tr>';
|
catTbody.innerHTML = '<tr><td colspan="4" class="no-data">No categories found</td></tr>';
|
||||||
} else {
|
} 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 percent = ((cat.playtime / totalPlaytime) * 100).toFixed(1);
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>${index + 1}</td>
|
<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="time">${formatTime(cat.playtime)}</td>
|
||||||
<td class="percent">${percent}%</td>
|
<td class="percent">${percent}%</td>
|
||||||
`;
|
`;
|
||||||
catTbody.appendChild(row);
|
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