123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- <template>
- <div class="popularity-prediction">
- <div class="tree-map-container">
- <!-- 筛选条件 -->
- <el-card class="filter-card">
- <el-form :inline="true" :model="form" size="small">
- <el-form-item label="病原体名称">
- <el-input
- v-model="form.bytName"
- placeholder="请输入病原体名称"
- style="width: 180px"
- @keyup.enter="initData"
- />
- </el-form-item>
- <el-form-item label="时间">
- <el-date-picker
- v-model="form.dateRange"
- type="daterange"
- range-separator="至"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- style="width: 240px"
- value-format="yyyy-MM-dd"
- />
- </el-form-item>
- <el-form-item>
- <el-button type="primary" @click="initData">查询</el-button>
- </el-form-item>
- </el-form>
- </el-card>
- <!-- <div class="phylo-tree">
- <div id="phyloTree" ref="treeContainer" style="width: 100%; height: 500px;"></div>
- </div> -->
- <el-row :gutter="20">
- <el-col :span="12">
- <div class="world-map">
- <h3>世界地图病毒预测图</h3>
- <div id="worldVirusMap" style="width: 100%; height: 500px;"></div>
- </div>
- </el-col>
- <el-col :span="12">
- <div class="china-map">
- <h3>中国地图病毒预测图</h3>
- <div id="chinaVirusMap" style="width: 100%; height: 500px;"></div>
- </div>
- </el-col>
- </el-row>
- </div>
- </div>
- </template>
- <script>
- import * as echarts from 'echarts';
- import worldJson from 'echarts/map/json/world.json';
- import chinaJson from 'echarts/map/json/china.json';
- import { generatesjbytfzzbqkInfo, generatezgbytfzzbqkInfo } from '@/api/statistics/report';
- echarts.registerMap('world', worldJson);
- echarts.registerMap('china', chinaJson);
- export default {
- name: 'PopularityPrediction',
- data() {
- return {
- form: {
- bytName: "",
- endDate: "",
- startDate: "",
- dateRange: []
- },
- worldData: [],
- chinaData: [],
- worldMapChart: null,
- chinaMapChart: null
- };
- },
- methods: {
- renderWorldVirusMap() {
- if (!this.worldMapChart) {
- this.worldMapChart = echarts.init(document.getElementById('worldVirusMap'));
- }
- this.renderMap(this.worldMapChart, this.worldData, 'world');
- },
- renderChinaVirusMap() {
- if (!this.chinaMapChart) {
- this.chinaMapChart = echarts.init(document.getElementById('chinaVirusMap'));
- }
- this.renderMap(this.chinaMapChart, this.chinaData, 'china');
- },
- renderMap(chart, data, mapType) {
- // 处理数据,生成区域数据,保留每个病原体的数量信息
- const regionData = data.map(item => {
- const pathogenData = {};
- let totalValue = 0;
- Object.keys(item).forEach(key => {
- if (key!== 'area' && key!== 'eng') {
- const value = item[key];
- if (!isNaN(value) && value!== null && value!== undefined) {
- pathogenData[key] = value;
- totalValue += value;
- }
- }
- });
- return {
- name: item.eng || item.area || '未知区域',
- value: isNaN(totalValue)? 0 : totalValue,
- cn: item.area || '未知区域',
- pathogenData: pathogenData
- };
- });
- // 校验数据,避免 min 或 max 为 NaN
- let visualMapMin = 0;
- let visualMapMax = 0;
- if (regionData.length > 0) {
- const values = regionData.map(item => item.value).filter(value =>!isNaN(value));
- if (values.length > 0) {
- visualMapMin = Math.min(...values);
- visualMapMax = Math.max(...values);
- }
- }
- // 清空旧的配置
- chart.clear();
- chart.setOption({
- title: { text: mapType === 'world' ? '全球病毒预测分布' : '中国病毒预测分布', left: 'center' },
- tooltip: {
- trigger: 'item',
- formatter: (params) => {
- const name = params.data && params.data.cn? params.data.cn : '未知区域';
- const value = params.data &&!isNaN(params.data.value)? params.data.value : 0;
- const pathogenData = params.data && params.data.pathogenData? params.data.pathogenData : {};
- let tooltipStr = `<div>${name}</div><div>总数: ${value}</div>`;
- Object.keys(pathogenData).forEach(pathogen => {
- tooltipStr += `<div>${pathogen}: ${pathogenData[pathogen]}</div>`;
- });
- return tooltipStr;
- }
- },
- // geo: {
- // map: mapType,
- // roam: true,
- // label: {
- // emphasis: {
- // show: false
- // }
- // },
- // itemStyle: {
- // normal: {
- // areaColor: '#f0f0f0',
- // borderColor: '#999'
- // },
- // emphasis: {
- // areaColor: '#e0e0e0'
- // }
- // },
- // center: mapType === 'china' ? [104.1954, 35.8617] : [10, 10],
- // zoom: mapType === 'china' ? 2 : 1.2
- // },
- series: [
- {
- type: 'map',
- map: mapType,
- // coordinateSystem: 'geo',
- data: regionData,
- label: {
- show: false
- },
- itemStyle: {
- normal: {
- areaColor: '#f0f0f0',
- borderColor: '#999'
- },
- emphasis: {
- areaColor: '#e0e0e0',
- borderWidth: 2,
- borderColor: '#fff'
- }
- }
- }
- ],
- visualMap: {
- min: visualMapMin,
- max: visualMapMax,
- left: 'left',
- top: 'bottom',
- text: ['高', '低'],
- calculable: true,
- // inRange: {
- // color: ['#ffeeee', '#ffcccc', '#ff9999', '#ff6666', '#ff3333']
- // }
- }
- });
- },
- renderPhyloTree() {
- const strainColors = {
- A: '#5470C6',
- B: '#91CC75',
- C: '#FAC858',
- D: '#EE6666',
- E: '#73C0DE',
- F: '#3BA272',
- G: '#FC8452',
- H: '#9A60B4',
- I: '#EA7CCC',
- J: '#FFA500'
- };
- const strains = Object.keys(strainColors);
- // 生成树结构
- function createExampleTree(rootStrain, rootYear, rootEndYear, depth = 0, maxDepth = 5, usedSet = new Set()) {
- if (depth > maxDepth) return null;
- const key = `${rootStrain}_${rootYear}`;
- if (usedSet.has(key)) return null; // 跳过重复
- usedSet.add(key);
- const node = {
- name: `病毒${rootStrain}`,
- strain: rootStrain,
- year: rootYear,
- endYear: rootEndYear,
- children: []
- };
- if (depth < maxDepth) {
- const childCount = Math.floor(Math.random() * 2) + 1;
- for (let i = 0; i < childCount; i++) {
- const nextStrain = strains[(strains.indexOf(rootStrain) + i + 1) % strains.length];
- const childYear = Math.min(rootEndYear - 1, rootYear + 1 + Math.floor(Math.random() * 3));
- const childEndYear = Math.min(rootEndYear, childYear + 3 + Math.floor(Math.random() * 6));
- const child = createExampleTree(nextStrain, childYear, childEndYear, depth + 1, maxDepth, usedSet);
- if (child) node.children.push(child);
- }
- }
- return node;
- }
- // 生成5条主干
- const roots = [];
- for (let i = 0; i < 5; i++) {
- const strain = strains[i % strains.length];
- const startYear = 2010 + Math.floor(Math.random() * 3);
- const endYear = startYear + 8 + Math.floor(Math.random() * 5);
- roots.push(createExampleTree(strain, startYear, endYear, 0, 5, new Set()));
- }
- // 展平树结构,分配y坐标
- let yIndex = 0;
- const lines = [];
- function flatten(node, parentY = null, parentName = null) {
- const myY = yIndex++;
- lines.push({
- name: `${node.name}(${node.year}~${node.endYear})`,
- strain: node.strain,
- year: node.year,
- endYear: node.endYear,
- y: myY,
- parentName // 记录父节点名称
- });
- if (node.children && node.children.length) {
- node.children.forEach(child => flatten(child, myY, `${node.name}(${node.year}~${node.endYear})`));
- }
- }
- // 主干之间插入空行,增加间距
- roots.forEach((root, idx) => {
- flatten(root, null, null);
- if (idx !== roots.length - 1) {
- // 每两个主干之间插入2个空行
- yIndex += 3;
- }
- });
- // 图例
- const legendData = strains.map(k => `病毒${k}`);
- const chart = echarts.init(this.$refs.treeContainer);
- chart.setOption({
- title: { text: '病毒进化树(横坐标为年份)', left: 'center' },
- tooltip: {
- trigger: 'item',
- formatter: params => params.data ? params.data.name : params.name
- },
- legend: {
- data: legendData,
- orient: 'vertical',
- right: 10,
- top: 60,
- formatter: name => `{a|●} {b|${name}}`,
- textStyle: {
- rich: {
- a: { color: (params) => params, fontSize: 18 },
- b: { color: '#333', fontSize: 14 }
- }
- }
- },
- grid: { left: 80, right: 120, top: 60, bottom: 40 },
- xAxis: {
- type: 'value',
- min: 2010,
- max: 2025,
- interval: 1,
- name: '年份',
- axisLabel: { formatter: '{value}' }
- },
- yAxis: {
- type: 'category',
- data: lines.map(l => l.name),
- show: false,
- axisLabel: { show: false },
- splitLine: { show: false }
- },
- series: [
- // 横线
- {
- type: 'custom',
- renderItem: function(params, api) {
- const y = api.coord([0, params.dataIndex])[1];
- const x1 = api.coord([lines[params.dataIndex].year, params.dataIndex])[0];
- const x2 = api.coord([lines[params.dataIndex].endYear, params.dataIndex])[0];
- const parentY = (() => {
- // 查找父节点的y坐标
- const parentName = lines[params.dataIndex].parentName;
- if (!parentName) return null;
- const parentIdx = lines.findIndex(l => l.name === parentName);
- if (parentIdx === -1) return null;
- return api.coord([lines[params.dataIndex].year, parentIdx])[1];
- })();
- const children = [];
- // 折线:所有节点都加竖线
- let startX;
- let endX = x1;
- let startY = y;
- if (parentY !== null && parentY !== y) {
- // 分支节点:父节点到当前节点y
- startX = x1;
- startY = parentY;
- } else {
- // 主干节点:从x轴(y轴0刻度线)到当前节点
- startX = x1;
- // 获取x轴像素y坐标
- const xAxisY = api.coord([lines[params.dataIndex].year, -0.8])[1];
- startY = xAxisY;
- }
- children.push(
- {
- type: 'line',
- shape: { x1: startX, y1: startY, x2: endX, y2: y },
- style: {
- stroke: strainColors[lines[params.dataIndex].strain],
- lineWidth: 4
- }
- }
- );
- // 横线:当前节点生命周期
- children.push({
- type: 'line',
- shape: { x1, y1: y, x2, y2: y },
- style: {
- stroke: strainColors[lines[params.dataIndex].strain],
- lineWidth: 4
- }
- });
- // 终点圆点
- children.push({
- type: 'circle',
- shape: { cx: x2, cy: y, r: 7 },
- style: {
- fill: strainColors[lines[params.dataIndex].strain],
- stroke: '#fff',
- lineWidth: 2
- }
- });
- return { type: 'group', children };
- },
- data: lines,
- encode: { x: [ 'year', 'endYear' ], y: 'y' }
- }
- ]
- });
- },
- async initData() {
- if (this.form.dateRange.length > 0) {
- const [startDate, endDate] = this.form.dateRange;
- this.form.startDate = startDate;
- this.form.endDate = endDate;
- }
- const res = await generatesjbytfzzbqkInfo(this.form);
- const res2 = await generatezgbytfzzbqkInfo(this.form);
- this.worldData = res.data;
- this.chinaData = res2.data;
- this.renderWorldVirusMap();
- this.renderChinaVirusMap();
- }
- },
- created() {
- this.initData();
- },
- mounted() {
- setTimeout(() => {
- this.renderWorldVirusMap();
- this.renderChinaVirusMap();
- // this.renderPhyloTree();
- }, 500);
- }
- };
- </script>
- <style scoped>
- .phylo-tree, .world-map {
- background: #fff;
- padding: 16px;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- }
- </style>
|