// API Base URL (统一使用相对路径,避免跨域问题) const API_BASE_URL = '/api'; // 数据存储 let taxonomyData = {}; let physchemData = {}; let nutritionData = []; let cultureData = []; let currentTab = 'phylum'; let maxValue = 50000; let currentPhyschemTab = 'o2'; // ECharts 实例存储 let pieChart = null; let phChart = null; let tempChart = null; // --- 数据加载函数 --- /** // * 加载分类学数据 // * @param {string} level - 分类级别 (如 phylum, class 等) // * @returns {Array} 处理后的分类学数据 // */ // async function loadTaxonomyData(level) { // try { // const response = await fetch(`${API_BASE_URL}/taxonomy-stats?level=${level}`); // if (!response.ok) { // throw new Error(`加载失败: HTTP 状态码 ${response.status}`); // } // const result = await response.json(); // if (result.success) { // taxonomyData[level] = { // data: result.data, // total: result.total // }; // return result.data; // } else { // console.error('加载分类学数据失败:', result.message); // showErrorNotification('分类学数据加载失败'); // return []; // } // } catch (error) { // console.error('加载分类学数据时出错:', error); // showErrorNotification('加载数据时发生错误,请稍后重试'); // return []; // } // } /** * 加载分类学数据 * @param {string} level - 分类级别 (如 phylum, class 等) * @param {string} culturedType - 培养类型 (默认: cultured,可选: uncultured 等) * @returns {Array} 处理后的分类学数据 */ async function loadTaxonomyData(level, culturedType = 'cultured') { try { // 1. 拼接完整请求URL:补充 cultured_type 参数(和后端默认值一致) const params = new URLSearchParams({ level: level || 'phylum', // 后端默认 phylum cultured_type: culturedType // 后端默认 cultured }); const response = await fetch(`${API_BASE_URL}/taxonomy-stats?${params.toString()}`); if (!response.ok) { throw new Error(`加载失败: HTTP 状态码 ${response.status}`); } const result = await response.json(); // 2. 处理后端返回:区分成功/失败场景 if (result.success) { // 后端返回 { success: true, data: [], total: 0 } taxonomyData[level] = { data: result.data || [], // 兜底空数组,避免undefined total: result.total || 0 // 兜底0,避免NaN }; return result.data || []; } else if (result.error) { // 后端返回 { error: "xxx" }(500错误时) console.error('加载分类学数据失败:', result.error); showErrorNotification(`分类学数据加载失败: ${result.error}`); taxonomyData[level] = { data: [], total: 0 }; // 兜底空数据 return []; } else { // 未知返回格式 console.error('加载分类学数据失败: 未知返回格式', result); showErrorNotification('分类学数据加载失败: 返回格式异常'); taxonomyData[level] = { data: [], total: 0 }; return []; } } catch (error) { // 网络错误/HTTP状态码错误(如404、500) console.error('加载分类学数据时出错:', error); showErrorNotification(`加载数据时发生错误: ${error.message},请稍后重试`); taxonomyData[level] = { data: [], total: 0 }; // 兜底空数据 return []; } } /** * 加载理化数据 * @param {string} type - 数据类型 (o2, ph, temperature) * @returns {Object|null} 处理后的理化数据 */ async function loadPhyschemData(type) { try { const response = await fetch(`${API_BASE_URL}/physchem-stats?type=${type}`); if (!response.ok) { throw new Error(`加载失败: HTTP 状态码 ${response.status}`); } const result = await response.json(); if (result.success) { physchemData[type] = result.data; return result.data; } else { console.error('加载理化数据失败:', result.message); showErrorNotification('理化数据加载失败'); return null; } } catch (error) { console.error('加载理化数据时出错:', error); showErrorNotification('加载数据时发生错误,请稍后重试'); return null; } } /** * 加载营养数据 * @returns {Array} 处理后的营养数据 */ async function loadNutritionData() { try { const response = await fetch(`${API_BASE_URL}/nutrition-stats`); if (!response.ok) { throw new Error(`加载失败: HTTP 状态码 ${response.status}`); } const result = await response.json(); if (result.success) { nutritionData = result.data || []; return result.data || []; } else { console.error('加载营养数据失败:', result.message); showErrorNotification('营养数据加载失败'); return []; } } catch (error) { console.error('加载营养数据时出错:', error); showErrorNotification('加载数据时发生错误,请稍后重试'); return []; } } // --- 图表渲染函数 --- /** * 更新分类学图表 * @param {string} tabName - 分类级别 */ async function updateTaxonomyChart(tabName) { currentTab = tabName; const badge = document.getElementById('taxonomyTotalBadge'); const content = document.getElementById('taxonomyChartContent'); // 显示加载状态 if (badge) badge.textContent = 'Loading...'; if (content) content.innerHTML = '
Loading data...
'; // 加载数据(如果未缓存) if (!taxonomyData[tabName]) { await loadTaxonomyData(tabName); } const data = taxonomyData[tabName]?.data || []; const total = taxonomyData[tabName]?.total || 0; // 处理无数据情况 if (!data || data.length === 0) { console.warn(`分类级别 ${tabName} 无数据`); if (badge) badge.textContent = 'Total taxa: 0'; if (content) content.innerHTML = '
No data available
'; return; } // 更新总数显示 if (badge) badge.textContent = `Total taxa: ${total}`; maxValue = Math.max(...data.map(item => item.value)) * 1.1; // 渲染图表内容 if (content) { const availableWidth = Math.max(content.offsetWidth - 220, 600); content.innerHTML = data.map(item => { const percentage = (item.value / maxValue) * 100; const lineWidth = Math.max(1, (percentage / 100) * availableWidth); return `
${item.name}
`; }).join(''); } // 使用 requestAnimationFrame 确保 DOM 已更新 requestAnimationFrame(() => { generateTaxonomyGridLines(); bindTaxonomyTooltipEvents(); }); } /** * 生成分类学图表网格线 */ function generateTaxonomyGridLines() { const gridLines = document.getElementById('taxonomyGridLines'); const xAxisLabels = document.getElementById('taxonomyXAxisLabels'); if (!gridLines || !xAxisLabels) return; const firstChartArea = document.querySelector('.taxonomy-chart-area'); const chartWidth = firstChartArea ? firstChartArea.offsetWidth : 600; gridLines.innerHTML = ''; xAxisLabels.innerHTML = ''; const maxVal = Math.round(maxValue); let gridCount = 5; let step; // 动态计算步长,确保坐标轴标签合理 if (maxVal <= 100) { step = Math.ceil(maxVal / 5); } else if (maxVal <= 1000) { step = Math.ceil(maxVal / 5 / 10) * 10; } else if (maxVal <= 10000) { step = Math.ceil(maxVal / 5 / 100) * 100; } else { step = Math.ceil(maxVal / 5 / 1000) * 1000; } // 生成网格线和标签 for (let i = 0; i <= gridCount; i++) { const position = (i / gridCount) * chartWidth; const gridLine = document.createElement('div'); gridLine.className = 'taxonomy-grid-line'; gridLine.style.left = `${position}px`; gridLines.appendChild(gridLine); const label = document.createElement('span'); label.textContent = (i * step).toLocaleString(); xAxisLabels.appendChild(label); } } /** * 绑定分类学图表提示框事件 */ function bindTaxonomyTooltipEvents() { const dots = document.querySelectorAll('.taxonomy-chart-dot'); const tooltip = document.getElementById('taxonomyTooltip'); if (!tooltip) return; dots.forEach(dot => { dot.addEventListener('mouseenter', (e) => { const name = e.target.getAttribute('data-name'); const value = e.target.getAttribute('data-value'); tooltip.innerHTML = `${name}
Strains: ${value}`; tooltip.style.display = 'block'; }); dot.addEventListener('mousemove', (e) => { tooltip.style.left = `${e.pageX + 10}px`; tooltip.style.top = `${e.pageY - 40}px`; }); dot.addEventListener('mouseleave', () => { tooltip.style.display = 'none'; }); }); } /** * 初始化分类学标签切换事件 */ function initializeTaxonomyTabEvents() { document.querySelectorAll('.taxonomy-container .taxonomy-tab-item').forEach(tab => { tab.addEventListener('click', async (e) => { // 防止重复点击同一标签 if (e.target.classList.contains('active')) return; document.querySelectorAll('.taxonomy-container .taxonomy-tab-item') .forEach(t => t.classList.remove('active')); e.target.classList.add('active'); const tabName = e.target.getAttribute('data-tab'); await updateTaxonomyChart(tabName); }); }); } /** * 初始化分类学图表 */ async function initializeTaxonomy() { initializeTaxonomyTabEvents(); await updateTaxonomyChart('phylum'); } /** * 更新理化性质图表 * @param {string} tabName - 图表类型 (o2, ph, temperature) */ async function updatePhyschemChart(tabName) { currentPhyschemTab = tabName; const badge = document.getElementById('physchem-total-badge'); // 显示加载状态 if (badge) badge.textContent = 'Loading...'; // 加载数据(如果未缓存) if (!physchemData[tabName]) { await loadPhyschemData(tabName); } const data = physchemData[tabName]; // 处理无数据情况 if (!data) { if (badge) badge.textContent = 'No data available'; return; } // 隐藏所有图表区域,显示当前激活的 document.querySelectorAll('.physchem-container .chart-area') .forEach(area => area.classList.remove('active')); // 氧气需求饼图 if (tabName === 'o2') { const chartArea = document.getElementById('chart-oxygen-pie'); if (chartArea) chartArea.classList.add('active'); if (badge) badge.textContent = `Total count: ${data.total || 0}`; // 初始化 ECharts 实例(确保 echarts 存在且 DOM 元素有效) if (window.echarts && !pieChart) { const dom = document.getElementById('chart-oxygen-pie'); if (dom) { pieChart = echarts.init(dom); // 监听窗口大小变化,自动调整图表 window.addEventListener('resize', () => pieChart?.resize()); } } if (pieChart) { const pieOption = { tooltip: { trigger: 'item', formatter: '{b}: {d}%', renderMode: 'html' }, legend: { orient: 'horizontal', left: 'center', top: '2%', data: ['Aerobe', 'Anaerobe', 'Facultative anaerobe'], textStyle: { fontSize: 18 } }, series: [{ name: 'O2 Requirements', type: 'pie', radius: ['40%', '70%'], center: ['45%', '50%'], label: { show: true, formatter: '{d}%', fontSize: 18, fontWeight: 'bold' }, labelLine: { show: true, length: 1, length2: 10, smooth: true }, data: [ { value: data.aerobe, name: 'Aerobe', itemStyle: { color: '#CCCC99' } }, { value: data.anaerobe, name: 'Anaerobe', itemStyle: { color: '#336666' } }, { value: data.facultative, name: 'Facultative anaerobe', itemStyle: { color: '#669999' } } ] }] }; pieChart.setOption(pieOption); requestAnimationFrame(() => pieChart.resize()); } } // pH 直方图 else if (tabName === 'ph') { const chartArea = document.getElementById('chart-ph-hist'); if (chartArea) chartArea.classList.add('active'); const totalCount = data.reduce((sum, item) => sum + item.count, 0); if (badge) badge.textContent = `Total count: ${totalCount}`; // 初始化 ECharts 实例 if (window.echarts && !phChart) { const dom = document.getElementById('chart-ph-hist'); if (dom) { phChart = echarts.init(dom); window.addEventListener('resize', () => phChart?.resize()); } } if (phChart) { const phOption = { tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, grid: { left: '3%', right: '4%', bottom: '15%', top: '8%', containLabel: true }, xAxis: { type: 'category', data: data.map(item => item.x), name: 'pH', nameLocation: 'middle', nameGap: 40 }, yAxis: { type: 'value', name: 'Count' }, series: [{ type: 'bar', data: data.map(item => item.count), itemStyle: { color: '#669999', borderRadius: [4, 4, 0, 0] } }] }; phChart.setOption(phOption); requestAnimationFrame(() => phChart.resize()); } } // 温度直方图 else if (tabName === 'temperature') { const chartArea = document.getElementById('chart-temp-hist'); if (chartArea) chartArea.classList.add('active'); const totalCount = data.reduce((sum, item) => sum + item.count, 0); if (badge) badge.textContent = `Total count: ${totalCount}`; // 初始化 ECharts 实例 if (window.echarts && !tempChart) { const dom = document.getElementById('chart-temp-hist'); if (dom) { tempChart = echarts.init(dom); window.addEventListener('resize', () => tempChart?.resize()); } } if (tempChart) { const tempOption = { tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, grid: { left: '3%', right: '4%', bottom: '18%', top: '8%', containLabel: true }, xAxis: { type: 'category', data: data.map(item => item.x), name: 'Temperature (°C)', nameLocation: 'middle', nameGap: 30 }, yAxis: { type: 'value', name: 'Count' }, series: [{ type: 'bar', data: data.map(item => item.count), itemStyle: { color: '#CCCC99', borderRadius: [4, 4, 0, 0] } }] }; tempChart.setOption(tempOption); requestAnimationFrame(() => tempChart.resize()); } } } /** * 初始化理化性质标签切换事件 */ function initializePhyschemTabEvents() { document.querySelectorAll('#physchem-tabs .taxonomy-tab-item').forEach(tab => { tab.addEventListener('click', async (e) => { if (e.target.classList.contains('active')) return; document.querySelectorAll('#physchem-tabs .taxonomy-tab-item') .forEach(t => t.classList.remove('active')); e.target.classList.add('active'); const tabName = e.target.getAttribute('data-tab'); await updatePhyschemChart(tabName); }); }); } /** * 更新营养图表 */ async function updateNutritionChart() { const badge = document.getElementById('nutrition-total-badge'); const content = document.getElementById('nutrition-chart-content'); // 显示加载状态 if (badge) badge.textContent = 'Loading...'; if (content) content.innerHTML = '
Loading data...
'; // 加载数据(如果未缓存) if (nutritionData.length === 0) { await loadNutritionData(); } const data = nutritionData || []; const total = data.length; // 更新总数显示 if (badge) badge.textContent = `Total nutrients: ${total}`; // 处理无数据情况 if (!data || !Array.isArray(data) || data.length === 0) { if (content) content.innerHTML = '
No data available
'; return; } const maxValue = data.length > 0 && data.every(item => item && typeof item.value !== 'undefined') ? Math.max(...data.map(item => item.value || 0)) * 1.1 : 1; // 渲染图表内容 if (content) { const availableWidth = Math.max(content.offsetWidth - 220, 600); content.innerHTML = data.map(item => { const percentage = (item.value / maxValue) * 100; const lineWidth = Math.max(1, (percentage / 100) * availableWidth); return `
${item.name}
`; }).join(''); } requestAnimationFrame(() => { generateNutritionGridLines(); bindNutritionTooltipEvents(); }); } /** * 生成营养图表网格线 */ function generateNutritionGridLines() { const gridLines = document.getElementById('nutrition-grid-lines'); const xAxisLabels = document.getElementById('nutrition-x-axis-labels'); if (!gridLines || !xAxisLabels) return; const firstChartArea = document.querySelector('.nutrition-chart-area'); const chartWidth = firstChartArea ? firstChartArea.offsetWidth : 600; const maxNutritionValue = nutritionData.length > 0 ? Math.max(...nutritionData.map(item => item.value)) * 1.001 : 100; const maxVal = Math.round(maxNutritionValue); gridLines.innerHTML = ''; xAxisLabels.innerHTML = ''; const gridCount = 5; const rawStep = maxVal / gridCount; const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep))); const normalizedStep = rawStep / magnitude; // 动态计算步长,确保坐标轴标签美观 let multiplier; if (normalizedStep <= 1) multiplier = 1; else if (normalizedStep <= 2) multiplier = 2; else if (normalizedStep <= 5) multiplier = 5; else multiplier = 10; let step = multiplier * magnitude; const minStep = Math.pow(10, Math.floor(Math.log10(maxVal)) - 1); if (step < minStep) step = minStep; // 生成网格线和标签 for (let i = 0; i <= gridCount; i++) { const position = (i / gridCount) * chartWidth; const gridLine = document.createElement('div'); gridLine.className = 'nutrition-grid-line'; gridLine.style.left = `${position}px`; gridLines.appendChild(gridLine); const label = document.createElement('span'); label.textContent = (i * step).toLocaleString(); xAxisLabels.appendChild(label); } } /** * 绑定营养图表提示框事件 */ function bindNutritionTooltipEvents() { const dots = document.querySelectorAll('.nutrition-chart-dot'); const tooltip = document.getElementById('nutrition-tooltip'); if (!tooltip) return; dots.forEach(dot => { dot.addEventListener('mouseenter', (e) => { const name = e.target.getAttribute('data-name'); const value = e.target.getAttribute('data-value'); tooltip.innerHTML = `${name}
Frequency: ${value}`; tooltip.style.display = 'block'; }); dot.addEventListener('mousemove', (e) => { tooltip.style.left = `${e.pageX + 10}px`; tooltip.style.top = `${e.pageY - 40}px`; }); dot.addEventListener('mouseleave', () => { tooltip.style.display = 'none'; }); }); } /** * 显示错误通知 * @param {string} message - 错误信息 */ function showErrorNotification(message) { // 检查是否已有错误通知元素 let notification = document.getElementById('error-notification'); if (!notification) { // 创建错误通知元素 notification = document.createElement('div'); notification.id = 'error-notification'; notification.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 15px 20px; background-color: #e74c3c; color: white; border-radius: 4px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); z-index: 1000; transition: opacity 0.3s ease; `; document.body.appendChild(notification); } // 设置错误信息并显示 notification.textContent = message; notification.style.opacity = '1'; // 3秒后自动隐藏 setTimeout(() => { notification.style.opacity = '0'; }, 3000); } // --- 初始化函数 --- /** * 初始化所有图表和事件 */ async function initializeAll() { try { await Promise.all([ initializeTaxonomy(), (async () => { initializePhyschemTabEvents(); await updatePhyschemChart('o2'); })(), updateNutritionChart() ]); } catch (error) { console.error('初始化失败:', error); showErrorNotification('页面初始化失败,请刷新页面重试'); } } /** * 执行 banner 搜索 */ function performBannerSearch() { const searchInput = document.getElementById('banner-search-input'); if (!searchInput) { showErrorNotification('搜索框未找到'); return; } const searchValue = searchInput.value.trim(); if (searchValue) { const encodedValue = encodeURIComponent(searchValue); window.location.href = `/Search_result.html?microbial=${encodedValue}`; } else { alert('Please enter a microbial name to search.'); } } // --- 事件监听 --- document.addEventListener('DOMContentLoaded', () => { // 初始化仪表盘逻辑 initializeAll(); // 初始化搜索逻辑 const searchBtn = document.getElementById('banner-search-btn'); if (searchBtn) { searchBtn.addEventListener('click', performBannerSearch); } // 为搜索框添加回车事件 const searchInput = document.getElementById('banner-search-input'); if (searchInput) { searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { performBannerSearch(); } }); } });