From ef15856979e6f735f5660af18e5763d8d29fbb4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=AC=A3=E7=84=B6?= <12817866+yueranzhishang@user.noreply.gitee.com> Date: Wed, 11 Jun 2025 17:29:26 +0000 Subject: [PATCH] =?UTF-8?q?add=20neo/static/js/underWater.js.=20=E6=B0=B4?= =?UTF-8?q?=E4=B8=8B=E7=B3=BB=E7=BB=9F=E6=B7=BB=E5=8A=A0js=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 刘欣然 <12817866+yueranzhishang@user.noreply.gitee.com> --- neo/static/js/underWater.js | 650 ++++++++++++++++++++++++++++++++++++ 1 file changed, 650 insertions(+) create mode 100644 neo/static/js/underWater.js diff --git a/neo/static/js/underWater.js b/neo/static/js/underWater.js new file mode 100644 index 0000000..f20a43c --- /dev/null +++ b/neo/static/js/underWater.js @@ -0,0 +1,650 @@ +// 鱼类介绍信息-文字+图片 +const speciesInfoData = { + "Bream": { + description: "鲤鱼(Bream)是一种常见的淡水鱼,体型较大,背部呈青灰色,腹部银白色。它们通常生活在湖泊和河流的底部,以水生植物和小型无脊椎动物为食。鲤鱼是欧洲重要的食用鱼之一。", + imageUrl: "../static/images/Bream.jpg" + }, + "Roach": { + description: "拟鲤(Roach)是一种小型淡水鱼,广泛分布于欧洲和亚洲。它们身体侧扁,呈银灰色,背鳍和尾鳍略带红色。拟鲤通常成群活动,以水生昆虫和植物为食。", + imageUrl: "../static/images/Roach.jpg" + }, + "Whitefish": { + description: "白鱼(Whitefish)是一类冷水性鱼类的统称,主要生活在北半球的淡水湖泊中。它们体型修长,银白色,是重要的商业捕捞鱼类。白鱼肉质细嫩,味道鲜美。", + imageUrl: "../static/images/Whitefish.jpg" + }, + "Parkki": { + description: "派克鱼(Parkki)是一种生活在北欧淡水中的鱼类,体型中等,背部呈橄榄绿色,腹部银白色。它们是肉食性鱼类,以小鱼和水生昆虫为食。", + imageUrl: "../static/images/Parkki.jpg" + }, + "Perch": { + description: "鲈鱼(Perch)是一种受欢迎的淡水鱼,身体呈黄绿色,有5-9条垂直的黑色条纹。它们是凶猛的捕食者,以小鱼和甲壳类为食。鲈鱼是垂钓者的热门目标。", + imageUrl: "../static/images/Whitefish.jpg" + }, + "Pike": { + description: "梭子鱼(Pike)是一种大型淡水掠食鱼,身体细长,口部大而尖。它们潜伏在水草中伏击猎物,包括鱼类、青蛙甚至小型水鸟。梭子鱼是运动钓鱼的热门目标。", + imageUrl: "../static/images/Whitefish.jpg" + }, + "Smelt": { + description: "胡瓜鱼(Smelt)是一种小型鱼类,生活在北半球的淡水和咸水中。它们身体细长,半透明,通常成群活动。胡瓜鱼是许多大型鱼类和鸟类的重要食物来源。", + imageUrl: "../static/images/Whitefish.jpg" + } +}; + +// 计算箱线图的各值d3 +function calculateBoxplotData(data) { + const sorted = [...data].sort((a, b) => a - b); + return [ + d3.min(sorted), // min + d3.quantile(sorted, 0.25), // Q1 + d3.quantile(sorted, 0.5), // median + d3.quantile(sorted, 0.75), // Q3 + d3.max(sorted) // max + ]; +} + +// 计算整体平均重量 +function calculateOverallAvg(fishData) { + const validWeights = fishData.weights.filter(w => !isNaN(w)); + if (validWeights.length === 0) return 0; + const sum = validWeights.reduce((a, b) => a + b, 0); + return Math.round(sum / validWeights.length); +} + +// 加载csv文件 +async function loadCSVData() { + try { + const response = await fetch('/static/Fish.csv'); + if (!response.ok) throw new Error("Failed to load CSV"); + const csvText = await response.text(); + return parseCSV(csvText); + } catch (error) { + console.error('Error loading CSV:', error); + return null; + } +} + +function parseCSV(csvText) { + const lines = csvText.split('\n'); + const result = { + species: [], + weights: [], + lengths: [], + heights: [], + widths: [], + records: [], + speciesData: {} + }; + + // 跳过表头行(如果有) + const startLine = lines[0].includes('Species') ? 1 : 0; + + for (let i = startLine; i < lines.length; i++) { + if (!lines[i].trim()) continue; + + // 处理CSV行,考虑到可能有逗号在引号中 + const row = lines[i].split(',').map(item => item.trim()); + const species = row[0]; + const weight = parseFloat(row[1]); + const length1 = parseFloat(row[2]); + const length2 = parseFloat(row[3]); + const length3 = parseFloat(row[4]); + const height = parseFloat(row[5]); + const width = parseFloat(row[6]); + + // 添加到总数据 + result.species.push(species); + result.weights.push(weight); + result.lengths.push(length3); // 使用Length3作为主要长度 + result.heights.push(height); + result.widths.push(width); + + // 按物种分组数据 + if (!result.speciesData[species]) { + result.speciesData[species] = { + weights: [], + lengths: [], + heights: [], + widths: [] + }; + } + + result.speciesData[species].weights.push(weight); + result.speciesData[species].lengths.push(length3); + result.speciesData[species].heights.push(height); + result.speciesData[species].widths.push(width); + + // 添加完整记录 + result.records.push({ + species, + weight, + length1, + length2, + length3, + height, + width + }); + } + + // 计算每个物种的统计数据 + const speciesStats = {}; + Object.keys(result.speciesData).forEach(species => { + const data = result.speciesData[species]; + + speciesStats[species] = { + count: data.weights.length, + avgWeight: calculateAverage(data.weights), + minWeight: Math.min(...data.weights), + maxWeight: Math.max(...data.weights), + avgLength: calculateAverage(data.lengths), + minLength: Math.min(...data.lengths), + maxLength: Math.max(...data.lengths), + avgHeight: calculateAverage(data.heights), + minHeight: Math.min(...data.heights), + maxHeight: Math.max(...data.heights), + avgWidth: calculateAverage(data.widths), + minWidth: Math.min(...data.widths), + maxWidth: Math.max(...data.widths) + }; + }); + + // 计算唯一物种列表 + result.uniqueSpecies = [...new Set(result.species)]; + result.totalRecords = result.records.length; + result.speciesStats = speciesStats; + + return result; +} + +// 计算平均数 +function calculateAverage(values) { + const validValues = values.filter(v => !isNaN(v)); + if (validValues.length === 0) return 0; + const sum = validValues.reduce((a, b) => a + b, 0); + return sum / validValues.length; +} + +// 初始化图表 +async function initCharts() { + const fishData = await loadCSVData(); + if (!fishData) { + alert('无法加载数据,请检查数据文件'); + return; + } + + // 在 initCharts 函数末尾添加: + window.fishData = fishData; + + // 更新统计卡片 + document.getElementById('species-count').textContent = fishData.uniqueSpecies.length; + document.getElementById('total-records').textContent = fishData.totalRecords; + document.getElementById('avg-weight').textContent = calculateOverallAvg(fishData) + 'g'; + + // 1. 鱼类分布饼图 - 按物种计数 + const speciesCounts = {}; + fishData.species.forEach(species => { + speciesCounts[species] = (speciesCounts[species] || 0) + 1; + }); + + const distChart = echarts.init(document.getElementById('species-distribution')); + const distOption = { + tooltip: { + trigger: 'item', + formatter: '{a}
{b}: {c} ({d}%)' + }, + legend: { + orient: 'vertical', + right: 10, + top: 'center', + data: fishData.uniqueSpecies + }, + series: [ + { + name: '鱼类分布', + type: 'pie', + radius: ['40%', '70%'], + avoidLabelOverlap: false, + itemStyle: { + color: function(params) { + const colorList = ['#7b51db', '#5a8dee', '#00c9db', '#00d4a0', '#ffc107', '#ff6b6b', '#a162e8']; + return colorList[params.dataIndex % colorList.length]; + }, + borderRadius: 10, + borderColor: '#fff', + borderWidth: 2 + }, + label: { + show: false, + position: 'center' + }, + emphasis: { + label: { + show: true, + fontSize: '18', + fontWeight: 'bold' + } + }, + labelLine: { + show: false + }, + data: fishData.uniqueSpecies.map(species => ({ + value: speciesCounts[species], + name: species + })) + } + ] + }; + distChart.setOption(distOption); + + // 2. 重量长度分布箱线图 + const weightChart = echarts.init(document.getElementById('weight-distribution')); + const weightOption = { + tooltip: { + trigger: 'item', + axisPointer: { + type: 'shadow' + }, + formatter: function(params) { + const data = params.data; + return [ + '鱼类: ' + params.name + '
', + '最大值: ' + data[5].toFixed(2) + 'g
', + '上四分位数: ' + data[4].toFixed(2) + 'g
', + '中位数: ' + data[3].toFixed(2) + 'g
', + '下四分位数: ' + data[2].toFixed(2) + 'g
', + '最小值: ' + data[1].toFixed(2) + 'g' + ].join(''); + } + }, + toolbox: { + show: true, + feature: { + dataView: { + readOnly: true + }, + saveAsImage: {}, + } + }, + grid: { + left: '10%', + right: '10%', + bottom: '15%' + }, + xAxis: { + type: 'category', + data: fishData.uniqueSpecies, + axisLabel: { + rotate: 30 + }, + boundaryGap: true, + nameGap: 30, + axisLine: { + lineStyle: { + color: '#999' + } + }, + axisTick: { + alignWithLabel: true + } + }, + yAxis: { + type: 'value', + name: '重量(g)', + scale: true, + splitLine: { + lineStyle: { + type: 'dashed' + } + } + }, + series: [{ + name: '重量分布', + type: 'boxplot', + data: fishData.uniqueSpecies.map(species => { + const weights = fishData.speciesData[species].weights; + // 过滤无效值并确保数据已排序 + const validWeights = weights.filter(w => !isNaN(w) && w !== null); + return calculateBoxplotData(validWeights); + }), + itemStyle: { + color: '#7b51db', + borderColor: '#333', + borderWidth: 1 + }, + emphasis: { + itemStyle: { + borderWidth: 2, + shadowBlur: 5, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.3)' + } + }, + boxWidth: [20, 50] + }] + }; + weightChart.setOption(weightOption); + + const lengthChart = echarts.init(document.getElementById('length-distribution')); + const lengthOption = { + toolbox: { + show: true, + feature: { + dataView: { + readOnly: true + }, + saveAsImage: {}, + } + }, + tooltip: { + trigger: 'item', + axisPointer: { + type: 'shadow' + }, + formatter: function(params) { + const data = params.data; + return [ + '鱼类: ' + params.name + '
', + '最大值: ' + data[5].toFixed(2) + 'cm
', + '上四分位数: ' + data[4].toFixed(2) + 'cm
', + '中位数: ' + data[3].toFixed(2) + 'cm
', + '下四分位数: ' + data[2].toFixed(2) + 'cm
', + '最小值: ' + data[1].toFixed(2) + 'cm' + ].join(''); + } + }, + grid: { + left: '10%', + right: '10%', + bottom: '15%' + }, + xAxis: { + type: 'category', + data: fishData.uniqueSpecies, + axisLabel: { + rotate: 30 + }, + boundaryGap: true, + nameGap: 30, + axisLine: { + lineStyle: { + color: '#999' + } + }, + axisTick: { + alignWithLabel: true + } + }, + yAxis: { + type: 'value', + name: '长度(cm)', + scale: true, + splitLine: { + lineStyle: { + type: 'dashed' + } + } + }, + series: [{ + name: '长度分布', + type: 'boxplot', + data: fishData.uniqueSpecies.map(species => { + const lengths = fishData.speciesData[species].lengths; + const validLengths = lengths.filter(l => !isNaN(l) && l !== null); + return calculateBoxplotData(validLengths); + }), + itemStyle: { + color: '#6495ED', // 使用不同颜色区分 + borderColor: '#333', + borderWidth: 1 + }, + emphasis: { + itemStyle: { + borderWidth: 2, + shadowBlur: 5, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.3)' + } + }, + boxWidth: [20, 50] + }] + }; + lengthChart.setOption(lengthOption); + + // 3. 初始化具体鱼类图表 + updateSpeciesDetail(fishData, fishData.uniqueSpecies[0]); + + // 更新下拉选择框 + const select = document.getElementById('species-select'); + select.innerHTML = ''; + fishData.uniqueSpecies.forEach(species => { + const option = document.createElement('option'); + option.value = species; + option.textContent = species; + select.appendChild(option); + }); +} + +function calculatePercentile(sortedArray, percentile) { + const index = (percentile / 100) * (sortedArray.length - 1); + const lower = Math.floor(index); + const upper = lower + 1; + const weight = index % 1; + + if (upper >= sortedArray.length) return sortedArray[lower]; + return sortedArray[lower] * (1 - weight) + sortedArray[upper] * weight; +} + +// 更新具体种类鱼类 +function updateSpeciesDetail(fishData, species) { + const stats = fishData.speciesStats[species]; + + // 更新鱼类介绍 + const descriptionElement = document.getElementById('species-description'); + descriptionElement.textContent = speciesInfoData[species]?.description || "暂无该鱼类的详细介绍。"; + + // 更新鱼类图片 + const imageElement = document.getElementById('species-image'); + const imageUrl = speciesInfoData[species]?.imageUrl; + + if (imageUrl) { + imageElement.src = imageUrl; + imageElement.alt = species; + imageElement.style.display = 'block'; + } else { + imageElement.style.display = 'none'; + } + + // 更新具体鱼类图表 + // 创建四个小图表 + const createMiniChart = (containerId, title, avg, min, max, unit) => { + const chart = echarts.init(document.getElementById(containerId)); + const option = { + title: { + text: title, + left: 'center', + textStyle: { + fontSize: 14 + } + }, + tooltip: { + trigger: 'axis', + formatter: `{b0}
+ 平均: {c0}${unit}
+ 最小: {c1}${unit}
+ 最大: {c2}${unit}` + }, + xAxis: { + type: 'category', + data: [''], + show: false // 隐藏x轴标签 + }, + yAxis: { + type: 'value', + name: unit, + nameLocation: 'end' + }, + grid: { + top: '25%', + bottom: '15%' + }, + series: [ + { + name: '平均', + type: 'bar', + data: [avg], + itemStyle: { color: '#7b51db' }, + barWidth: '40%' + }, + { + name: '最小', + type: 'bar', + data: [min], + itemStyle: { color: '#5a8dee', opacity: 0.7 }, + barWidth: '30%' + }, + { + name: '最大', + type: 'bar', + data: [max], + itemStyle: { color: '#ff6b6b', opacity: 0.7 }, + barWidth: '30%' + } + ], + toolbox: { + show: true, + feature: { + dataView: { + readOnly: true + }, + saveAsImage: {}, + } + } + }; + chart.setOption(option); + return chart; + }; + + // 初始化四个图表 + createMiniChart('weight-chart', '重量指标', stats.avgWeight, stats.minWeight, stats.maxWeight, 'g'); + createMiniChart('length-chart', '长度指标', stats.avgLength, stats.minLength, stats.maxLength, 'cm'); + createMiniChart('height-chart', '高度指标', stats.avgHeight, stats.minHeight, stats.maxHeight, 'cm'); + createMiniChart('width-chart', '宽度指标', stats.avgWidth, stats.minWidth, stats.maxWidth, 'cm'); + + // 更新统计表格 + const statsTable = document.getElementById('species-stats'); + statsTable.innerHTML = ` + + 重量(g) + ${stats.avgWeight.toFixed(2)} + ${stats.minWeight} + ${stats.maxWeight} + + + 长度(cm) + ${stats.avgLength.toFixed(2)} + ${stats.minLength} + ${stats.maxLength} + + + 高度(cm) + ${stats.avgHeight.toFixed(2)} + ${stats.minHeight} + ${stats.maxHeight} + + + 宽度(cm) + ${stats.avgWidth.toFixed(2)} + ${stats.minWidth} + ${stats.maxWidth} + + + 记录数 + ${stats.count} + + `; + + const speciesRecords = fishData.records.filter(record => record.species === species); + + // 更新重量长度散点图 + const scatterChart = echarts.init(document.getElementById('scatter-chart')); + const scatterOption = { + title: { + text: `${species} 长度与重量关系散点图`, + left: 'center' + }, + tooltip: { + formatter: function(params) { + return ` + 长度: ${params.value[0]} cm
+ 重量: ${params.value[1]} g`; + } + }, + xAxis: { + type: 'value', + name: '长度 (cm)', + nameLocation: 'middle', + nameGap: 30, + min: stats.minLength*0.9, + max: stats.maxLength*1.1 + }, + yAxis: { + name: '重量 (g)', + nameLocation: 'middle', + nameGap: 30, + min: stats.minWeight*0.6, + max: stats.maxWeight*1.1 + }, + series: [{ + data: speciesRecords.map(record => [ + record.length3, // x轴 - 长度 + record.weight // y轴 - 宽度 + + ]), + type: 'scatter', + + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)' + } + } + }], + toolbox: { + show: true, + feature: { + dataView: { + readOnly: true + }, + saveAsImage: {}, + } + } + }; + scatterChart.setOption(scatterOption); +} + +// 修改事件监听器: +document.addEventListener('DOMContentLoaded', function() { + initCharts(); + + // 绑定下拉选择事件 + document.getElementById('species-select').addEventListener('change', function() { + const fishData = window.fishData; + if (fishData) { + updateSpeciesDetail(fishData, this.value); + } + }); + + // 窗口大小变化时重新调整图表大小 + window.addEventListener('resize', function() { + echarts.init(document.getElementById('species-distribution')).resize(); + echarts.init(document.getElementById('weight-distribution')).resize(); + echarts.init(document.getElementById('length-distribution')).resize(); + echarts.init(document.getElementById('weight-chart')).resize(); + echarts.init(document.getElementById('length-chart')).resize(); + echarts.init(document.getElementById('height-chart')).resize(); + echarts.init(document.getElementById('width-chart')).resize(); + echarts.init(document.getElementById('scatter-chart')).resize(); + }); + + +}); \ No newline at end of file -- Gitee