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:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user