fix: preload player rating history to fix first-click chart render (#10) #15

Merged
shcizo merged 2 commits from fix/preload-player-history-10 into main 2026-05-22 12:59:28 +02:00
5 changed files with 69 additions and 18 deletions
Showing only changes of commit a63da6f3ca - Show all commits
+21 -13
View File
@@ -26,23 +26,31 @@ function applyDeltaPill(pillEl, value) {
pillEl.appendChild(numSpan);
}
function initChartsIn(rootEl) {
rootEl.querySelectorAll('.player-chart').forEach(function(container) {
if (container.dataset.charted === 'true') return;
if (!container.dataset.history) return;
try {
const history = JSON.parse(container.dataset.history);
createRatingChart(container, history);
container.dataset.charted = 'true';
} catch (e) {
console.error('Error rendering chart:', e);
}
});
}
function setupTooltipsAfterSwap() {
document.body.addEventListener('htmx:afterSwap', function(event) {
if (event.detail.target.id === 'ratings-table') {
initRatingsTooltips();
}
// After player history partial loads, render the chart
const target = event.detail.target;
if (target.id === 'ratings-table') {
initRatingsTooltips();
initChartsIn(target); // initial table render — chart any pre-loaded .player-chart
return;
}
// refreshRatingHistory still re-fetches into #history-content-<id>
if (target.id && target.id.startsWith('history-content-')) {
const container = target.querySelector('.player-chart, .chart-container');
if (container && container.dataset.history) {
try {
const history = JSON.parse(container.dataset.history);
createRatingChart(container, history);
} catch (e) {
console.error('Error rendering chart:', e);
}
}
initChartsIn(target);
}
});
}
+28 -1
View File
@@ -253,6 +253,32 @@ function getAllMonthlyHistoriesFromDB(months = 12) {
});
}
/**
* Fetches the full rating history for ALL players in one query.
* Returns Map<pdgaNumber, {rating, date}[]> ordered chronologically (oldest → newest).
* Mirrors getAllMonthlyHistoriesFromDB but returns every point, not monthly snapshots.
*/
function getAllRatingHistoriesFromDB() {
return new Promise((resolve, reject) => {
db.all(
`SELECT p.pdga_number, rh.date, rh.rating
FROM rating_history rh
JOIN players p ON rh.player_id = p.id
ORDER BY p.pdga_number, rh.date ASC`,
[],
(err, rows) => {
if (err) return reject(err);
const map = new Map();
for (const row of rows) {
if (!map.has(row.pdga_number)) map.set(row.pdga_number, []);
map.get(row.pdga_number).push({ date: row.date, rating: row.rating });
}
resolve(map);
}
);
});
}
function getLastRefresh() {
return new Promise((resolve, reject) => {
db.get(
@@ -278,5 +304,6 @@ module.exports = {
savePredictedRatingToDB,
getLastRefresh,
getMonthlyHistory,
getAllMonthlyHistoriesFromDB
getAllMonthlyHistoriesFromDB,
getAllRatingHistoriesFromDB
};
+2
View File
@@ -43,6 +43,8 @@ router.get('/partials/ratings-table', async (req, res) => {
}
});
// Used only by the per-player "refresh rating history" button. The initial table render
// pre-attaches history via getAllRatingsFromDB to avoid the load-then-fetch race.
router.get('/partials/player-history/:pdgaNumber', async (req, res) => {
try {
const { pdgaNumber } = req.params;
+12 -2
View File
@@ -1,5 +1,5 @@
const { db } = require('../db');
const { getPlayerFromDB, getRoundHistoryFromDB, savePredictedRatingToDB, savePlayerToDB, getMonthlyHistory, getAllMonthlyHistoriesFromDB } = require('../models/player');
const { getPlayerFromDB, getRoundHistoryFromDB, savePredictedRatingToDB, savePlayerToDB, getMonthlyHistory, getAllMonthlyHistoriesFromDB, getAllRatingHistoriesFromDB } = require('../models/player');
const { fetchPlayerDataHTTP, parsePlayerData } = require('../scrapers/player-http');
const { calculatePredictedRating } = require('./rating-calculator');
const logger = require('../logger');
@@ -167,6 +167,7 @@ async function getAllRatingsFromDB(progressCallback = null) {
// Fetch all monthly histories in one query so the per-player loop doesn't add N extra queries
const monthlyHistoryMap = await getAllMonthlyHistoriesFromDB(12);
const ratingHistoryMap = await getAllRatingHistoriesFromDB();
const ratings = [];
const total = allPlayers.length;
@@ -189,6 +190,14 @@ async function getAllRatingsFromDB(progressCallback = null) {
if (playerData) {
playerData.monthlyHistory = monthlyHistoryMap.get(pdgaNumber) ?? [];
const rawHistory = ratingHistoryMap.get(pdgaNumber) ?? [];
playerData.ratingHistory = rawHistory.map(row => ({
date: row.date,
rating: row.rating,
displayDate: new Date(row.date).toLocaleDateString('en-US', {
day: '2-digit', month: 'short', year: 'numeric'
})
}));
// Re-derive now that history is attached — bulk path skipped includeMonthlyHistory
const derived = deriveMonthlyDeltas(playerData.rating, player.rating_change, playerData.monthlyHistory);
playerData.lastMonthRating = derived.lastMonthRating;
@@ -218,7 +227,8 @@ async function getAllRatingsFromDB(progressCallback = null) {
stdDev: null,
lastMonthRating: (errorRating != null && errorRatingChange != null) ? errorRating - errorRatingChange : null,
deltaPredicted: null,
monthlyHistory: []
monthlyHistory: [],
ratingHistory: []
};
ratings.push(errorData);
+6 -2
View File
@@ -89,8 +89,12 @@ function renderSparkline(values) {
</tr>
<tr id="history-<%= player.pdgaNumber %>" class="expanded-content">
<td colspan="5" class="expanded-cell">
<div id="history-content-<%= player.pdgaNumber %>">
<div class="loading-chart">Click to load rating history...</div>
<div id="history-content-<%= player.pdgaNumber %>" data-loaded="true">
<%- include('player-history', {
pdgaNumber: player.pdgaNumber,
history: player.ratingHistory || [],
player: player
}) %>
</div>
</td>
</tr>