heavyMetalEnterprisechart.vue 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. <template>
  2. <div class="region-average-chart">
  3. <!-- 图表容器 -->
  4. <div ref="chartRef" class="chart-box"></div>
  5. <!-- 加载/错误提示 -->
  6. <div v-if="loading" class="status">数据加载中...</div>
  7. <div v-else-if="error" class="status error">{{ error }}</div>
  8. </div>
  9. </template>
  10. <script setup>
  11. import { ref, onMounted, onUnmounted } from 'vue';
  12. import * as echarts from 'echarts';
  13. import axios from 'axios';
  14. import { icon } from 'leaflet';
  15. // ========== 1. 配置项(根据实际需求修改) ==========
  16. const POLLUTANT_API = 'http://localhost:3000/table/Atmosphere_company_data';
  17. // 韶关市区县白名单(确保与接口返回的“所属区县”完全一致)
  18. const SG_REGIONS = [
  19. '浈江区', '武江区', '曲江区', '乐昌市',
  20. '南雄市', '始兴县', '仁化县', '翁源县',
  21. '新丰县', '乳源县'
  22. ];
  23. // 排除的非污染物字段(根据接口结构调整)
  24. const EXCLUDE_FIELDS = [
  25. '污染源序号', '公司', '类型', '经度', '纬度' // 这些字段不是污染物,需排除
  26. ];
  27. // ========== 2. 响应式数据 & 变量 ==========
  28. const chartRef = ref(null); // ECharts容器引用
  29. const loading = ref(true); // 加载状态
  30. const error = ref(''); // 错误信息
  31. let myChart = null; // ECharts实例
  32. // ========== 3. 数据处理核心逻辑 ==========
  33. const processData = (apiData) => {
  34. // 3.1 空数据保护
  35. if (!apiData || apiData.length === 0) {
  36. console.error('接口返回空数据');
  37. return { regions: [], series: [], totalSamples: 0 };
  38. }
  39. // 3.2 提取**污染物字段**(自动过滤非数值字段)
  40. const pollutantFields = Object.keys(apiData[0])
  41. .filter(key =>
  42. !EXCLUDE_FIELDS.includes(key) && // 排除非污染物字段
  43. !isNaN(parseFloat(apiData[0][key])) // 只保留数值字段
  44. );
  45. // 3.3 无有效污染物字段的保护
  46. if (pollutantFields.length === 0) {
  47. console.error('未识别到有效污染物字段,请检查 EXCLUDE_FIELDS 配置');
  48. return { regions: [], series: [], totalSamples: 0 };
  49. }
  50. // 3.4 按区县分组统计(总和 + 计数)
  51. const regionGroups = {}; // 结构:{ 区县: { 污染物: { sum, count } } }
  52. const globalStats={};//全局统计对象
  53. let totalSamples = 0; // 总样本数
  54. pollutantFields.forEach(field=>{
  55. globalStats[field] = {sum:0,count:0};
  56. })
  57. apiData.forEach(item => {
  58. const region = item['所属区县'] || '未知区县'; // 提取区县
  59. totalSamples++;
  60. // 初始化区县分组(每个污染物字段都要初始化)
  61. if (!regionGroups[region]) {
  62. regionGroups[region] = {};
  63. pollutantFields.forEach(field => {
  64. regionGroups[region][field] = { sum: 0, count: 0 };
  65. });
  66. }
  67. // 累加每个污染物的浓度
  68. pollutantFields.forEach(field => {
  69. const value = parseFloat(item[field]);
  70. if (!isNaN(value)) { // 过滤无效值(如空字符串、null)
  71. regionGroups[region][field].sum += value;
  72. regionGroups[region][field].count++;
  73. globalStats[field].sum+=value;
  74. globalStats[field].count++;
  75. }
  76. });
  77. });
  78. // 3.5 筛选**有效区县**(仅韶关市区县白名单内的区域)
  79. const validRegions = SG_REGIONS.filter(region =>
  80. regionGroups[region] !== undefined
  81. );
  82. validRegions.push('全部样本平均');//全局平均的分类
  83. // 3.6 构建ECharts所需的series数据
  84. const series = pollutantFields.map((field, index) => ({
  85. name: field, // 污染物名称作为系列名
  86. type: 'bar',
  87. data: validRegions.map(region => {
  88. if(region === '全部样本平均'){
  89. const stats = globalStats[field];
  90. return stats.count>0
  91. ?(stats.sum/stats.count).toFixed(2)
  92. :0;
  93. }
  94. const group = regionGroups[region][field];
  95. return group.count > 0
  96. ? (group.sum / group.count).toFixed(2) // 计算平均值,保留2位小数
  97. : 0; // 无数据时显示0
  98. }),
  99. itemStyle: { color: (params)=>{
  100. return params.name ==='全部样本平均'?'#ff0000' : '#1890ff'
  101. } },
  102. label: {
  103. show: true,
  104. position: 'top',
  105. fontSize: 18,
  106. color: '#333'
  107. }
  108. }));
  109. return { regions: validRegions, series, totalSamples };
  110. };
  111. // ========== 4. ECharts 初始化 & 更新 ==========
  112. const initChart = ({ regions, series, totalSamples }) => {
  113. if (!chartRef.value) return;
  114. // 销毁旧实例(避免重复初始化)
  115. if (myChart && !myChart.isDisposed()) {
  116. myChart.dispose();
  117. }
  118. myChart = echarts.init(chartRef.value);
  119. const option = {
  120. title: {
  121. left: 'center',
  122. subtext: `(基于 ${totalSamples} 个有效样本计算)`,
  123. subtextStyle: { fontSize: 14, color: '#666' }
  124. },
  125. tooltip: {
  126. trigger: 'axis',
  127. formatter: (params) => {
  128. const region = params[0].name;
  129. let content = `${region}:<br>`;
  130. params.forEach(p => {
  131. content += `${p.seriesName}: ${p.value} t/a<br>`;
  132. });
  133. return content;
  134. },
  135. textStyle: { fontSize: 16 }
  136. },
  137. xAxis: {
  138. type: 'category',
  139. data: regions,
  140. axisLabel: {
  141. formatter: val => val.replace('韶关市', ''), // 简化显示(可选)
  142. fontSize: 16,
  143. interval:0
  144. }
  145. },
  146. yAxis: {
  147. type: 'value',
  148. name: '浓度 (t/a)',
  149. nameTextStyle: { fontSize: 16 },
  150. axisLabel: { fontSize: 16 }
  151. },
  152. legend: {
  153. data: series.map(s => s.name),
  154. bottom: 10,
  155. textStyle: { fontSize: 14 },
  156. icon:'none'
  157. },
  158. series:series,
  159. grid: { left: '5%', right: '5%', bottom: '15%', containLabel: true }
  160. };
  161. myChart.setOption(option);
  162. };
  163. // ========== 5. 生命周期 & 响应式 ==========
  164. onMounted(async () => {
  165. loading.value = true;
  166. try {
  167. // 1. 请求接口数据
  168. const res = await axios.get(POLLUTANT_API, { timeout: 10000 });
  169. // 2. 处理数据
  170. const processedData = processData(res.data);
  171. // 3. 渲染图表
  172. initChart(processedData);
  173. } catch (err) {
  174. // 细化错误提示(网络/服务器/超时等)
  175. if (err.response) {
  176. error.value = `数据加载失败(${err.response.status})`;
  177. } else if (err.request) {
  178. error.value = '网络错误,无法连接服务器';
  179. } else {
  180. error.value = `加载失败:${err.message}`;
  181. }
  182. console.error('接口请求错误:', err);
  183. } finally {
  184. loading.value = false;
  185. }
  186. });
  187. // 窗口resize时自动调整图表
  188. const resizeHandler = () => {
  189. if (myChart) myChart.resize();
  190. };
  191. onMounted(() => window.addEventListener('resize', resizeHandler));
  192. onUnmounted(() => window.removeEventListener('resize', resizeHandler));
  193. </script>
  194. <style scoped>
  195. .region-average-chart {
  196. width: 100%;
  197. max-width: 1200px;
  198. margin: 20px auto;
  199. position: relative;
  200. }
  201. .chart-box {
  202. width: 100%;
  203. height: 600px;
  204. min-height: 400px;
  205. background: white;
  206. border-radius: 8px;
  207. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  208. }
  209. .status {
  210. position: absolute;
  211. top: 50%;
  212. left: 50%;
  213. transform: translate(-50%, -50%);
  214. padding: 12px 20px;
  215. background: rgba(255,255,255,0.9);
  216. border-radius: 6px;
  217. font-size: 16px;
  218. }
  219. .error {
  220. color: #ff4d4f;
  221. font-weight: bold;
  222. }
  223. </style>