index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. <template>
  2. <div class="popularity-prediction">
  3. <div class="tree-map-container">
  4. <!-- 筛选条件 -->
  5. <el-card class="filter-card">
  6. <el-form :inline="true" :model="form" size="small">
  7. <el-form-item label="病原体">
  8. <el-select v-model="form.pathogenId" placeholder="请选择病原体">
  9. <el-option v-for="item in pathogen" :key="item.id" :label="item.name" :value="item.id"></el-option>
  10. </el-select>
  11. </el-form-item>
  12. <el-form-item label="时间">
  13. <el-date-picker v-model="form.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width: 240px" value-format="yyyy-MM-dd"/>
  14. </el-form-item>
  15. <el-form-item>
  16. <el-button type="primary" @click="initData">查询</el-button>
  17. </el-form-item>
  18. </el-form>
  19. </el-card>
  20. <!-- <div class="phylo-tree">
  21. <div id="phyloTree" ref="treeContainer" style="width: 100%; height: 500px;"></div>
  22. </div> -->
  23. <div style="width:100%;height:500px;overflow:auto;">
  24. <svg id="tree_display" ></svg>
  25. </div>
  26. <el-row :gutter="20">
  27. <el-col :span="12">
  28. <div class="world-map">
  29. <div id="worldVirusMap" style="width: 100%; height: 500px;"></div>
  30. </div>
  31. </el-col>
  32. <el-col :span="12">
  33. <div class="china-map">
  34. <div id="chinaVirusMap" style="width: 100%; height: 500px;"></div>
  35. </div>
  36. </el-col>
  37. </el-row>
  38. </div>
  39. </div>
  40. </template>
  41. <script>
  42. import * as echarts from 'echarts';
  43. import worldJson from 'echarts/map/json/world.json';
  44. import chinaJson from 'echarts/map/json/china.json';
  45. import { generatesjbytfzzbqkInfo, generatezgbytfzzbqkInfo,pathogenConfigGetAll } from '@/api/statistics/report';
  46. import { getPathogenConfig } from '@/api/system/pathogenConfig';
  47. echarts.registerMap('world', worldJson);
  48. echarts.registerMap('china', chinaJson);
  49. export default {
  50. name: 'PopularityPrediction',
  51. data() {
  52. return {
  53. form: {
  54. bytName: "",
  55. endDate: "",
  56. startDate: "",
  57. dateRange: []
  58. },
  59. worldData: [],
  60. chinaData: [],
  61. worldMapChart: null,
  62. chinaMapChart: null,
  63. pathogen:null
  64. };
  65. },
  66. methods: {
  67. renderWorldVirusMap() {
  68. if (!this.worldMapChart) {
  69. this.worldMapChart = echarts.init(document.getElementById('worldVirusMap'));
  70. }
  71. this.renderMap(this.worldMapChart, this.worldData, 'world');
  72. },
  73. renderChinaVirusMap() {
  74. if (!this.chinaMapChart) {
  75. this.chinaMapChart = echarts.init(document.getElementById('chinaVirusMap'));
  76. }
  77. this.renderMap(this.chinaMapChart, this.chinaData, 'china');
  78. },
  79. renderMap(chart, data, mapType) {
  80. // 处理数据,生成区域数据,保留每个病原体的数量信息
  81. const regionData = data.map(item => {
  82. const pathogenData = {};
  83. let totalValue = 0;
  84. Object.keys(item).forEach(key => {
  85. if (key!== 'area' && key!== 'eng') {
  86. const value = item[key];
  87. if (!isNaN(value) && value!== null && value!== undefined) {
  88. pathogenData[key] = value;
  89. totalValue += value;
  90. }
  91. }
  92. });
  93. return {
  94. name: item.eng || item.area || '未知区域',
  95. value: isNaN(totalValue)? 0 : totalValue,
  96. cn: item.area || '未知区域',
  97. pathogenData: pathogenData
  98. };
  99. });
  100. // 校验数据,避免 min 或 max 为 NaN
  101. let visualMapMin = 0;
  102. let visualMapMax = 0;
  103. if (regionData.length > 0) {
  104. const values = regionData.map(item => item.value).filter(value =>!isNaN(value));
  105. if (values.length > 0) {
  106. visualMapMin = Math.min(...values);
  107. visualMapMax = Math.max(...values);
  108. }
  109. }
  110. // 清空旧的配置
  111. chart.clear();
  112. chart.setOption({
  113. title: { text: mapType === 'world' ? '全球病毒分布' : '中国病毒分布', left: 'center' },
  114. tooltip: {
  115. trigger: 'item',
  116. formatter: (params) => {
  117. const name = params.data && params.data.cn? params.data.cn : '未知区域';
  118. const value = params.data &&!isNaN(params.data.value)? params.data.value : 0;
  119. const pathogenData = params.data && params.data.pathogenData? params.data.pathogenData : {};
  120. let tooltipStr = `<div>${name}</div><div>总数: ${value}</div>`;
  121. Object.keys(pathogenData).forEach(pathogen => {
  122. tooltipStr += `<div>${pathogen}: ${pathogenData[pathogen]}</div>`;
  123. });
  124. return tooltipStr;
  125. }
  126. },
  127. // geo: {
  128. // map: mapType,
  129. // roam: true,
  130. // label: {
  131. // emphasis: {
  132. // show: false
  133. // }
  134. // },
  135. // itemStyle: {
  136. // normal: {
  137. // areaColor: '#f0f0f0',
  138. // borderColor: '#999'
  139. // },
  140. // emphasis: {
  141. // areaColor: '#e0e0e0'
  142. // }
  143. // },
  144. // center: mapType === 'china' ? [104.1954, 35.8617] : [10, 10],
  145. // zoom: mapType === 'china' ? 2 : 1.2
  146. // },
  147. series: [
  148. {
  149. type: 'map',
  150. map: mapType,
  151. // coordinateSystem: 'geo',
  152. data: regionData,
  153. label: {
  154. show: false
  155. },
  156. itemStyle: {
  157. normal: {
  158. areaColor: '#f0f0f0',
  159. borderColor: '#999'
  160. },
  161. emphasis: {
  162. areaColor: '#e0e0e0',
  163. borderWidth: 2,
  164. borderColor: '#fff'
  165. }
  166. }
  167. }
  168. ],
  169. visualMap: {
  170. min: visualMapMin,
  171. max: visualMapMax,
  172. left: 'left',
  173. top: 'bottom',
  174. text: ['高', '低'],
  175. calculable: true,
  176. // inRange: {
  177. // color: ['#ffeeee', '#ffcccc', '#ff9999', '#ff6666', '#ff3333']
  178. // }
  179. }
  180. });
  181. },
  182. renderPhyloTree() {
  183. const strainColors = {
  184. A: '#5470C6',
  185. B: '#91CC75',
  186. C: '#FAC858',
  187. D: '#EE6666',
  188. E: '#73C0DE',
  189. F: '#3BA272',
  190. G: '#FC8452',
  191. H: '#9A60B4',
  192. I: '#EA7CCC',
  193. J: '#FFA500'
  194. };
  195. const strains = Object.keys(strainColors);
  196. // 生成树结构
  197. function createExampleTree(rootStrain, rootYear, rootEndYear, depth = 0, maxDepth = 5, usedSet = new Set()) {
  198. if (depth > maxDepth) return null;
  199. const key = `${rootStrain}_${rootYear}`;
  200. if (usedSet.has(key)) return null; // 跳过重复
  201. usedSet.add(key);
  202. const node = {
  203. name: `病毒${rootStrain}`,
  204. strain: rootStrain,
  205. year: rootYear,
  206. endYear: rootEndYear,
  207. children: []
  208. };
  209. if (depth < maxDepth) {
  210. const childCount = Math.floor(Math.random() * 2) + 1;
  211. for (let i = 0; i < childCount; i++) {
  212. const nextStrain = strains[(strains.indexOf(rootStrain) + i + 1) % strains.length];
  213. const childYear = Math.min(rootEndYear - 1, rootYear + 1 + Math.floor(Math.random() * 3));
  214. const childEndYear = Math.min(rootEndYear, childYear + 3 + Math.floor(Math.random() * 6));
  215. const child = createExampleTree(nextStrain, childYear, childEndYear, depth + 1, maxDepth, usedSet);
  216. if (child) node.children.push(child);
  217. }
  218. }
  219. return node;
  220. }
  221. // 生成5条主干
  222. const roots = [];
  223. for (let i = 0; i < 5; i++) {
  224. const strain = strains[i % strains.length];
  225. const startYear = 2010 + Math.floor(Math.random() * 3);
  226. const endYear = startYear + 8 + Math.floor(Math.random() * 5);
  227. roots.push(createExampleTree(strain, startYear, endYear, 0, 5, new Set()));
  228. }
  229. // 展平树结构,分配y坐标
  230. let yIndex = 0;
  231. const lines = [];
  232. function flatten(node, parentY = null, parentName = null) {
  233. const myY = yIndex++;
  234. lines.push({
  235. name: `${node.name}(${node.year}~${node.endYear})`,
  236. strain: node.strain,
  237. year: node.year,
  238. endYear: node.endYear,
  239. y: myY,
  240. parentName // 记录父节点名称
  241. });
  242. if (node.children && node.children.length) {
  243. node.children.forEach(child => flatten(child, myY, `${node.name}(${node.year}~${node.endYear})`));
  244. }
  245. }
  246. // 主干之间插入空行,增加间距
  247. roots.forEach((root, idx) => {
  248. flatten(root, null, null);
  249. if (idx !== roots.length - 1) {
  250. // 每两个主干之间插入2个空行
  251. yIndex += 3;
  252. }
  253. });
  254. // 图例
  255. const legendData = strains.map(k => `病毒${k}`);
  256. const chart = echarts.init(this.$refs.treeContainer);
  257. chart.setOption({
  258. title: { text: '病毒进化树(横坐标为年份)', left: 'center' },
  259. tooltip: {
  260. trigger: 'item',
  261. formatter: params => params.data ? params.data.name : params.name
  262. },
  263. legend: {
  264. data: legendData,
  265. orient: 'vertical',
  266. right: 10,
  267. top: 60,
  268. formatter: name => `{a|●} {b|${name}}`,
  269. textStyle: {
  270. rich: {
  271. a: { color: (params) => params, fontSize: 18 },
  272. b: { color: '#333', fontSize: 14 }
  273. }
  274. }
  275. },
  276. grid: { left: 80, right: 120, top: 60, bottom: 40 },
  277. xAxis: {
  278. type: 'value',
  279. min: 2010,
  280. max: 2025,
  281. interval: 1,
  282. name: '年份',
  283. axisLabel: { formatter: '{value}' }
  284. },
  285. yAxis: {
  286. type: 'category',
  287. data: lines.map(l => l.name),
  288. show: false,
  289. axisLabel: { show: false },
  290. splitLine: { show: false }
  291. },
  292. series: [
  293. // 横线
  294. {
  295. type: 'custom',
  296. renderItem: function(params, api) {
  297. const y = api.coord([0, params.dataIndex])[1];
  298. const x1 = api.coord([lines[params.dataIndex].year, params.dataIndex])[0];
  299. const x2 = api.coord([lines[params.dataIndex].endYear, params.dataIndex])[0];
  300. const parentY = (() => {
  301. // 查找父节点的y坐标
  302. const parentName = lines[params.dataIndex].parentName;
  303. if (!parentName) return null;
  304. const parentIdx = lines.findIndex(l => l.name === parentName);
  305. if (parentIdx === -1) return null;
  306. return api.coord([lines[params.dataIndex].year, parentIdx])[1];
  307. })();
  308. const children = [];
  309. // 折线:所有节点都加竖线
  310. let startX;
  311. let endX = x1;
  312. let startY = y;
  313. if (parentY !== null && parentY !== y) {
  314. // 分支节点:父节点到当前节点y
  315. startX = x1;
  316. startY = parentY;
  317. } else {
  318. // 主干节点:从x轴(y轴0刻度线)到当前节点
  319. startX = x1;
  320. // 获取x轴像素y坐标
  321. const xAxisY = api.coord([lines[params.dataIndex].year, -0.8])[1];
  322. startY = xAxisY;
  323. }
  324. children.push(
  325. {
  326. type: 'line',
  327. shape: { x1: startX, y1: startY, x2: endX, y2: y },
  328. style: {
  329. stroke: strainColors[lines[params.dataIndex].strain],
  330. lineWidth: 4
  331. }
  332. }
  333. );
  334. // 横线:当前节点生命周期
  335. children.push({
  336. type: 'line',
  337. shape: { x1, y1: y, x2, y2: y },
  338. style: {
  339. stroke: strainColors[lines[params.dataIndex].strain],
  340. lineWidth: 4
  341. }
  342. });
  343. // 终点圆点
  344. children.push({
  345. type: 'circle',
  346. shape: { cx: x2, cy: y, r: 7 },
  347. style: {
  348. fill: strainColors[lines[params.dataIndex].strain],
  349. stroke: '#fff',
  350. lineWidth: 2
  351. }
  352. });
  353. return { type: 'group', children };
  354. },
  355. data: lines,
  356. encode: { x: [ 'year', 'endYear' ], y: 'y' }
  357. }
  358. ]
  359. });
  360. },
  361. async initData() {
  362. if (this.form.dateRange.length > 0) {
  363. const [startDate, endDate] = this.form.dateRange;
  364. this.form.startDate = startDate;
  365. this.form.endDate = endDate;
  366. }
  367. const res0 =await getPathogenConfig(this.form.pathogenId)
  368. this.$nextTick(() => {
  369. let tree = d3.layout.phylotree().svg(d3.select("#tree_display"))
  370. .options({
  371. 'left-right-spacing': 'fit-to-step'
  372. }).font_size(10)
  373. .size([1, 900])
  374. .node_circle_size(2);
  375. tree(res0.data.treeTxt).layout();
  376. });
  377. const res = await generatesjbytfzzbqkInfo(this.form);
  378. const res2 = await generatezgbytfzzbqkInfo(this.form);
  379. this.worldData = res.data;
  380. this.chinaData = res2.data;
  381. this.renderWorldVirusMap();
  382. this.renderChinaVirusMap();
  383. },
  384. async loadPathogen(){
  385. const res0 = await pathogenConfigGetAll();
  386. this.pathogen = res0.data;
  387. }
  388. },
  389. created() {
  390. this.loadPathogen();
  391. // this.initData();
  392. },
  393. mounted() {
  394. // setTimeout(() => {
  395. // this.renderWorldVirusMap();
  396. // this.renderChinaVirusMap();
  397. // // this.renderPhyloTree();
  398. // }, 500);
  399. }
  400. };
  401. </script>
  402. <style scoped>
  403. .phylo-tree, .world-map {
  404. background: #fff;
  405. padding: 16px;
  406. border-radius: 8px;
  407. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  408. }
  409. </style>