diff --git a/frontend/src/pages/search.js b/frontend/src/pages/search.js index eb868f9..5498607 100644 --- a/frontend/src/pages/search.js +++ b/frontend/src/pages/search.js @@ -96,13 +96,25 @@ const SearchPageLight = ({ darkMode = true }) => { const searchParam = urlParams.get('search'); const yearParam = urlParams.get('publication_year'); const institutionIdParam = urlParams.get('institution_id'); + const authorIdParams = urlParams.getAll('author_id'); + const institutionIdParams = urlParams.getAll('institution_id'); + const publicationTypeParams = urlParams.getAll('publication_type'); + const journalIdParams = urlParams.getAll('journal_id'); + const startYearParam = urlParams.get('start_year'); + const endYearParam = urlParams.get('end_year'); // Check if we have any chart-related parameters (coming from graph click) - const hasChartParams = searchParam || yearParam || institutionIdParam; + const hasChartParams = searchParam || yearParam || institutionIdParam || authorIdParams.length > 0 || + institutionIdParams.length > 0 || publicationTypeParams.length > 0 || + journalIdParams.length > 0 || startYearParam || endYearParam; if (hasChartParams) { // Has URL parameters - came from graph click, auto-search - console.log('Graph navigation detected - auto-searching with params:', { searchParam, yearParam, institutionIdParam }); + console.log('Graph navigation detected - auto-searching with params:', { + searchParam, yearParam, institutionIdParam, authorIdParams, + institutionIdParams, publicationTypeParams, journalIdParams, + startYearParam, endYearParam + }); // Set search keyword if provided if (searchParam) { @@ -114,15 +126,91 @@ const SearchPageLight = ({ darkMode = true }) => { setPublicationYear(yearParam); } - // Handle institution if provided - if (institutionIdParam) { - fetchInstitutionById(institutionIdParam); + // Set year range if provided + if (startYearParam) { + setStartYear(startYearParam); + } + if (endYearParam) { + setEndYear(endYearParam); + } + + // Handle authors if provided + if (authorIdParams.length > 0) { + console.log('Setting author details for:', authorIdParams); + + // Get author names from URL parameters if available + const authorNameParams = urlParams.getAll('author_name'); + console.log('Author name params received:', authorNameParams); + + // Create author objects with real names if available, otherwise use IDs + const authors = authorIdParams.map((id, index) => { + const displayName = authorNameParams[index] || `Author ${id}`; + console.log(`Author ${id}: using display name "${displayName}"`); + return { + id: `A${id}`, + display_name: displayName + }; + }); + + console.log('Setting selected authors:', authors); + setSelectedAuthors(authors); + } + + // Handle institutions if provided + if (institutionIdParams.length > 0) { + console.log('Setting institution details for:', institutionIdParams); + + // Get institution names from URL parameters if available + const institutionNameParams = urlParams.getAll('institution_name'); + console.log('Institution name params received:', institutionNameParams); + + // Create institution objects with real names if available, otherwise use IDs + const institutions = institutionIdParams.map((id, index) => { + const displayName = institutionNameParams[index] || `Institution ${id}`; + console.log(`Institution ${id}: using display name "${displayName}"`); + return { + id: `I${id}`, + display_name: displayName + }; + }); + + console.log('Setting selected institutions:', institutions); + setSelectedInstitutions(institutions); + } + + // Handle publication types if provided + if (publicationTypeParams.length > 0) { + const selectedTypes = publicationTypes.filter(pt => + publicationTypeParams.includes(pt.id) + ); + setSelectedPublicationTypes(selectedTypes); + } + + // Handle journals if provided + if (journalIdParams.length > 0) { + // Fetch journal details for each journal ID + Promise.all(journalIdParams.map(async (journalId) => { + try { + const response = await fetch(`${OPENALEX_API_BASE}/sources/S${journalId}`); + if (response.ok) { + const journalData = await response.json(); + return { id: journalData.id, display_name: journalData.display_name }; + } + } catch (error) { + console.error('Failed to fetch journal details:', error); + } + })).then(journals => { + const validJournals = journals.filter(journal => journal); + setSelectedJournals(validJournals); + }); } // Auto-search with URL parameters setTimeout(() => { - performAutoSearchWithParams(searchParam, yearParam, institutionIdParam); - }, 200); + performAutoSearchWithParams(searchParam, yearParam, institutionIdParam, authorIdParams, + institutionIdParams, publicationTypeParams, journalIdParams, + startYearParam, endYearParam); + }, 1000); // Increased timeout to allow for author/institution fetching } else { // No URL parameters - any other navigation, just clear fields console.log('Direct navigation - clearing fields, no auto-search'); @@ -145,8 +233,14 @@ const SearchPageLight = ({ darkMode = true }) => { }; // Separate function for auto-search with URL parameters - const performAutoSearchWithParams = async (searchParam, yearParam, institutionIdParam, page = 1) => { - console.log('performAutoSearchWithParams called with:', { searchParam, yearParam, institutionIdParam, page }); + const performAutoSearchWithParams = async (searchParam, yearParam, institutionIdParam, authorIdParams, + institutionIdParams, publicationTypeParams, journalIdParams, + startYearParam, endYearParam, page = 1) => { + console.log('performAutoSearchWithParams called with:', { + searchParam, yearParam, institutionIdParam, authorIdParams, + institutionIdParams, publicationTypeParams, journalIdParams, + startYearParam, endYearParam, page + }); setLoading(true); setError(null); @@ -161,7 +255,6 @@ const SearchPageLight = ({ darkMode = true }) => { // Use search parameter directly from URL if (searchParam && searchParam.trim()) { const keyword = searchParam.trim(); - // Format: title_and_abstract.search:keyword (spaces become + in URL) filters.push(`title_and_abstract.search:${keyword}`); } @@ -170,9 +263,33 @@ const SearchPageLight = ({ darkMode = true }) => { filters.push(`publication_year:${yearParam.trim()}`); } - // Use institution parameter directly from URL - if (institutionIdParam && institutionIdParam.trim()) { - filters.push(`authorships.institutions.id:I${institutionIdParam.trim()}`); + // Use year range parameters directly from URL + if (startYearParam && endYearParam && startYearParam.trim() && endYearParam.trim()) { + filters.push(`publication_year:${startYearParam.trim()}-${endYearParam.trim()}`); + } + + // Use institution parameters directly from URL + if (institutionIdParams && institutionIdParams.length > 0) { + const institutionFilters = institutionIdParams.map(id => `authorships.institutions.id:I${id.trim()}`); + filters.push(institutionFilters.join('|')); + } + + // Use author parameters directly from URL + if (authorIdParams && authorIdParams.length > 0) { + const authorFilters = authorIdParams.map(id => `authorships.author.id:A${id.trim()}`); + filters.push(authorFilters.join('|')); + } + + // Use publication type parameters directly from URL + if (publicationTypeParams && publicationTypeParams.length > 0) { + const typeFilters = publicationTypeParams.map(type => `type:${type}`); + filters.push(typeFilters.join('|')); + } + + // Use journal parameters directly from URL + if (journalIdParams && journalIdParams.length > 0) { + const journalFilters = journalIdParams.map(id => `primary_location.source.id:S${id.trim()}`); + filters.push(journalFilters.join('|')); } const filterString = filters.join(','); @@ -182,20 +299,35 @@ const SearchPageLight = ({ darkMode = true }) => { params.append('page', page.toString()); params.append('sort', 'cited_by_count:desc'); - const finalUrl = `${OPENALEX_API_BASE}/works?${params.toString()}`; - console.log('Auto-search URL:', finalUrl); - console.log('Search filters:', filters); - console.log('URL matches format: https://openalex.org/works?page=X&filter=...&sort=cited_by_count:desc'); + const url = `${OPENALEX_API_BASE}/works?${params.toString()}`; // Track API call for disclaimer - setApiCalls([finalUrl]); + setApiCalls([url]); - const url = `${OPENALEX_API_BASE}/works?${params.toString()}`; const response = await fetch(url); if (!response.ok) throw new Error('Failed to fetch search results'); const data = await response.json(); - setResults(data.results || []); + // Deduplicate results based on work ID and title to prevent duplicates + const uniqueResults = []; + const seenIds = new Set(); + const seenTitles = new Set(); + + if (data.results && Array.isArray(data.results)) { + data.results.forEach(result => { + const title = result.title || result.display_name || ''; + const normalizedTitle = title.toLowerCase().trim(); + + // Check both ID and title for duplicates + if (result.id && !seenIds.has(result.id) && !seenTitles.has(normalizedTitle)) { + seenIds.add(result.id); + seenTitles.add(normalizedTitle); + uniqueResults.push(result); + } + }); + } + + setResults(uniqueResults); setTotalResults(data.meta?.count || 0); setTotalPages(Math.ceil((data.meta?.count || 0) / resultsPerPage)); setCurrentPage(page); @@ -293,7 +425,26 @@ const SearchPageLight = ({ darkMode = true }) => { if (!response.ok) throw new Error('Failed to fetch search results'); const data = await response.json(); - setResults(data.results || []); + // Deduplicate results based on work ID and title to prevent duplicates + const uniqueResults = []; + const seenIds = new Set(); + const seenTitles = new Set(); + + if (data.results && Array.isArray(data.results)) { + data.results.forEach(result => { + const title = result.title || result.display_name || ''; + const normalizedTitle = title.toLowerCase().trim(); + + // Check both ID and title for duplicates + if (result.id && !seenIds.has(result.id) && !seenTitles.has(normalizedTitle)) { + seenIds.add(result.id); + seenTitles.add(normalizedTitle); + uniqueResults.push(result); + } + }); + } + + setResults(uniqueResults); setTotalResults(data.meta?.count || 0); setTotalPages(Math.ceil((data.meta?.count || 0) / resultsPerPage)); setCurrentPage(page); @@ -318,10 +469,20 @@ const SearchPageLight = ({ darkMode = true }) => { const searchParam = urlParams.get('search'); const yearParam = urlParams.get('publication_year'); const institutionIdParam = urlParams.get('institution_id'); - - if (searchParam || yearParam || institutionIdParam) { + const authorIdParams = urlParams.getAll('author_id'); + const institutionIdParams = urlParams.getAll('institution_id'); + const publicationTypeParams = urlParams.getAll('publication_type'); + const journalIdParams = urlParams.getAll('journal_id'); + const startYearParam = urlParams.get('start_year'); + const endYearParam = urlParams.get('end_year'); + + if (searchParam || yearParam || institutionIdParam || authorIdParams.length > 0 || + institutionIdParams.length > 0 || publicationTypeParams.length > 0 || + journalIdParams.length > 0 || startYearParam || endYearParam) { // Use auto-search with parameters - performAutoSearchWithParams(searchParam, yearParam, institutionIdParam, newPage); + performAutoSearchWithParams(searchParam, yearParam, institutionIdParam, authorIdParams, + institutionIdParams, publicationTypeParams, journalIdParams, + startYearParam, endYearParam, newPage); } else { // Use regular search handleSearch(newPage); diff --git a/frontend/src/pages/trend_graphs.js b/frontend/src/pages/trend_graphs.js index 203da1a..239a9cb 100644 --- a/frontend/src/pages/trend_graphs.js +++ b/frontend/src/pages/trend_graphs.js @@ -150,23 +150,32 @@ export const PositionDetailLight = ({ darkMode = true }) => { filters.push(`title_and_abstract.search:${keyword}`); } - // Add author filters (AND logic) + // Add author filters (OR logic for multiple authors) if (selectedAuthors.length > 0) { - selectedAuthors.forEach(author => { + const authorFilters = selectedAuthors.map(author => { if (author.id) { const authorId = author.id.split('/').pop(); - filters.push(`authorships.author.id:A${authorId}`); + return `authorships.author.id:A${authorId}`; } - }); + return null; + }).filter(Boolean); + if (authorFilters.length > 0) { + filters.push(authorFilters.join('|')); + } } - // Add institution filters (AND logic) + + // Add institution filters (OR logic for multiple institutions) if (selectedInstitutions.length > 0) { - selectedInstitutions.forEach(inst => { + const institutionFilters = selectedInstitutions.map(inst => { if (inst.id) { const instId = inst.id.split('/').pop(); - filters.push(`authorships.institutions.id:I${instId}`); + return `authorships.institutions.id:I${instId}`; } - }); + return null; + }).filter(Boolean); + if (institutionFilters.length > 0) { + filters.push(institutionFilters.join('|')); + } } // Add publication types filter @@ -197,81 +206,170 @@ export const PositionDetailLight = ({ darkMode = true }) => { const filterString = filters.join(','); const params = new URLSearchParams(); if (filterString) params.append('filter', filterString); - params.append('group_by', 'publication_year'); - params.append('per_page', '200'); - - const apiUrl = `https://api.openalex.org/works?${params.toString()}`; - - // Track API call for disclaimer - setApiCalls([apiUrl]); - - const response = await fetch(apiUrl); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.details || `HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - - // Check if we have any results - if (!data.group_by || data.group_by.length === 0) { - setError("No publications found for the given keyword and filters."); - return; - } - - // Process group_by data to create trend analysis - const yearlyDistribution = {}; - let totalPublications = 0; - // Process group_by results - data.group_by.forEach(group => { - const year = group.key; - const count = group.count; - if (year && count > 0) { - yearlyDistribution[year] = count; - totalPublications += count; + // Use different API approach based on whether authors are selected + if (selectedAuthors.length > 0) { + // When authors are selected, fetch individual papers and process them + params.append('per_page', '200'); // Get more papers for better trend analysis + params.append('sort', 'cited_by_count:desc'); + + const apiUrl = `https://api.openalex.org/works?${params.toString()}`; + setApiCalls([apiUrl]); + + const response = await fetch(apiUrl); + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.details || `HTTP error! status: ${response.status}`); } - }); - - // Calculate growth rates - const years = Object.keys(yearlyDistribution).sort((a, b) => parseInt(a) - parseInt(b)); - const yearOverYearGrowth = []; - - for (let i = 1; i < years.length; i++) { - const currentYear = parseInt(years[i]); - const previousYear = parseInt(years[i - 1]); - const currentCount = yearlyDistribution[currentYear]; - const previousCount = yearlyDistribution[previousYear]; - if (previousCount > 0) { - const growthRate = ((currentCount - previousCount) / previousCount) * 100; - yearOverYearGrowth.push({ - year: currentYear, - rate: Math.round(growthRate * 100) / 100 + const data = await response.json(); + + // Deduplicate results based on work ID and title + const uniqueResults = []; + const seenIds = new Set(); + const seenTitles = new Set(); + + if (data.results && Array.isArray(data.results)) { + data.results.forEach(result => { + const title = result.title || result.display_name || ''; + const normalizedTitle = title.toLowerCase().trim(); + + // Check both ID and title for duplicates + if (result.id && !seenIds.has(result.id) && !seenTitles.has(normalizedTitle)) { + seenIds.add(result.id); + seenTitles.add(normalizedTitle); + uniqueResults.push(result); + } }); } - } - - // Calculate average growth rate - const averageGrowthRate = yearOverYearGrowth.length > 0 - ? Math.round((yearOverYearGrowth.reduce((sum, item) => sum + item.rate, 0) / yearOverYearGrowth.length) * 100) / 100 - : 0; - - // Create trend data structure - const trendData = { - publication_count: totalPublications, - yearly_distribution: yearlyDistribution, - meta: { - trend_factors: { - average_growth_rate: averageGrowthRate, - year_over_year_growth: yearOverYearGrowth, - relative_popularity: Math.round((totalPublications / 1000) * 100) / 100 // Simple popularity metric + + // Process individual papers to create trend data + const yearlyDistribution = {}; + let totalPublications = 0; + + uniqueResults.forEach(paper => { + const year = paper.publication_year; + if (year) { + yearlyDistribution[year] = (yearlyDistribution[year] || 0) + 1; + totalPublications++; + } + }); + + // Calculate growth rates + const years = Object.keys(yearlyDistribution).sort((a, b) => parseInt(a) - parseInt(b)); + const yearOverYearGrowth = []; + + for (let i = 1; i < years.length; i++) { + const currentYear = parseInt(years[i]); + const previousYear = parseInt(years[i - 1]); + const currentCount = yearlyDistribution[currentYear]; + const previousCount = yearlyDistribution[previousYear]; + + if (previousCount > 0) { + const growthRate = ((currentCount - previousCount) / previousCount) * 100; + yearOverYearGrowth.push({ + year: currentYear, + rate: Math.round(growthRate * 100) / 100 + }); } } - }; - - setTrendData(trendData); + + // Calculate average growth rate + const averageGrowthRate = yearOverYearGrowth.length > 0 + ? Math.round((yearOverYearGrowth.reduce((sum, item) => sum + item.rate, 0) / yearOverYearGrowth.length) * 100) / 100 + : 0; + + // Create trend data structure + const trendData = { + publication_count: totalPublications, + yearly_distribution: yearlyDistribution, + meta: { + trend_factors: { + average_growth_rate: averageGrowthRate, + year_over_year_growth: yearOverYearGrowth, + relative_popularity: Math.round((totalPublications / 1000) * 100) / 100 + } + } + }; + + setTrendData(trendData); + + } else { + // When no authors are selected, use the original group_by approach + params.append('group_by', 'publication_year'); + params.append('per_page', '200'); + + const apiUrl = `https://api.openalex.org/works?${params.toString()}`; + setApiCalls([apiUrl]); + + const response = await fetch(apiUrl); + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.details || `HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + // Check if we have any results + if (!data.group_by || data.group_by.length === 0) { + setError("No publications found for the given keyword and filters."); + return; + } + + // Process group_by data to create trend analysis + const yearlyDistribution = {}; + let totalPublications = 0; + + // Process group_by results + data.group_by.forEach(group => { + const year = group.key; + const count = group.count; + if (year && count > 0) { + yearlyDistribution[year] = count; + totalPublications += count; + } + }); + + // Calculate growth rates + const years = Object.keys(yearlyDistribution).sort((a, b) => parseInt(a) - parseInt(b)); + const yearOverYearGrowth = []; + + for (let i = 1; i < years.length; i++) { + const currentYear = parseInt(years[i]); + const previousYear = parseInt(years[i - 1]); + const currentCount = yearlyDistribution[currentYear]; + const previousCount = yearlyDistribution[previousYear]; + + if (previousCount > 0) { + const growthRate = ((currentCount - previousCount) / previousCount) * 100; + yearOverYearGrowth.push({ + year: currentYear, + rate: Math.round(growthRate * 100) / 100 + }); + } + } + + // Calculate average growth rate + const averageGrowthRate = yearOverYearGrowth.length > 0 + ? Math.round((yearOverYearGrowth.reduce((sum, item) => sum + item.rate, 0) / yearOverYearGrowth.length) * 100) / 100 + : 0; + + // Create trend data structure + const trendData = { + publication_count: totalPublications, + yearly_distribution: yearlyDistribution, + meta: { + trend_factors: { + average_growth_rate: averageGrowthRate, + year_over_year_growth: yearOverYearGrowth, + relative_popularity: Math.round((totalPublications / 1000) * 100) / 100 + } + } + }; + + setTrendData(trendData); + } + } catch (e) { setError("Failed to fetch publication trend. Please try again with a different keyword or date range."); console.error("Keyword trend fetch error:", e); @@ -314,11 +412,66 @@ export const PositionDetailLight = ({ darkMode = true }) => { // Add keyword search searchParams.append('search', searchKeyword.trim()); - // Add year filter + // Add year filter (override any existing year range with the clicked year) searchParams.append('publication_year', data.year); - // Navigate to search page with filters - navigate(`/search?${searchParams.toString()}`); + // Add selected authors + if (selectedAuthors.length > 0) { + selectedAuthors.forEach(author => { + if (author.id) { + const authorId = author.id.split('/').pop(); + searchParams.append('author_id', authorId); + // Also pass the display name as a string + if (author.display_name) { + searchParams.append('author_name', author.display_name); + console.log('Adding author to URL:', authorId, author.display_name); + } + } + }); + } + + // Add selected institutions + if (selectedInstitutions.length > 0) { + selectedInstitutions.forEach(institution => { + if (institution.id) { + const institutionId = institution.id.split('/').pop(); + searchParams.append('institution_id', institutionId); + // Also pass the display name as a string + if (institution.display_name) { + searchParams.append('institution_name', institution.display_name); + console.log('Adding institution to URL:', institutionId, institution.display_name); + } + } + }); + } + + // Add publication types + if (selectedPublicationTypes.length > 0) { + selectedPublicationTypes.forEach(type => { + searchParams.append('publication_type', type.id); + }); + } + + // Add journals + if (selectedJournals.length > 0) { + selectedJournals.forEach(journal => { + if (journal.id) { + const journalId = journal.id.split('/').pop(); + searchParams.append('journal_id', journalId); + } + }); + } + + // Add year range (if specified, will be overridden by the clicked year) + if (startYear.trim() && endYear.trim()) { + searchParams.append('start_year', startYear.trim()); + searchParams.append('end_year', endYear.trim()); + } + + // Navigate to search page with all filters + const finalUrl = `/search?${searchParams.toString()}`; + console.log('Navigating to search page with URL:', finalUrl); + navigate(finalUrl); }; const renderTrendIndicators = () => { diff --git a/frontend/src/pages/world_map.js b/frontend/src/pages/world_map.js index c6468f4..38c37e4 100644 --- a/frontend/src/pages/world_map.js +++ b/frontend/src/pages/world_map.js @@ -157,8 +157,27 @@ const WorldMapPapersPage = () => { if (!response.ok) throw new Error('Failed to fetch search results'); const data = await response.json(); + // Deduplicate results based on work ID and title to prevent duplicates + const uniqueResults = []; + const seenIds = new Set(); + const seenTitles = new Set(); + + if (data.results && Array.isArray(data.results)) { + data.results.forEach(result => { + const title = result.title || result.display_name || ''; + const normalizedTitle = title.toLowerCase().trim(); + + // Check both ID and title for duplicates + if (result.id && !seenIds.has(result.id) && !seenTitles.has(normalizedTitle)) { + seenIds.add(result.id); + seenTitles.add(normalizedTitle); + uniqueResults.push(result); + } + }); + } + // Store the search results for the world map - setSearchResults(data.results || []); + setSearchResults(uniqueResults); } catch (e) { setSearchResults([]);