1. Echarts版本
因为在vue中使用,也没使用vue-echarts插件,看了一下官网可以npm安装并且可以按需加载,我的demo是直接把所有全部引入,所以直接写了一个插件在vue中使用,所有动画的操作都可以在这个插件中写
我加了边箭头动画,结点涟漪动画
demo代码
import { init } from 'echarts'
import * as echarts from 'echarts'
// import Csys from '../tools/csys'
// console.log('csys', csys)
const install = function (Vue) {
Object.defineProperties(Vue.prototype, {
$chart: {
get () {
return {
// 画拓扑图
topologicalTravel: function (id, obj) {
this.chart = init(document.getElementById(id))
this.chart.clear()
// console.log(obj.serviceNodeList.sort())
// const pointData = [] // 点的位置的数组
// const links = [] // links数组
const coods = []
console.log('coods', coods)
// console.log('pointData', pointData)
const data = [
{
name: '数据中心人员专题库',
tooltip: {
formatter: '{b}: 19999<br />'
},
symbol: 'circle',
symbolSize: [40, 40],
value: [700, 400],
x: 800,
y: 400,
fixed: true,
// draggable: false,
category: 1,
label: {
color: '#303133',
position: 'bottom'
},
rippleEffect: { // 涟漪特效相关配置。
brushType: 'stroke' // 波纹的绘制方式,可选 'stroke' 和 'fill'。
},
hoverAnimation: true,
itemStyle: {
normal: {
color: '#157eff'
}
}
},
{
name: '数据治理服务',
x: 400,
y: 400,
value: [400, 400],
fixed: true,
symbol: 'circle',
symbolSize: [35, 35],
label: {
color: '#303133',
position: 'bottom'
},
category: 1,
itemStyle: {
normal: {
color: '#157eff'
}
}
},
{
name: 'OSM',
x: 200,
y: 200,
value: [100, 200],
fixed: true,
symbol: 'circle',
symbolSize: [30, 30],
label: {
color: '#303133'
},
category: 1,
itemStyle: {
normal: {
color: '#ffb402'
}
}
},
{
name: '人员专题库',
symbol: 'circle',
symbolSize: [30, 30],
label: {
color: '#303133',
position: 'bottom'
},
value: [100, 500],
x: 200,
y: 500,
fixed: true,
category: 1,
itemStyle: {
normal: {
color: '#157eff'
}
}
},
{
name: '人员专题库2',
symbol: 'circle',
symbolSize: [30, 30],
label: {
color: '#303133',
position: 'bottom'
},
value: [300, 100],
x: 300,
y: 200,
fixed: true,
category: 2,
itemStyle: {
normal: {
color: '#157eff'
}
}
},
{
name: '国家队',
symbol: 'circle',
symbolSize: [30, 30],
label: {
color: '#303133',
position: 'bottom'
},
x: 1000,
y: 300,
value: [1000, 300],
fixed: true,
category: 1,
itemStyle: {
normal: {
color: '#157eff'
}
}
},
{
name: '社会专家',
symbol: 'circle',
symbolSize: [30, 30],
label: {
color: '#303133',
position: 'bottom'
},
value: [1000, 100],
x: 1000,
y: 100,
fixed: true,
category: 2,
itemStyle: {
normal: {
color: '#157eff'
}
}
},
{
name: '志愿者',
symbol: 'circle',
symbolSize: [30, 30],
label: {
color: '#303133',
position: 'bottom'
},
x: 1000,
y: 200,
value: [1000, 200],
fixed: true,
category: 2,
itemStyle: {
normal: {
// borderColor: "#b457ff",
// borderWidth: 4,
// shadowBlur: 10,
// shadowColor: "#b457ff",
color: 'rgb(255, 197, 61)' // 黄色
}
}
},
{
name: '专职队',
symbol: 'circle',
symbolSize: [30, 30],
label: {
color: '#303133',
position: 'bottom'
},
x: 1000,
y: 400,
value: [1000, 400],
fixed: true,
itemStyle: {
normal: {
color: '#157eff'
}
},
category: 2
},
{
name: '主机',
symbol: 'circle',
symbolSize: [30, 30],
label: {
color: '#303133',
position: 'bottom'
},
x: 1000,
y: 600,
value: [1000, 600],
fixed: true,
itemStyle: {
normal: {
color: '#157eff'
}
},
category: 2
},
{
name: '自建',
symbol: 'circle',
symbolSize: [30, 30],
label: {
color: '#303133',
position: 'bottom'
},
x: 600,
y: 200,
value: [600, 600],
fixed: true,
itemStyle: {
normal: {
color: '#157eff'
}
},
category: 2
},
{
name: '组织机构',
symbol: 'circle',
symbolSize: [30, 30],
label: {
color: '#303133',
position: 'bottom'
},
value: [1000, 500],
x: 1000,
y: 500,
fixed: true,
category: 2,
itemStyle: {
normal: {
color: '#157eff'
}
}
}
]
const option = ({
backgroundColor: '#fff',
xAxis: {
show: false,
type: 'value'
},
yAxis: {
show: false,
type: 'value'
},
tooltip: {},
series: [
{
type: 'graph',
zlevel: 5,
draggable: false,
coordinateSystem: 'cartesian2d', // 使用二维的直角坐标系(也称笛卡尔坐标系)
symbol: 'rect',
// symbolOffset: ['15%', 0],
label: {
normal: {
show: true
}
},
data: data,
links: [
{
source: '数据治理服务',
target: '数据中心人员专题库'
},
{
source: 'OSM',
target: '数据治理服务',
symbol: ['none', 'arrow'],
label: {
show: true,
formatter: '×',
padding: [0, 0, -13, 0],
fontSize: 20
},
lineStyle: {
color: 'red'
}
},
{
source: '人员专题库',
target: '数据治理服务',
lineStyle: {
normal: {
// color: "#12b5d0",
}
}
},
{
source: '数据中心人员专题库',
target: '社会专家',
lineStyle: {
normal: {
// color: "#12b5d0",
}
}
},
{
source: '数据中心人员专题库',
target: '志愿者',
lineStyle: {
normal: {
// color: "#12b5d0",
}
}
},
{
source: '数据中心人员专题库',
target: '国家队',
lineStyle: {
normal: {
// color: "#12b5d0",
}
}
},
{
source: '数据中心人员专题库',
target: '专职队'
// lineStyle: {
// normal: {
// color: "#12b5d0",
//
// }
// }
},
{
source: '数据中心人员专题库',
target: '组织机构',
lineStyle: {
normal: {
// color: "#12b5d0",
}
}
},
{
source: '数据中心人员专题库',
target: '主机',
lineStyle: {
normal: {
// color: "#12b5d0",
}
}
},
{
source: '自建',
target: '数据中心人员专题库',
lineStyle: {
normal: {
// color: "#12b5d0",
}
}
},
{
source: '数据中心人员专题库',
target: '人员专题库2',
lineStyle: {
normal: {
// color: "#12b5d0",
}
}
}
],
// links: links,
lineStyle: {
normal: {
opacity: 1,
color: '#53B5EA',
curveness: 0.2,
width: 2
}
}
},
{
type: 'effectScatter',
coordinateSystem: 'cartesian2d',
data: data, // 2d坐标系
symbolSize: 0,
showEffectOn: 'render',
rippleEffect: {
brushType: 'stroke'
},
hoverAnimation: true,
itemStyle: {
normal: {
color: 'red',
shadowBlur: 10,
shadowColor: '#333'
}
},
zlevel: 1
},
{
type: 'lines',
coordinateSystem: 'cartesian2d',
z: 1,
zlevel: 2,
animation: false,
effect: {
show: true,
period: 5,
trailLength: 0.01,
symbolSize: 12,
symbol: 'arrow',
loop: true,
color: 'rgba(55,155,255,0.5)'
},
lineStyle: {
normal: {
color: '#22AC38',
width: 0,
curveness: 0.2
}
},
data: [
{
coords: [
[100, 200],
[200, 300]
]
},
{
coords: [
[100, 500],
[400, 400]
]
},
{
coords: [
[400, 400],
[700, 400]
]
},
{
coords: [
[700, 400],
[1000, 100]
]
},
{
coords: [
[700, 400],
[1000, 200]
]
},
{
coords: [
[700, 400],
[1000, 300]
]
},
{
coords: [
[700, 400],
[1000, 400]
]
},
{
coords: [
[700, 400],
[1000, 500]
]
},
{
coords: [
[700, 400],
[1000, 600]
]
},
{
coords: [
[600, 600],
[700, 400]
]
},
{
coords: [
[700, 400],
[300, 100]
]
}
]
},
{
type: 'lines',
coordinateSystem: 'cartesian2d',
z: 1,
zlevel: 2,
animation: false,
effect: {
show: true,
period: 6,
trailLength: 0.01,
symbolSize: 12,
symbol: 'arrow',
loop: true,
// period: 2.5,
// trailLength:0,
// symbolSize: 12,
// symbol: "circle",
color: 'rgba(55,155,255,0.5)'
},
lineStyle: {
normal: {
color: '#22AC38',
width: 0,
curveness: 0.2
}
},
data: [
{
coords: [
[100, 200],
[400, 400]
]
},
{
coords: [
[100, 500],
[400, 400]
]
},
{
coords: [
[400, 400],
[700, 400]
]
},
{
coords: [
[700, 400],
[1000, 100]
]
},
{
coords: [
[700, 400],
[1000, 200]
]
},
{
coords: [
[700, 400],
[1000, 300]
]
},
{
coords: [
[700, 400],
[1000, 400]
]
},
{
coords: [
[700, 400],
[1000, 500]
]
},
{
coords: [
[700, 400],
[1000, 600]
]
},
{
coords: [
[600, 600],
[700, 400]
]
},
{
coords: [
[700, 400],
[300, 100]
]
}
]
},
{
type: 'lines',
coordinateSystem: 'cartesian2d',
z: 1,
zlevel: 2,
animation: false,
effect: {
show: true,
period: 8,
trailLength: 0.01,
symbolSize: 12,
symbol: 'arrow',
loop: true,
// period: 2.5,
// trailLength:0,
// symbolSize: 12,
// symbol: "circle",
color: 'rgba(55,155,255,0.5)'
},
lineStyle: {
normal: {
color: '#22AC38',
width: 0,
curveness: 0.2
}
},
data: [
{
coords: [
[100, 200],
[400, 400]
]
},
{
coords: [
[100, 500],
[400, 400]
]
},
{
coords: [
[400, 400],
[700, 400]
]
},
{
coords: [
[700, 400],
[1000, 100]
]
},
{
coords: [
[700, 400],
[1000, 200]
]
},
{
coords: [
[700, 400],
[1000, 300]
]
},
{
coords: [
[700, 400],
[1000, 400]
]
},
{
coords: [
[700, 400],
[1000, 500]
]
},
{
coords: [
[700, 400],
[1000, 600]
]
},
{
coords: [
[600, 600],
[700, 400]
]
},
{
coords: [
[700, 400],
[300, 100]
]
}
]
}
]
})
this.chart.setOption(option)
this.chart.on('click', (params) => {
console.log('params', params)
})
},
// 拓扑图2
topolog (id) {
this.chart = init(document.getElementById(id))
this.chart.clear()
// 节点图标
const nodeDataList = [{
name: '块1-1', // 节点名
linkTargetName: '节点B', // 连线目标节点
linkValue: ' ', // 连线介绍
coordConfig: {
level: '0-error',
effect: {
show: true,
smooth: false,
trailLength: 0,
symbol: 'circle',
color: '#fb3f3f',
symbolSize: 10,
period: 3,
delay: 1500,
loop: true
},
lineStyle: {
normal: {
curveness: 0,
color: '#fb3f3f',
width: 1.4
}
}
}, // 连线动态箭头配置,没有就不需要此配置
value: [10, 190],
draggable: false,
fixed: true,
symbol: 'circle',
symbolSize: 40,
itemStyle: {
color: '#fb3f3f'
}
},
{
name: '块1-2',
linkTargetName: '节点B',
linkValue: ' ',
coordConfig: {
level: '0'
},
value: [10, 150],
draggable: false,
fixed: true,
symbol: 'circle',
symbolSize: 40
}, {
name: '块2',
linkTargetName: '节点C',
linkValue: ' ',
coordConfig: {
level: '0'
},
value: [10, 100],
draggable: false,
fixed: true,
symbol: 'circle',
symbolSize: 40
}, {
name: '块3',
linkTargetName: '节点D',
linkValue: ' ',
coordConfig: {
level: '0'
},
value: [10, 40],
draggable: false,
symbol: 'circle',
symbolSize: 40
},
// 节点B,C,D ....n
{
name: '节点B',
linkTargetName: '应用端',
linkValue: ' ',
coordConfig: {
level: '1'
},
symbol: 'circle',
symbolSize: 50,
draggable: false,
value: [220, 160]
}, {
name: '节点C',
linkTargetName: '应用端',
linkValue: ' ',
coordConfig: {
level: '1'
},
symbol: 'circle',
symbolSize: 60,
draggable: false,
value: [220, 100]
}, {
name: '节点D',
linkTargetName: '应用端',
linkValue: ' ',
coordConfig: {
level: '1'
},
symbol: 'circle',
symbolSize: 60,
draggable: false,
value: [220, 40]
}, {
name: '应用端',
linkTargetName: '应用端',
linkValue: ' ',
// coordConfig: {level: 2},
symbolSize: 60,
symbol: 'circle',
draggable: false,
value: [400, 100]
}
]
const getCoordDataList = function () {
const coorDataDict = {}
const defaultConfig = {
type: 'lines', // 块1,2...n到节点A,B...N
coordinateSystem: 'cartesian2d',
// animationDelay: 10000,
z: 1,
effect: {
show: true,
smooth: true,
trailLength: 0,
symbol: 'arrow',
color: '#67c23a',
symbolSize: 10,
period: 3,
delay: 1500,
loop: true
},
lineStyle: {
normal: {
curveness: 0,
color: '#67c23a',
width: 1
}
},
data: []
}
nodeDataList.map(item => {
if (item.coordConfig !== undefined) {
if (!(item.coordConfig.level in coorDataDict)) {
const coorConfig = JSON.parse(JSON.stringify(defaultConfig))
if (item.coordConfig.lineStyle !== undefined) {
coorConfig.lineStyle = item.coordConfig.lineStyle
}
if (item.coordConfig.effect !== undefined) {
coorConfig.effect = item.coordConfig.effect
}
coorDataDict[item.coordConfig.level] = coorConfig
}
const coordData = [
item.value,
nodeDataList.filter(i => i.name === item.linkTargetName)[0].value
]
coorDataDict[item.coordConfig.level].data.push({
coords: coordData
})
if (item.coordConfig.bilateral) {
coorDataDict[item.coordConfig.level].data.push({
coords: coordData.reverse()
})
}
}
})
return Object.values(coorDataDict)
}
console.log(getCoordDataList())
const option = {
title: {
text: 'MySQL应用架构拓扑图',
top: 'top',
left: 'center'
},
itemStyle: {
normal: {
color: '#67C23A'
},
shadowBlur: 0
},
textStyle: {
color: '#444',
fontSize: 16,
fontWeight: 600
},
// legend: [{
// formatter: function (name) {
// return echarts.format.truncateText(name, 200, '12px', '…')
// },
// tooltip: {
// show: true
// },
// selectedMode: 'false',
// bottom: 20
// }],
// animationDuration: 500,
// animationEasingUpdate: 'quinticInOut',
xAxis: {
show: false,
type: 'value'
},
yAxis: {
show: false,
type: 'value'
},
series: [{
type: 'graph',
coordinateSystem: 'cartesian2d',
// legendHoverLink: false,
// hoverAnimation: true,
// nodeScaleRatio: false,
// 建头
edgeSymbol: ['circle', 'none'],
edgeSymbolSize: [2, 15],
edgeLabel: {
show: false,
normal: {
show: true,
position: 'middle',
textStyle: {
fontSize: 12
},
formatter: '{c}'
}
},
// focusNodeAdjacency: true,
roam: false,
// 圆形上面的文字
label: {
normal: {
position: 'bottom',
show: true,
textStyle: {
fontSize: 12
}
}
},
itemStyle: {
normal: {
color: '#409eff'
},
shadowBlur: 0
},
lineStyle: {
normal: {
width: 0,
shadowColor: 'none',
color: '#ff0000'
}
},
data: nodeDataList,
links: nodeDataList.map(item => {
return {
source: item.name,
value: item.linkValue,
target: item.targetName
}
})
}].concat(getCoordDataList())
}
this.chart.setOption(option)
},
// click事件
chartsClick (id) {
document.getElementById(id).addEventListener('click', (params) => {
console.log('params', params)
})
}
}
}
}
})
}
export default {
install
}
2. Antv G6
本来想使用Echarts就可以完成任务了,但是没想到的是,当把数据引入发现当结点动态的时候,每一个结点的坐标很难处理,导致画出来的图非常丑,然后使用AntV 的G6关系图,然后自定义自己需要的特效,demo中实现了涟漪特效,边动画,文字截取,虚线边。不过代码量有点大。
const container = document.getElementById('mountNode')
const width = container.scrollWidth
const height = container.scrollHeight || 500
// let legendDiv = document.createElement('div')
// 自定义tooltip
const tooltip = new G6.Tooltip({
// offsetX and offsetY include the padding of the parent container
// offsetX 与 offsetY 需要加上父容器的 padding
offsetX: 10,
offsetY: 10,
// the types of items that allow the tooltip show up
// 允许出现 tooltip 的 item 类型
itemTypes: ['node', 'edge'],
// custom the tooltip's content
// 自定义 tooltip 内容
getContent: (e) => {
// console.log('e', e)
const outDiv = document.createElement('div')
outDiv.style.width = 'fit-content'
outDiv.style.height = 'fit-content'
outDiv.style.textAlign = 'justify'
const model = e.item.getModel()
// console.log('model', model)
if (e.item.getType() === 'node') {
outDiv.innerHTML = `记录数:${model.logNumber}</br> 耗时:${model.costTime}</br> 系统: ${model.systemName}</br> 服务: ${model.serviceName}`
} else {
// console.log('model', model)
// outDiv.style.display = 'none'
outDiv.innerHTML = `原点:${model.source}, \n 终点:${model.target}`
}
return outDiv
}
})
// Background Animation 背景特效
G6.registerNode('background-animate', {
afterDraw (cfg, group) {
let r = cfg.size / 2
if (isNaN(r)) {
r = cfg.size[0] / 2
}
// 第一个背景圆
const back1 = group.addShape('circle', {
zIndex: -3,
attrs: {
x: 0,
y: 0,
r,
fill: cfg.color,
opacity: 0.6
},
// must be assigned in G6 3.3 and later versions. it can be any value you want
name: 'circle-shape1'
})
// 第二个背景圆
const back2 = group.addShape('circle', {
zIndex: -2,
attrs: {
x: 0,
y: 0,
r,
fill: '#4592FF', // 为了显示清晰,随意设置了颜色
opacity: 0.6
},
// must be assigned in G6 3.3 and later versions. it can be any value you want
name: 'circle-shape2'
})
// 第三个背景圆
const back3 = group.addShape('circle', {
zIndex: -1,
attrs: {
x: 0,
y: 0,
r,
fill: '#52D5CC',
opacity: 0.6
},
// must be assigned in G6 3.3 and later versions. it can be any value you want
name: 'circle-shape3'
})
group.sort() // 排序,根据 zIndex 排序
// 第一个背景圆逐渐放大,并消失
back1.animate({
r: r + 10,
opacity: 0.1
}, {
repeat: true, // 循环
duration: 3000,
easing: 'easeCubic',
delay: 0 // 无延迟
})
// 第二个背景圆逐渐放大,并消失
back2.animate({
r: r + 10,
opacity: 0.1
}, {
repeat: true, // 循环
duration: 3000,
easing: 'easeCubic',
delay: 1000 // 1 秒延迟
}) // 1 秒延迟
// 第三个背景圆逐渐放大,并消失
back3.animate({
r: r + 10,
opacity: 0.1
}, {
repeat: true, // 循环
duration: 3000,
easing: 'easeCubic',
delay: 2000 // 2 秒延迟
})
}
}, 'circle')
// 边的动画特效
G6.registerEdge('circle-running', {
afterDraw (cfg, group) {
// get the first shape in the group, it is the edge's path here=
// console.log('group', group)
const shape = group.get('children')[0]
// the start position of the edge's path
const startPoint = shape.getPoint(0)
// add red circle shape
const circle = group.addShape('circle', {
attrs: {
x: startPoint.x,
y: startPoint.y,
fill: 'rgba(82,213,204,0.54)',
r: 3
},
name: 'circle-shape'
})
// animation for the red circle
circle.animate(
(ratio) => {
// the operations in each frame. Ratio ranges from 0 to 1 indicating the prograss of the animation. Returns the modified configurations
// get the position on the edge according to the ratio
const tmpPoint = shape.getPoint(ratio)
// returns the modified configurations here, x and y here
return {
x: tmpPoint.x,
y: tmpPoint.y
}
},
{
repeat: true, // Whether executes the animation repeatly
duration: 3000 // the duration for executing once
}
)
}
},
'cubic' // extend the built-in edge 'cubic'
)
const lineDash = [4, 2, 1, 2]
// 线的特效
G6.registerEdge('line-dash', {
afterDraw (cfg, group) {
// get the first shape in the group, it is the edge's path here=
const shape = group.get('children')[0]
let index = 0
// Define the animation
shape.animate(
() => {
index++
if (index > 9) {
index = 0
}
const res = {
lineDash,
lineDashOffset: -index
}
// returns the modified configurations here, lineDash and lineDashOffset here
return res
},
{
repeat: true, // whether executes the animation repeatly
duration: 3000 // the duration for executing once
}
)
}
},
'cubic' // extend the built-in edge 'cubic'
)
const graph = new G6.TreeGraph({
container: 'mountNode',
width,
height,
plugins: [tooltip],
modes: {
default: [
{
type: 'collapse-expand',
onChange: function onChange (item, collapsed) {
const data = item.get('model').data
data.collapsed = true
return true
}
},
'drag-canvas',
'zoom-canvas'
]
},
defaultEdge: {
type: 'circle-running',
size: 1,
style: {
radius: 0,
offset: 10,
endArrow: true,
lineWidth: 0.5,
/* and other styles */
stroke: '#B2B2B2'
}
},
layout: {
type: 'mindmap',
direction: 'H',
getHeight: () => {
return 60
},
getWidth: () => {
return 50
},
getVGap: () => {
return 30
},
getHGap: () => {
return 80
}
}
})
// 结点位置
let centerX = 0
graph.node(function (node) {
if (node.id === 'Z') {
centerX = node.x
}
// 记录数、节点耗时、系统名称、服务名称
return {
// label: `${fittingString(node.label, 140, 12)}`,
label: `${node.label}`,
labelCfg: {
fontSize: 14,
position:
node.children && node.children.length > 0
? node.id === 'Z' ? 'left' : 'bottom'
: node.x > centerX
? 'right'
: 'left',
offset: 10
}
}
})
graph.data(nodeData[0])
graph.render()
graph.fitView()
if (typeof window !== 'undefined') {
window.onresize = () => {
if (!graph || graph.get('destroyed')) return
if (!container || !container.scrollWidth || !container.scrollHeight) return
graph.changeSize(container.scrollWidth, container.scrollHeight)
}
}