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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -259,3 +259,4 @@ flycheck_*.el
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
pga.db
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -79,6 +79,14 @@ OF THIS SOFTWARE.
|
||||
<div class="chart-title">By Category</div>
|
||||
<canvas id="categories-chart"></canvas>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">By Runner</div>
|
||||
<canvas id="runners-chart"></canvas>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">By Source</div>
|
||||
<canvas id="sources-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
<div class="chart-title">By Category</div>
|
||||
<canvas id="categories-chart"></canvas>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">By Runner</div>
|
||||
<canvas id="runners-chart"></canvas>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">By Source</div>
|
||||
<canvas id="sources-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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