765 lines
26 KiB
JavaScript
765 lines
26 KiB
JavaScript
// 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 = '<div style="text-align: center; padding: 50px; color: #666;">Loading data...</div>';
|
||
|
||
// 加载数据(如果未缓存)
|
||
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 = '<div style="text-align: center; padding: 50px; color: #666;">No data available</div>';
|
||
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 `
|
||
<div class="taxonomy-chart-row">
|
||
<div class="taxonomy-taxa-name" title="${item.name}">${item.name}</div>
|
||
<div class="taxonomy-chart-area">
|
||
<div class="taxonomy-chart-line" style="width: ${lineWidth}px;"></div>
|
||
<div class="taxonomy-chart-dot"
|
||
style="left: ${lineWidth}px;"
|
||
data-name="${item.name}"
|
||
data-value="${item.value.toLocaleString()}"></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).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}<br>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 = '<div style="text-align: center; padding: 50px; color: #666;">Loading data...</div>';
|
||
|
||
// 加载数据(如果未缓存)
|
||
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 = '<div style="text-align: center; padding: 50px; color: #666;">No data available</div>';
|
||
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 `
|
||
<div class="nutrition-chart-row">
|
||
<div class="nutrition-taxa-name" title="${item.name}">${item.name}</div>
|
||
<div class="nutrition-chart-area">
|
||
<div class="nutrition-chart-line" style="width: ${lineWidth}px;"></div>
|
||
<div class="nutrition-chart-dot"
|
||
style="left: ${lineWidth}px;"
|
||
data-name="${item.name}"
|
||
data-value="${item.value.toLocaleString()}"></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).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}<br>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();
|
||
}
|
||
});
|
||
}
|
||
}); |