/** * 行业估值分析工具前端JS */ document.addEventListener('DOMContentLoaded', function() { // 获取DOM元素 const industryForm = document.getElementById('industryForm'); const industryNameSelect = document.getElementById('industryName'); const startDateInput = document.getElementById('startDate'); const metricSelect = document.getElementById('metric'); const showCrowdingCheckbox = document.getElementById('showCrowding'); const analyzeBtn = document.getElementById('analyzeBtn'); const resetBtn = document.getElementById('resetBtn'); const loadingSpinner = document.getElementById('loadingSpinner'); const resultCard = document.getElementById('resultCard'); const resultTitle = document.getElementById('resultTitle'); const errorAlert = document.getElementById('errorAlert'); const errorMessage = document.getElementById('errorMessage'); const percentileTable = document.getElementById('percentileTable'); const crowdingStats = document.getElementById('crowdingStats'); const crowdingTable = document.getElementById('crowdingTable'); const industryStatsTable = document.getElementById('industryStatsTable'); const crowdingChartRow = document.getElementById('crowdingChartRow'); // 定义图表实例 let valuationChart = null; let crowdingChart = null; // 初始化 - 加载行业列表 loadIndustryList(); // 监听表单提交事件 industryForm.addEventListener('submit', function(event) { event.preventDefault(); analyzeIndustry(); }); // 监听重置按钮点击事件 resetBtn.addEventListener('click', function() { resetForm(); }); // 拥挤度显示控制 showCrowdingCheckbox.addEventListener('change', function() { if (crowdingStats.classList.contains('d-none')) { // 如果还没有数据,不做任何操作 return; } if (this.checked) { // 显示拥挤度 crowdingStats.classList.remove('d-none'); crowdingChartRow.classList.remove('d-none'); } else { // 隐藏拥挤度 crowdingStats.classList.add('d-none'); crowdingChartRow.classList.add('d-none'); } }); /** * 加载行业列表 */ function loadIndustryList() { showLoading(true); fetch('/api/industry/list') .then(response => { if (!response.ok) { return response.json().then(data => { throw new Error(data.message || '请求失败'); }); } return response.json(); }) .then(data => { if (data.status === 'success') { // 填充行业下拉列表 populateIndustrySelect(data.data); } else { showError(data.message || '获取行业列表失败'); } }) .catch(error => { showError(error.message || '请求失败,请检查网络连接'); }) .finally(() => { showLoading(false); }); } /** * 填充行业下拉列表 */ function populateIndustrySelect(industries) { // 清空选项(保留第一个默认选项) industryNameSelect.innerHTML = ''; // 排序行业列表(按名称) industries.sort((a, b) => a.name.localeCompare(b.name, 'zh')); // 添加行业选项 industries.forEach(industry => { const option = document.createElement('option'); option.value = industry.name; option.textContent = industry.name; industryNameSelect.appendChild(option); }); } /** * 分析行业估值 */ function analyzeIndustry() { // 显示加载中状态 showLoading(true); // 隐藏之前的结果和错误信息 resultCard.classList.add('d-none'); errorAlert.classList.add('d-none'); // 获取表单数据 const industryName = industryNameSelect.value; const startDate = startDateInput.value; const metric = metricSelect.value; // 构建请求URL let url = `/api/industry/analysis?industry_name=${encodeURIComponent(industryName)}&metric=${metric}`; if (startDate) { url += `&start_date=${startDate}`; } // 发送API请求 fetch(url) .then(response => { if (!response.ok) { return response.json().then(data => { throw new Error(data.message || '请求失败'); }); } return response.json(); }) .then(data => { if (data.status === 'success') { // 渲染分析结果 renderIndustryResults(data); } else { showError(data.message || '分析失败,请稍后再试'); } }) .catch(error => { showError(error.message || '请求失败,请检查网络连接'); }) .finally(() => { showLoading(false); }); } /** * 渲染行业分析结果 */ function renderIndustryResults(data) { // 显示结果卡片(在渲染图表前确保容器可见) resultCard.classList.remove('d-none'); // 更新标题 resultTitle.textContent = data.data.title.text; // 渲染分位数数据表格 renderPercentileTable(data.data.percentiles, data.data.yAxis[0].name); // 渲染行业基本统计 renderIndustryStats(data.data.percentiles); // 渲染拥挤度数据(如果有) if (data.data.crowding) { renderCrowdingStats(data.data.crowding); // 根据复选框状态决定是否显示拥挤度 if (showCrowdingCheckbox.checked) { crowdingStats.classList.remove('d-none'); crowdingChartRow.classList.remove('d-none'); } else { crowdingStats.classList.add('d-none'); crowdingChartRow.classList.add('d-none'); } } else { crowdingStats.classList.add('d-none'); crowdingChartRow.classList.add('d-none'); } // 渲染图表(等待DOM更新后) setTimeout(() => { renderValuationChart(data.data); // 如果有拥挤度数据且复选框被选中,渲染拥挤度图表 if (data.data.crowding && showCrowdingCheckbox.checked) { renderCrowdingChart(data.data); } // 滚动到结果区域 resultCard.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 100); } /** * 渲染估值分位数ECharts图表 */ function renderValuationChart(chartData) { // 初始化图表 const chartContainer = document.getElementById('valuationChart'); // 确保容器可见 resultCard.classList.remove('d-none'); // 如果已经存在图表实例,则销毁 if (valuationChart) { valuationChart.dispose(); } // 创建新的图表实例 valuationChart = echarts.init(chartContainer); // 处理图例数据 let legendData = chartData.legend.data.filter(item => !item.includes('拥挤度')); // 处理系列数据 let seriesData = chartData.series.filter(series => !series.name.includes('拥挤度') && series.yAxisIndex !== 1 ); // 去掉所有折线上的圆圈标记 seriesData.forEach(series => { series.symbol = 'none'; // 不显示折线上的圆点标记 }); // 检查数据有效性 // 注意:现在所有图例都是存在的,因为我们使用行业平均值的历史统计生成它们 // 不再需要检查q1_values, q3_values等是否存在 // 计算Y轴范围 const percentiles = chartData.percentiles; const avgValues = chartData.valuation.avg_values.filter(v => v !== null && v !== undefined); // 找出数据的最小值和最大值 let minValue = percentiles.min; let maxValue = percentiles.max; // 如果有avgValues,也考虑其最小值和最大值 if (avgValues.length > 0) { const avgMin = Math.min(...avgValues); const avgMax = Math.max(...avgValues); minValue = Math.min(minValue, avgMin); maxValue = Math.max(maxValue, avgMax); } // 计算Y轴范围,给出更好的视觉展示 // 下限:取最小值的80%或最小值减去范围的20%,但不小于0 // 上限:取最大值的120%或最大值加上范围的20% const range = maxValue - minValue; const padding = range * 0.2; let yMinValue = Math.max(0, minValue - padding); let yMaxValue = maxValue + padding; // 如果最小值与最大值之间的差距太小,进一步扩大范围以便更好地展示 if (range / maxValue < 0.1) { // 如果范围小于最大值的10% yMinValue = Math.max(0, minValue * 0.5); // 下限可以更低,但不低于0 yMaxValue = maxValue * 1.3; // 上限可以更高 } // 设置图表选项 const option = { title: { text: chartData.title.text, subtext: chartData.title.subtext, left: 'center', top: 5 }, tooltip: { trigger: 'axis', axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }, formatter: function(params) { const date = params[0].axisValue; let result = `${date}
`; params.forEach(param => { const value = param.value; const color = param.color; const marker = ``; const name = param.seriesName; if (name.includes('历史')) { // 对于历史统计线,使用固定值显示 result += `${marker}${name}: ${value !== null ? value.toFixed(2) : 'N/A'}
`; } else { // 对于当前值,显示更多小数位 result += `${marker}${name}: ${value !== null ? value.toFixed(2) : 'N/A'}
`; } }); return result; } }, legend: { data: legendData, top: 60 }, grid: chartData.grid[0], toolbox: chartData.toolbox, xAxis: chartData.xAxis[0], yAxis: { type: 'value', name: chartData.yAxis[0].name, min: yMinValue, max: yMaxValue, axisLabel: { formatter: function(value) { return value.toFixed(1); } }, splitLine: { show: true, lineStyle: { type: 'dashed' } } }, dataZoom: [ { type: 'inside', start: 0, end: 100, xAxisIndex: [0] }, { start: 0, end: 100, xAxisIndex: [0] } ], series: seriesData }; // 设置图表选项 valuationChart.setOption(option); // 手动触发一次resize,确保图表正确渲染 setTimeout(() => { valuationChart.resize(); }, 50); // 响应式调整 window.addEventListener('resize', function() { if (valuationChart) { valuationChart.resize(); } }); } /** * 渲染拥挤度ECharts图表 */ function renderCrowdingChart(chartData) { // 初始化图表 const chartContainer = document.getElementById('crowdingChart'); // 确保容器可见 crowdingChartRow.classList.remove('d-none'); // 如果已经存在图表实例,则销毁 if (crowdingChart) { crowdingChart.dispose(); } // 创建新的图表实例 crowdingChart = echarts.init(chartContainer); // 使用拥挤度的完整数据,不受其他时间筛选影响 // 拥挤度数据在后端已经固定为3年数据 const crowdingDates = chartData.crowding.dates; const crowdingPercentiles = chartData.crowding.percentiles; // 筛选拥挤度相关的系列 const crowdingSeries = { name: '行业交易拥挤度历史百分位', type: 'line', data: crowdingPercentiles, symbol: 'none', // 不显示折线上的圆点标记 lineStyle: {width: 2, color: '#ff7f50'}, areaStyle: {opacity: 0.2, color: '#ff7f50'}, markLine: { data: [ {name: "20%", yAxis: 20, lineStyle: {color: "#28a745", type: "dashed"}}, {name: "80%", yAxis: 80, lineStyle: {color: "#dc3545", type: "dashed"}} ] } }; // 获取拥挤度数据,用于计算Y轴范围 // 使用百分位数值替代原始比例 let percentileValues = crowdingPercentiles.filter(v => v !== null && v !== undefined); // 计算Y轴范围 let yMinValue = 0; // 默认最小值为0 let yMaxValue = 100; // 默认最大值为100(百分比) // 如果有实际数据,可以根据数据调整范围 if (percentileValues.length > 0) { const minPercentile = Math.min(...percentileValues); const maxPercentile = Math.max(...percentileValues); // 但拥挤度图表通常应该保持0-100的范围,所以只在必要时调整 if (minPercentile > 20 && maxPercentile < 80) { // 如果数据都集中在20-80之间,可以适当缩小范围 yMinValue = Math.max(0, Math.floor(minPercentile / 10) * 10 - 10); // 向下取整到最接近的10的倍数再减10 yMaxValue = Math.min(100, Math.ceil(maxPercentile / 10) * 10 + 10); // 向上取整到最接近的10的倍数再加10 } } // 设置图表选项 const option = { title: { text: '行业交易拥挤度分析', subtext: `当前拥挤度历史百分位: ${chartData.crowding.current_percentile.toFixed(2)}%`, left: 'center', top: 5 }, tooltip: { trigger: 'axis', axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }, formatter: function(params) { const date = params[0].axisValue; let result = `${date}
`; params.forEach(param => { const value = param.value; const color = param.color; const marker = ``; result += `${marker}${param.seriesName}: ${value ? value.toFixed(2) : 'N/A'}%
`; }); return result; } }, legend: { data: ['行业交易拥挤度历史百分位'], top: 60 }, grid: { left: '3%', right: '4%', bottom: '10%', top: '100', containLabel: true }, xAxis: { type: 'category', boundaryGap: false, data: crowdingDates, axisLabel: { rotate: 45 } }, yAxis: { type: 'value', name: '历史百分位 (%)', min: yMinValue, max: yMaxValue, axisLine: { show: true }, axisLabel: { formatter: function(value) { return value.toFixed(1) + '%'; } }, splitLine: { show: true, lineStyle: { type: 'dashed' } } }, dataZoom: [ { type: 'inside', start: 0, end: 100 }, { start: 0, end: 100 } ], series: [crowdingSeries], visualMap: { show: false, dimension: 1, pieces: [ {min: 0, max: 20, color: '#91cc75'}, // 绿色 - 不拥挤 {min: 20, max: 40, color: '#91cc75'}, // 绿色 - 较不拥挤 {min: 40, max: 60, color: '#fac858'}, // 黄色 - 中性 {min: 60, max: 80, color: '#ee6666'}, // 红色 - 较为拥挤 {min: 80, max: 100, color: '#ee6666'} // 红色 - 极度拥挤 ] } }; // 设置图表选项 crowdingChart.setOption(option); // 手动触发一次resize,确保图表正确渲染 setTimeout(() => { crowdingChart.resize(); }, 50); // 响应式调整 window.addEventListener('resize', function() { if (crowdingChart) { crowdingChart.resize(); } }); } /** * 渲染分位数表格 */ function renderPercentileTable(percentiles, metricName) { // 清空表格 percentileTable.innerHTML = ''; // 格式化函数 const formatValue = value => value ? value.toFixed(2) : 'N/A'; // 获取百分位类别 const getPercentileClass = value => { if (value <= 33) return 'percent-low'; if (value <= 66) return 'percent-medium'; return 'percent-high'; }; // 创建表格行 const rows = [ { label: '当前行业均值', value: formatValue(percentiles.current), class: 'current-value' // 添加特殊样式类 }, { label: '历史百分位', value: `${formatValue(percentiles.percentile)}%`, class: getPercentileClass(percentiles.percentile) }, { label: '历史最小值', value: formatValue(percentiles.min), class: 'percent-low' // 绿色 }, { label: '历史最大值', value: formatValue(percentiles.max), class: 'percent-high' // 红色 }, { label: '历史均值', value: formatValue(percentiles.mean) }, { label: '历史中位数', value: formatValue(percentiles.median) }, { label: '历史第一四分位', value: formatValue(percentiles.q1), class: 'percent-low' // 绿色 }, { label: '历史第三四分位', value: formatValue(percentiles.q3), class: 'percent-high' // 红色 } ]; // 添加行到表格 rows.forEach(row => { const tr = document.createElement('tr'); // 添加特殊类以突出当前值行 if (row.label === '当前行业均值' || row.label === '历史百分位') { tr.classList.add('highlight-row'); } const tdLabel = document.createElement('td'); tdLabel.textContent = row.label; tr.appendChild(tdLabel); const tdValue = document.createElement('td'); tdValue.textContent = row.value; if (row.class) { tdValue.classList.add('percent-value', row.class); } tr.appendChild(tdValue); percentileTable.appendChild(tr); }); } /** * 渲染行业统计数据 */ function renderIndustryStats(percentiles) { // 清空表格 industryStatsTable.innerHTML = ''; // 获取用户选择的开始日期,如果没有选择则显示"最近三年" const userStartDate = startDateInput.value ? startDateInput.value : "最近三年"; // 创建表格行 const rows = [ { label: '行业成份股数量', value: percentiles.stock_count + '只' }, { label: '分析开始日期', value: userStartDate } ]; // 添加行到表格 rows.forEach(row => { const tr = document.createElement('tr'); const tdLabel = document.createElement('td'); tdLabel.textContent = row.label; tr.appendChild(tdLabel); const tdValue = document.createElement('td'); tdValue.textContent = row.value; tr.appendChild(tdValue); industryStatsTable.appendChild(tr); }); } /** * 渲染拥挤度统计数据 */ function renderCrowdingStats(crowding) { // 清空表格 crowdingTable.innerHTML = ''; // 获取拥挤度级别的颜色类 const getCrowdingLevelClass = level => { switch(level) { case '不拥挤': case '较不拥挤': return 'percent-low'; // 绿色 case '中性': return 'percent-medium'; // 黄色 case '较为拥挤': case '极度拥挤': return 'percent-high'; // 红色 default: return ''; } }; // 创建表格行 const rows = [ { label: '当前成交占比', value: crowding.current_ratio.toFixed(2) + '%', tooltip: '行业成交额占全市场成交额的百分比' }, { label: '历史百分位', value: crowding.current_percentile.toFixed(2) + '%', class: getCrowdingLevelClass(crowding.level), tooltip: '当前行业成交占比在历史分布中的位置' }, { label: '拥挤度评级', value: crowding.level, class: getCrowdingLevelClass(crowding.level), tooltip: '根据历史百分位划分的拥挤度等级' } ]; // 添加行到表格 rows.forEach(row => { const tr = document.createElement('tr'); const tdLabel = document.createElement('td'); tdLabel.textContent = row.label; // 添加工具提示 if (row.tooltip) { tdLabel.title = row.tooltip; tdLabel.style.cursor = 'help'; } tr.appendChild(tdLabel); const tdValue = document.createElement('td'); tdValue.textContent = row.value; if (row.class) { tdValue.classList.add('percent-value', row.class); } tr.appendChild(tdValue); crowdingTable.appendChild(tr); }); } /** * 显示错误信息 */ function showError(message) { errorMessage.textContent = message; errorAlert.classList.remove('d-none'); // 滚动到错误信息 errorAlert.scrollIntoView({ behavior: 'smooth', block: 'start' }); } /** * 重置表单 */ function resetForm() { industryForm.reset(); // 隐藏结果和错误信息 resultCard.classList.add('d-none'); errorAlert.classList.add('d-none'); // 销毁图表 if (valuationChart) { valuationChart.dispose(); valuationChart = null; } if (crowdingChart) { crowdingChart.dispose(); crowdingChart = null; } } /** * 显示/隐藏加载状态 */ function showLoading(isLoading) { if (isLoading) { loadingSpinner.classList.remove('d-none'); analyzeBtn.disabled = true; } else { loadingSpinner.classList.add('d-none'); analyzeBtn.disabled = false; } } });