echarts 实现拓扑图
效果图
<template>
<div class="echart-block">
<!-- echarts容器 -->
<div style="height:100%" ref="graphchart"></div>
</div>
</template>
<script>
import { nodeData } from '@/views/nodeData.js'
export default {
name: 'Echarts1',
data () {
return {
echarts: null,
nodes: [], // 节点
links: [] // 连线
}
},
mounted () {
this.drawChart()
},
methods: {
// 画出拓扑图
drawChart () {
// 第一步:初始化 nodes,就是每个节点
nodeData.ne.forEach(value => {
this.nodes.push({
'name': value.nename,
'symbol': 'circle'
})
})
// 第二步,添加节点关系的数据
nodeData.neToNe.forEach(value => {
this.links.push({
'source': value.nename,
'target': value.snkNeName
})
})
// 初始化echarts对象
this.echarts = this.$echarts.init(this.$refs.graphchart)
// 写配置
let option = {
series: [
{
// 关系图类型
type: 'graph',
// 鼠标放上去相关节点高亮显示
focusNodeAdjacency: true,
// 布局 力引布局。两个节点之间添加一个斥力,每条边的两个节点之间添加一个引力。力引导布局的结果有良好的对称性和局部聚合性,也比较美观
layout: 'force',
// 力引布局初始配置
force: {
initLayout: 'circular',// 初始化布局为环行布局
layoutAnimation: false,// 迭代动画
repulsion: 4000, // 节点之间的斥力因子
edgeLength: 100,// 边的两个节点之间的距离
},
// 节点大小
symbolSize: 60,
// 是否开启鼠标缩放和平移漫游
roam: true,
// 当前视角的缩放比例
zoom: 0.6,
// 节点name
label: {
show: true,
fontSize: 16,
},
// 节点颜色
color: ['#66b1ff'],
// 连接线两端的标记类型
edgeSymbol: ['circle', 'arrow'],
// 连接线两端的标记大小
edgeSymbolSize: [2, 8],
// 节点数据
data: this.nodes,
// 连接线
links: this.links,
}
]
}
// 挂载
this.echarts.setOption(option)
this.onClickEcharts() // 添加点击事件
},
/**
* 点击图表
*/
onClickEcharts() {
this.echarts.on('click', function (params) {
// 点击功能
alert(params.name)
})
}
}
}
</script>
<style scoped>
.echart-block {
height: 100vh;
}
</style>
进阶一下,完善功能
- 渲染拓扑视图
- 点击空白处显示所有节点
- 单击节点,显示有关系的节点
- 单 / 双击节点触发单 / 双击事件
- 下拉框显示节点
- 点击连接线事件
export default {
name: 'index',
data() {
return {
// 节点容器
dataArr: [],
// 指向关系
liArr: [],
// 节点数组,下拉框使用
listNe: [],
echart: null,
// 当前选中节点
srcname: null,
// 当前锁定节点
locked: null, // 锁定状态
clickCount: 0, // 默认就是0,表示单击次数(用于判断单击和双击)
lastClickTime: 0,// 上次单击时间
clickTimeout: null // 单击超时
}
},
created() {
// 获取节点
this.neList()
},
mounted() {
// 绘制拓扑图
this.drawChart(this.dataArr)
},
methods: {
// 拿节点数据
neList() {
getneList().then(res => this.listNe = res.data)
},
drawChart(data) {
getTopList().then(res => { // 发送请求获取指向关系和节点
res.data.ne.forEach(value => {
// 根据数据开始配置节点
this.dataArr.push({
name: value.nename, // 节点名称
symbol: 'circle', // 节点形状
neid: value.neid, // 唯一ID--可以根据需求自定义
itemStyle: { color: '#66b1ff' }, // 配置颜色
layout: 'force', // force 布局
highlighted: true // 是否显示,默认显示状态
})
})
// 配置指向关系
res.data.neToNe.forEach((value) => { // 遍历指向关系
// 设置透明度默认是1
let opacityValue = 1
// 如果degree为null表示正常
if (value.degree === null) {
value.degree = 'black' // 颜色
opacityValue = 0.05 // 透明度
}
this.liArr.push({
'source': value.nename, // 原节点
'target': value.snkNeName, // 指向节点
// 设置颜色和透明度
'lineStyle': { color: value.degree, opacity: opacityValue },
'item': value // 自定义数据,可根据需求
})
})
// 初始化并赋值
this.echart = this.$echarts.init(this.$refs.myEcharts)
let option = {
series: [
{
type: 'graph', // graph 图形
layout: 'force', // force 算法布局
focusNodeAdjacency: true, // 鼠标放上去相关节点高亮显示
roam: true, // 可拖动缩放
scaleLimit: { // 设置缩放范围限制
min: 0.3,
max: 1
},
force: {
initLayout: 'circular', // 环形布局
layoutAnimation: false, // 迭代动画
repulsion: 7000, // 节点之间的斥力因子
edgeLength: 1600 // 边的两个节点之间的距离
},
emphasis: {
focus: false // 关闭鼠标经过高亮
},
symbolSize: 60, // 元素默认大小
label: {
show: true, // 打开标签显示
fontSize: 16 // 标签字体大小
},
edgeSymbol: ['arrow', 'arrow'], // 表示连接线两边都是箭头
edgeSymbolSize: [8, 8], // 连接线箭头大小
data: data, // 赋值数据和指向关系
links: this.liArr
}
]
}
// 渲染视图
this.echart.setOption(option)
// 点击空白处事件
this.echart.getZr().on('click', (params) => {
// 表示点击的不是事件源,并且当前没有锁定元素,才触发
if (!params.target && this.locked !== null) {
// 重新渲染
this.restNode()
// 设置非锁定
this.locked = null
this.srcname = null // 关闭下拉框选中
}
})
// 点击非空白处
this.echart.on('click', (param) => {
// 双击事件
if (this.clickCount === 2) {
this.dbClick(param)
} else {
// 当前单击事件
const currentTime = new Date().getTime()
// 点击超时时间不为空和不超过500,表示双击事件,否则单击
if (this.clickTimeout !== null && currentTime - this.lastClickTime <= 500) {
// 双击事件
this.dbClick(param)
} else {
this.clickTimeout = setTimeout(() => {
// 加载元素
this.nodeLoading(param.data.name)
this.clickCount = 0 // 清零计数
this.clickTimeout = null // 时间清零
}, 300)
}
// 上次点击时间更新
this.lastClickTime = currentTime
}
})
})
},
// 下拉框
changeList(val) {
// 如果为空,表示显示所有节点
if (val === '') {
this.restNode()
return
}
// 选中当前选择的节点
this.nodeLoading(val)
},
// 加载节点的方法
nodeLoading(name) {
// 等于说明点击的一样,无需渲染
if (name === this.locked) return
// 单击连接线,不做操作,点击连接线是没有name字段的所以是undefined
if (name === undefined) return
// 锁定当前节点
this.locked = name
const option = this.echart.getOption()
// 数据---这里必须深克隆
const nodes = JSON.parse(JSON.stringify(this.dataArr))
// 节点链接线---这里必须深克隆
const links = JSON.parse(JSON.stringify(this.liArr))
// 将当前节点及相关连接的节点加入集合中(存入有关节点名字)
const linkedSet = links.reduce((set, link) => {
if (link.source === name || link.target === name) {
set.add(link.source)
set.add(link.target)
}
return set
}, new Set())
const currentLink = new Set()
// 根据有关节点名称获取节点
const currentNode = nodes.filter(node => linkedSet.has(node.name))
// 将不在linkedSet集合中的node元素的highlighted属性设置为false
// 表示不需要显示
nodes.filter(node => !linkedSet.has(node.name)).forEach(node => {
node.highlighted = false
})
// 遍历集合中的节点及其连接,并进一步更新集合
// (获得的是所有节点与当前节点有关系的节点链接线)
Array.from(linkedSet).forEach(currentName => {
nodes.forEach(node => {
// 如果元素的名字等于当前元素,将他设置为true
if (node.name === currentName) {
// 遍历当前节点连接的所有链接,将这些链接的源和目标节点的名称添加到集合中
links.forEach(link => {
if (link.source === currentName || link.target === currentName) {
linkedSet.add(link.source)
linkedSet.add(link.target)
}
})
}
})
})
// 将没有关系的节点隐藏掉
links.forEach(link => {
if (isNodeVisible(link.source) && isNodeVisible(link.target)) {
let co = 'black'
if (link.lineStyle.color === 'red') co = 'red'
link.lineStyle = {
normal: {
color: co, // 默认连接线的颜色
width: 2 // 默认连接线的宽度
},
emphasis: {
color: co, // 高亮连接线的颜色
width: 3 // 高亮连接线的宽度
}
}
currentLink.add(link)
}
})
// 判断节点是否存在关系
function isNodeVisible(name) {
const node = nodes.find(node => node.name === name)
return node && node.highlighted === true
}
option.series[0].links = Array.from(currentLink)
option.series[0].data = Array.from(currentNode)
// 选中元素
this.srcname = name
this.restShow(option)
},
// 还原所有节点到初始状态
restNode() {
// 将全部节点显示出来
const option = this.echart.getOption()
option.series[0].links = this.liArr
option.series[0].data = this.dataArr
this.restShow(option)
},
// 将最新数据更新
restShow(option) {
this.echart.setOption(option)
this.echart.resize()
},
dbClick(param) {
// 将其他节点全部异常
if (param.dataType === 'node') {
// 点击节点事件
}
if (param.dataType === 'edge') {
// 点击连接线事件
}
this.clickCount = 0 // 清零
clearTimeout(this.clickTimeout)
this.clickTimeout = null
}
}
}
</script>
上面的方式定位会出现问题,修复定位
主组件
<template>
<div class="echarts-block">
<el-row>
<!--拓扑图组件-->
<!--缓存该组件,AllGraph-->
<keep-alive include="AllGraph">
<component :is="currentName" @switchComponent="updateChart"
@doubleClick="getElementOrNode" :dataArr="dataArr"
:links="links" ref="child" :selected="selected" :listNe="listNe"
></component>
</keep-alive>
</el-row>
</div>
</template>
<script>
import AllGraph from '@/components/AllGraph/AllGraph.vue'
import index2 from '@/components/AllGraph/index2.vue'
import eventBus from '@/utils/event-bus'
import drawer from '@/components/AllGraph/drawer.vue'
export default {
name: 'index',
data() {
return {
currentName: 'AllGraph',
dataArr: null, // 节点数据
links: null, // 连接线数据
clickCount: 0, // 默认就是0,表示单击次数(用于判断单击和双击)
lastClickTime: 0,// 上次单击时间
clickTimeout: null, // 单击超时
selected: null, // 下拉框选中的元素
display: false,
drawerWidth: '1235px'
}
},
components: { drawer, AllGraph, index2 },
methods: {
// 切换展示方法
updateChart(val, name) {
if (val === undefined) {
this.currentName = 'AllGraph'
this.selected = null
} else {
this.dataArr = val.data
this.links = val.links
this.currentName = 'index2'
}
},
// 双击之后的获取数据
getElementOrNode(param) {
// 双击事件
if (this.clickCount === 2) {
this.dbClick(param)
} else {
const currentTime = new Date().getTime() // 获取当前时间毫秒
if (this.clickTimeout !== null && currentTime - this.lastClickTime <= 500) {
this.dbClick(param) // 双击事件
} else {
this.clickTimeout = setTimeout(() => {
// 点击节点没有反应
if (param.dataType === 'edge') return
if (this.currentName === 'index2' && this.selected !== param.data.name) {
// 更新显示节点元素
this.showOrHideElement(param.data.name)
} else if (this.currentName === 'AllGraph') {
// 调用子组件根据元素名称获取数据
const series = this.$refs.child.nodeLoading(param.data.name, 1).series[0]
// 单击了组件---切换组件
this.updateChart(series, param.data.name)
}
// 更新当前选中元素
this.selected = param.data.name
this.clickCount = 0 // 清零计数
this.clickTimeout = null // 时间清零
}, 300)
}
this.lastClickTime = currentTime
}
},
// 双击事件处理函数
dbClick(param) {
// 双击了元素
if (param.dataType === 'node') {
}
// 双击了节点
if (param.dataType === 'edge') {
}
this.clickCount = 0 // 清零
clearTimeout(this.clickTimeout)
this.clickTimeout = null
},
// 组件中更新echarts元素
// 下拉框选中元素,或者点击了元素
showOrHideElement(name) {
// 如果为空表示返回首页组件
if (name === '') {
// 跳转到首页---获取数据
this.currentName = 'AllGraph'
return
}
// 判断当前页面是不是不等于 index2,就切换到index2,显示元素
if (this.currentName !== 'index2') {
const series = this.$refs.child.nodeLoading(name, 1).series[0]
// 单击了组件---切换组件
this.updateChart(series, name)
} else {
// 跳转页面获取数据,最后重新渲染当前index2的元素
eventBus.$emit('goIndex', name)
}
},
// 移动方法
onMouseMoveEntry() {
// 表示调用方法,将抽屉打开
this.$refs.drawer.changeXY(0)
// 下面两个是将文字居中
this.$refs.neTitleAssay.style.textAlign = 'center'
this.$refs.predictionTitleAssay.style.textAlign = 'center'
},
onMouseMoveOut() {
// 表示调用方法,将抽屉关闭
this.$refs.drawer.changeXY(-830)
// 下面两个是将文字不居中
this.$refs.neTitleAssay.style.textAlign = null
this.$refs.predictionTitleAssay.style.textAlign = null
}
}
}
</script>
<style scoped>
.echarts-block {
position: relative;
}
.select-tool {
position: absolute;
top: 10px;
left: 20px;
}
</style>
默认显示的组件 AllGraph
<template>
<div>
<div class="echarts-block">
<!--Echarts渲染的位置-->
<div id="myEcharts" ref="myEcharts" style="width:100%;height: 100vh;"></div>
</div>
</div>
</template>
<script>
import eventBus from '@/utils/event-bus'
export default {
name: 'AllGraph',
data() {
return {
// 元素容器
dataArr: [],
// 指向关系
liArr: [],
echart: null
}
},
created() {
// 在显示相邻元素的页面触发方法,获取渲染数据
eventBus.$on('goIndex', (val) => {
this.rerenderEChartsByName(val, 0)
})
},
mounted() {
// 初始化echarts
this.drawChart(this.dataArr)
},
methods: {
drawChart(data) {
// 调用接口获取数据
getTopList().then(res => {
// 配置元素信息
res.data.ne.forEach(value => {
let color = '#66b1ff' // 默认颜色
if (value.degree === 'red') color = 'red' // 修改为红色
this.dataArr.push({
name: value.nename, // 元素名称
symbol: 'circle', // 元素形状
neid: value.neid, // 自定义信息
itemStyle: { color: color }, // 元素样式
layout: 'force', // 布局方式 force算法
highlighted: true // 是否高亮
})
})
// 连接关系配置
res.data.neToNe.forEach((value, index) => {
let opacityValue = 0.07 // 连接线默认透明度
// 渐变色
let accessArr = [] // 渐变色每一段默认为 1 / 分段数量
let len = 1.0 / value.accessList.length
// 计算每个渐变段的颜色以及所属的 Access 对象
if (value.accessList.length !== 0) {
value.accessList.forEach(value => {
let color = 'gray'
if (value.colors === 'red') {
color = 'red'
// 若 access 的颜色为 red,线条的透明度改为不透明
opacityValue = 1
}
// 添加渐变段
accessArr.push({ offset: len, color: color, 'access': value })
})
} else {
// 如果 accessList 为空,则添加一个默认的渐变段
accessArr[0] = { offset: 0, color: 'gray', 'access': null }
}
this.liArr.push({
source: value.nename,
target: value.snkNeName,
lineStyle: {
color: {
// 将渐变配置赋值给线条颜色
colorStops: accessArr
},
// 将透明度信息赋给线条透明度
opacity: opacityValue
},
// 自定义对象
'item': value
})
})
this.echart = this.$echarts.init(this.$refs.myEcharts)
let option = {
// 也就是鼠标放到元素上显示的信息框
tooltip: {
formatter: function(params) {
// 放到连接线上
if (params.dataType === 'edge') {
}
}
},
series: [
{
type: 'graph',
layout: 'force',
roam: true, // 开启平移缩放
scaleLimit: { // 设置缩放范围限制
min: 0.1,
max: 0.4
},
animation: false, // 关闭渲染动画
force: {
initLayout: 'circular', // 初始化布局方式环形布局
layoutAnimation: false, // 关闭布局动画
repulsion: 7000, // 连接线之间斥力因子
edgeLength: 1600 // 连接线长度
},
emphasis: {
focus: false // 关闭鼠标经过高亮
},
symbolSize: 60, // 元素大小
label: {
show: true, // 是否显示标签
fontSize: 16
},
edgeSymbol: ['arrow', 'arrow'], // 连接使用箭头样式
edgeSymbolSize: [8, 8], // 箭头大小
data: data, // 元素数据
links: this.liArr // 连接线数据
}
]
}
this.echart.setOption(option)
// 点击事件
this.echart.on('click', (param) => {
// 点击了元素,直接触发父组件方法,进行元素切换
this.$emit('doubleClick', param)
})
})
},
// 加载节点的方法(这里可以看上面的,没有改变)
nodeLoading(name, status) {
const option = this.echart.getOption()
// 数据
const nodes = JSON.parse(JSON.stringify(this.dataArr))
// 节点链接线
const links = JSON.parse(JSON.stringify(this.liArr))
const linkedSet = links.reduce((set, link) => {
if (link.source === name || link.target === name) {
set.add(link.source)
set.add(link.target)
}
return set
}, new Set())
const currentLink = new Set()
const currentNode = nodes.filter(node => linkedSet.has(node.name))
nodes.filter(node => !linkedSet.has(node.name)).forEach(node => {
node.highlighted = false
})
Array.from(linkedSet).forEach(currentName => {
nodes.forEach(node => {
if (node.name === currentName) {
links.forEach(link => {
if (link.source === currentName || link.target === currentName) {
linkedSet.add(link.source)
linkedSet.add(link.target)
}
})
}
})
})
links.forEach(link => {
if (isNodeVisible(link.source) && isNodeVisible(link.target)) {
link.lineStyle = {
normal: {
color: link.lineStyle.color,
width: 2
},
emphasis: {
color: link.lineStyle.color,
width: 3
}
}
currentLink.add(link)
}
})
function isNodeVisible(name) {
const node = nodes.find(node => node.name === name)
return node && node.highlighted === true
}
option.series[0].data = currentNode
option.series[0].links = Array.from(currentLink)
this.srcname = name
if (status === 1) return option
this.echart.setOption(option)
this.echart.resize()
},
// 进行跳转页面
rerenderEChartsByName(name, status) {
// 根据节点获取最新的 option加载项
let series = this.nodeLoading(name, 1).series[0]
if (status === 1) {
// 表示从首页跳转到详细页,直接跳转
this.$router.push({ name: 'index2', params: { dataArr: series.data, links: series.links } })
} else {
// 当前就在详细页,可以直接渲染
eventBus.$emit('backIndex', series)
}
}
}
}
</script>
详细页面的组件
<template>
<div>
<div class="echarts-block">
<!--渲染的div-->
<div id="myEcharts" ref="myEcharts" style="width:100%;height: 100vh;"></div>
</div>
</div>
</template>
<script>
import eventBus from '@/utils/event-bus'
export default {
name: 'index2',
data() {
return {
name: 'index2',
echart: null
}
},
// 接收父组件传递来的元素信息和连接线信息
props: ['dataArr', 'links'],
mounted() {
this.drawChart()
// 监听到了方法,执行重新渲染
eventBus.$on('backIndex', (val) => {
const option = this.echart.getOption()
const series = option.series[0]
series.data = val.data
series.links = val.links
this.echart.setOption(option)
this.echart.resize()
})
},
methods: {
drawChart() {
this.echart = this.$echarts.init(this.$refs.myEcharts)
let option = {
// 跟上面一样....
}
this.echart.setOption(option)
// 空白处---返回首页
this.echart.getZr().on('click', (params) => {
if (!params.target && this.currentValue !== null) {
this.$emit('switchComponent')
}
})
// 执行重新渲染
this.echart.on('click', (param) => {
this.$emit('doubleClick', param)
})
}
}
}
</script>