// 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 `
`;
}).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 `
`;
}).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();
}
});
}
});