|
|
@@ -25,13 +25,7 @@
|
|
|
<!-- 1️⃣ 作物态Cd指标 -->
|
|
|
<section class="mb-4 chart-container">
|
|
|
<h3 class="section-title text-base font-semibold">作物态Cd主要指标</h3>
|
|
|
- <div class="flex flex-wrap mb-3">
|
|
|
- <div class="legend-item" v-for="(item, index) in fieldConfig.pollution" :key="index">
|
|
|
- <div class="legend-color" :style="{ backgroundColor: item.color }"></div>
|
|
|
- <span>{{ item.legendName }}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div ref="cdBarChart" style="width: 100%; height: 320px;"></div>
|
|
|
+ <div ref="cdBarChart" style="width: 100%; height: 415px;"></div>
|
|
|
</section>
|
|
|
|
|
|
<!-- 2️⃣ 养分元素 -->
|
|
|
@@ -93,24 +87,21 @@ const fieldConfig = {
|
|
|
pollution: [
|
|
|
{
|
|
|
key: '002_0002IDW',
|
|
|
- name: '002_0002IDW', // 横坐标保持原标识
|
|
|
- legendName: '002_0002IDW:粒径在0.002~0.02mm的粉粒组分质量占比,%', // 图例显示中文
|
|
|
+ name: '粉粒组分质量占比', // 横坐标保持原标识
|
|
|
color: '#5470c6' ,
|
|
|
unit:"%",
|
|
|
convert: false
|
|
|
},
|
|
|
{
|
|
|
key: '02_002IDW',
|
|
|
- name: '02_002IDW', // 横坐标保持原标识
|
|
|
- legendName: '02_002IDW:粒径在0.02~0.2mm的砂粒组分质量占比,%', // 图例显示中文
|
|
|
+ name: '砂粒组分质量占比', // 横坐标保持原标识
|
|
|
color: '#91cc75' ,
|
|
|
unit:"%",
|
|
|
convert: false
|
|
|
},
|
|
|
{
|
|
|
key: '2_02IDW',
|
|
|
- name: '2_02IDW', // 横坐标保持原标识
|
|
|
- legendName: '2_02IDW:粒径大于2mm的石砾组分质量占比,%', // 图例显示中文
|
|
|
+ name: '石砾组分质量占比', // 横坐标保持原标识
|
|
|
color: '#fac858' ,
|
|
|
unit:"%",
|
|
|
convert: false
|
|
|
@@ -136,116 +127,15 @@ const fieldConfig = {
|
|
|
// 数据请求(作物态Cd接口)
|
|
|
const fetchData = async () => {
|
|
|
try {
|
|
|
- const apiUrl = 'http://localhost:8000/api/vector/export/all?table_name=CropCd_input_data&format=json';
|
|
|
+ const apiUrl = 'http://localhost:8000/api/vector/stats/CropCd_input_data';
|
|
|
const response = await axios.get(apiUrl);
|
|
|
|
|
|
- // 接口返回格式判断(GeoJSON或直接数组)
|
|
|
- const rawData = response.data.features
|
|
|
- ? response.data.features.map(f => f.properties)
|
|
|
- : response.data;
|
|
|
- return rawData;
|
|
|
+ return response.data;
|
|
|
} catch (err) {
|
|
|
throw new Error('数据加载失败: ' + err.message);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 分位数计算(采用QUARTILE.INC算法)
|
|
|
-const calculatePercentile = (sortedArray, percentile) => {
|
|
|
- const n = sortedArray.length;
|
|
|
- if (n === 0) return null;
|
|
|
- if (percentile <= 0) return sortedArray[0];
|
|
|
- if (percentile >= 100) return sortedArray[n - 1];
|
|
|
-
|
|
|
- const index = (n - 1) * (percentile / 100);
|
|
|
- const lowerIndex = Math.floor(index);
|
|
|
- const upperIndex = lowerIndex + 1;
|
|
|
- const fraction = index - lowerIndex;
|
|
|
-
|
|
|
- if (upperIndex >= n) return sortedArray[lowerIndex];
|
|
|
- return sortedArray[lowerIndex] + fraction * (sortedArray[upperIndex] - sortedArray[lowerIndex]);
|
|
|
-};
|
|
|
-
|
|
|
-// 计算单个字段的统计量
|
|
|
-const calculateFieldStats = (data, fieldKey, fieldName) => {
|
|
|
- // 获取字段配置
|
|
|
- const fieldConfigItem = [
|
|
|
- ...fieldConfig.pollution,
|
|
|
- ...fieldConfig.nutrient,
|
|
|
- ...fieldConfig.extra
|
|
|
- ].find(f => f.key === fieldKey);
|
|
|
- // 提取并清洗数据
|
|
|
- const rawValues = data.map(item => item[fieldKey]);
|
|
|
- const values = rawValues
|
|
|
- .map((val, idx) => {
|
|
|
- let num = Number(val);
|
|
|
- // 应用单位转换
|
|
|
- if (fieldConfigItem?.convert && fieldConfigItem.conversionFactor) {
|
|
|
- num = num * fieldConfigItem.conversionFactor;
|
|
|
- }
|
|
|
- //if (isNaN(num)) log(`无效数据: 第${idx+1}条 → ${val}`, fieldName);
|
|
|
- return isNaN(num) ? null : num;
|
|
|
- })
|
|
|
- .filter(v => v !== null);
|
|
|
-
|
|
|
- if (values.length === 0) {
|
|
|
- //log(`无有效数据`, fieldName);
|
|
|
- return { key: fieldKey, name: fieldName, min: null, q1: null, median: null, q3: null, max: null };
|
|
|
- }
|
|
|
-
|
|
|
- // 排序并计算统计量
|
|
|
- const sorted = [...values].sort((a, b) => a - b);
|
|
|
- const min = sorted[0];
|
|
|
- const max = sorted[sorted.length - 1];
|
|
|
- const q1 = calculatePercentile(sorted, 25);
|
|
|
- const median = calculatePercentile(sorted, 50);
|
|
|
- const q3 = calculatePercentile(sorted, 75);
|
|
|
-
|
|
|
- // 强制校正顺序
|
|
|
- const sortedStats = [min, q1, median, q3, max].sort((a, b) => a - b);
|
|
|
- return {
|
|
|
- key: fieldKey,
|
|
|
- name: fieldName,
|
|
|
- min: sortedStats[0],
|
|
|
- q1: sortedStats[1],
|
|
|
- median: sortedStats[2],
|
|
|
- q3: sortedStats[3],
|
|
|
- max: sortedStats[4],
|
|
|
- avg: values.reduce((sum, val) => sum + val, 0) / values.length // 计算平均值
|
|
|
- };
|
|
|
-};
|
|
|
-
|
|
|
-// 批量计算所有统计量
|
|
|
-const calculateAllStats = (data) => {
|
|
|
- // 1. 作物态Cd指标统计
|
|
|
- pollutionStats.value = fieldConfig.pollution.map(field => {
|
|
|
- const stats =calculateFieldStats(data, field.key, field.name);
|
|
|
- //console.log(`${field.name}统计结果:`,stats);
|
|
|
- return stats;
|
|
|
- });
|
|
|
-
|
|
|
- // 2. 养分元素统计
|
|
|
- nutrientStats.value = fieldConfig.nutrient.map(field =>
|
|
|
- calculateFieldStats(data, field.key, field.name)
|
|
|
- );
|
|
|
-
|
|
|
- // 3. 其他理化性质统计
|
|
|
- extraStats.value = fieldConfig.extra.map(field =>
|
|
|
- calculateFieldStats(data, field.key, field.name)
|
|
|
- );
|
|
|
-
|
|
|
- // 更新平均值统计
|
|
|
- const cd002Stats = pollutionStats.value.find(s => s.key === '002_0002IDW');
|
|
|
- const cd02Stats = pollutionStats.value.find(s => s.key === '02_002IDW');
|
|
|
- const cd2Stats = pollutionStats.value.find(s => s.key === '2_02IDW');
|
|
|
-
|
|
|
- stats.value = {
|
|
|
- cd002Avg: cd002Stats?.avg || 0,
|
|
|
- cd02Avg: cd02Stats?.avg || 0,
|
|
|
- cd2Avg: cd2Stats?.avg || 0,
|
|
|
- samples: data.length
|
|
|
- };
|
|
|
-};
|
|
|
-
|
|
|
// 构建箱线图数据
|
|
|
const buildBoxplotData = (statsArray) => {
|
|
|
return statsArray.map(stat => {
|
|
|
@@ -256,69 +146,74 @@ const buildBoxplotData = (statsArray) => {
|
|
|
|
|
|
// 初始化作物态Cd指标图表
|
|
|
const initPollutionChart = () => {
|
|
|
- if (chartInstanceCd) chartInstanceCd.dispose();
|
|
|
- chartInstanceCd = echarts.init(cdBarChart.value);
|
|
|
-
|
|
|
- const xAxisData = fieldConfig.pollution.map(f => f.name);
|
|
|
- const barData = pollutionStats.value.map(stat => stat.avg || 0);
|
|
|
-
|
|
|
- chartInstanceCd.setOption({
|
|
|
- title: { },
|
|
|
- tooltip: {
|
|
|
- trigger: 'axis',
|
|
|
- formatter: (params) => `${params[0].name}<br/>平均值: ${params[0].value.toFixed(4)}`
|
|
|
- },
|
|
|
- grid: { top: 40, right: 15, bottom: 50, left: 40 },
|
|
|
- xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 12,rotate:30 } },
|
|
|
- yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
|
|
|
- series: [{
|
|
|
- name: '平均值', type: "bar",
|
|
|
- itemStyle: { color: (p) => fieldConfig.pollution[p.dataIndex].color },
|
|
|
- data: barData
|
|
|
- }]
|
|
|
+ nextTick(() => {
|
|
|
+ // 若图表实例已存在,先销毁避免内存泄漏
|
|
|
+ if (chartInstanceCd) chartInstanceCd.dispose();
|
|
|
+ // 校验 DOM 存在性(防止 ref 未关联到有效 DOM)
|
|
|
+ if (!cdBarChart.value) return;
|
|
|
+ // 初始化 ECharts 实例
|
|
|
+ chartInstanceCd = echarts.init(cdBarChart.value);
|
|
|
+
|
|
|
+ const xAxisData = fieldConfig.pollution.map(f => f.name);
|
|
|
+ const barData = pollutionStats.value.map(stat => stat.avg || 0);
|
|
|
+
|
|
|
+ chartInstanceCd.setOption({
|
|
|
+ title: {},
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ formatter: (params) => `${params[0].name}<br/>平均值: ${params[0].value.toFixed(4)}`
|
|
|
+ },
|
|
|
+ grid: { top: 40, right: 15, bottom: '18%', left: '10%' },
|
|
|
+ xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 12, rotate: 30 } },
|
|
|
+ yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
|
|
|
+ series: [{
|
|
|
+ name: '平均值', type: "bar",
|
|
|
+ itemStyle: { color: (p) => fieldConfig.pollution[p.dataIndex].color },
|
|
|
+ data: barData
|
|
|
+ }]
|
|
|
+ });
|
|
|
});
|
|
|
-
|
|
|
};
|
|
|
|
|
|
// 初始化养分元素图表
|
|
|
const initNutrientChart = () => {
|
|
|
- if (chartInstanceNutrient) chartInstanceNutrient.dispose();
|
|
|
- chartInstanceNutrient = echarts.init(nutrientBoxChart.value);
|
|
|
-
|
|
|
- const xAxisData = fieldConfig.nutrient.map(f => f.name);
|
|
|
- const boxData = buildBoxplotData(nutrientStats.value);
|
|
|
-
|
|
|
- chartInstanceNutrient.setOption({
|
|
|
- title: { text: "主要养分元素分布", left: 'center', textStyle: { fontSize: 14 } },
|
|
|
- tooltip: {
|
|
|
- trigger: "item",
|
|
|
- formatter: (params) => {
|
|
|
- const stat = nutrientStats.value[params.dataIndex];
|
|
|
- const fieldConfigItem = fieldConfig.nutrient.find(f => f.key === stat.key);
|
|
|
- return formatTooltip(stat, fieldConfigItem?.unit);
|
|
|
- }
|
|
|
- },
|
|
|
- grid: { top: 40, right: 15, bottom: 45, left: 40 },
|
|
|
- xAxis: {
|
|
|
- type: "category",
|
|
|
- data: xAxisData,
|
|
|
- axisLabel: {
|
|
|
- fontSize: 11,
|
|
|
- rotate: 30,
|
|
|
- }
|
|
|
- },
|
|
|
- yAxis: {
|
|
|
- type: "value",
|
|
|
- name: 'mg/kg',
|
|
|
- nameTextStyle: { fontSize: 12 },
|
|
|
- axisLabel: { fontSize: 11 }
|
|
|
- },
|
|
|
- series: [{
|
|
|
- name: '养分元素',
|
|
|
- type: "boxplot",
|
|
|
- itemStyle: { color: '#ee6666', borderColor: '#fac858' },
|
|
|
- data: boxData
|
|
|
- }]
|
|
|
+ nextTick(() => {
|
|
|
+ if (chartInstanceNutrient) chartInstanceNutrient.dispose();
|
|
|
+ if (!nutrientBoxChart.value) return;
|
|
|
+ chartInstanceNutrient = echarts.init(nutrientBoxChart.value);
|
|
|
+
|
|
|
+ const xAxisData = fieldConfig.nutrient.map(f => f.name);
|
|
|
+ const boxData = buildBoxplotData(nutrientStats.value);
|
|
|
+
|
|
|
+ chartInstanceNutrient.setOption({
|
|
|
+ title: { text: "主要养分元素分布", left: 'center', textStyle: { fontSize: 14 } },
|
|
|
+ tooltip: {
|
|
|
+ trigger: "item",
|
|
|
+ formatter: (params) => {
|
|
|
+ const stat = nutrientStats.value[params.dataIndex];
|
|
|
+ const fieldConfigItem = fieldConfig.nutrient.find(f => f.key === stat.key);
|
|
|
+ return formatTooltip(stat, fieldConfigItem?.unit);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ grid: { top: 40, right: 15, bottom: 45, left: 40 },
|
|
|
+ xAxis: {
|
|
|
+ type: "category",
|
|
|
+ data: xAxisData,
|
|
|
+ axisLabel: { fontSize: 11, rotate: 30 }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: "value",
|
|
|
+ name: 'mg/kg',
|
|
|
+ nameTextStyle: { fontSize: 12 },
|
|
|
+ axisLabel: { fontSize: 11 }
|
|
|
+ },
|
|
|
+ series: [{
|
|
|
+ name: '养分元素',
|
|
|
+ type: "boxplot",
|
|
|
+ itemStyle: { color: '#ee6666', borderColor: '#fac858' },
|
|
|
+ data: boxData
|
|
|
+ }]
|
|
|
+ });
|
|
|
});
|
|
|
};
|
|
|
|
|
|
@@ -337,7 +232,7 @@ const initExtraChart = () => {
|
|
|
formatter: (params) => formatTooltip(extraStats.value[params.dataIndex])
|
|
|
},
|
|
|
grid: { top: 40, right: 15, bottom: 40, left: 40 },
|
|
|
- xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 11,rotate:30} },
|
|
|
+ xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 11} },
|
|
|
yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
|
|
|
series: [{
|
|
|
name: '理化性质', type: "boxplot",
|
|
|
@@ -370,9 +265,143 @@ const initCharts = async () => {
|
|
|
isLoading.value = true;
|
|
|
error.value = null;
|
|
|
|
|
|
- const data = await fetchData();
|
|
|
- calculateAllStats(data);
|
|
|
+ const statsData = await fetchData(); // 新接口返回的统计数据
|
|
|
|
|
|
+ // -------- 1. 处理「作物态Cd指标」统计 --------
|
|
|
+ pollutionStats.value = fieldConfig.pollution.map(field => {
|
|
|
+ const fieldStats = statsData[field.key]; // 从接口数据中取对应字段的统计
|
|
|
+ if (!fieldStats) {
|
|
|
+ return {
|
|
|
+ key: field.key,
|
|
|
+ name: field.name,
|
|
|
+ min: null,
|
|
|
+ q1: null,
|
|
|
+ median: null,
|
|
|
+ q3: null,
|
|
|
+ max: null,
|
|
|
+ avg: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+ // (可选)单位转换:若接口返回原始值,需按fieldConfig的convert规则转换
|
|
|
+ let min = fieldStats.min;
|
|
|
+ let q1 = fieldStats.q1;
|
|
|
+ let median = fieldStats.median;
|
|
|
+ let q3 = fieldStats.q3;
|
|
|
+ let max = fieldStats.max;
|
|
|
+ let avg = fieldStats.mean;
|
|
|
+ if (field.convert && field.conversionFactor) {
|
|
|
+ min *= field.conversionFactor;
|
|
|
+ q1 *= field.conversionFactor;
|
|
|
+ median *= field.conversionFactor;
|
|
|
+ q3 *= field.conversionFactor;
|
|
|
+ max *= field.conversionFactor;
|
|
|
+ avg *= field.conversionFactor;
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ key: field.key,
|
|
|
+ name: field.name,
|
|
|
+ min,
|
|
|
+ q1,
|
|
|
+ median,
|
|
|
+ q3,
|
|
|
+ max,
|
|
|
+ avg
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ // -------- 2. 处理「主要养分元素」统计 --------
|
|
|
+ nutrientStats.value = fieldConfig.nutrient.map(field => {
|
|
|
+ const fieldStats = statsData[field.key];
|
|
|
+ if (!fieldStats) {
|
|
|
+ return {
|
|
|
+ key: field.key,
|
|
|
+ name: field.name,
|
|
|
+ min: null,
|
|
|
+ q1: null,
|
|
|
+ median: null,
|
|
|
+ q3: null,
|
|
|
+ max: null,
|
|
|
+ avg: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+ // (可选)单位转换
|
|
|
+ let min = fieldStats.min;
|
|
|
+ let q1 = fieldStats.q1;
|
|
|
+ let median = fieldStats.median;
|
|
|
+ let q3 = fieldStats.q3;
|
|
|
+ let max = fieldStats.max;
|
|
|
+ let avg = fieldStats.mean;
|
|
|
+ if (field.convert && field.conversionFactor) {
|
|
|
+ min *= field.conversionFactor;
|
|
|
+ q1 *= field.conversionFactor;
|
|
|
+ median *= field.conversionFactor;
|
|
|
+ q3 *= field.conversionFactor;
|
|
|
+ max *= field.conversionFactor;
|
|
|
+ avg *= field.conversionFactor;
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ key: field.key,
|
|
|
+ name: field.name,
|
|
|
+ min,
|
|
|
+ q1,
|
|
|
+ median,
|
|
|
+ q3,
|
|
|
+ max,
|
|
|
+ avg
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ // -------- 3. 处理「其他理化性质」统计 --------
|
|
|
+ extraStats.value = fieldConfig.extra.map(field => {
|
|
|
+ const fieldStats = statsData[field.key];
|
|
|
+ if (!fieldStats) {
|
|
|
+ return {
|
|
|
+ key: field.key,
|
|
|
+ name: field.name,
|
|
|
+ min: null,
|
|
|
+ q1: null,
|
|
|
+ median: null,
|
|
|
+ q3: null,
|
|
|
+ max: null,
|
|
|
+ avg: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+ // (可选)单位转换
|
|
|
+ let min = fieldStats.min;
|
|
|
+ let q1 = fieldStats.q1;
|
|
|
+ let median = fieldStats.median;
|
|
|
+ let q3 = fieldStats.q3;
|
|
|
+ let max = fieldStats.max;
|
|
|
+ let avg = fieldStats.mean;
|
|
|
+ if (field.convert && field.conversionFactor) {
|
|
|
+ min *= field.conversionFactor;
|
|
|
+ q1 *= field.conversionFactor;
|
|
|
+ median *= field.conversionFactor;
|
|
|
+ q3 *= field.conversionFactor;
|
|
|
+ max *= field.conversionFactor;
|
|
|
+ avg *= field.conversionFactor;
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ key: field.key,
|
|
|
+ name: field.name,
|
|
|
+ min,
|
|
|
+ q1,
|
|
|
+ median,
|
|
|
+ q3,
|
|
|
+ max,
|
|
|
+ avg
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ // -------- 更新「样本数量」等汇总统计 --------
|
|
|
+ const firstFieldKey = fieldConfig.pollution[0]?.key;
|
|
|
+ stats.value = {
|
|
|
+ cd002Avg: pollutionStats.value.find(s => s.key === '002_0002IDW')?.avg || 0,
|
|
|
+ cd02Avg: pollutionStats.value.find(s => s.key === '02_002IDW')?.avg || 0,
|
|
|
+ cd2Avg: pollutionStats.value.find(s => s.key === '2_02IDW')?.avg || 0,
|
|
|
+ samples: statsData[firstFieldKey]?.count || 0 // 从接口的count字段取样本数
|
|
|
+ };
|
|
|
+
|
|
|
// 初始化图表
|
|
|
initPollutionChart();
|
|
|
initNutrientChart();
|
|
|
@@ -392,7 +421,7 @@ onMounted(() => {
|
|
|
|
|
|
// 窗口resize处理
|
|
|
const handleResize = () => {
|
|
|
- [chartInstanceCd, chartInstanceNutrient, chartInstanceExtra, chartInstancePopup]
|
|
|
+ [chartInstanceCd, chartInstanceNutrient, chartInstanceExtra]
|
|
|
.forEach(inst => inst && inst.resize());
|
|
|
};
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
@@ -400,7 +429,7 @@ onMounted(() => {
|
|
|
// 组件卸载清理
|
|
|
return () => {
|
|
|
window.removeEventListener('resize', handleResize);
|
|
|
- [chartInstanceCd, chartInstanceNutrient, chartInstanceExtra, chartInstancePopup]
|
|
|
+ [chartInstanceCd, chartInstanceNutrient, chartInstanceExtra]
|
|
|
.forEach(inst => inst && inst.dispose());
|
|
|
};
|
|
|
});
|