一,工作需求,展示几类数据关系,可缩放大小,可拖拽位置,在节点之间的连线上展示相关日期,每个节点展示本身信息,并且要求每个关系节点能点击。
实现情况如图所示:
二,实现过程中遇到的问题: 关系图完美呈现,但关系节点点击后,整个关系图会杂乱无章的浮动,导致不知道点击了哪个关系节点。
三,解决思路,等关系图稳定后,获取每个关系节点的位置坐标,再次将关系节点定在相应的位置。
四,整体代码:
相关参数options.js
export default {title: {text: '综合关系图(以GID为中心)',},tooltip: {backgroundColor: '#666',formatter(params) {// console.log("wq",params)return `<div>${params.name}</div> `;},},animationDurationUpdate: 1000, // 1500animationEasingUpdate: 'quinticInOut',label: {normal: {show: true,textStyle: {fontSize: 12,},},},legend: {x: 'center',show: true,data: ['GID', 'UID', 'DID', 'IP', 'PHONE'],},series: [{type: 'graph',layout: 'force',symbolSize: 35, // 关系节点大小focusNodeAdjacency: true, // 是否在鼠标移到节点上的时候突出显示节点以及节点的边和邻接节点。roam: true, // 是否开启鼠标缩放和平移漫游 可以设置成 'scale' 或者 'move'。设置成 true 为都开启 categories: [{// 节点分类类目name: 'GID',itemStyle: {normal: {color: '#C23531',},},},{name: 'UID',itemStyle: {normal: {color: '#2F4554',},},},{name: 'DID',itemStyle: {normal: {color: '#F9AB3F',},},},{name: 'IP',itemStyle: {normal: {color: '#6F9B03',},},},{name: 'PHONE',itemStyle: {normal: {color: '#1890FF',},},},],label: {normal: {show: true,textStyle: {fontSize: 12,},},},force: {repulsion: 1000, // 节点之间的斥力因子。支持数组表达斥力范围,值越大斥力越大。gravity: 0.03, // 节点受到的向中心的引力因子。该值越大节点越往中心点靠拢。edgeLength: 160, // 边的两个节点之间的距离,这个距离也会受 repulsion。[10, 50] 。值越小则长度越长layoutAnimation: true, // 这个参数决定是否显示布局的迭代动画,在浏览器端节点数据较多(>100)的时候不建议关闭,布局过程会造成浏览器假死。 },// edgeSymbolSize: [4, 50], edgeLabel: {normal: {show: true,textStyle: {fontSize: 10,},formatter: '{c}',},},data: [],links: [],lineStyle: {normal: {opacity: 0.9,width: 1,curveness: 0,},},},], };
实现页代码:在react代码中实现的关系图
import React, { PureComponent } from 'react';
import { Card, Form, Modal, } from 'antd';
import { connect } from 'dva';
import echarts from 'echarts/lib/echarts';
import PageHeaderWrapper from '../../../components/PageHeaderWrapper';
import IpMoudle from './ipMoudle';
import GidMoudle from './gidMoudle';
import UidMoudle from './accountMoudle';
import DidMoudle from './didMoudle';
import PhoneMoudle from './phoneMoudle';
import 'echarts/lib/chart/graph';
import 'echarts/lib/component/tooltip';
import 'echarts/lib/component/legend';
import 'echarts/lib/component/title';
import options from './options';
const getzf = val => {
return val * 1 < 10 ? `0${val}` : val;
};
const shortFormatDate = d => {
if (d === undefined) {
return '';
}
const now = new Date(d * 1000);
const year = now.getFullYear();
const month = now.getMonth() + 1;
const date = now.getDate();
return `${year}/${getzf(month)}/${getzf(date)}`;
};
@connect(({ allVal, loading }) => ({
allVal,
loading: loading.models.allVal,
}))
@Form.create()
class Over extends PureComponent {
constructor(props) {
super(props);
this.state = {
start: '',
end: '',
flog: 'gid',
lei: '',
};
}
componentDidMount() {
const myChart = echarts.init(document.getElementById('main'));
// 节点绑定点击事件
myChart.on('click', params => {
this.openShow(params);
});
this.getCharts();
}
getCharts = () => {
const myChart = echarts.init(document.getElementById('main'));
const {
allVal: { baseData },
} = this.props;
// baseData: [
// {uid: "1553919784", gid: "000000002548ba6e", create_time: 1595606400, update_time: 1638979200},
// {ip: "123.157.158.59", gid: "000000002548ba6e", create_time: 1601049600, update_time: 1639065600},
// {did: "EFBA0B48-4B7E-42B1-9D5D-B33359C3AEDF", gid: "000000015508f569", create_time: 1609516800,update_time: 1642608000},
// {uid: "1553919784", gid: "000000015508f569", create_time: 1612454400, update_time: 1642608000},
// {did: "ANDROID_04b0673ccef36422", gid: "000000002548ba6e", create_time: 1634227200,update_time: 1638979200},
// {ip: "112.10.61.105", gid: "000000015508f569", create_time: 1636214400, update_time: 1638547200},
// {ip: "115.195.90.61", gid: "000000015508f569", create_time: 1637164800, update_time: 1637164800},
// {ip: "60.163.254.188", gid: "000000015508f569", create_time: 1638460800, update_time: 1638460800},
// {ip: "122.231.208.82", gid: "000000015508f569", create_time: 1638806400, update_time: 1638806400},
// {did: "ANDROID_04b0673ccef36423", gid: "000000002548ba6e", create_time: 1638979200,update_time: 1639065600},
// {ip: "115.220.137.100", gid: "000000015508f569", create_time: 1638979200, update_time: 1639238400},
// {ip: "39.182.14.22", gid: "000000015508f569", create_time: 1641657600, update_time: 1641657600},
// {ip: "223.104.19.214", gid: "000000015508f569", create_time: 1642608000, update_time: 1642608000},
// ],
const newdata = this.getNewData(baseData); // 处理点的数据
const newlinks = this.getNewlinks(baseData); // 处理节点之间的关系
options.series[0].data = newdata;
options.series[0].links = newlinks;
// 绘制图表
myChart.setOption(options);
// 获取图表固定坐标,再次绘制图表
const layout = myChart._chartsViews[0]._symbolDraw._data._itemLayouts;
layout.map((item, index) => {
newdata[index].x = item[0];
newdata[index].y = item[1];
});
options.series[0].data = newdata;
myChart.setOption(options);
// 节点被选中的操作
myChart.on('legendselectchanged', params => {
// const { selected } = params;
// ...
});
};
// 再次获取关系节点数据后重绘,暂未调用,有需要再次重绘的可以调用
handleSubmit = e => {
e.preventDefault();
const {
form: { validateFields },
} = this.props;
validateFields((err, values) => {
if (!err) {
const { dispatch } = this.props;
const { idtype, idvalue } = values;
const { start, end } = this.state;
const date1 = new Date(start);
const date2 = new Date(end);
const s = date1.getTime() / 1000;
const en = date2.getTime() / 1000;
const val = { idtype, idvalue, s, en };
if (idtype === 'phone') {
dispatch({
type: 'allVal/fetchAllValP',
payload: val,
});
} else {
dispatch({
type: 'allVal/fetchAllVal',
payload: val,
});
}
}
});
setTimeout(this.getCharts, 1000);
};
// 节点点击触发函数
openShow = e => {
if (e.dataType === 'node') {
const { data } = e;
this.showSelect(data);
}
};
// 根据节点类型不同请求不同的接口 ["GID", "UID", 'DID','IP']
showSelect = data => {
const { category, name } = data;
switch (category) {
case 'GID':
this.gidSearch(name);
break;
case 'UID':
this.uidSearch(name);
break;
case 'IP':
this.ipSearch(name);
break;
case 'PHONE':
this.phoneSearch(name);
break;
case 'DID':
this.didSearch(name);
break;
default:
break;
}
};
ipSearch = name => {
const { dispatch } = this.props;
dispatch({
type: 'allVal/fetchIpVal',
payload: name,
});
this.setState({ lei: 'ip' });
this.showModal();
};
phoneSearch = phone => {
const { dispatch } = this.props;
dispatch({
type: 'allVal/fetchPhoneVal',
payload: { phone },
});
this.setState({ lei: 'phone' });
};
gidSearch = name => {
const { dispatch } = this.props;
dispatch({
type: 'allVal/fetchGidVal',
payload: name,
});
this.setState({ lei: 'gid' });
this.showModal();
};
uidSearch = name => {
const { dispatch } = this.props;
dispatch({
type: 'allVal/fetchUidVal',
payload: { phone: name },
});
this.setState({ lei: 'uid' });
this.showModal();
};
didSearch = name => {
const { dispatch } = this.props;
dispatch({
type: 'allVal/fetchDidVal',
payload: { did: name },
});
this.setState({ lei: 'did' });
this.showModal();
};
// 模态框展示
showModal = () => {
const { dispatch } = this.props;
dispatch({
type: 'allVal/showM',
payload: true,
});
};
// 模态框隐藏
handleCancel = () => {
const { dispatch } = this.props;
dispatch({
type: 'allVal/showM',
payload: false,
});
};
// 判断节点类目
getGrade = (val, b) => {
const a = Object.keys(val);
const c = a.indexOf(b);
a.splice(c, 1);
let grade = '';
for (let i = 0; i < a.length; i += 1) {
switch (a[i]) {
case 'gid':
if (a.indexOf('gid') > -1) {
grade = 'GID';
}
break;
case 'did':
if (a.indexOf('did') > -1) {
grade = 'DID';
}
break;
case 'uid':
if (a.indexOf('uid') > -1) {
grade = 'UID';
}
break;
case 'ip':
if (a.indexOf('ip') > -1) {
grade = 'IP';
}
break;
case 'phone':
if (a.indexOf('phone') > -1) {
grade = 'PHONE';
}
break;
default:
break;
}
}
return grade;
};
// 处理成需要的data格式
getNewData = val => {
const { flog } = this.state;
if (val.length === 0) {
return [];
}
const arr = val.map(item => item.gid);
const newArr = Array.from(new Set(arr));
const newdata = [];
// eslint-disable-next-line array-callback-return
newArr.map(item => {
newdata.push({
name: item,
draggable: false,
label: {
show: false,
},
category: 'GID',
});
});
// eslint-disable-next-line array-callback-return
val.map(item => {
newdata.push({
name: item[this.getGrade(item, flog).toLowerCase()],
draggable: false,
label: {
show: false,
},
category: this.getGrade(item, flog),
});
});
const hash = {};
const data = newdata.reduce(function(item, next) {
// eslint-disable-next-line no-unused-expressions
hash[next.name] ? '' : (hash[next.name] = true && item.push(next));
return item;
}, []);
return data;
};
// 处理成需要的links 格式
getNewlinks = val => {
const { flog } = this.state;
if (val.length === 0) {
return [];
}
const newlinks = [];
// eslint-disable-next-line array-callback-return
val.map(item => {
newlinks.push({
source: item[this.getGrade(item, flog).toLowerCase()],
target: item[flog],
value: `${shortFormatDate(item.create_time)}-${shortFormatDate(item.update_time)}`,
});
});
return newlinks;
};
render() {
const { lei } = this.state;
const {
allVal: { visible },
} = this.props;
return (
<PageHeaderWrapper>
<Card style={{ position: 'relative' }}>
<div id="main" style={{ height: '1000px', width: '100%' }}>
{' '}
</div>
<Modal
visible={visible}
title="详细信息"
onCancel={this.handleCancel}
footer={null}
width="800px"
>
<div>
{lei === 'gid' && <GidMoudle />}
{lei === 'ip' && <IpMoudle />}
{lei === 'phone' && <PhoneMoudle />}
{lei === 'uid' && <UidMoudle />}
{lei === 'did' && <DidMoudle />}
</div>
</Modal>
</Card>
</PageHeaderWrapper>
);
}
}
export default Over;