diff --git a/public/js/players.js b/public/js/players.js index 78503b4..563434c 100644 --- a/public/js/players.js +++ b/public/js/players.js @@ -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- 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); } }); } diff --git a/src/models/player.js b/src/models/player.js index 5460378..979d388 100644 --- a/src/models/player.js +++ b/src/models/player.js @@ -253,6 +253,32 @@ function getAllMonthlyHistoriesFromDB(months = 12) { }); } +/** + * Fetches the full rating history for ALL players in one query. + * Returns Map 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 }; diff --git a/src/routes/players.js b/src/routes/players.js index 4d7ecc4..e09a85c 100644 --- a/src/routes/players.js +++ b/src/routes/players.js @@ -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; diff --git a/src/services/player-service.js b/src/services/player-service.js index eb5e4dd..e7efe7e 100644 --- a/src/services/player-service.js +++ b/src/services/player-service.js @@ -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); diff --git a/views/partials/ratings-table.ejs b/views/partials/ratings-table.ejs index 5fc0783..4fc19f4 100644 --- a/views/partials/ratings-table.ejs +++ b/views/partials/ratings-table.ejs @@ -89,8 +89,12 @@ function renderSparkline(values) { -
-
Click to load rating history...
+
+ <%- include('player-history', { + pdgaNumber: player.pdgaNumber, + history: player.ratingHistory || [], + player: player + }) %>