d3-graphviz实现拓扑图
源码地址:https://gitee.com/simod/d3-graphviz
具体代码如下:
<template>
//防止节点数据太多页面出现空白,所以添加loading
<div class="main-charts" v-loading="loadList" element-loading-text="拼命加载中">
// 承载画布的元素
<div class="graph-container" id="graph"></div>
</div>
</template>
<script>
export default {
data () {
return {
source: {}, //或缺节点数据
layers: [], //节点所对应的层
graph: {}, // d3-graphviz的对象
ciTypesName: {},
loadList: false,
tabList:[]
}
},
methods: {
initGraph () {
this.loadList = true
let graph
const initEvent = () => {
//获取承载拓扑图画布的对象
graph = d3.select('#graph')
graph
.on('dblclick.zoom', null)
.on('wheel.zoom', null)
.on('mousewheel.zoom', null)
// 这些属性确保拓扑图可以双击或者鼠标滚动可以放大缩小图
this.graph.graphviz = graph
.graphviz()
.zoom(true) //允许拓扑图可以放大缩小
.scale(2) //设置拓扑图放大缩小的比例
.width(window.innerWidth - 92)
// 画布的宽度
.height(window.innerHeight - 190)
// 设置画布的高度
.attributer(function (d) {
if (d.attributes.class === 'edge') {
var keys = d.key.split('->')
var from = keys[0].trim()
var to = keys[1].trim()
d.attributes.from = from
d.attributes.to = to
}
if (d.tag === 'text') {
var key = d.children[0].text
d3.select(this).attr('text-key', key)
}
})
//渲染拓扑图的函数
this.renderGraph(this.source)
}
let layerResponse = {}
_fetch({
url: `/cmdb/cmdb/getAllLevel`,
type: "get",
})
.then(response => {
layerResponse = response.data
if (layerResponse.status) {
let tempLayer = layerResponse.body.data
.map(_ => {
return { name: _.name, layerId: _._id, ..._ }
})
// 将获取到的层排序
this.layers = tempLayer.sort((a, b) => {
return a.seqNo - b.seqNo
})
_fetch({
url: `/cmdb/cmdb/getFullCitRelation`,
type: "get",
})
.then(response => {
let ciResponse = response.data
if (ciResponse.status) {
//获取节点后,将节点处理
this.source = ciResponse.body.data
this.source.forEach(_ => {
_.ciTypes &&
_.ciTypes.forEach(async i => {
this.ciTypesName[i.ciTypeId] = i.name
let imgFileSource = i.imageFileId === undefined
? '/controlview/assets/img/icon//icon (1).png'
: i.imageFileId
this.$set(i, 'form', {
...i,
imgSource: imgFileSource,
imgUploadURL: `imgFileSource`
})
i.references &&
i.references.forEach(j => {
this.$set(j, 'form', {
...j,
isAccessControlled: j.isAccessControlled ? 'yes' : 'no',
isNullable: j.isNullable ? 'yes' : 'no',
isSystem: j.isSystem ? 'yes' : 'no'
})
})
})
})
let uploadToken = document.cookie.split(';').find(i => i.indexOf('XSRF-TOKEN') !== -1)
setHeaders({
'X-XSRF-TOKEN': uploadToken && uploadToken.split('=')[1]
})
initEvent()
}
})
}
})
},
genDOT (data) {
let nodes = []
data.forEach(_ => {
if (_.ciTypes) nodes = nodes.concat(_.ciTypes)
})
// 组装成d3-graphviz所需要的数据
var dots = [
'digraph {',
'bgcolor="transparent";',
'node [fontname=Arial,shape="ellipse", fixedsize="true", width="1.1", height="1.1", color="transparent", fontsize=11];',
'edge [fontname=Arial,minlen="1", color="#7f8fa6", fontsize=10];',
'ranksep = 1.1; nodesep=.7; size = "11,8";rankdir=TB'
]
//设置基础属性
let layerTag = `node [];`
// 将所有节点的层数据组装
let tempClusterObjForGraph = {}
let tempClusterAryForGraph = []
this.layers.map((_, index) => {
if (index !== this.layers.length - 1) {
layerTag += `"layer_${_.layerId}"->`
} else {
layerTag += `"layer_${_.layerId}"`
}
// “rank=same”属性很重要,此属性是将顶点分组
tempClusterObjForGraph[index] = [
`{ rank=same; "layer_${_.layerId}"[id="layerId_${_.layerId}",class="layer",label="${_.name}",tooltip="${_.name}"];`
]
// 根据层id找出每一层所对应的节点组装节点数据
nodes.forEach((node, nodeIndex) => {
if (node.layerId === _.layerId) {
tempClusterObjForGraph[index].push(
`"ci_${node.ciTypeId}"[id="${node.ciTypeId}~${node.alias}",label="${node.name}",tooltip="${node.name}",class="ci cit_${node.ciTypeId}",image="${node.form.imgSource}", labelloc="b"]`
)
}
if (nodeIndex === nodes.length - 1) {
tempClusterObjForGraph[index].push('} ')
}
})
if (nodes.length === 0) {
tempClusterObjForGraph[index].push('} ')
}
tempClusterAryForGraph.push(tempClusterObjForGraph[index].join(''))
})
dots.push(tempClusterAryForGraph.join(''))
dots.push('{' + layerTag + '[style=invis]}')
// 找出每个节点之间所对应的连线关系
nodes.forEach(node => {
node.references &&
node.references.forEach(attr => {
if (attr) {
const target = nodes.find(_ => _.ciTypeId === attr.referenceId)
if (target) {
dots.push(this.genEdge(nodes, node, attr))
}
}
})
})
dots.push('}')
return dots.join('')
},
genEdge (nodes, from, to) {
const target = nodes.find(_ => _.ciTypeId === to.referenceId)
let labels = to.referenceName ? to.referenceName.trim() : ''
return `"ci_${from.ciTypeId}"->"ci_${target.ciTypeId}"[taillabel="${labels}",labeldistance=3];`
},
// 设置节点以及连线的样式
shadeAll () {
d3.selectAll('g path')
.attr('stroke', '#7f8fa6')
.attr('stroke-opacity', '.2')
d3.selectAll('g polygon')
.attr('stroke', '#7f8fa6')
.attr('stroke-opacity', '.2')
.attr('fill', '#7f8fa6')
.attr('fill-opacity', '.2')
d3.selectAll('.edge text').attr('fill', '#7f8fa6')
},
colorNode (nodeName) {
d3.selectAll('g[from="' + nodeName + '"] path')
.attr('stroke', 'red')
.attr('stroke-opacity', '1')
d3.selectAll('g[from="' + nodeName + '"] text').attr('fill', 'red')
d3.selectAll('g[from="' + nodeName + '"] polygon')
.attr('stroke', 'red')
.attr('fill', 'red')
.attr('fill-opacity', '1')
.attr('stroke-opacity', '1')
d3.selectAll('g[to="' + nodeName + '"] path')
.attr('stroke', 'green')
.attr('stroke-opacity', '1')
d3.selectAll('g[to="' + nodeName + '"] text').attr('fill', 'green')
d3.selectAll('g[to="' + nodeName + '"] polygon')
.attr('stroke', 'green')
.attr('fill', 'green')
.attr('fill-opacity', '1')
.attr('stroke-opacity', '1')
},
renderGraph (data) {
let nodesString = this.genDOT(data)
this.graph.graphviz
.transition()
.renderDot(nodesString, () => {
this.loadList = false
this.$nextTick(() => {
this.startApps()
})
})
//渲染完成 的回调函数
.on('end', () => {
this.shadeAll()
})
},
// 节点添加事件
startApps() {
d3.selectAll(".node").attr('cursor', 'pointer')
var nodes = d3.selectAll(".node");
var _that=this
nodes.on("mouseover", function(el) {
const e = d3.event
e.preventDefault()
e.stopPropagation()
const endNode = d3.select(this)
var nodeName = endNode.selectWithoutDataPropagation("title").text().trim()
_that.shadeAll()
_that.colorNode(nodeName)
})
nodes.on("dblclick", function(el) {
_that.handleNodeClick(this)
})
},
handleNodeClick (kl) {
const e = d3.event
e.preventDefault()
e.stopPropagation()
const endNode = d3.select(kl)
const g = endNode._groups[0][0]
let isLayerSelected = g.getAttribute('class').indexOf('layer') >= 0
if (isLayerSelected) {
return
}
const found = this.tabList.find(_ => _.id === g.id)
if (!found) {
const ci = {
name: endNode.selectWithoutDataPropagation("text").text().trim(),
id: g.id.split('~')[0],
aliasName: g.id.split('~')[1]
}
}
}
},
mounted() {
this.initGraph()
}
}
</script>
后端应该返回的接口数据
返回节点所对应的层的数据
{
data:[{
"name": "应用架构层",
"seqNo": 1,
"_id": 1,
}]
}
返回所有节点的数据格式:
{
data:[{
"codeId" : "1",//层ID
"value" : "应用系统",//层名称
"ciTypes" : [ {
//节点所对应的层id
"layerId" : "1",
"seqNo" : 0,
// 当前节点所对应的关系
"references" : [ {
//连线的目标ID,以及连线的关系
"referenceId" : "16fb095d60484b08952a63661a059164",
"referenceName" : "组成"
} ],
//节点名称
"name" : "产品",
//当前节点的id
"ciTypeId" : "8ce47ddc86cc44fbb59e15cb5d683b48",
// 节点的图片
"imageFileId" : "/controlview/assets/img/linkShow//icon (8).png"
}]
}
引入依赖:
使用时引入的框架
1,引入d3.js
2,引入vize.js
<script src="https://unpkg.com/viz.js@1.8.1/viz.js" type="javascript/worker"></script>
3,引入@hpcc-js/wasm,d3-graphviz中渲染数据需要用到,
4,最后引入d3-graphviz
最终效果图的展示