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

  1. 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
  2. GitHub Action Testing:
    • Run the GitHub Action manually
    • Verify assets/js/publications.json contains summary_stats object
    • Check that all publications have the is_first_author flag
  3. 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

  1. First Author Detection: The logic checks for “Yuan, Sihan” or “Yuan, S.” as the first author, accounting for ADS formatting
  2. Backward Compatibility: The implementation preserves existing tab functionality
  3. Statistics Source: All metrics come from ADS citation data
  4. Update Frequency: Statistics refresh with the weekly GitHub Action
  5. Performance: The JavaScript loads data once and filters in memory for better performance

Migration Path

  1. First, test the Python script changes locally
  2. Deploy CSS changes (non-breaking)
  3. Deploy JavaScript with backward compatibility
  4. Deploy Python script changes
  5. Run GitHub Action to generate new data structure
  6. Deploy the updated publications page

This ensures zero downtime and maintains functionality throughout the deployment.