fix: preload player rating history to fix first-click chart render (#10)
This commit is contained in:
+21
-13
@@ -26,23 +26,31 @@ function applyDeltaPill(pillEl, value) {
|
|||||||
pillEl.appendChild(numSpan);
|
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() {
|
function setupTooltipsAfterSwap() {
|
||||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
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;
|
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-')) {
|
if (target.id && target.id.startsWith('history-content-')) {
|
||||||
const container = target.querySelector('.player-chart, .chart-container');
|
initChartsIn(target);
|
||||||
if (container && container.dataset.history) {
|
|
||||||
try {
|
|
||||||
const history = JSON.parse(container.dataset.history);
|
|
||||||
createRatingChart(container, history);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error rendering chart:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-1
@@ -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() {
|
function getLastRefresh() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.get(
|
db.get(
|
||||||
@@ -278,5 +304,6 @@ module.exports = {
|
|||||||
savePredictedRatingToDB,
|
savePredictedRatingToDB,
|
||||||
getLastRefresh,
|
getLastRefresh,
|
||||||
getMonthlyHistory,
|
getMonthlyHistory,
|
||||||
getAllMonthlyHistoriesFromDB
|
getAllMonthlyHistoriesFromDB,
|
||||||
|
getAllRatingHistoriesFromDB
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) => {
|
router.get('/partials/player-history/:pdgaNumber', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { pdgaNumber } = req.params;
|
const { pdgaNumber } = req.params;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const { db } = require('../db');
|
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 { fetchPlayerDataHTTP, parsePlayerData } = require('../scrapers/player-http');
|
||||||
const { calculatePredictedRating } = require('./rating-calculator');
|
const { calculatePredictedRating } = require('./rating-calculator');
|
||||||
const logger = require('../logger');
|
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
|
// Fetch all monthly histories in one query so the per-player loop doesn't add N extra queries
|
||||||
const monthlyHistoryMap = await getAllMonthlyHistoriesFromDB(12);
|
const monthlyHistoryMap = await getAllMonthlyHistoriesFromDB(12);
|
||||||
|
const ratingHistoryMap = await getAllRatingHistoriesFromDB();
|
||||||
|
|
||||||
const ratings = [];
|
const ratings = [];
|
||||||
const total = allPlayers.length;
|
const total = allPlayers.length;
|
||||||
@@ -189,6 +190,14 @@ async function getAllRatingsFromDB(progressCallback = null) {
|
|||||||
|
|
||||||
if (playerData) {
|
if (playerData) {
|
||||||
playerData.monthlyHistory = monthlyHistoryMap.get(pdgaNumber) ?? [];
|
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
|
// Re-derive now that history is attached — bulk path skipped includeMonthlyHistory
|
||||||
const derived = deriveMonthlyDeltas(playerData.rating, player.rating_change, playerData.monthlyHistory);
|
const derived = deriveMonthlyDeltas(playerData.rating, player.rating_change, playerData.monthlyHistory);
|
||||||
playerData.lastMonthRating = derived.lastMonthRating;
|
playerData.lastMonthRating = derived.lastMonthRating;
|
||||||
@@ -218,7 +227,8 @@ async function getAllRatingsFromDB(progressCallback = null) {
|
|||||||
stdDev: null,
|
stdDev: null,
|
||||||
lastMonthRating: (errorRating != null && errorRatingChange != null) ? errorRating - errorRatingChange : null,
|
lastMonthRating: (errorRating != null && errorRatingChange != null) ? errorRating - errorRatingChange : null,
|
||||||
deltaPredicted: null,
|
deltaPredicted: null,
|
||||||
monthlyHistory: []
|
monthlyHistory: [],
|
||||||
|
ratingHistory: []
|
||||||
};
|
};
|
||||||
ratings.push(errorData);
|
ratings.push(errorData);
|
||||||
|
|
||||||
|
|||||||
@@ -89,8 +89,12 @@ function renderSparkline(values) {
|
|||||||
</tr>
|
</tr>
|
||||||
<tr id="history-<%= player.pdgaNumber %>" class="expanded-content">
|
<tr id="history-<%= player.pdgaNumber %>" class="expanded-content">
|
||||||
<td colspan="5" class="expanded-cell">
|
<td colspan="5" class="expanded-cell">
|
||||||
<div id="history-content-<%= player.pdgaNumber %>">
|
<div id="history-content-<%= player.pdgaNumber %>" data-loaded="true">
|
||||||
<div class="loading-chart">Click to load rating history...</div>
|
<%- include('player-history', {
|
||||||
|
pdgaNumber: player.pdgaNumber,
|
||||||
|
history: player.ratingHistory || [],
|
||||||
|
player: player
|
||||||
|
}) %>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user