fix: preload player rating history to fix first-click chart render (#10) #15
+21
-14
@@ -26,23 +26,30 @@ function applyDeltaPill(pillEl, value) {
|
||||
pillEl.appendChild(numSpan);
|
||||
}
|
||||
|
||||
function setupTooltipsAfterSwap() {
|
||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||
if (event.detail.target.id === 'ratings-table') {
|
||||
initRatingsTooltips();
|
||||
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);
|
||||
}
|
||||
// After player history partial loads, render the chart
|
||||
});
|
||||
}
|
||||
|
||||
function setupAfterTableSwap() {
|
||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||
const target = event.detail.target;
|
||||
if (target.id === 'ratings-table') {
|
||||
initRatingsTooltips();
|
||||
initChartsIn(target);
|
||||
return;
|
||||
}
|
||||
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
@@ -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
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ const { getPlayerFromDB, savePlayerToDB, getRatingHistoryFromDB, saveRatingHisto
|
||||
const { fetchPlayerDataHTTP, parsePlayerData, fetchRatingHistory, parseRatingHistory } = require('../scrapers/player-http');
|
||||
const { getOfficialRatingHistory, getOptimizedPlayerRounds } = require('../scrapers/player-puppeteer');
|
||||
const { launchBrowser } = require('../scrapers/browser');
|
||||
const { getPlayerDataFromDB, scrapePDGARating, getAllRatingsFromDB, refreshAllPlayersInDB, getPredictedRatingFromDB } = require('../services/player-service');
|
||||
const { getPlayerDataFromDB, scrapePDGARating, getAllRatingsFromDB, refreshAllPlayersInDB, getPredictedRatingFromDB, formatDisplayDate } = require('../services/player-service');
|
||||
const { getTopbarLocals } = require('../services/topbar-service');
|
||||
const { calculatePredictedRating } = require('../services/rating-calculator');
|
||||
const logger = require('../logger');
|
||||
@@ -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;
|
||||
@@ -61,7 +63,7 @@ router.get('/partials/player-history/:pdgaNumber', async (req, res) => {
|
||||
const formattedHistory = (history || []).map(row => ({
|
||||
date: row.date,
|
||||
rating: row.rating,
|
||||
displayDate: new Date(row.date).toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' })
|
||||
displayDate: formatDisplayDate(row.date)
|
||||
}));
|
||||
|
||||
const player = await getPlayerDataFromDB(pdgaNumber);
|
||||
@@ -117,11 +119,7 @@ router.get('/api/rating-history/:pdgaNumber', async (req, res) => {
|
||||
const formattedHistory = cachedHistory.map(row => ({
|
||||
date: row.date,
|
||||
rating: row.rating,
|
||||
displayDate: new Date(row.date).toLocaleDateString('en-US', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})
|
||||
displayDate: formatDisplayDate(row.date)
|
||||
}));
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
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');
|
||||
|
||||
function formatDisplayDate(dateStr) {
|
||||
return new Date(dateStr).toLocaleDateString('en-US', {
|
||||
day: '2-digit', month: 'short', year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
// Derives previous-month rating and the delta to it. Prefers PDGA's reported
|
||||
// rating_change (canonical), falls back to our own monthly snapshots when
|
||||
// rating_change is missing — common for players whose latest scrape failed.
|
||||
@@ -167,6 +173,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 +196,12 @@ 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: formatDisplayDate(row.date)
|
||||
}));
|
||||
// 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 +231,8 @@ async function getAllRatingsFromDB(progressCallback = null) {
|
||||
stdDev: null,
|
||||
lastMonthRating: (errorRating != null && errorRatingChange != null) ? errorRating - errorRatingChange : null,
|
||||
deltaPredicted: null,
|
||||
monthlyHistory: []
|
||||
monthlyHistory: [],
|
||||
ratingHistory: []
|
||||
};
|
||||
ratings.push(errorData);
|
||||
|
||||
@@ -344,5 +358,6 @@ module.exports = {
|
||||
getPredictedRatingFromDB,
|
||||
getAllRatingsFromDB,
|
||||
refreshAllPlayersInDB,
|
||||
computeKpis
|
||||
computeKpis,
|
||||
formatDisplayDate
|
||||
};
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
activePage: 'players',
|
||||
cssFiles: ['players.css'],
|
||||
jsFiles: ['tooltips.js', 'chart.js', 'players.js'],
|
||||
initScript: 'setupTooltipsAfterSwap();',
|
||||
initScript: 'setupAfterTableSwap();',
|
||||
body: body,
|
||||
modals: modals
|
||||
}) %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user