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
This commit is contained in:
Miguel Astor
2026-03-06 06:19:06 -04:00
parent afd11fba3a
commit c0c25e2719
7 changed files with 333 additions and 17 deletions

View File

@@ -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) => {