Publication Summary Statistics Implementation Guide
Publication Summary Statistics Implementation Guide
Overview
Add comprehensive summary statistics (total papers, citations, h-index, i10-index) to the publications page while preserving existing tab functionality for filtering All/First Author publications.
Implementation Steps
1. Enhance Python Script (.github/scripts/fetch_publications.py)
Add the following function after the existing imports:
def calculate_summary_stats(publications):
"""Calculate comprehensive publication statistics."""
if not publications:
return {
'total_papers': 0,
'total_citations': 0,
'h_index': 0,
'i10_index': 0,
'first_author_papers': 0,
'avg_citations_per_paper': 0.0,
'last_updated': datetime.now().strftime("%Y-%m-%d")
}
total_papers = len(publications)
total_citations = sum(pub.get('citation_count', 0) for pub in publications)
# H-index calculation
citations = sorted([pub.get('citation_count', 0) for pub in publications], reverse=True)
h_index = 0
for i, citation_count in enumerate(citations):
if citation_count >= i + 1:
h_index = i + 1
else:
break
# i10-index (papers with 10+ citations)
i10_index = sum(1 for pub in publications if pub.get('citation_count', 0) >= 10)
# First author papers - check if Yuan, Sihan appears first
# The author field format from ADS is "LastName, FirstName, ..."
first_author_papers = 0
for pub in publications:
authors = pub.get('authors', '')
if authors:
# Check if the first author is Yuan, S. or Yuan, Sihan
first_author = authors.split(',')[0:2] # Get first two parts
if len(first_author) >= 2:
if first_author[0].strip().lower() == 'yuan' and \
(first_author[1].strip().lower().startswith('s') or
first_author[1].strip().lower().startswith('sihan')):
first_author_papers += 1
# Add a flag to the publication for frontend filtering
pub['is_first_author'] = True
else:
pub['is_first_author'] = False
avg_citations = round(total_citations / total_papers, 1) if total_papers > 0 else 0.0
return {
'total_papers': total_papers,
'total_citations': total_citations,
'h_index': h_index,
'i10_index': i10_index,
'first_author_papers': first_author_papers,
'avg_citations_per_paper': avg_citations,
'last_updated': datetime.now().strftime("%Y-%m-%d")
}
Modify the output data structure in the main() function:
# Calculate stats before saving
summary_stats = calculate_summary_stats(processed_publications)
# Create output directory if it doesn't exist
os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
# Save the result with summary stats
output_data = {
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"summary_stats": summary_stats,
"publications": processed_publications
}
2. Update Publications Page (_pages/publications.md)
Replace the existing content with:
---
layout: archive
title: "Publications"
permalink: /publications/
author_profile: true
---
<!-- start custom head snippets -->
<link rel="apple-touch-icon" sizes="57x57" href="https://sandyyuan.github.io/images/apple-touch-icon-57x57.png?v=M44lzPylqQ">
<link rel="apple-touch-icon" sizes="60x60" href="https://sandyyuan.github.io/images/apple-touch-icon-60x60.png?v=M44lzPylqQ">
<link rel="apple-touch-icon" sizes="72x72" href="https://sandyyuan.github.io/images/apple-touch-icon-72x72.png?v=M44lzPylqQ">
<link rel="apple-touch-icon" sizes="76x76" href="https://sandyyuan.github.io/images/apple-touch-icon-76x76.png?v=M44lzPylqQ">
<link rel="apple-touch-icon" sizes="114x114" href="https://sandyyuan.github.io/images/apple-touch-icon-114x114.png?v=M44lzPylqQ">
<link rel="apple-touch-icon" sizes="120x120" href="https://sandyyuan.github.io/images/apple-touch-icon-120x120.png?v=M44lzPylqQ">
<link rel="apple-touch-icon" sizes="144x144" href="https://sandyyuan.github.io/images/apple-touch-icon-144x144.png?v=M44lzPylqQ">
<link rel="apple-touch-icon" sizes="152x152" href="https://sandyyuan.github.io/images/apple-touch-icon-152x152.png?v=M44lzPylqQ">
<link rel="apple-touch-icon" sizes="180x180" href="https://sandyyuan.github.io/images/apple-touch-icon-180x180.png?v=M44lzPylqQ">
<link rel="icon" type="image/png" href="https://sandyyuan.github.io/images/favicon-32x32.png?v=M44lzPylqQ" sizes="32x32">
<link rel="icon" type="image/png" href="https://sandyyuan.github.io/images/android-chrome-192x192.png?v=M44lzPylqQ" sizes="192x192">
<link rel="icon" type="image/png" href="https://sandyyuan.github.io/images/favicon-96x96.png?v=M44lzPylqQ" sizes="96x96">
<link rel="icon" type="image/png" href="https://sandyyuan.github.io/images/favicon-16x16.png?v=M44lzPylqQ" sizes="16x16">
<link rel="manifest" href="https://sandyyuan.github.io/images/manifest.json?v=M44lzPylqQ">
<link rel="mask-icon" href="https://sandyyuan.github.io/images/safari-pinned-tab.svg?v=M44lzPylqQ" color="#000000">
<link rel="icon" type="image/svg+xml" href="https://sandyyuan.github.io/images/favicon.svg">
<link rel="alternate icon" type="image/png" href="https://sandyyuan.github.io/images/favicon-32x32.png" sizes="32x32">
<link rel="alternate icon" type="image/png" href="https://sandyyuan.github.io/images/favicon-16x16.png" sizes="16x16">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-TileImage" content="https://sandyyuan.github.io/images/mstile-144x144.png?v=M44lzPylqQ">
<meta name="msapplication-config" content="https://sandyyuan.github.io/images/browserconfig.xml?v=M44lzPylqQ">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="https://sandyyuan.github.io/assets/css/academicons.css"/>
<script type="text/x-mathjax-config"> MathJax.Hub.Config({ TeX: { equationNumbers: { autoNumber: "all" } } }); </script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {
inlineMath: [ ['$','$'], ["\\(","\\)"] ],
processEscapes: true
}
});
</script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/latest.js?config=TeX-MML-AM_CHTML' async></script>
<!-- Theme Toggle Script - Inline for immediate loading -->
<script>
(function() {
'use strict';
// Set dark mode as default
const defaultTheme = 'dark';
// Get stored theme or use default
function getStoredTheme() {
return localStorage.getItem('theme') || defaultTheme;
}
// Store theme preference
function setStoredTheme(theme) {
localStorage.setItem('theme', theme);
}
// Apply theme to document
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
}
// Toggle between themes
function toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
applyTheme(newTheme);
setStoredTheme(newTheme);
// Update icon visibility
const sunIcon = document.querySelector('.theme-toggle .sun-icon');
const moonIcon = document.querySelector('.theme-toggle .moon-icon');
if (sunIcon && moonIcon) {
if (newTheme === 'dark') {
sunIcon.style.display = 'block';
moonIcon.style.display = 'none';
} else {
sunIcon.style.display = 'none';
moonIcon.style.display = 'block';
}
}
}
// Create theme toggle button
function createThemeToggle() {
const toggleButton = document.createElement('button');
toggleButton.className = 'theme-toggle';
toggleButton.setAttribute('aria-label', 'Toggle theme');
toggleButton.innerHTML = `
<svg class="sun-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2V4M12 20V22M4 12H2M6.31412 6.31412L4.8999 4.8999M17.6859 6.31412L19.1001 4.8999M6.31412 17.69L4.8999 19.1042M17.6859 17.69L19.1001 19.1042M22 12H20M17 12C17 14.7614 14.7614 17 12 17C9.23858 17 7 14.7614 7 12C7 9.23858 9.23858 7 12 7C14.7614 7 17 9.23858 17 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg class="moon-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.32031 11.6835C3.32031 16.6541 7.34975 20.6835 12.3203 20.6835C16.1075 20.6835 19.3483 18.3443 20.6768 15.032C19.6402 15.4486 18.5059 15.6834 17.3203 15.6834C12.3497 15.6834 8.32031 11.654 8.32031 6.68342C8.32031 5.50338 8.55165 4.36259 8.96453 3.32996C5.65605 4.66028 3.32031 7.89912 3.32031 11.6835Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;
toggleButton.addEventListener('click', toggleTheme);
return toggleButton;
}
// Initialize theme immediately
const storedTheme = getStoredTheme();
applyTheme(storedTheme);
// Add toggle button when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
const toggleButton = createThemeToggle();
document.body.appendChild(toggleButton);
});
} else {
const toggleButton = createThemeToggle();
document.body.appendChild(toggleButton);
}
})();
</script>
<!-- end custom head snippets -->
<style>
/* Make profile avatar rectangular site-wide */
.sidebar .author__avatar img,
.author__avatar img,
img.author__avatar,
.author__avatar {
border-radius: 0 !important;
padding: 0 !important;
border: none !important;
}
/* Restore CV container sizing to original responsive behavior */
.cv-container {
position: relative;
height: 0;
padding-bottom: 170%;
overflow: hidden;
max-width: 100%;
}
.cv-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
@media (min-width: 64em) {
.cv-container { padding-bottom: 190%; }
}
@media (min-width: 48em) and (max-width: 64em) {
.cv-container { padding-bottom: 200%; }
}
@media (max-width: 48em) {
.cv-container { padding-bottom: 220%; }
}
</style>
<link rel="stylesheet" href="/assets/css/publications.css">
<script src="/assets/js/publications-loader.js" type="text/javascript"></script>
<!-- Summary Statistics Section -->
<div id="publication-stats" class="publication-summary" style="display: none;">
<h2>Research Impact</h2>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-number" id="total-papers">-</span>
<span class="stat-label">Publications</span>
</div>
<div class="stat-item">
<span class="stat-number" id="total-citations">-</span>
<span class="stat-label">Citations</span>
</div>
<div class="stat-item">
<span class="stat-number" id="h-index">-</span>
<span class="stat-label">h-index</span>
</div>
<div class="stat-item">
<span class="stat-number" id="i10-index">-</span>
<span class="stat-label">i10-index</span>
</div>
</div>
<div class="stats-secondary">
<span><strong>First author:</strong> <span id="first-author-count">-</span> papers</span>
<span><strong>Average citations:</strong> <span id="avg-citations">-</span> per paper</span>
<span class="last-updated">Updated: <span id="last-updated">-</span></span>
</div>
</div>
Full publication list can be found here: [ADS](https://ui.adsabs.harvard.edu/search/q=orcid%3A0000-0002-5992-7586&sort=date%20desc%2C%20bibcode%20desc&p_=0) and here: [arXiv](https://arxiv.org/search/?query=sihan+yuan&searchtype=all&source=header)
<!-- Tab Navigation -->
<div class="publication-tabs">
<button id="all-tab" class="tab-button active">All Publications</button>
<button id="first-author-tab" class="tab-button">First Author</button>
</div>
<!-- Publications Container -->
<div id="publications-container">
<p>Loading publications...</p>
</div>
<noscript>
<div style="background-color: #dc3545; color: white; padding: 10px; margin-top: 20px; border-radius: 5px;">
<p><strong>JavaScript is disabled or not supported in your browser.</strong></p>
<p>This page requires JavaScript to display publications. Please enable JavaScript or use a different browser.</p>
</div>
</noscript>
3. Update JavaScript (assets/js/publications-loader.js)
Replace or create this file with enhanced functionality:
// Global variables to store publication data
let allPublications = [];
let currentView = 'all'; // 'all' or 'first-author'
// Load and display publications with summary statistics
document.addEventListener('DOMContentLoaded', function() {
loadPublications();
setupTabListeners();
});
function loadPublications() {
fetch('/assets/js/publications.json')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
// Store all publications globally
allPublications = data.publications || [];
// Display summary statistics
displaySummaryStats(data.summary_stats);
// Display publications based on current view
updatePublicationDisplay();
})
.catch(error => {
console.error('Error loading publications:', error);
displayError();
});
}
function displaySummaryStats(stats) {
if (!stats) return;
const statsSection = document.getElementById('publication-stats');
document.getElementById('total-papers').textContent = stats.total_papers || 0;
document.getElementById('total-citations').textContent = stats.total_citations || 0;
document.getElementById('h-index').textContent = stats.h_index || 0;
document.getElementById('i10-index').textContent = stats.i10_index || 0;
document.getElementById('first-author-count').textContent = stats.first_author_papers || 0;
document.getElementById('avg-citations').textContent = stats.avg_citations_per_paper || 0;
document.getElementById('last-updated').textContent = stats.last_updated || 'Unknown';
statsSection.style.display = 'block';
}
function setupTabListeners() {
const allTab = document.getElementById('all-tab');
const firstAuthorTab = document.getElementById('first-author-tab');
if (allTab) {
allTab.addEventListener('click', function() {
currentView = 'all';
allTab.classList.add('active');
firstAuthorTab.classList.remove('active');
updatePublicationDisplay();
});
}
if (firstAuthorTab) {
firstAuthorTab.addEventListener('click', function() {
currentView = 'first-author';
firstAuthorTab.classList.add('active');
allTab.classList.remove('active');
updatePublicationDisplay();
});
}
}
function updatePublicationDisplay() {
let publicationsToShow = allPublications;
// Filter for first author if needed
if (currentView === 'first-author') {
publicationsToShow = allPublications.filter(pub => pub.is_first_author === true);
}
displayPublications(publicationsToShow);
}
function displayPublications(publications) {
const container = document.getElementById('publications-container');
if (!publications || publications.length === 0) {
container.innerHTML = currentView === 'first-author'
? '<p>No first-author publications found.</p>'
: '<p>No publications found.</p>';
return;
}
let html = '<div class="publications-list">';
publications.forEach((pub, index) => {
const firstAuthorBadge = pub.is_first_author ? '<span class="first-author-badge">First Author</span>' : '';
html += `
<div class="publication-item">
<h3 class="publication-title">
${pub.title || 'Untitled'}
${firstAuthorBadge}
</h3>
<p class="publication-authors">${pub.authors || 'No authors listed'}</p>
<p class="publication-venue">${pub.journal_info || 'No venue information'}</p>
<div class="publication-metrics">
${pub.citation_count ? `<span class="metric">Citations: ${pub.citation_count}</span>` : ''}
${pub.read_count ? `<span class="metric">Reads: ${pub.read_count}</span>` : ''}
</div>
<div class="publication-links">
${pub.ads_link ? `<a href="${pub.ads_link}" target="_blank" class="btn btn-small">ADS</a>` : ''}
${pub.arxiv_link ? `<a href="${pub.arxiv_link}" target="_blank" class="btn btn-small">arXiv</a>` : ''}
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
function displayError() {
const container = document.getElementById('publications-container');
container.innerHTML = `
<div class="error-message">
<p><strong>Unable to load publications.</strong></p>
<p>Please try refreshing the page or visit my <a href="https://ui.adsabs.harvard.edu/search/q=orcid%3A0000-0002-5992-7586&sort=date%20desc%2C%20bibcode%20desc&p_=0" target="_blank">ADS profile</a> directly.</p>
</div>
`;
}
4. Enhanced CSS Styling (assets/css/publications.css)
Create or update this file:
/* Publication Statistics Styling */
.publication-summary {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 2rem;
border-radius: 12px;
margin-bottom: 2rem;
border: 1px solid #dee2e6;
}
.publication-summary h2 {
margin-top: 0;
margin-bottom: 1.5rem;
color: #2c3e50;
text-align: center;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.stat-item {
text-align: center;
padding: 1rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stat-number {
display: block;
font-size: 2.2rem;
font-weight: bold;
color: #3498db;
line-height: 1;
margin-bottom: 0.5rem;
}
.stat-label {
display: block;
font-size: 0.85rem;
color: #7f8c8d;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 500;
}
.stats-secondary {
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
font-size: 0.9rem;
color: #6c757d;
}
.last-updated {
font-style: italic;
}
/* Tab Navigation */
.publication-tabs {
display: flex;
gap: 0.5rem;
margin: 2rem 0 1rem 0;
border-bottom: 2px solid #e9ecef;
}
.tab-button {
padding: 0.75rem 1.5rem;
background: transparent;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
color: #6c757d;
transition: all 0.3s ease;
margin-bottom: -2px;
}
.tab-button:hover {
color: #3498db;
}
.tab-button.active {
color: #3498db;
border-bottom-color: #3498db;
}
/* Publications List Styling */
.publications-list {
margin-top: 1.5rem;
}
.publication-item {
background: white;
padding: 1.5rem;
margin-bottom: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-left: 4px solid #3498db;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.publication-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.publication-title {
margin: 0 0 0.5rem 0;
color: #2c3e50;
font-size: 1.1rem;
line-height: 1.4;
display: flex;
align-items: center;
gap: 0.5rem;
}
.first-author-badge {
display: inline-block;
background: #27ae60;
color: white;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.publication-authors {
margin: 0 0 0.5rem 0;
color: #7f8c8d;
font-size: 0.9rem;
}
.publication-venue {
margin: 0 0 1rem 0;
color: #6c757d;
font-size: 0.9rem;
font-style: italic;
}
.publication-metrics {
margin-bottom: 1rem;
}
.metric {
display: inline-block;
background: #e9ecef;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
margin-right: 0.5rem;
color: #495057;
}
.publication-links {
display: flex;
gap: 0.5rem;
}
.btn {
padding: 0.5rem 1rem;
text-decoration: none;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 500;
transition: all 0.2s ease;
}
.btn-small {
background: #3498db;
color: white;
}
.btn-small:hover {
background: #2980b9;
transform: translateY(-1px);
}
.error-message {
background: #f8d7da;
color: #721c24;
padding: 1rem;
border-radius: 4px;
border: 1px solid #f5c6cb;
}
/* Responsive Design */
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.stats-secondary {
flex-direction: column;
gap: 0.5rem;
text-align: center;
}
.publication-summary {
padding: 1.5rem;
}
.tab-button {
padding: 0.6rem 1rem;
font-size: 0.9rem;
}
.publication-item {
padding: 1rem;
}
.publication-title {
font-size: 1rem;
flex-wrap: wrap;
}
}
Testing Checklist
- Python Script Testing:
- Run the script locally with test data
- Verify the
is_first_author
flag is correctly set - Check that all statistics are calculated correctly
- Ensure backward compatibility with existing JSON structure
- GitHub Action Testing:
- Run the GitHub Action manually
- Verify
assets/js/publications.json
containssummary_stats
object - Check that all publications have the
is_first_author
flag
- Frontend Testing:
- Statistics display correctly
- Tab switching works properly
- First author filtering is accurate
- Responsive design works on mobile
- Error handling displays appropriate messages
Error Handling
The implementation includes:
- Graceful handling of missing or invalid data
- Fallback values for all statistics (0 or empty)
- Clear error messages for users
- Console logging for debugging
- Preservation of existing functionality if new features fail
Important Notes
- First Author Detection: The logic checks for “Yuan, Sihan” or “Yuan, S.” as the first author, accounting for ADS formatting
- Backward Compatibility: The implementation preserves existing tab functionality
- Statistics Source: All metrics come from ADS citation data
- Update Frequency: Statistics refresh with the weekly GitHub Action
- Performance: The JavaScript loads data once and filters in memory for better performance
Migration Path
- First, test the Python script changes locally
- Deploy CSS changes (non-breaking)
- Deploy JavaScript with backward compatibility
- Deploy Python script changes
- Run GitHub Action to generate new data structure
- Deploy the updated publications page
This ensures zero downtime and maintains functionality throughout the deployment.