diff --git a/.gitignore b/.gitignore
index db8d585..2f60589 100644
--- a/.gitignore
+++ b/.gitignore
@@ -259,3 +259,4 @@ flycheck_*.el
# Built Visual Studio Code Extensions
*.vsix
+pga.db
diff --git a/templates/brutalism.css b/templates/brutalism.css
index e64c371..b2039b9 100644
--- a/templates/brutalism.css
+++ b/templates/brutalism.css
@@ -286,14 +286,15 @@ body {
/* Charts */
.charts-wrapper {
- display: flex;
- gap: 20px;
+ display: grid;
+ grid-auto-columns: minmax(280px, 450px);
+ grid-template-columns: repeat(auto-fill, minmax(280px, 450px));
+ grid-gap: 24px;
justify-content: center;
flex-wrap: wrap;
}
.chart-container {
- flex: 1;
min-width: 280px;
max-width: 450px;
padding: 16px;
diff --git a/templates/glassmorphism.css b/templates/glassmorphism.css
index b2494db..c76c97d 100644
--- a/templates/glassmorphism.css
+++ b/templates/glassmorphism.css
@@ -274,14 +274,14 @@ body {
/* Charts */
.charts-wrapper {
- display: flex;
- gap: 24px;
+ display: grid;
+ grid-auto-columns: minmax(280px, 450px);
+ grid-template-columns: repeat(auto-fill, minmax(280px, 450px));
+ grid-gap: 24px;
justify-content: center;
- flex-wrap: wrap;
}
.chart-container {
- flex: 1;
min-width: 280px;
max-width: 450px;
padding: 16px;
diff --git a/templates/modern.html b/templates/modern.html
index 9165db9..502b458 100644
--- a/templates/modern.html
+++ b/templates/modern.html
@@ -79,6 +79,14 @@ OF THIS SOFTWARE.
By Category
+
+
diff --git a/templates/neumorphism.css b/templates/neumorphism.css
index 3fe93e9..62dcbc7 100644
--- a/templates/neumorphism.css
+++ b/templates/neumorphism.css
@@ -294,14 +294,15 @@ body {
/* Charts */
.charts-wrapper {
- display: flex;
- gap: 24px;
+ display: grid;
+ grid-auto-columns: minmax(280px, 450px);
+ grid-template-columns: repeat(auto-fill, minmax(280px, 450px));
+ grid-gap: 24px;
justify-content: center;
flex-wrap: wrap;
}
.chart-container {
- flex: 1;
min-width: 280px;
max-width: 450px;
padding: 20px;
diff --git a/templates/platinum.html b/templates/platinum.html
index d058c99..0d33818 100644
--- a/templates/platinum.html
+++ b/templates/platinum.html
@@ -217,14 +217,15 @@ OF THIS SOFTWARE.
/* Chart containers */
.charts-wrapper {
- display: flex;
- gap: 20px;
+ display: grid;
+ grid-auto-columns: minmax(280px, 450px);
+ grid-template-columns: repeat(auto-fill, minmax(280px, 450px));
+ grid-gap: 20px;
justify-content: center;
flex-wrap: wrap;
}
.chart-container {
position: relative;
- flex: 1;
min-width: 280px;
max-width: 450px;
padding: 10px;
@@ -580,6 +581,14 @@ OF THIS SOFTWARE.
By Category
+
+
@@ -694,8 +703,12 @@ OF THIS SOFTWARE.
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');
function getSelectedServices() {
const checkboxes = filtersDiv.querySelectorAll('input[type="checkbox"]');
@@ -769,12 +782,24 @@ OF THIS SOFTWARE.
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 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);
@@ -785,6 +810,12 @@ OF THIS SOFTWARE.
if (categoriesChart) {
categoriesChart.destroy();
}
+ if (runnersChart) {
+ runnersChart.destroy();
+ }
+ if (sourcesChart) {
+ sourcesChart.destroy();
+ }
if (chartData.length === 0) {
document.getElementById('games-table').innerHTML =
@@ -906,6 +937,116 @@ OF THIS SOFTWARE.
});
}
+ // 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: colors.slice(0, runnersChartData.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 + '%)';
+ }
+ }
+ }
+ }
+ }
+ });
+ }
+
+ // 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: colors.slice(0, sourcesChartData.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) => {
diff --git a/templates/script.js b/templates/script.js
index 3c04cb5..b1030a1 100644
--- a/templates/script.js
+++ b/templates/script.js
@@ -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) => {