123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- <template>
- <div class="boxplot-container">
- <div ref="chartRef" style="width: 100%; height: 500px;"></div>
- </div>
- </template>
- <!--各种重金属的箱图-->
- <script setup lang="ts">
- import * as echarts from 'echarts';
- import { ref, onMounted, onUnmounted } from 'vue';
- import axios from 'axios';
- // 明确定义数据类型
- interface HeavyMetalData {
- sampleId: string;
- Cr: number | null;
- As: number | null;
- Cd: number | null;
- Hg: number | null;
- Pb: number | null;
- }
- const METALS = ['Cr', 'As', 'Cd', 'Hg', 'Pb'] as const;
- type MetalType = typeof METALS[number];
- const METAL_LABELS: Record<MetalType, string> = {
- Cr: '铬(Cr)',
- As: '砷(As)',
- Cd: '镉(Cd)',
- Hg: '汞(Hg)',
- Pb: '铅(Pb)'
- };
- // 图表变量
- const chartRef = ref<HTMLElement | null>(null);
- const chartInstance = ref<echarts.ECharts | null>(null);
- const metalData = ref<HeavyMetalData[]>([]);
- let resizeHandler: (() => void) | null = null; // 用于存储resize处理函数
- // 数据清洗函数
- const cleanData = (rawValue: any): number | null => {
- if (typeof rawValue === 'string') {
- const num = parseFloat(rawValue);
- return isNaN(num) || num < 0 ? null : num;
- }
- return typeof rawValue === 'number' && rawValue >= 0 ? rawValue : null;
- };
- // 修复后的四分位数计算算法
- const calculateBoxplotStats = (values: number[]): [number, number, number, number, number] | null => {
- if (values.length < 5) return null; // 至少需要5个数据点才能生成有效的箱线图
-
- // 升序排序
- const sorted = [...values].sort((a, b) => a - b);
- const n = sorted.length;
- // 正确的分位位置计算
- const quantile = (p: number) => {
- const pos = (n + 1) * p;
- const lowerIndex = Math.max(0, Math.min(n - 1, Math.floor(pos) - 1));
- const fraction = pos - Math.floor(pos);
-
- if (lowerIndex >= n - 1) return sorted[n - 1];
- return sorted[lowerIndex] + fraction * (sorted[lowerIndex + 1] - sorted[lowerIndex]);
- };
- return [
- sorted[0], // 最小值
- quantile(0.25), // Q1
- quantile(0.5), // 中位数
- quantile(0.75), // Q3
- sorted[n - 1] // 最大值
- ];
- };
- // 渲染图表
- const renderBoxplot = () => {
- if (!chartRef.value || metalData.value.length === 0) return;
-
- // 移除旧的resize监听器
- if (resizeHandler) {
- window.removeEventListener('resize', resizeHandler);
- }
- // 分组收集每种金属的有效数值
- const metalValues = Object.fromEntries(
- METALS.map(metal => [
- metal,
- metalData.value
- .map(item => item[metal])
- .filter((val): val is number => val !== null)
- ])
- ) as Record<MetalType, number[]>;
- // 准备箱线图数据
- const validBoxplotData: ([number, number, number, number, number] | null)[] =
- METALS.map(metal => calculateBoxplotStats(metalValues[metal]));
- // ECharts配置
- const option: echarts.EChartsOption = {
- backgroundColor: '#FFFFFF',
- title: {
- text: '重金属浓度分布箱线图',
- left: 'center',
- textStyle: { color: '#333', fontSize: 16 }
- },
- tooltip: {
- trigger: 'item',
- formatter: (params: any) => {
- const metalIndex = params.dataIndex;
- const metal = METALS[metalIndex];
- const stats = validBoxplotData[metalIndex];
-
- // 处理空数据情况(修复图片中的null错误)
- if (stats === null || stats[0] === null) {
- return `<span style="color:#ff0000">${METAL_LABELS[metal]}数据不足,无法生成统计值</span>`;
- }
-
- // 类型安全解构(确保所有值都是number类型)
- const [min, q1, median, q3, max] = stats;
-
- return `
- <b>${METAL_LABELS[metal]}</b><br/>
- 最小值: ${min.toFixed(4)} mg/L<br/>
- 下四分位: ${q1.toFixed(4)} mg/L<br/>
- 中位数: ${median.toFixed(4)} mg/L<br/>
- 上四分位: ${q3.toFixed(4)} mg/L<br/>
- 最大值: ${max.toFixed(4)} mg/L
- `;
- }
- },
- xAxis: {
- type: 'category',
- data: METALS.map(metal => METAL_LABELS[metal]),
- axisLabel: { color: '#333', interval: 0 }
- },
- yAxis: {
- type: 'value',
- name: '浓度(mg/L)',
- nameTextStyle: { color: '#333' },
- axisLabel: {
- color: '#333',
- formatter: (value: number) => value.toFixed(4)
- }
- },
- series: [{
- type: 'boxplot',
- // 过滤无效数据(解决ts 2322错误)
- data: validBoxplotData.filter(arr => arr !== null) as [number, number, number, number, number][],
- itemStyle: {
- color: '#4285F4',
- borderWidth: 1.5
- },
- emphasis: {
- itemStyle: {
- borderColor: '#333',
- borderWidth: 2
- }
- }
- }]
- };
- // 初始化图表
- if (chartInstance.value) {
- chartInstance.value.dispose();
- }
- chartInstance.value = echarts.init(chartRef.value);
- chartInstance.value.setOption(option);
-
- // 响应式处理
- resizeHandler = () => chartInstance.value?.resize();
- window.addEventListener('resize', resizeHandler);
- };
- // 数据加载
- const loadData = async () => {
- try {
- const response = await axios.get<any[]>(
- 'http://localhost:3000/table/Water_assay_data',
- { timeout: 5000 }
- );
-
- // 数据转换与过滤
- metalData.value = response.data
- .map(item => ({
- sampleId: String(item.sampleId),
- Cr: cleanData(item.Cr),
- As: cleanData(item.As),
- Cd: cleanData(item.Cd),
- Hg: cleanData(item.Hg),
- Pb: cleanData(item.Pb)
- }))
- // 修复:允许部分有效数据
- .filter(item => METALS.some(metal => item[metal] !== null));
-
- renderBoxplot();
- } catch (error) {
- console.error('数据加载失败:', error);
- alert('数据加载错误,请查看控制台日志');
- }
- };
- onMounted(() => loadData());
- onUnmounted(() => {
- // 清理资源
- if (resizeHandler) {
- window.removeEventListener('resize', resizeHandler);
- }
- chartInstance.value?.dispose();
- });
- </script>
- <style scoped>
- .boxplot-container {
- width: 100%;
- max-width: 1000px;
- margin: 20px auto;
- padding: 20px;
- background: white;
- border-radius: 8px;
- box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
- }
- </style>
|