waterassaydata1.vue 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. <template>
  2. <div class="boxplot-container">
  3. <div ref="chartRef" style="width: 100%; height: 500px;"></div>
  4. </div>
  5. </template>
  6. <!--各种重金属的箱图-->
  7. <script setup lang="ts">
  8. import * as echarts from 'echarts';
  9. import { ref, onMounted, onUnmounted } from 'vue';
  10. import axios from 'axios';
  11. // 明确定义数据类型
  12. interface HeavyMetalData {
  13. sampleId: string;
  14. Cr: number | null;
  15. As: number | null;
  16. Cd: number | null;
  17. Hg: number | null;
  18. Pb: number | null;
  19. }
  20. const METALS = ['Cr', 'As', 'Cd', 'Hg', 'Pb'] as const;
  21. type MetalType = typeof METALS[number];
  22. const METAL_LABELS: Record<MetalType, string> = {
  23. Cr: '铬(Cr)',
  24. As: '砷(As)',
  25. Cd: '镉(Cd)',
  26. Hg: '汞(Hg)',
  27. Pb: '铅(Pb)'
  28. };
  29. // 图表变量
  30. const chartRef = ref<HTMLElement | null>(null);
  31. const chartInstance = ref<echarts.ECharts | null>(null);
  32. const metalData = ref<HeavyMetalData[]>([]);
  33. let resizeHandler: (() => void) | null = null; // 用于存储resize处理函数
  34. // 数据清洗函数
  35. const cleanData = (rawValue: any): number | null => {
  36. if (typeof rawValue === 'string') {
  37. const num = parseFloat(rawValue);
  38. return isNaN(num) || num < 0 ? null : num;
  39. }
  40. return typeof rawValue === 'number' && rawValue >= 0 ? rawValue : null;
  41. };
  42. // 修复后的四分位数计算算法
  43. const calculateBoxplotStats = (values: number[]): [number, number, number, number, number] | null => {
  44. if (values.length < 5) return null; // 至少需要5个数据点才能生成有效的箱线图
  45. // 升序排序
  46. const sorted = [...values].sort((a, b) => a - b);
  47. const n = sorted.length;
  48. // 正确的分位位置计算
  49. const quantile = (p: number) => {
  50. const pos = (n + 1) * p;
  51. const lowerIndex = Math.max(0, Math.min(n - 1, Math.floor(pos) - 1));
  52. const fraction = pos - Math.floor(pos);
  53. if (lowerIndex >= n - 1) return sorted[n - 1];
  54. return sorted[lowerIndex] + fraction * (sorted[lowerIndex + 1] - sorted[lowerIndex]);
  55. };
  56. return [
  57. sorted[0], // 最小值
  58. quantile(0.25), // Q1
  59. quantile(0.5), // 中位数
  60. quantile(0.75), // Q3
  61. sorted[n - 1] // 最大值
  62. ];
  63. };
  64. // 渲染图表
  65. const renderBoxplot = () => {
  66. if (!chartRef.value || metalData.value.length === 0) return;
  67. // 移除旧的resize监听器
  68. if (resizeHandler) {
  69. window.removeEventListener('resize', resizeHandler);
  70. }
  71. // 分组收集每种金属的有效数值
  72. const metalValues = Object.fromEntries(
  73. METALS.map(metal => [
  74. metal,
  75. metalData.value
  76. .map(item => item[metal])
  77. .filter((val): val is number => val !== null)
  78. ])
  79. ) as Record<MetalType, number[]>;
  80. // 准备箱线图数据
  81. const validBoxplotData: ([number, number, number, number, number] | null)[] =
  82. METALS.map(metal => calculateBoxplotStats(metalValues[metal]));
  83. // ECharts配置
  84. const option: echarts.EChartsOption = {
  85. backgroundColor: '#FFFFFF',
  86. title: {
  87. text: '重金属浓度分布箱线图',
  88. left: 'center',
  89. textStyle: { color: '#333', fontSize: 16 }
  90. },
  91. tooltip: {
  92. trigger: 'item',
  93. formatter: (params: any) => {
  94. const metalIndex = params.dataIndex;
  95. const metal = METALS[metalIndex];
  96. const stats = validBoxplotData[metalIndex];
  97. // 处理空数据情况(修复图片中的null错误)
  98. if (stats === null || stats[0] === null) {
  99. return `<span style="color:#ff0000">${METAL_LABELS[metal]}数据不足,无法生成统计值</span>`;
  100. }
  101. // 类型安全解构(确保所有值都是number类型)
  102. const [min, q1, median, q3, max] = stats;
  103. return `
  104. <b>${METAL_LABELS[metal]}</b><br/>
  105. 最小值: ${min.toFixed(4)} mg/L<br/>
  106. 下四分位: ${q1.toFixed(4)} mg/L<br/>
  107. 中位数: ${median.toFixed(4)} mg/L<br/>
  108. 上四分位: ${q3.toFixed(4)} mg/L<br/>
  109. 最大值: ${max.toFixed(4)} mg/L
  110. `;
  111. }
  112. },
  113. xAxis: {
  114. type: 'category',
  115. data: METALS.map(metal => METAL_LABELS[metal]),
  116. axisLabel: { color: '#333', interval: 0 }
  117. },
  118. yAxis: {
  119. type: 'value',
  120. name: '浓度(mg/L)',
  121. nameTextStyle: { color: '#333' },
  122. axisLabel: {
  123. color: '#333',
  124. formatter: (value: number) => value.toFixed(4)
  125. }
  126. },
  127. series: [{
  128. type: 'boxplot',
  129. // 过滤无效数据(解决ts 2322错误)
  130. data: validBoxplotData.filter(arr => arr !== null) as [number, number, number, number, number][],
  131. itemStyle: {
  132. color: '#4285F4',
  133. borderWidth: 1.5
  134. },
  135. emphasis: {
  136. itemStyle: {
  137. borderColor: '#333',
  138. borderWidth: 2
  139. }
  140. }
  141. }]
  142. };
  143. // 初始化图表
  144. if (chartInstance.value) {
  145. chartInstance.value.dispose();
  146. }
  147. chartInstance.value = echarts.init(chartRef.value);
  148. chartInstance.value.setOption(option);
  149. // 响应式处理
  150. resizeHandler = () => chartInstance.value?.resize();
  151. window.addEventListener('resize', resizeHandler);
  152. };
  153. // 数据加载
  154. const loadData = async () => {
  155. try {
  156. const response = await axios.get<any[]>(
  157. 'http://localhost:3000/table/Water_assay_data',
  158. { timeout: 5000 }
  159. );
  160. // 数据转换与过滤
  161. metalData.value = response.data
  162. .map(item => ({
  163. sampleId: String(item.sampleId),
  164. Cr: cleanData(item.Cr),
  165. As: cleanData(item.As),
  166. Cd: cleanData(item.Cd),
  167. Hg: cleanData(item.Hg),
  168. Pb: cleanData(item.Pb)
  169. }))
  170. // 修复:允许部分有效数据
  171. .filter(item => METALS.some(metal => item[metal] !== null));
  172. renderBoxplot();
  173. } catch (error) {
  174. console.error('数据加载失败:', error);
  175. alert('数据加载错误,请查看控制台日志');
  176. }
  177. };
  178. onMounted(() => loadData());
  179. onUnmounted(() => {
  180. // 清理资源
  181. if (resizeHandler) {
  182. window.removeEventListener('resize', resizeHandler);
  183. }
  184. chartInstance.value?.dispose();
  185. });
  186. </script>
  187. <style scoped>
  188. .boxplot-container {
  189. width: 100%;
  190. max-width: 1000px;
  191. margin: 20px auto;
  192. padding: 20px;
  193. background: white;
  194. border-radius: 8px;
  195. box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
  196. }
  197. </style>