Files
labweb/public/js/search_merged.js
2025-12-16 11:39:15 +08:00

742 lines
28 KiB
JavaScript

// API Base URL
const API_BASE_URL = '/api';
// Global variables to store related microbial data
let relatedMicrobes = [];
let allRelatedMicrobes = []; // Store all related microbial data
let currentRelatedPage = 1;
let relatedRowsPerPage = 20;
let totalRelatedRows = 0;
let totalRelatedPages = 0;
let currentSort = { column: null, direction: 'asc' }; // Current sort state
// --- Search Functions ---
// Search function (with auto scroll)
async function performSearch() {
const microbialInput = document.getElementById('microbial-name-input').value;
if (microbialInput.trim()) {
try {
showLoadingState();
console.log('Searching for:', microbialInput);
// Call Search API
const response = await fetch(`${API_BASE_URL}/microbial-detail?name=${encodeURIComponent(microbialInput)}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
const data = result.data;
console.log('Search results:', data);
hideLoadingState();
if (data) {
// Show results
displaySearchResult(data);
// Scroll to results
document.getElementById('results-section').scrollIntoView({ behavior: 'smooth' });
} else {
showNoResults();
}
} catch (error) {
console.error('Search error:', error);
hideLoadingState();
showErrorState(error.message);
}
} else {
alert('Please enter a microbial name.');
}
}
// Search function (without scroll - for initial load)
async function performSearchWithoutScroll() {
const microbialInput = document.getElementById('microbial-name-input').value;
if (microbialInput.trim()) {
try {
showLoadingState();
console.log('Searching for:', microbialInput);
const response = await fetch(`${API_BASE_URL}/microbial-detail?name=${encodeURIComponent(microbialInput)}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
const data = result.data;
console.log('Search results:', data);
hideLoadingState();
if (data) {
displaySearchResult(data);
} else {
showNoResults();
}
} catch (error) {
console.error('Search error:', error);
hideLoadingState();
showErrorState(error.message);
}
}
}
// Display search results
function displaySearchResult(microbial) {
console.log('Displaying result for:', microbial);
if (!microbial) {
console.error('No microbial data provided');
showNoResults();
return;
}
// Update basic info
updateMicrobialInfo(microbial);
// Update culture medium info
updateCultureMediumInfo(microbial);
// Update related species statistics and table
if (microbial.related_microbes && microbial.related_microbes.length > 0) {
updateRelatedMicrobes(microbial.related_microbes);
updateStatistics(microbial.related_microbes);
} else {
// Clear related data if none
updateRelatedMicrobes([]);
updateStatistics([]);
}
console.log('Search result displayed successfully');
}
// Format probability
function formatProbability(probability) {
if (!probability || probability === 'N/A') {
return 'N/A';
}
const probValue = parseFloat(probability);
if (isNaN(probValue)) {
return 'N/A';
}
return probValue.toFixed(2);
}
// Get confidence color
function getConfidenceColor(probability) {
if (!probability || probability === 'N/A') {
return '#f0f0f0'; // Default gray
}
let probValue = parseFloat(probability);
if (isNaN(probValue)) {
return '#f0f0f0';
}
if (probValue <= 1) {
probValue = probValue * 100;
}
if (probValue >= 0 && probValue < 50) {
return '#D0E7ED';
} else if (probValue >= 50 && probValue < 70) {
return '#9392BE';
} else if (probValue >= 70 && probValue <= 100) {
return '#D5E4A8';
} else {
return '#f0f0f0';
}
}
// --- DOM Update Functions ---
function updateMicrobialInfo(microbial) {
console.log('Updating microbial info with:', microbial);
// Specie name
const specie_name = document.getElementById('specie-name');
if (specie_name) {
const specieNameElement = specie_name.querySelector('p');
if (specieNameElement) {
specieNameElement.textContent = microbial.microbial_name || 'N/A';
}
}
// Taxonomy
const Taxonomy = document.getElementById('Taxonomy');
if (Taxonomy) {
const taxonomyElement = Taxonomy.querySelector('p');
if (taxonomyElement) {
taxonomyElement.textContent = microbial.taxonomy || 'N/A';
}
}
// pH
const pH = document.getElementById('pH');
if (pH) {
const pHValue = pH.querySelector('p');
if (pHValue) {
pHValue.textContent = microbial.ppH ? parseFloat(microbial.ppH).toFixed(0) : 'N/A';
}
}
// Temperature
const Temperature = document.getElementById('Temperature');
if (Temperature) {
const tempValue = Temperature.querySelector('p');
const tempConfidenceColor = getConfidenceColor(microbial.ptemperature_probability);
const formattedTempProbability = formatProbability(microbial.ptemperature_probability);
if (tempValue) {
tempValue.textContent = microbial.ptemperature || 'N/A';
const existingConfidence = tempValue.parentElement.querySelector('.confidence-span');
if (existingConfidence) {
existingConfidence.remove();
}
if (microbial.ptemperature_probability) {
const confidenceSpan = document.createElement('span');
confidenceSpan.style.cssText = 'margin-left: 10px; padding: 5px; font-weight: 400; border-radius: 4px; background: ' + tempConfidenceColor + ';';
confidenceSpan.textContent = 'Confidence: ' + formattedTempProbability;
confidenceSpan.className = 'confidence-span';
tempValue.parentElement.insertBefore(confidenceSpan, tempValue.nextSibling);
}
}
}
// Oxygen
const Oxygen = document.getElementById('Oxygen-resistance');
if (Oxygen) {
const o2Value = Oxygen.querySelector('p');
const o2ConfidenceColor = getConfidenceColor(microbial.po2_probability);
const formattedO2Probability = formatProbability(microbial.po2_probability);
if (o2Value) {
o2Value.textContent = microbial.po2 || 'N/A';
const existingConfidence = o2Value.parentElement.querySelector('.confidence-span');
if (existingConfidence) {
existingConfidence.remove();
}
if (microbial.po2_probability) {
const confidenceSpan = document.createElement('span');
confidenceSpan.style.cssText = 'margin-left: 10px; padding: 5px; font-weight: 400; border-radius: 4px; background: ' + o2ConfidenceColor + ';';
confidenceSpan.textContent = 'Confidence: ' + formattedO2Probability;
confidenceSpan.className = 'confidence-span';
o2Value.parentElement.insertBefore(confidenceSpan, o2Value.nextSibling);
}
}
}
}
function updateCultureMediumInfo(microbial) {
const cultureMediumCard = document.getElementById('culture-medium');
if (!cultureMediumCard || !microbial.nutrition) return;
const nutritionArrays = microbial.nutrition.split(';').map(arrStr => {
const trimmed = arrStr.trim().replace(/^\[|\]$/g, '');
return trimmed
? trimmed
.split(',')
.map(item => item.trim().replace(/'/g, ''))
: [];
});
if (nutritionArrays && nutritionArrays.length > 0) {
let mediaHtml = '';
mediaHtml += `<span>Media</span>`;
nutritionArrays.forEach((media, index) => {
if (media && media.length > 0) {
mediaHtml += `
<div class="medium-item">
<h4 style="display: inline; font-weight:400;">${index + 1}:</h4>
<span style="font-weight: 400; color: #666;">${Array.isArray(media) ? media.join(', ') : media}</span>
</div>
`;
}
});
cultureMediumCard.innerHTML = mediaHtml;
}
}
// --- Related Microbes Table Logic ---
function updateRelatedMicrobes(relatedMicrobes) {
allRelatedMicrobes = relatedMicrobes || [];
totalRelatedRows = allRelatedMicrobes.length;
totalRelatedPages = Math.ceil(totalRelatedRows / relatedRowsPerPage);
if(totalRelatedPages === 0) totalRelatedPages = 1;
updateRelatedTable();
updateRelatedPaginationInfo();
}
function updateRelatedTable() {
const tableBody = document.querySelector('.table-container tbody');
if (!tableBody) {
console.error('Table body not found');
return;
}
if (!allRelatedMicrobes || allRelatedMicrobes.length === 0) {
tableBody.innerHTML = '<tr><td colspan="8" style="text-align:center; padding: 20px;">No relevant microbial data available</td></tr>';
return;
}
const startIndex = (currentRelatedPage - 1) * relatedRowsPerPage;
const endIndex = Math.min(startIndex + relatedRowsPerPage, totalRelatedRows);
const currentPageData = allRelatedMicrobes.slice(startIndex, endIndex);
let tableHtml = '';
currentPageData.forEach((microbe, index) => {
const micppH = microbe.ppH ? parseFloat(microbe.ppH).toFixed(0) : 'Unknown';
const rowNumber = startIndex + index + 1;
tableHtml += `
<tr onclick="window.navigateToDetail('${(microbe.microbial_name || '').replace(/'/g, "\\'")}')" style="cursor: pointer;">
<td>${rowNumber}</td>
<td><a href="#" class="microbial-link" onclick="event.stopPropagation(); window.navigateToDetail('${(microbe.microbial_name || '').replace(/'/g, "\\'")}')">${microbe.microbial_name || 'N/A'}</a></td>
<td><a href="#" class="nutrition-link" onclick="event.stopPropagation()">${microbe.nutrition || 'N/A'}</a></td>
<td>${microbe.domain || 'N/A'}</td>
<td>${micppH}</td>
<td>${microbe.ptemperature || 'Unknown'}</td>
<td>${microbe.po2 || 'Unknown'}</td>
<td>${microbe.cultured_type || 'Unknown'}</td>
</tr>
`;
});
tableBody.innerHTML = tableHtml;
}
window.navigateToDetail = function(microbialName) {
// If you want to redirect to a detail page, uncomment below
// window.location.href = `Search_result.html?microbial=${encodeURIComponent(microbialName)}`;
// If you want to update the current page (SPA style) since we are on Search_merged
document.getElementById('microbial-name-input').value = microbialName;
performSearch();
// Scroll to top to see new input
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function updateRelatedPaginationInfo() {
const showingText = document.getElementById('showing-text');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
if (showingText) {
const startIndex = totalRelatedRows > 0 ? (currentRelatedPage - 1) * relatedRowsPerPage + 1 : 0;
const endIndex = Math.min(currentRelatedPage * relatedRowsPerPage, totalRelatedRows);
showingText.textContent = `Showing ${startIndex} to ${endIndex} of ${totalRelatedRows} entries.`;
}
if (prevBtn) {
prevBtn.disabled = currentRelatedPage <= 1;
}
if (nextBtn) {
nextBtn.disabled = currentRelatedPage >= totalRelatedPages;
}
updateRelatedPageNumbers();
}
function updateRelatedPageNumbers() {
const pageNumbers = document.getElementById('page-numbers');
if (!pageNumbers) return;
pageNumbers.innerHTML = '';
if (totalRelatedPages <= 7) {
for (let i = 1; i <= totalRelatedPages; i++) {
const pageBtn = document.createElement('button');
pageBtn.textContent = i;
pageBtn.className = `page-num ${i === currentRelatedPage ? 'active' : ''}`;
pageBtn.addEventListener('click', () => {
currentRelatedPage = i;
updateRelatedTable();
updateRelatedPaginationInfo();
});
pageNumbers.appendChild(pageBtn);
}
} else {
const startPage = Math.max(1, currentRelatedPage - 2);
const endPage = Math.min(totalRelatedPages, startPage + 4);
if (startPage > 1) {
const firstButton = document.createElement('button');
firstButton.className = `page-num ${1 === currentRelatedPage ? 'active' : ''}`;
firstButton.textContent = '1';
firstButton.addEventListener('click', () => {
currentRelatedPage = 1;
updateRelatedTable();
updateRelatedPaginationInfo();
});
pageNumbers.appendChild(firstButton);
if (startPage > 2) {
const ellipsis = document.createElement('span');
ellipsis.className = 'page-ellipsis';
ellipsis.textContent = '...';
ellipsis.style.cssText = `padding: 8px 4px; color: #666; font-weight: bold; display: flex; align-items: center; justify-content: center; min-width: 20px;`;
pageNumbers.appendChild(ellipsis);
}
}
for (let i = startPage; i <= endPage; i++) {
if (i === 1 && startPage > 1) continue;
const pageBtn = document.createElement('button');
pageBtn.textContent = i;
pageBtn.className = `page-num ${i === currentRelatedPage ? 'active' : ''}`;
pageBtn.addEventListener('click', () => {
currentRelatedPage = i;
updateRelatedTable();
updateRelatedPaginationInfo();
});
pageNumbers.appendChild(pageBtn);
}
if (endPage < totalRelatedPages) {
if (endPage < totalRelatedPages - 1) {
const ellipsis = document.createElement('span');
ellipsis.className = 'page-ellipsis';
ellipsis.textContent = '...';
ellipsis.style.cssText = `padding: 8px 4px; color: #666; font-weight: bold; display: flex; align-items: center; justify-content: center; min-width: 20px;`;
pageNumbers.appendChild(ellipsis);
}
const lastButton = document.createElement('button');
lastButton.className = `page-num ${totalRelatedPages === currentRelatedPage ? 'active' : ''}`;
lastButton.textContent = totalRelatedPages;
lastButton.addEventListener('click', () => {
currentRelatedPage = totalRelatedPages;
updateRelatedTable();
updateRelatedPaginationInfo();
});
pageNumbers.appendChild(lastButton);
}
}
}
// --- Statistics and Charts ---
function updateStatistics(relatedMicrobes) {
if (!relatedMicrobes || relatedMicrobes.length === 0) {
// Clear charts if no data
updatePHChart([]);
updateO2Chart([]);
updateTemperatureChart([]);
updateNutritionChart([]);
return;
}
const pHData = calculatePHDistribution(relatedMicrobes);
updatePHChart(pHData);
const o2Data = calculateO2Distribution(relatedMicrobes);
updateO2Chart(o2Data);
const tempData = calculateTemperatureDistribution(relatedMicrobes);
updateTemperatureChart(tempData);
const nutritionData = calculateNutritionDistribution(relatedMicrobes);
updateNutritionChart(nutritionData);
}
function calculatePHDistribution(relatedMicrobes) {
const pHCounts = {};
relatedMicrobes.forEach(microbe => {
if (microbe.pH || microbe.ppH) {
const pH = parseFloat(microbe.pH || microbe.ppH);
let category;
if (pH < 6) category = 'acidic (pH < 6)';
else if (pH > 8) category = 'alkaline (pH > 8)';
else category = 'neutral (pH 6-8)';
pHCounts[category] = (pHCounts[category] || 0) + 1;
}
});
return Object.entries(pHCounts).map(([name, value]) => ({ name: name.replace(' ', '\n'), value }));
}
function calculateO2Distribution(relatedMicrobes) {
const o2Counts = {};
relatedMicrobes.forEach(microbe => {
const o2 = microbe.o2 || microbe.po2;
if (o2) {
const o2Lower = o2.toLowerCase();
o2Counts[o2Lower] = (o2Counts[o2Lower] || 0) + 1;
}
});
return Object.entries(o2Counts).map(([name, value]) => ({ name, value }));
}
function calculateTemperatureDistribution(relatedMicrobes) {
const tempCounts = {};
relatedMicrobes.forEach(microbe => {
const tVal = microbe.temperature || microbe.ptemperature;
if (tVal) {
const temp = parseFloat(tVal);
let category;
if (temp < 20) category = 'low (< 20°C)';
else if (temp > 40) category = 'high (> 40°C)';
else category = 'medium (20-40°C)';
tempCounts[category] = (tempCounts[category] || 0) + 1;
}
});
return Object.entries(tempCounts).map(([name, value]) => ({ name: name.replace(' ', '\n'), value }));
}
function calculateNutritionDistribution(relatedMicrobes) {
const nutritionCounts = {};
relatedMicrobes.forEach(microbe => {
if (microbe.nutrition) {
let nutrients = [];
if (typeof microbe.nutrition === 'string') {
const nutritionStr = microbe.nutrition.replace(/[\[\]']/g, '');
nutrients = nutritionStr.split(',').map(n => n.trim()).filter(n => n !== '');
}
nutrients.forEach(nutrient => {
if (nutrient.trim() !== '') {
nutritionCounts[nutrient] = (nutritionCounts[nutrient] || 0) + 1;
}
});
}
});
return Object.entries(nutritionCounts)
.sort((a, b) => b[1] - a[1])
.map(([name, value]) => ({
name,
value,
frequency: (relatedMicrobes.length > 0) ? (value / relatedMicrobes.length).toFixed(2) : 0
}));
}
// Chart Updating Functions (using ECharts)
function updatePHChart(data) {
const chartElement = document.querySelector('.chart-pH');
if (!chartElement) return;
let chart = echarts.getInstanceByDom(chartElement);
if (!chart) chart = echarts.init(chartElement);
const option = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: '1%', right: '4%', bottom: '3%', top: '6%', containLabel: true },
xAxis: { type: 'category', data: data.map(item => item.name), axisLabel: { fontSize: 10 } },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: data.map(item => item.value), itemStyle: { color: '#9fbaae' } }]
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
}
function updateO2Chart(data) {
const chartElement = document.querySelector('.chart-O2');
if (!chartElement) return;
let chart = echarts.getInstanceByDom(chartElement);
if (!chart) chart = echarts.init(chartElement);
const option = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: '1%', right: '4%', bottom: '3%', top: '6%', containLabel: true },
xAxis: { type: 'value' },
yAxis: {
type: 'category',
data: data.map(item => item.name),
axisLabel: {
fontSize: 10,
formatter: function(value) { return value.length > 10 ? value.substring(0, 10) + '...' : value; }
}
},
series: [{ type: 'bar', data: data.map(item => item.value), itemStyle: { color: '#9fb3ba' } }]
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
}
function updateTemperatureChart(data) {
const chartElement = document.querySelector('.chart-temp');
if (!chartElement) return;
let chart = echarts.getInstanceByDom(chartElement);
if (!chart) chart = echarts.init(chartElement);
const option = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: '1%', right: '4%', bottom: '3%', top: '6%', containLabel: true },
xAxis: { type: 'category', data: data.map(item => item.name), axisLabel: { fontSize: 10 } },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: data.map(item => item.value), itemStyle: { color: '#9ea0b9' } }]
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
}
function updateNutritionChart(data) {
const chartElement = document.querySelector('.chart-nutrition');
if (!chartElement) return;
const sortedData = data.sort((a, b) => parseFloat(b.frequency) - parseFloat(a.frequency)).slice(0, 15); // Top 15
chartElement.innerHTML = '';
const scrollContainer = document.createElement('div');
scrollContainer.style.cssText = `width: 100%; height: 100%; overflow-y: auto; overflow-x: hidden;`;
const chartContainer = document.createElement('div');
const itemHeight = 30;
const calculatedHeight = Math.max(200, sortedData.length * itemHeight);
chartContainer.style.cssText = `width: 100%; height: ${calculatedHeight}px;`;
chartElement.appendChild(scrollContainer);
scrollContainer.appendChild(chartContainer);
const chart = echarts.init(chartContainer);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: function(params) {
const dataIndex = params[0].dataIndex;
const item = sortedData[dataIndex];
return item.name + '<br/>Count: ' + item.value + '<br/>Freq: ' + item.frequency;
}
},
grid: { left: '0%', right: '12%', bottom: '3%', top: '1%', containLabel: true },
xAxis: { type: 'value', name: 'Freq' },
yAxis: {
type: 'category',
data: sortedData.map(item => item.name),
axisLabel: {
formatter: function(value) { return value.length > 10 ? value.substring(0, 10) + '...' : value; }
}
},
series: [{
type: 'bar',
data: sortedData.map(item => parseFloat(item.frequency)),
itemStyle: { color: '#af9eb9' },
label: { show: true, position: 'right', formatter: '{@score}' }
}]
};
chart.setOption(option);
window.addEventListener('resize', () => chart.resize());
}
// --- Utils ---
function showLoadingState() { console.log('Loading...'); }
function hideLoadingState() { console.log('Loading complete'); }
function showErrorState(message) { alert('Error: ' + message); }
function showNoResults() { alert('No results found'); }
async function downloadCSV() {
if (!allRelatedMicrobes || allRelatedMicrobes.length === 0) {
alert('No data available for download');
return;
}
const headers = ['Number', 'Microbial', 'Nutritional combination', 'Taxo', 'pH', 'Temperature', 'O2', 'Type'];
const csvContent = [
headers.join(','),
...allRelatedMicrobes.map((row, index) => {
return [
index + 1,
`"${row.microbial_name || ''}"`,
`"${(row.nutrition || '').replace(/"/g, '""')}"`,
`"${row.domain || ''}"`,
row.ppH || row.pH || '',
row.ptemperature || '',
row.po2 || '',
row.cultured_type || ''
].join(',');
})
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.setAttribute('download', 'related_microbes_data.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// --- Initialization ---
document.addEventListener('DOMContentLoaded', function() {
// Tabs
const tabButtons = document.querySelectorAll('.tab-btn');
const tabPanels = document.querySelectorAll('.tab-panel');
tabButtons.forEach(button => {
button.addEventListener('click', function() {
const targetTab = this.getAttribute('data-tab');
tabButtons.forEach(btn => btn.classList.remove('active'));
tabPanels.forEach(panel => panel.classList.remove('active'));
this.classList.add('active');
document.getElementById(targetTab).classList.add('active');
});
});
// Pagination Controls
document.getElementById('per_page').addEventListener('change', function() {
relatedRowsPerPage = parseInt(this.value);
currentRelatedPage = 1;
updateRelatedTable();
updateRelatedPaginationInfo();
});
document.getElementById('prev-btn').addEventListener('click', function() {
if (currentRelatedPage > 1) {
currentRelatedPage--;
updateRelatedTable();
updateRelatedPaginationInfo();
}
});
document.getElementById('next-btn').addEventListener('click', function() {
if (currentRelatedPage < totalRelatedPages) {
currentRelatedPage++;
updateRelatedTable();
updateRelatedPaginationInfo();
}
});
// Buttons
document.getElementById('reset-btn').addEventListener('click', function() {
document.getElementById('microbial-name-input').value = '';
window.scrollTo({ top: 0, behavior: 'smooth' });
});
document.getElementById('search-btn').addEventListener('click', performSearch);
document.getElementById('download-btn').addEventListener('click', downloadCSV);
// Auto-search if sample data present (Simulation)
const microbialInput = document.getElementById('microbial-name-input');
setTimeout(() => {
if (microbialInput && !microbialInput.value) {
microbialInput.value = 'Escherichia coli B3';
performSearchWithoutScroll();
}
}, 500);
});