- 有一个需求实现类似于企查查之类的企业图谱功能。调研了几个工具后,决定使用g6绘图来实现。
具体使用方式查看G6官网文档
下面附上写的一个demo代码。涵盖了节点位置,文字,颜色,节点事件等。
父组件
<template>
<div id="graphContainer">
<div
id="container-map"
ref="main-left"
v-loading="mapLoading"
/>
</div>
</template>
<script>
import G6 from "@antv/g6";
import { register } from './register-node'
import registorEdge from './register-edge'
export default {
name: 'scenceCompany',
data () {
return {
graph: '',
mapLoading: false,
pageSize: 8,
// 记录6个一级分类
pageNum1: 1,
pageNum2: 1,
pageNum3: 1,
pageNum4: 1,
pageNum5: 1,
pageNum6: 1,
}
},
mounted () {
this.initG6()
this.getMapData()
},
methods: {
// 图谱数据
getMapData () {
this.mapLoading = true
// 设置默认
this.pageNum1 = 1
this.pageNum2 = 1
this.pageNum3 = 1
this.pageNum4 = 1
this.pageNum5 = 1
this.pageNum6 = 1
// this.$http.get('').then(res => {
let res = {
data: {
id: '1',
label: 'xx银行',
companyCode: '600000',
children: [
{
id: '1-1',
label: '股东',
location: 'left',
childrenSize: 9,
children: [
{
id: '1-1-1',
label: 'xx国际集团',
post: '债',
edgesLabel: '21.57%',
companyType: '股东',
location: 'left',
},
{
id: '1-1-2',
companyType: '股东',
post: '债',
label: 'xx集团广东',
location: 'left',
},
{
id: '1-1-3',
label: 'xx国际集团',
post: '债',
edgesLabel: '21.57%',
companyType: '股东',
location: 'left',
},
{
id: '1-1-4',
companyType: '股东',
post: '债',
label: 'xxx集团广东',
location: 'left',
},
{
id: '1-1-5',
label: 'xx国际集团',
edgesLabel: '21.57%',
companyType: '股东',
location: 'left',
},
{
id: '1-1-6',
companyType: '股东',
label: 'xx集团广东',
location: 'left',
},
{
id: '1-1-7',
companyType: '股东',
label: 'xx集团广东',
location: 'left',
},
{
id: '1-1-8',
companyType: '股东',
label: 'xx集团广东',
location: 'left',
}
]
},
{
id: '1-2',
label: '高管',
location: 'right',
childrenSize: 10,
children: [
{
id: '1-2-1',
label: 'xx',
post: '董事长、执行董事',
location: 'right',
companyType: '高管'
},
{
id: '1-2-2',
label: 'xxx',
post: '副董事长、执行董事、行长',
location: 'right',
companyType: '高管'
},
{
id: '1-2-3',
label: 'xxx',
post: '副行长、执行董事、首席风险官',
location: 'right',
companyType: '高管'
},
{
id: '1-2-4',
label: 'xx',
post: '非执行董事',
location: 'right',
companyType: '高管'
},
{
id: '1-2-5',
label: 'xx',
post: '非执行董事',
location: 'right',
companyType: '高管'
},
{
id: '1-2-6',
label: 'xx',
post: '非执行董事',
location: 'right',
companyType: '高管'
},
{
id: '1-2-7',
label: 'xx',
post: '非执行董事',
location: 'right',
companyType: '高管'
},
{
id: '1-2-8',
label: 'xx',
post: '非执行董事',
location: 'right',
companyType: '高管'
},
]
},
{
id: '1-3',
label: '对外投资',
location: 'left',
childrenSize: 2,
children: [
{
id: '1-3-1',
label: 'xx发展基金',
edgesLabel: '21.57%',
post: '债',
companyType: '对外投资',
location: 'left',
},
{
id: '1-3-2',
companyType: '对外投资',
label: 'xx理财',
post: '债',
location: 'left',
},
{
id: '1-3-3',
label: 'xx发展基金',
post: '债',
edgesLabel: '21.57%',
companyType: '对外投资',
location: 'left',
},
{
id: '1-3-4',
companyType: '对外投资',
label: 'xx理财',
location: 'left',
},
]
},
{
id: '1-4',
label: '客户',
location: 'right',
childrenSize: 3,
children: [
{
id: '1-4-1',
label: 'xx投资(北京)',
post: '三板',
location: 'right',
},
{
id: '1-4-2',
label: 'xx集团',
post: '债',
location: 'right',
},
{
id: '1-4-3',
label: 'xx凯',
post: '688079',
location: 'right',
},
]
},
{
id: '1-5',
label: '供应商',
location: 'right',
childrenSize: 3,
children: [
{
id: '1-5-1',
label: 'xx信息',
post: '300380',
location: 'right',
},
{
id: '1-5-2',
label: 'xx金融',
post: '430656',
location: 'right',
},
{
id: '1-5-3',
label: 'xx经营发展啊啊啊啊',
post: '债',
location: 'right',
},
]
},
{
id: '1-6',
label: '实控人',
location: 'left',
childrenSize: 2,
children: [
{
id: '1-6-1',
label: '无实际控制人',
location: 'left',
},
]
},
]
}
}
this.mapLoading = false
if (res.data.children.length) {
for (let item of res.data.children) {
if (item.childrenSize > this.pageSize) { // 添加更多按钮
item.children.push({
id: item.label + 'more',
companyType: item.label,
label: `更多(${item.childrenSize - this.pageSize})`,
location: item.children[0].location,
num: this.pageSize > (item.childrenSize - this.pageSize) ? item.childrenSize - this.pageSize : this.pageSize
})
}
}
}
this.graph.data(res.data)
this.graph.render()
// }).catch(() => {
// this.mapLoading = false
// })
},
async getMoreCompanyData (model) {
let pageNum = 0
switch (model.companyType) {
case '股东':
this.pageNum1++
pageNum = this.pageNum1
break
case '高管':
this.pageNum2++
pageNum = this.pageNum2
break
case '对外投资':
this.pageNum3++
pageNum = this.pageNum3
break
case '客户':
this.pageNum4++
pageNum = this.pageNum4
break
case '实控人':
this.pageNum5++
pageNum = this.pageNum5
break
case '供应商':
this.pageNum6++
pageNum = this.pageNum6
break
}
let data = {}
console.log(model)
if (model.companyType == '股东') {
data = {
data: {
isShow:false,
total: 9,
list: [
{
id: '1-1-9',
companyType: '股东',
label: 'xx集团广东',
location: 'left',
},
]
}
}
} else if (model.companyType == '高管') {
data = {
data: {
isShow: false,
total: 10,
list: [
{
id: '1-2-9',
label: 'xx',
post: '董事长、执行董事',
location: 'right',
companyType: '高管'
},
{
id: '1-2-10',
label: 'xxx',
post: '副董事长、执行董事、行长',
location: 'right',
companyType: '高管'
},
]
}
}
}
return data.data
},
initG6 () {
register(G6)
registorEdge(G6)
const ele = document.getElementById('container-map')
const width = ele.clientWidth
const height = ele.clientHeight
const graph = new G6.TreeGraph({
container: 'container-map',
width: width,
height: height,
defaultNode: {
type: "tree-node",
anchorPoints: [
[1, 0.5],
[0, 0.5],
],
// 节点样式
style: {
cursor: "pointer",
radius: 4,
},
},
// 配置边的属性
defaultEdge: {
type: "kaimo-line",
},
// 布局配置项
layout: {
type: "mindmap", // 脑图树布局
direction: "H",
// 节点 id 的回调函数
getId: function getId(d) {
return d.id;
},
// 下面都是一些控制节点与节点间距离的回调函数,具体可以试着修改一下值。
// 节点高度的回调函数
getHeight: function getHeight() {
return 16;
},
// 节点宽度的回调函数
getWidth: function getWidth(cfg) {
return 60;
},
// 节点纵向间距的回调函数
getVGap: function getVGap() {
return 20;
},
// 节点横向间距的回调函数
getHGap: function getHGap(val) {
return 70;
},
getSide: (node) => {
if (node.data.location === "left") {
return "left";
}
return "right";
},
},
modes: {
default: [
'drag-canvas',
'zoom-canvas'
]
},
fitView: true,
fitCenter: true,
// minZoom: 0.5,
maxZoom: 1
})
this.graph = graph
// 植入事件
this.bindEvents()
this.graph.get('canvas').set('localRefresh', false)
},
bindEvents () {
const that = this
this.graph.on('node:click', async e => {
console.log('item', e)
const model = e.item.getModel()
const timeFlag = this.stopClik()
if (timeFlag) return
if (model.label.includes('更多')) {
let newChildrenData = []
const data = await that.getMoreCompanyData(model)
console.log(123321, data)
// 获取原有的子节点
let oldChildren = e.item._cfg.parent._cfg.children
oldChildren.forEach(ele => {
const eleModel = ele.getModel()
if (!eleModel.label.includes('更多')) newChildrenData.push(ele.getModel())
})
newChildrenData = [...newChildrenData, ...data.list]
if (data.isShow) {
newChildrenData.push({
id: data.list[0].companyType + data.list[0].id + 'more',
label: `更多(${data.total - newChildrenData.length})`,
location: data.list[0].location,
companyType: data.list[0].companyType,
num: data.total - newChildrenData.length
})
}
// 更新父节点下面的子节点
this.graph.updateItem(e.item._cfg.parent._cfg.id, {
children: JSON.parse(JSON.stringify(newChildrenData))
})
this.graph.layout()
return
}
})
},
// 防止连续点击
stopClik () {
let nowName = new Date().getTime()
let filterDataTime = Number(window.localStorage.getItem('filterDataTime'))
if (filterDataTime) {
if (nowName - filterDataTime < 2000) { // 防止重复点击
window.localStorage.setItem('filterDataTime', filterDataTime)
return true
} else {
window.localStorage.setItem('filterDataTime', nowName)
}
} else {
window.localStorage.setItem('filterDataTime', nowName)
}
},
}
}
</script>
<style lang="scss" scoped>
#graphContainer {
width: 100%;
height: 100%;
#container-map {
width: 100%;
height: 100%;
}
}
</style>
register-edge.js
export default G6 => {
G6.registerEdge("kaimo-line", {
/**
* 绘制边,包含文本
* @param {Object} cfg 边的配置项
* @param {G.Group} group 图形分组,边中的图形对象的容器
* @return {G.Shape} 绘制的图形,通过 node.get('keyShape') 可以获取到
*/
draw(cfg, group) {
const startPoint = cfg.startPoint;
const endPoint = cfg.endPoint;
const target = cfg.targetNode.getModel()
const source = cfg.sourceNode.getModel()
let shape;
shape = group.addShape("path", {
//线条
attrs: {
stroke: "#E4E7ED",
path: [
["M", startPoint.x, startPoint.y],
["L", endPoint.x / 3 + (2 / 3) * startPoint.x, startPoint.y],
["L", endPoint.x / 3 + (2 / 3) * startPoint.x, endPoint.y],
["L", endPoint.x, endPoint.y],
],
endArrow: {
path: G6.Arrow.triangle(5, 5, 0), // 使用内置箭头路径函数,参数为箭头的 宽度、长度、偏移量(默认为 0,与 d 对应)
d: 0,
fill: "#E4E7ED",
opacity: 0.5,
lineWidth: 1,
},
},
// must be assigned in G6 3.3 and later versions. it can be any value you want
name: "path-shape",
});
// 关系描述
if (target.edgesLabel) {
group.addShape('text', {
attrs: {
x: endPoint.x + 10,
y: endPoint.y,
fontSize: 12,
text: target.edgesLabel,
fill: '#666'
}
})
}
return shape;
},
});
}
register-node.js
import G6 from '@antv/g6'
// 文本超出隐藏 (字段, 最大长度, 字体大小)
const fittingString = (str, maxWidth, fontSize) => {
const ellipsis = "..."
const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0]
let currentWidth = 0
let res = str
const pattern = new RegExp("[\u4E00-\u9FA5]+")
str.split("").forEach((letter, i) => {
if (currentWidth > maxWidth - ellipsisLength) return
if (pattern.test(letter)) {
currentWidth += fontSize
} else {
currentWidth += G6.Util.getLetterWidth(letter, fontSize)
}
if (currentWidth > maxWidth - ellipsisLength) {
res = `${str.substr(0, i)}${ellipsis}`
}
})
return res
};
// 获取文本的长度
const getTextSize = (str, maxWidth, fontSize) => {
let width = G6.Util.getTextSize(str, fontSize)[0]
return width > maxWidth ? maxWidth : width
}
const getItemBakColor = (str) => {
if (str === '股东') return '#FD924E'
if (str === '高管') return '#7685E4'
if (str === '客户') return '#3FCDC1'
if (str === '对外投资') return '#F3AF17'
if (str === '供应商') return '#FA9B2D'
if (str === '实控人') return '#FA7370'
}
const register = G6 => {
G6.registerNode("tree-node", {
draw(cfg, group) {
let rect;
if (cfg.depth === 0) {
rect = group.addShape("circle", {
attrs: {
x: 100, // x 轴移动距离
y: 20, // y 轴移动距离
r: 40,
fill: "#0D85EE",
stroke: "#0D85EE", // 边框色
},
name: "big-circle-shape",
});
group.addShape("image", {
attrs: {
x: 75, // x 轴移动距离
y: -8, // y 轴移动距离
width: 50,
height: 50,
img: require('@/assets/img/fangzi.png'),
},
name: "big-image-shape",
});
group.addShape("text", {
attrs: {
text: fittingString(cfg.label, 90, 16),
x: 100,
// x: getTextSize(cfg.label, 90, 16) + 35,
y: 90,
fontSize: 18,
textAlign: "center",
fontWeight: 700,
textBaseline: "middle",
fill: "#0D85EE",
},
name: "text-shape",
});
group.addShape("text", {
attrs: {
text: `[${cfg.companyCode}]`,
x: 100,
y: 110,
fontSize: 18,
textAlign: "center",
fontWeight: 700,
textBaseline: "middle",
fill: "#0D85EE",
},
name: "text-shape",
});
}
if (cfg.location === "left") {
if (cfg.depth === 1) {
rect = group.addShape("circle", {
attrs: {
x: 160, // x 轴移动距离
y: 20, // y 轴移动距离
r: 35,
fill: getItemBakColor(cfg.label),
stroke: getItemBakColor(cfg.label), // 边框色
},
name: "first-circle-shape",
});
if (cfg.label === '对外投资') {
group.addShape("text", {
attrs: {
text: '对外',
x: 160,
y: 12,
fontSize: 18,
textAlign: "center",
textBaseline: "middle",
fill: "#fff",
},
name: "text-shape",
});
group.addShape("text", {
attrs: {
text: '投资',
x: 160,
y: 32,
fontSize: 18,
textAlign: "center",
textBaseline: "middle",
fill: "#fff",
},
name: "text-shape",
});
} else {
group.addShape("text", {
attrs: {
text: cfg.label,
x: 160,
y: 20,
fontSize: 18,
textAlign: "center",
textBaseline: "middle",
fill: "#fff",
},
name: "text-shape",
});
}
} else {
if (cfg.label.indexOf('更多') > -1) {
rect = group.addShape("rect", {
attrs: {
x: cfg.post ? 160 - getTextSize(cfg.label, 210, 18) - getTextSize(cfg.post, 210, 18) + 50 : 160 - getTextSize(cfg.label, 210, 18) + 50, // x 轴移动距离
y: 0, // y 轴移动距离
width: cfg.post ? getTextSize(cfg.label, 210, 18) + getTextSize(cfg.post, 210, 18) + 30 : getTextSize(cfg.label, 210, 18) + 30, // 宽
height: 40, // 高
fontSize: 18,
radius: 4,
},
name: "big-rect-shape",
});
group.addShape("text", {
attrs: {
text: fittingString(cfg.label, 210, 18),
x: 160 - getTextSize(cfg.label, 210, 18) + 80,
y: 20,
fontSize: 18,
textAlign: "left",
textBaseline: "middle",
fill: '#0D85EE',
cursor: 'pointer'
},
name: "text-shape",
});
} else {
rect = group.addShape("rect", {
attrs: {
x: cfg.post ? 160 - getTextSize(cfg.label, 210, 18) - getTextSize(cfg.post, 210, 18) + 50 : 160 - getTextSize(cfg.label, 210, 18) + 50, // x 轴移动距离
y: 0, // y 轴移动距离
width: cfg.post ? getTextSize(cfg.label, 210, 18) + getTextSize(cfg.post, 210, 18) + 30 : getTextSize(cfg.label, 210, 18) + 30, // 宽
height: 40, // 高
fontSize: 18,
// stroke: "#666",
// fontWeight: 600,
radius: 4,
},
name: "big-rect-shape",
});
// 左文本点
group.addShape("text", {
attrs: {
text: fittingString(cfg.label, 210, 18),
x: cfg.post ? 160 - getTextSize(cfg.label, 210, 18) + 50 : 160 - getTextSize(cfg.label, 210, 18) + 80,
y: 20,
fontSize: 18,
textAlign: "left",
textBaseline: "middle",
fill: document.body.className.indexOf('custom-f4c46d')>-1?'#fff' :'#333',
},
name: "text-shape",
});
if (cfg.post) {
group.addShape("text", {
attrs: {
text: `[${cfg.post}]`,
x: 195 + getTextSize(cfg.post, 210, 18),
y: 20,
fontSize: 18,
textAlign: "left",
textBaseline: "middle",
fill: '#666',
},
name: "text-shape",
});
}
}
}
}
if (cfg.location === "right") {
if (cfg.depth === 1) {
rect = group.addShape("circle", {
attrs: {
x: 35, // x 轴移动距离
y: 20, // y 轴移动距离
r: 35,
fill: getItemBakColor(cfg.label),
stroke: getItemBakColor(cfg.label), // 边框色
},
name: "first-circle-shape",
});
group.addShape("text", {
attrs: {
text: cfg.label,
x: 35,
y: 20,
fontSize: 18,
textAlign: "center",
textBaseline: "middle",
fill: "#fff",
},
name: "text-shape",
});
} else {
if (cfg.label.indexOf('更多') > -1) {
rect = group.addShape("text", {
attrs: {
text: fittingString(cfg.label, 210, 18),
x: 10,
y: 20,
fontSize: 18,
textAlign: "left",
textBaseline: "middle",
fill: '#0D85EE',
cursor: 'pointer'
},
name: "text-shape",
});
} else {
rect = group.addShape("text", {
attrs: {
text: cfg.companyType === '高管' ? cfg.label : fittingString(cfg.label, 210, 18),
x: 10,
y: 20,
fontSize: 18,
textAlign: "left",
textBaseline: "middle",
fill: document.body.className.indexOf('custom-f4c46d')>-1?'#fff' :'#333',
},
name: "text-shape",
});
if (cfg.post) {
group.addShape("text", {
attrs: {
text: `[${cfg.post}]`,
x: getTextSize(cfg.label, 210, 18) + 15,
y: 20,
fontSize: 18,
textAlign: "left",
textBaseline: "middle",
fill: '#666',
},
name: "text-shape",
});
}
}
}
}
return rect
}
})
}
export { register }