refluxcdStatictics.vue 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. <template>
  2. <div class="container">
  3. <div class="chart-container">
  4. <div ref="chartRef" class="chart-wrapper"></div>
  5. </div>
  6. </div>
  7. </template>
  8. <script setup>
  9. import { ref, onMounted, onUnmounted, nextTick } from 'vue'
  10. import * as echarts from 'echarts'
  11. const indicators = [
  12. //{ key: 'Al3_plus', name: 'Al3_plus', color: '#3498db' },
  13. //{ key: 'CEC', name: 'CEC', color: '#2ecc71' },
  14. // { key: 'CL', name: 'CL', color: '#e74c3c' },
  15. { key: 'Delta_pH', name: 'Delta_pH', color: '#f39c12' },
  16. //{ key: 'H_plus', name: 'H_plus', color: '#9b59b6' },
  17. //{ key: 'N', name: 'N', color: '#1abc9c' },
  18. //{ key: 'OM', name: 'OM', color: '#d35400' }
  19. ];
  20. let chart = ref(null) // 存储 ECharts 实例
  21. let chartData = ref([]) // 存储图表数据
  22. const chartRef = ref(null) // 图表容器
  23. const statsByIndex = ref([]);//存储每个指标的统计量
  24. const isLoading = ref(true);
  25. const error = ref(false);
  26. const errorMessage = ref('');
  27. function initChart() {
  28. // 确保图表容器已渲染
  29. if (!chartRef.value) return;
  30. //console.log('图表容器宽高:', chartRef.value.clientWidth, chartRef.value.clientHeight);
  31. // 初始化 ECharts 实例
  32. chart.value = echarts.init(chartRef.value);
  33. const option = {
  34. tooltip: {
  35. trigger: 'item',
  36. axisPointer: { type: 'shadow' },
  37. formatter: function(params) {
  38. const stat = statsByIndex.value[params.dataIndex];
  39. if (!stat || stat.min === null) {
  40. return `<div style="font-weight:bold;margin-bottom:8px;color:#2c3e50;">${params.name}</div>
  41. <div>无有效数据</div>`;
  42. }
  43. return `
  44. <div style="font-weight:bold;margin-bottom:8px;color:#2c3e50;">${stat.name}</div>
  45. <div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;">
  46. <span style="color:#7f8c8d;">最小值:</span> <span style="text-align:right;font-weight:bold;">${stat.min}</span>
  47. <span style="color:#7f8c8d;">下四分位:</span> <span style="text-align:right;font-weight:bold;color:#e74c3c;">${stat.q1}</span>
  48. <span style="color:#7f8c8d;">中位数:</span> <span style="text-align:right;font-weight:bold;color:#3498db;">${stat.median}</span>
  49. <span style="color:#7f8c8d;">上四分位:</span> <span style="text-align:right;font-weight:bold;color:#e74c3c;">${stat.q3}</span>
  50. <span style="color:#7f8c8d;">最大值:</span> <span style="text-align:right;font-weight:bold;">${stat.max}</span>
  51. </div>
  52. `;
  53. }
  54. },
  55. title: {
  56. text: '酸化加剧指标箱线图展示',
  57. left: 'center',
  58. textStyle: {
  59. fontSize: 18,
  60. fontWeight: 'normal'
  61. },
  62. top: 10
  63. },
  64. grid: {
  65. left: '0px',
  66. right: '0px',
  67. bottom: '0px',
  68. top: '70px',
  69. containLabel: true
  70. },
  71. xAxis: {
  72. type: 'category',
  73. data: indicators.map(i => i.name),
  74. axisLabel: {
  75. rotate: 30,
  76. fontSize: 12,
  77. margin: 15
  78. },
  79. axisTick: {
  80. alignWithLabel: true
  81. },
  82. },
  83. yAxis: {
  84. type: 'value',
  85. name: '数值',
  86. nameTextStyle: {
  87. fontSize: 14
  88. },
  89. nameGap: 25,
  90. splitLine: {
  91. lineStyle: {
  92. type: 'dashed',
  93. color: '#ddd'
  94. }
  95. }
  96. },
  97. series: [{
  98. name: '指标分布',
  99. type: 'boxplot',
  100. itemStyle: {
  101. color: function(params) {
  102. return indicators[params.dataIndex].color;
  103. },
  104. borderWidth: 2
  105. },
  106. emphasis: {
  107. itemStyle: {
  108. shadowBlur: 10,
  109. shadowColor: 'rgba(0, 0, 0, 0.3)'
  110. }
  111. }
  112. }]
  113. };
  114. chart.value.setOption(option);
  115. }
  116. function calculatePercentile(sortedArray, percentile) {
  117. const n = sortedArray.length;
  118. if (n === 0) return null;
  119. if (percentile <= 0) return sortedArray[0];
  120. if (percentile >= 100) return sortedArray[n - 1];
  121. const index = (n - 1) * (percentile / 100);
  122. const lowerIndex = Math.floor(index);
  123. const upperIndex = lowerIndex + 1;
  124. const fraction = index - lowerIndex;
  125. if (upperIndex >= n) return sortedArray[lowerIndex];
  126. return sortedArray[lowerIndex] + fraction * (sortedArray[upperIndex] - sortedArray[lowerIndex]);
  127. }
  128. function calculateBoxplotStats(data, indicators) {
  129. const boxplotData = [];
  130. const statsArray = [];
  131. indicators.forEach(indicator => {
  132. const values = data
  133. .map(item => Number(item[indicator.key]))
  134. .filter(val => !isNaN(val))
  135. .sort((a, b) => a - b);
  136. if (values.length === 0) {
  137. boxplotData.push([null, null, null, null, null]);
  138. statsArray.push({
  139. min: null, q1: null, median: null, q3: null, max: null,
  140. name: indicator.name,
  141. color: indicator.color
  142. });
  143. } else {
  144. const min = Math.min(...values);
  145. const max = Math.max(...values);
  146. const q1 = calculatePercentile(values, 25);
  147. const median = calculatePercentile(values, 50);
  148. const q3 = calculatePercentile(values, 75);
  149. boxplotData.push([min, q1, median, q3, max]);
  150. statsArray.push({
  151. min, q1, median, q3, max,
  152. name: indicator.name,
  153. color: indicator.color
  154. });
  155. }
  156. });
  157. return { boxplotData, statsArray };
  158. }
  159. async function loadData() {
  160. isLoading.value = true;
  161. error.value = false;
  162. try {
  163. const response = await fetch('http://localhost:5000/api/table-data?table_name=dataset_60');
  164. const result = await response.json();
  165. chartData.value = result.data;
  166. const { boxplotData, statsArray } = calculateBoxplotStats(chartData.value, indicators);
  167. statsByIndex.value = statsArray;
  168. chart.value.setOption({
  169. series: [{
  170. data: boxplotData
  171. }]
  172. });
  173. } catch (err) {
  174. error.value = true;
  175. errorMessage.value = `加载失败: ${err.message || '网络错误'}`;
  176. console.error('数据加载失败:', err);
  177. } finally {
  178. isLoading.value = false;
  179. }
  180. }
  181. const handleResize = () => {
  182. if (chart.value) {
  183. chart.value.resize();
  184. }
  185. };
  186. onMounted(() => {
  187. nextTick(() => {
  188. initChart();
  189. loadData();
  190. });
  191. window.addEventListener('resize', handleResize);
  192. });
  193. onUnmounted(() => {
  194. if (chart.value) {
  195. chart.value.dispose(); // 销毁 ECharts 实例
  196. chart.value = null;
  197. }
  198. window.removeEventListener('resize', handleResize);
  199. });
  200. </script>
  201. <style scoped>
  202. * {
  203. margin: 0;
  204. padding: 0;
  205. box-sizing: border-box;
  206. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  207. }
  208. .container {
  209. width: 100%;
  210. height: 100%;
  211. margin: 0 auto;
  212. background: white;
  213. border-radius: 12px;
  214. box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
  215. overflow: hidden;
  216. }
  217. .chart-container {
  218. width: 100%;
  219. height: 100%;
  220. padding: 20px;
  221. position: relative;
  222. }
  223. .chart-wrapper {
  224. width: 100%;
  225. height: 100%;
  226. min-height: 200px;
  227. }
  228. </style>