diff --git a/index.html b/index.html
index cda76bf..8cb587a 100644
--- a/index.html
+++ b/index.html
@@ -196,6 +196,25 @@
font-weight: bold;
color: #007bff;
}
+ .std-dev-tooltip {
+ position: absolute;
+ background-color: rgba(0, 0, 0, 0.9);
+ color: white;
+ padding: 8px 12px;
+ border-radius: 4px;
+ font-size: 12px;
+ pointer-events: none;
+ z-index: 10000;
+ display: none;
+ white-space: nowrap;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
+ border: 1px solid rgba(255,255,255,0.2);
+ font-weight: normal;
+ }
+ .predicted-value {
+ position: relative;
+ display: inline-block;
+ }
.pdga-number {
color: #6c757d;
font-size: 14px;
@@ -582,18 +601,20 @@
#${player.pdgaNumber} |
- ${player.rating || 'Click refresh'}
+ ${player.rating || 'Click refresh'}
${ratingChangeText}
+
|
${ratingChangeText} |
- ${player.predictedRating || 'N/A'}
+ ${player.predictedRating || 'N/A'}
+
|
@@ -619,9 +640,63 @@
`;
tableDiv.innerHTML = tableHTML;
+
+ // Add hover listeners for predicted rating standard deviation tooltips
+ document.querySelectorAll('.predicted-value').forEach(span => {
+ const pdgaNumber = span.dataset.pdga;
+ const stdDev = span.dataset.stddev;
+ const tooltip = document.getElementById(`tooltip-stddev-${pdgaNumber}`);
+
+ if (stdDev && tooltip) {
+ span.addEventListener('mouseenter', (e) => {
+ tooltip.textContent = `Standard Deviation: ±${stdDev}`;
+ tooltip.style.display = 'block';
+ tooltip.style.left = `${e.clientX + 15}px`;
+ tooltip.style.top = `${e.clientY - 35}px`;
+ });
+
+ span.addEventListener('mousemove', (e) => {
+ tooltip.style.left = `${e.clientX + 15}px`;
+ tooltip.style.top = `${e.clientY - 35}px`;
+ });
+
+ span.addEventListener('mouseleave', () => {
+ tooltip.style.display = 'none';
+ });
+ }
+ });
+
+ // Add hover listeners for current rating range tooltips
+ document.querySelectorAll('.rating-value').forEach(span => {
+ const pdgaNumber = span.dataset.pdga;
+ const rating = parseInt(span.dataset.rating);
+ const stdDev = parseInt(span.dataset.stddev);
+ const tooltip = document.getElementById(`tooltip-rating-${pdgaNumber}`);
+
+ if (rating && stdDev && tooltip) {
+ const minRating = rating - stdDev;
+ const maxRating = rating + stdDev;
+
+ span.addEventListener('mouseenter', (e) => {
+ tooltip.textContent = `Rating Range: ${minRating} - ${maxRating} (±${stdDev})`;
+ tooltip.style.display = 'block';
+ tooltip.style.left = `${e.clientX + 15}px`;
+ tooltip.style.top = `${e.clientY - 35}px`;
+ });
+
+ span.addEventListener('mousemove', (e) => {
+ tooltip.style.left = `${e.clientX + 15}px`;
+ tooltip.style.top = `${e.clientY - 35}px`;
+ });
+
+ span.addEventListener('mouseleave', () => {
+ tooltip.style.display = 'none';
+ });
+ }
+ });
}
-
-
+
+
async function togglePlayerHistory(pdgaNumber) {
const historyRow = document.getElementById(`history-${pdgaNumber}`);
const chartContainer = document.getElementById(`chart-${pdgaNumber}`);
@@ -846,11 +921,48 @@
const ratingChangeClass = data.player.ratingChange > 0 ? 'positive' :
data.player.ratingChange < 0 ? 'negative' : 'neutral';
- const refreshSection = ratingCell.querySelector('.refresh-section');
- refreshSection.innerHTML = `${data.player.rating || 'N/A'} `;
+ // Update rating value
+ const ratingValue = ratingCell.querySelector('.rating-value');
+ if (ratingValue) {
+ ratingValue.textContent = data.player.rating || 'N/A';
+ ratingValue.dataset.rating = data.player.rating || '';
+ // stdDev stays the same - only updates with predicted rating refresh
+
+ // Re-attach tooltip listeners if we have both rating and stdDev
+ const stdDev = parseInt(ratingValue.dataset.stddev);
+ const rating = parseInt(data.player.rating);
+ const tooltip = document.getElementById(`tooltip-rating-${pdgaNumber}`);
+
+ if (rating && stdDev && tooltip) {
+ // Remove old listeners by cloning and replacing
+ const newRatingValue = ratingValue.cloneNode(true);
+ ratingValue.parentNode.replaceChild(newRatingValue, ratingValue);
+
+ const minRating = rating - stdDev;
+ const maxRating = rating + stdDev;
+
+ // Add new listeners
+ newRatingValue.addEventListener('mouseenter', (e) => {
+ tooltip.textContent = `Rating Range: ${minRating} - ${maxRating} (±${stdDev})`;
+ tooltip.style.display = 'block';
+ tooltip.style.left = `${e.clientX + 15}px`;
+ tooltip.style.top = `${e.clientY - 35}px`;
+ });
+
+ newRatingValue.addEventListener('mousemove', (e) => {
+ tooltip.style.left = `${e.clientX + 15}px`;
+ tooltip.style.top = `${e.clientY - 35}px`;
+ });
+
+ newRatingValue.addEventListener('mouseleave', () => {
+ tooltip.style.display = 'none';
+ });
+ }
+ }
+
if (ratingChangeCell) ratingChangeCell.textContent = ratingChangeText;
if (ratingChangeCell) ratingChangeCell.className = `rating-change ${ratingChangeClass} mobile-hide`;
-
+
// Update mobile rating change
const mobileChange = ratingCell.querySelector('.mobile-only.rating-change');
if (mobileChange) {
@@ -887,13 +999,79 @@
if (data.debugLog) {
cachedDebugInfo[pdgaNumber] = data.debugLog;
}
-
+
// Update predicted rating if the element exists
const predictedCell = document.getElementById(`predicted-${pdgaNumber}`);
if (predictedCell) {
- const refreshSection = predictedCell.querySelector('.refresh-section');
- if (refreshSection && refreshSection.firstChild) {
- refreshSection.firstChild.textContent = data.predictedRating || 'N/A';
+ const predictedValue = predictedCell.querySelector('.predicted-value');
+ if (predictedValue) {
+ predictedValue.textContent = data.predictedRating || 'N/A';
+ // Update data attribute for tooltip
+ predictedValue.dataset.stddev = data.stdDev || '';
+
+ // Re-attach tooltip listeners
+ const tooltip = document.getElementById(`tooltip-stddev-${pdgaNumber}`);
+ if (data.stdDev && tooltip) {
+ // Remove old listeners by cloning and replacing
+ const newPredictedValue = predictedValue.cloneNode(true);
+ predictedValue.parentNode.replaceChild(newPredictedValue, predictedValue);
+
+ // Add new listeners
+ newPredictedValue.addEventListener('mouseenter', (e) => {
+ tooltip.textContent = `Standard Deviation: ±${data.stdDev}`;
+ tooltip.style.display = 'block';
+ tooltip.style.left = `${e.clientX + 15}px`;
+ tooltip.style.top = `${e.clientY - 35}px`;
+ });
+
+ newPredictedValue.addEventListener('mousemove', (e) => {
+ tooltip.style.left = `${e.clientX + 15}px`;
+ tooltip.style.top = `${e.clientY - 35}px`;
+ });
+
+ newPredictedValue.addEventListener('mouseleave', () => {
+ tooltip.style.display = 'none';
+ });
+ }
+ }
+ }
+
+ // Also update the rating value's stddev attribute and tooltip
+ const row = document.getElementById(`row-${pdgaNumber}`);
+ const ratingCell = row.querySelector('.rating');
+ const ratingValue = ratingCell.querySelector('.rating-value');
+ if (ratingValue && data.stdDev) {
+ ratingValue.dataset.stddev = data.stdDev;
+
+ // Re-attach rating tooltip listeners if we have both rating and stdDev
+ const rating = parseInt(ratingValue.dataset.rating);
+ const stdDev = parseInt(data.stdDev);
+ const ratingTooltip = document.getElementById(`tooltip-rating-${pdgaNumber}`);
+
+ if (rating && stdDev && ratingTooltip) {
+ // Remove old listeners by cloning and replacing
+ const newRatingValue = ratingValue.cloneNode(true);
+ ratingValue.parentNode.replaceChild(newRatingValue, ratingValue);
+
+ const minRating = rating - stdDev;
+ const maxRating = rating + stdDev;
+
+ // Add new listeners
+ newRatingValue.addEventListener('mouseenter', (e) => {
+ ratingTooltip.textContent = `Rating Range: ${minRating} - ${maxRating} (±${stdDev})`;
+ ratingTooltip.style.display = 'block';
+ ratingTooltip.style.left = `${e.clientX + 15}px`;
+ ratingTooltip.style.top = `${e.clientY - 35}px`;
+ });
+
+ newRatingValue.addEventListener('mousemove', (e) => {
+ ratingTooltip.style.left = `${e.clientX + 15}px`;
+ ratingTooltip.style.top = `${e.clientY - 35}px`;
+ });
+
+ newRatingValue.addEventListener('mouseleave', () => {
+ ratingTooltip.style.display = 'none';
+ });
}
}
diff --git a/server.js b/server.js
index 02b4efe..e8b4a1c 100644
--- a/server.js
+++ b/server.js
@@ -54,6 +54,7 @@ function initializeDatabase() {
const hasLastRoundUpdate = columns.some(col => col.name === 'last_round_update');
const hasPredictedRating = columns.some(col => col.name === 'predicted_rating');
+ const hasStdDev = columns.some(col => col.name === 'std_dev');
if (!hasLastRoundUpdate) {
console.log('Adding last_round_update column to players table...');
@@ -80,6 +81,19 @@ function initializeDatabase() {
}
});
}
+
+ if (!hasStdDev) {
+ console.log('Adding std_dev column to players table...');
+ db.run(`
+ ALTER TABLE players ADD COLUMN std_dev INTEGER DEFAULT NULL
+ `, (err) => {
+ if (err) {
+ console.error('Error adding std_dev column:', err.message);
+ } else {
+ console.log('Successfully added std_dev column');
+ }
+ });
+ }
});
});
@@ -533,8 +547,12 @@ async function getPlayerDataFromDB(pdgaNumber) {
// Use stored predicted_rating if available, otherwise calculate it from round history
let predictedRating = cachedPlayer.predicted_rating;
+ let stdDev = cachedPlayer.std_dev;
if (!predictedRating || predictedRating === 0) {
predictedRating = await getPredictedRatingFromDB(pdgaNumber);
+ // After calculation, re-fetch to get the updated std_dev
+ const updatedPlayer = await getPlayerFromDB(pdgaNumber);
+ stdDev = updatedPlayer?.std_dev;
}
return {
@@ -542,7 +560,8 @@ async function getPlayerDataFromDB(pdgaNumber) {
name: cachedPlayer.name,
rating: cachedPlayer.current_rating,
ratingChange: cachedPlayer.rating_change,
- predictedRating: predictedRating > 0 ? predictedRating : null
+ predictedRating: predictedRating > 0 ? predictedRating : null,
+ stdDev: stdDev > 0 ? stdDev : null
};
}
return null; // No data in DB
@@ -653,7 +672,7 @@ async function getPredictedRatingFromDB(pdgaNumber) {
const result = calculatePredictedRating(roundRatings);
// Save the calculated prediction to database
- await savePredictedRatingToDB(pdgaNumber, result.rating);
+ await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev);
return result.rating;
}
@@ -664,11 +683,11 @@ async function getPredictedRatingFromDB(pdgaNumber) {
}
}
-function savePredictedRatingToDB(pdgaNumber, predictedRating) {
+function savePredictedRatingToDB(pdgaNumber, predictedRating, stdDev = null) {
return new Promise((resolve, reject) => {
db.run(
- 'UPDATE players SET predicted_rating = ? WHERE pdga_number = ?',
- [predictedRating, pdgaNumber],
+ 'UPDATE players SET predicted_rating = ?, std_dev = ? WHERE pdga_number = ?',
+ [predictedRating, stdDev, pdgaNumber],
function(err) {
if (err) reject(err);
else resolve();
@@ -1483,15 +1502,19 @@ function calculatePredictedRating(roundRatings) {
const sum = weightedRatings.reduce((sum, r) => sum + r, 0);
const average = sum / weightedRatings.length;
const finalRating = Math.round(average);
-
+
+ // Calculate standard deviation of the weighted ratings
+ const stdDev = calculateStandardDeviation(weightedRatings);
+
debugLog.push('🎯 FINAL CALCULATION:');
debugLog.push(` Sum: ${sum}`);
debugLog.push(` Count: ${weightedRatings.length}`);
debugLog.push(` Average: ${average.toFixed(1)}`);
+ debugLog.push(` Standard Deviation: ${stdDev.toFixed(1)}`);
debugLog.push(` Final Rating: ${finalRating}`);
debugLog.push('=== END PDGA CALCULATION ===');
-
- return { rating: finalRating, debugLog };
+
+ return { rating: finalRating, stdDev: Math.round(stdDev), debugLog };
}
function calculateStandardDeviation(ratings) {
@@ -2773,7 +2796,7 @@ app.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => {
const result = calculatePredictedRating(roundsForPrediction);
// Save the predicted rating to database for persistence
- await savePredictedRatingToDB(pdgaNumber, result.rating);
+ await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev);
// Count official vs new rounds
const officialCount = allRounds.filter(r => r.source === 'official').length;
@@ -2782,6 +2805,7 @@ app.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => {
res.json({
success: true,
predictedRating: result.rating,
+ stdDev: result.stdDev,
debugLog: result.debugLog,
totalRounds: roundsForPrediction.length,
officialRounds: officialCount,
@@ -3319,10 +3343,11 @@ app.post('/api/predicted-rating/:pdgaNumber', async (req, res) => {
}));
const result = calculatePredictedRating(roundRatings);
-
- res.json({
+
+ res.json({
pdgaNumber: parseInt(pdgaNumber),
predictedRating: result.rating,
+ stdDev: result.stdDev,
debugLog: result.debugLog
});
} catch (error) {