使用zrender(canvas) + d3绘制关系图谱
在这个dome中d3只用于位置计算和缩放,在关系图谱中,大量节点的情况下,对比与svg,canvas有着性能优势,因为它不需要频繁操作dom节点,不需要创建深层次的dom结构。 使用原生的canvas api比较复杂,很多第三方库提供了更加方便操作canvas的库,这里选用的是zrender。 canvas的一些比较复杂的东西比如事件,层级在zrender中也可以很好地实现。
zrender
api文档:ecomfe.github.io/zrender-doc… 仓库地址:github.com/ecomfe/zren… demo:github.com/ecomfe/zren… 由于目前最新版本对一些功能的支持和文档不一样(图形文字),所以当前demo采用4.3.2版本。
核心api
初始化
选择html中对应存在的dom作为容器,返回一个实例对象,用于创建图形,这里要记得给#main
设置宽高
const zr = zrender.init(document.querySelector('#main'));
创建一个圆形
<body>
<div id="main" style="width:100vw;height:100vh"></div>
<script src="../lib/zrender.v4.3.2.js"></script>
<script>
const zr = new zrender.init(document.querySelector('#main'));
console.log(zr);
const circle = new zrender.Circle({
shape: {
// 圆中心点x坐标
cx: 100,
// 圆中心点y坐标
cy: 100,
// 圆半径
r: 100
},
style: {
// 设置圆样式
fill: 'blue',
stroke: 'black',
// 设置圆内文本样式
text: '圆内文本',
textFill: 'white',
fontSize: 20,
// 缩放文本大小跟随
transformText: true
}
})
zr.add(circle)
</script>
</body>
创建一条直线
const line = new zrender.Line({
shape: {
// 起始x坐标
x1: 100,
// 起始y坐标
y1: 300,
// 结束x坐标
x2: 100,
// 结束y坐标
y2: 500
},
style: {
// 线条颜色
stroke: 'blue',
// 线条粗细
lineWidth: 2,
text: '直线中间的文本',
// 文字的包围矩形背景色
textBackgroundColor: '#fff',
}
})
zr.add(line);
绘制三角形
const polygon = new zrender.Polygon({
shape: {
// 填充图形的坐标
points: [[x3, y3], [x4, y4], [x2, y2]],
},
style: {
fill: '填充颜色',
opacity
},
z
})
创建带箭头的直线
箭头的位置需要一些三角函数的运算,下面代码为封装好的绘制函数,offset为0的时候尖端是个矩形,可以调整为负数实现更好的效果,当前这个dome中,由于箭头指向圆变,所以要偏移圆半径的距离。
const zr = new zrender.init(document.querySelector('#main'));
/*
x1,y1:起点坐标
x2,y2:终点坐标
offset:箭头距离终点偏移量
l:箭头大小控制参数
*/
function drawArrow(zr, x1, y1, x2, y2, offset = 0, l = 10) {
const line = new zrender.Line({
shape: {
x1,
y1,
x2,
y2
},
style: {
// 线条颜色
stroke: 'blue',
// 线条粗细
lineWidth: 2,
text: '直线中间的文本',
// 文字的包围矩形背景色
textBackgroundColor: '#fff',
}
})
const a = Math.atan2((y2 - y1), (x2 - x1));
let tx = x2;
let ty = y2
// 偏移长度
let totalLength = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
x2 = x1 + (totalLength - offset) * Math.cos(a)
y2 = y1 + (totalLength - offset) * Math.sin(a)
// D点x轴坐标
const x3 = x2 - l * Math.cos(a + 30 * Math.PI / 180);
// D点y轴坐标
const y3 = y2 - l * Math.sin(a + 30 * Math.PI / 180);
// C点x轴坐标
const x4 = x2 - l * Math.cos(a - 30 * Math.PI / 180);
// C点y轴坐标
const y4 = y2 - l * Math.sin(a - 30 * Math.PI / 180);
const polygon = new zrender.Polygon({
shape: {
// 填充图形的坐标
points: [[x3, y3], [x4, y4], [x2, y2]],
},
style: {
fill: '填充颜色',
opacity: 1
},
z: 1
})
zr.add(line);
zr.add(polygon)
}
drawArrow(zr, 200, 200, 300, 300);
d3.js
这是一个强大的可视化库,感兴趣更多细节可以查看文档或者我之前的文章,这个demo中主要使用了d3的Force布局。
完整代码
要注意版本号,zrender
和d3
都是v4
版本,下面代码中对于画箭头的部分为之前探索的方案,在一些常见不适用(箭头消失),可以利用上面画直线箭头的方法进行修改。
这部分的难点是曲线箭头,需要调整贝塞尔曲线,利用三角函数计算出最合适的控制线,如有需要留言,我会出一篇文章记录。
拖动、点击显示关联等细节,可以下下面的完整demo中查看。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>d3关系图谱</title>
<style>
html,
body,
#main {
width: 100%;
height: 100%;
margin: 0;
}
</style>
</head>
<body>
<div id="main"></div>
<script src="./lib/zrender.v4.3.2.js"></script>
<script src="./lib/d3.v5.16.0.js"></script>
<script>
// 处理关系数组(分组)
function dealRelations(links) {
let linkGroup = {}; //用来分组,将两点之间的连线进行归类
let linkMap = {}; //对连接线的计数
for (let i = 0; i < links.length; i++) {
let key = links[i].source.name < links[i].target.name ? links[i].source.name + ':' + links[i].target.name : links[i].target.name + ':' + links[i].source.name;
if (!Object.prototype.hasOwnProperty.call(linkMap, key)) {
linkMap[key] = 0;
}
linkMap[key] += 1;
if (!Object.prototype.hasOwnProperty.call(linkGroup, key)) {
linkGroup[key] = [];
}
linkGroup[key].push(links[i]);
}
//为每一条连接线分配size属性,同时对每一组连接线进行编号
for (let i = 0; i < links.length; i++) {
let key = links[i].source.name < links[i].target.name ? links[i].source.name + ':' + links[i].target.name : links[i].target.name + ':' + links[i].source.name;
links[i].size = linkMap[key];
//同一组的关系进行编号
let group = linkGroup[key];
let keyPair = key.split(':');
let type = 'noself';
if (keyPair[0] == keyPair[1]) { //指向两个不同实体还是同一个实体
type = 'self';
}
setLinkNumber(group, type); //给关系编号
}
}
// 设置linknum
function setLinkNumber(group, type) {
if (group.length == 0) return;
//对该分组内的关系按照方向进行分类,此处根据连接的实体ASCII值大小分成两部分
let linksA = [], linksB = [];
for (let i = 0; i < group.length; i++) {
let link = group[i];
if (link.source.name < link.target.name) {
link.direction = 1;
linksA.push(link);
} else {
link.direction = -1;
linksB.push(link);
}
}
//确定关系最大编号。为了使得连接两个实体的关系曲线呈现对称,根据关系数量奇偶性进行平分。
//特殊情况:当关系都是连接到同一个实体时,不平分
let maxLinkNumber = 1;
if (type == 'self') {
maxLinkNumber = group.length;
} else {
maxLinkNumber = group.length % 2 == 0 ? group.length / 2 : (group.length + 1) / 2;
}
//如果两个方向的关系数量一样多,直接分别设置编号即可
if (linksA.length == linksB.length) {
let startLinkNumber = 1;
for (let i = 0; i < linksA.length; i++) {
linksA[i].linknum = startLinkNumber++;
}
startLinkNumber = 1;
for (let i = 0; i < linksB.length; i++) {
linksB[i].linknum = startLinkNumber++;
}
} else {
//当两个方向的关系数量不对等时,先对数量少的那组关系从最大编号值进行逆序编号,
// 然后在对另一组数量多的关系从编号1一直编号到最大编号,再对剩余关系进行负编号
//如果抛开负号,可以发现,最终所有关系的编号序列一定是对称的(对称是为了保证后续绘图时曲线的弯曲程度也是对称的)
let biggerLinks, smallerLinks;
if (linksA.length > linksB.length) {
biggerLinks = linksA;
smallerLinks = linksB;
} else {
biggerLinks = linksB;
smallerLinks = linksA;
}
let startLinkNumber = maxLinkNumber;
// 对短links进行逆序编号
for (let i = 0; i < smallerLinks.length; i++) {
smallerLinks[i].linknum = startLinkNumber--;
}
let tmpNumber = startLinkNumber;
startLinkNumber = 1;
let p = 0;
while (startLinkNumber <= maxLinkNumber) {
biggerLinks[p++].linknum = startLinkNumber++;
}
//开始负编号
startLinkNumber = 0 - tmpNumber;
for (let i = p; i < biggerLinks.length; i++) {
biggerLinks[i].linknum = startLinkNumber++;
}
}
}
class RelationShip {
constructor(config) {
this.drawConfig = {
company_circle_r: 35,
company_circle_c: '#76aae8',
company_circle_stroke: '#4089e6',
man_circle_r: 25,
man_circle_c: '#ed716d',
man_circle_stroke: '#ed716d',
circleTextSize: 10,
circleLineHeight: 14,
strokeColor: '#d2d2d2',
relationTextColor: '#8e8e8e',
relationTextSize: 10
}
this.data = config.data;
this.zr = zrender.init(config.rootDom);
this.zoomIdentity = d3.zoomIdentity;
this.transform = null;
this.simulation = null;
this.isLockStyle = false;
this.groupMap = new zrender.Group({ draggable: false, });
this.zr.add(this.groupMap);
// 点击事件处理
this.handleEventListeners();
// 处理力运动
this.handleSimulation();
// 处理分组
dealRelations(this.data.links);
setTimeout(() => {
this.initDrag();
}, 100)
}
// 事件监听
handleEventListeners() {
this.zr.on('click', (e) => {
const { links } = this.data;
const { x, y } = this.getPos(e);
const node = this.simulation.find(x, y);
if (!this.isContain(node.circle, x, y)) {
this.resetHighlight(true);
this.isLockStyle = false;
} else {
this.isLockStyle = true;
links.forEach(link => {
link.source.circle.attr({ style: { opacity: .1 } })
link.target.circle.attr({ style: { opacity: .1 } })
link.opacity = .01;
})
links.forEach(link => {
if (link.source === node || link.target === node) {
link.source.circle.attr({ style: { opacity: 1 } })
link.target.circle.attr({ style: { opacity: 1 } })
link.opacity = 1;
const { company_circle_c, man_circle_c } = this.drawConfig;
link.straightLine?.line?.attr({
style: {
stroke: this.getArrowColor(link),
textFill: this.getArrowColor(link),
}
})
if (link.curve) {
link.curve.bc.attr({
style: {
stroke: this.getArrowColor(link),
textFill: this.getArrowColor(link),
}
})
}
}
})
e.event.stopPropagation();
}
})
this.zr.on('mousemove', e => {
if (this.isLockStyle) return;
const { x, y } = this.getPos(e);
const node = this.simulation.find(x, y);
if (this.isContain(node.circle, x, y)) {
this.isDynamicHoverHandle = true;
this.data.links.forEach(link => {
link.straightLine?.line?.attr({
style: {
stroke: this.drawConfig.strokeColor,
textFill: this.drawConfig.relationTextColor,
}
})
if (link.curve) {
link.curve.bc.attr({
stroke: this.drawConfig.strokeColor,
textFill: this.drawConfig.relationTextColor,
})
}
if (link.source === node || link.target === node) {
link.source.circle.attr({ style: { opacity: 1 } })
link.target.circle.attr({ style: { opacity: 1 } })
link.opacity = 1;
const { company_circle_c, man_circle_c } = this.drawConfig;
link.straightLine?.line?.attr({
style: {
stroke: this.getArrowColor(link),
textFill: this.getArrowColor(link),
}
})
if (link.curve) {
link.curve.bc.attr('style', {
stroke: this.getArrowColor(link),
textFill: this.getArrowColor(link),
})
}
}
})
} else {
this.resetHighlight();
}
})
}
// 拖动
initDrag() {
const canvas = document.querySelector('canvas')
d3.select(canvas)
.call(this.tick.bind(this))
const dragFunc = d3.drag().container(canvas)
.subject(this.subject_from_event.bind(this))
.on("start", this.drag_started.bind(this))
.on("drag", this.dragged.bind(this))
.on("end", this.drag_ended.bind(this))
dragFunc(d3.select('#main'))
d3.select('#main')
.call(
d3.zoom()
.scaleExtent([0.2, 5])
.on("zoom",
(e) => {
const transform = d3.event.transform;
this.zoomIdentity = transform;
const height = document.documentElement.clientHeight;
const width = document.documentElement.clientWidth;
this.groupMap.attr({
scale: [transform.k, transform.k],
position: [transform.x, transform.y]
})
}
)
)
d3.select('#main').on("dblclick.zoom", null)
}
// d3力学
handleSimulation() {
const { nodes, links } = this.data;
const width = document.documentElement.clientWidth;
const height = document.documentElement.clientHeight;
this.formatLink(links, nodes);
const simulation = d3
.forceSimulation(nodes)
.force("link", d3.forceLink(links).distance(200))
.force("charge", d3.forceManyBody().strength(-200).distanceMax(100))
.force("center", d3.forceCenter(width / 2, height / 2))
.on('tick', this.tick.bind(this))
.on("end", function () {
nodes.forEach((node) => {
node.fx = node.x;
node.fy = node.y;
});
}).alphaDecay(0.03)
.tick(300);
this.simulation = simulation;
}
// 格式化连接关系数据
formatLink(links, nodes) {
links.forEach((link, i) => {
link.index = i;
const src = nodes.find((node) => node.id == link.src);
const dst = nodes.find((node) => node.id == link.dst);
link["source"] = src;
link["target"] = dst;
});
}
// 绘制三角箭头
_drawArrow(ctx, x1, y1, x2, y2, offset, l, color) {
const a = Math.atan2((y2 - y1), (x2 - x1));
let tx = x2;
let ty = y2
// 偏移长度
let totalLength = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
x2 = x1 + (totalLength - offset) * Math.cos(a)
y2 = y1 + (totalLength - offset) * Math.sin(a)
// D点x轴坐标
const x3 = x2 - l * Math.cos(a + 30 * Math.PI / 180);
// D点y轴坐标
const y3 = y2 - l * Math.sin(a + 30 * Math.PI / 180);
// C点x轴坐标
const x4 = x2 - l * Math.cos(a - 30 * Math.PI / 180);
// C点y轴坐标
const y4 = y2 - l * Math.sin(a - 30 * Math.PI / 180);
ctx.lineTo(x3, y3);
ctx.lineTo(x4, y4);
ctx.lineTo(x2, y2);
}
// 绘制曲线箭头
drawCurvedArrow({
x1,
y1,
x2,
y2,
reverse = false,
strokeColor = 'black',
fillColor = 'red',
deg = 10,
offset = 0,
text = '',
opacity = 1,
level = -1
}, curve) {
const zr = this.groupMap;
// function _drawArrow(ctx, x1, y1, x2, y2, offset, l, color) {
// const a = Math.atan2((y2 - y1), (x2 - x1));
// let tx = x2;
// let ty = y2
// // 偏移长度
// let totalLength = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
// x2 = x1 + (totalLength - offset) * Math.cos(a)
// y2 = y1 + (totalLength - offset) * Math.sin(a)
// // D点x轴坐标
// const x3 = x2 - l * Math.cos(a + 30 * Math.PI / 180);
// // D点y轴坐标
// const y3 = y2 - l * Math.sin(a + 30 * Math.PI / 180);
// // C点x轴坐标
// const x4 = x2 - l * Math.cos(a - 30 * Math.PI / 180);
// // C点y轴坐标
// const y4 = y2 - l * Math.sin(a - 30 * Math.PI / 180);
// ctx.lineTo(x3, y3);
// ctx.lineTo(x4, y4);
// ctx.lineTo(x2, y2);
// }
const a = Math.atan2((y2 - y1), (x2 - x1));
// 偏移长度
let totalLength = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
x2 = x1 + (totalLength - offset) * Math.cos(a)
y2 = y1 + (totalLength - offset) * Math.sin(a)
if (totalLength > 200 && totalLength < 800) {
deg -= (totalLength - 200) / 80
} else if (totalLength >= 800) {
deg = 3;
}
const degBAF = a / (Math.PI / 180);
const LAD = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) / 2;
const LAC = LAD / Math.cos(deg * Math.PI / 180);
let degCAE = 90 - deg - degBAF;
let CE = LAC * Math.sin(degCAE * Math.PI / 180);
let AE = LAC * Math.cos(degCAE * Math.PI / 180);
// 曲线偏移
const centerX = x1 + (x2 - x1) / 2;
const centerY = y1 + (y2 - y1) / 2;
// 方向箭头
if (reverse) {
degCAE = degBAF - deg;
CE = LAC * Math.cos(degCAE * Math.PI / 180);
AE = LAC * Math.sin(degCAE * Math.PI / 180);
}
const Cx = x1 + CE;
const Cy = y1 + AE;
let cpx1 = Cx, cpy1 = Cy;
if (curve) {
// 更新三角箭头
curve.ar.attr({
shape: {
x1: cpx1,
y1: cpy1,
x2,
y2,
},
style: { opacity },
z: level
})
// 更新曲线
curve.bc.attr({
shape: {
x1,
y1,
x2,
y2,
cpx1,
cpy1,
},
style: {
textOffset: [(cpx1 - centerX) / 2, (cpy1 - centerY) / 2],
opacity
},
z: level
})
return;
}
// 画曲线
const bc = new zrender.BezierCurve({
shape: {
x1,
y1,
x2,
y2,
cpx1,
cpy1,
},
style: {
stroke: strokeColor,
text,
opacity,
transformText: true,
textFill: this.drawConfig.relationTextColor,
fontSize: this.drawConfig.relationTextSize,
textBackgroundColor: '#fff'
},
z: level
})
// 画箭头
const Arrow = zrender.Path.extend({
shape: {
x: 0,
y: 0,
width: 0,
height: 0
},
buildPath: (ctx, shape) => {
const { x1, y1, x2, y2 } = shape;
this._drawArrow(ctx, x1, y1, x2, y2, 0, 10);
ctx.closePath();
this.tick();
}
});
const ar = new Arrow({
style: {
// stroke: fillColor,
fill: fillColor,
opacity
},
shape: {
x1: cpx1,
y1: cpy1,
x2,
y2,
},
z: level
})
zr.add(bc);
zr.add(ar);
return {
bc,
ar
}
}
isContain(sub, x, y) {
const shape = sub.shape;
const { cx, cy, r } = shape;
const left = cx - r;
const top = cy - r;
const right = cx + r;
const bottom = cy + r;
return x >= left && x <= right && y >= top && y <= bottom;
}
getPos(e) {
const { x, y, k } = this.zoomIdentity;
const ex = (e.offsetX - x) / k;
const ey = (e.offsetY - y) / k;
return { x: ex, y: ey }
}
// 重置样式
resetLevel() {
this.data.links.forEach(link => {
link.source.level = 0;
link.target.level = 0;
link.level = -1;
})
}
resetHighlight(isOpacity) {
this.data.links.forEach(link => {
if (isOpacity) {
link.source.circle.attr({ style: { opacity: 1 } })
link.target.circle.attr({ style: { opacity: 1 } })
link.opacity = 1;
}
if (link.straightLine) {
link.straightLine.line.attr('style', {
stroke: this.drawConfig.strokeColor,
textFill: this.drawConfig.relationTextColor,
})
}
if (link.curve) {
link.curve.bc.attr({
style: {
stroke: this.drawConfig.strokeColor,
textFill: this.drawConfig.relationTextColor,
}
})
}
})
}
// 根据不同节点,获取不同样式
getRadius(node) {
const { company_circle_r, man_circle_r } = this.drawConfig;
return node.type === 'company' ? company_circle_r : man_circle_r;
}
getColor(node, stroke) {
const { company_circle_c, company_circle_stroke, man_circle_c, man_circle_stroke } = this.drawConfig;
if (stroke) {
if (node.isSelf) {
return '#ed882b';
}
return node.type === 'company' ?
company_circle_stroke :
man_circle_stroke
}
if (node.isSelf) {
return '#f59429';
}
return node.type === 'company' ?
company_circle_c :
man_circle_c
}
getArrowColor(node) {
const invs = ['投资'];
if (invs.includes(node.type)) {
return '#fe5557'
}
return '#4099ea'
}
// 绘制更新图元
createAndUpdateCircle(node) {
if (node.circle) {
let text = node.texts.text1
if (node.texts.text2) {
text += '\n' + node.texts.text2
}
if (node.texts.text3) {
text += '\n' + node.texts.text3
}
if (node.texts.text4) {
text += '\n' + node.texts.text4
}
node.circle.attr({
shape: {
cx: node.fx,
cy: node.fy,
},
style: {
text: text,
},
z: node.level || 0
})
return;
}
const circle = new zrender.Circle({
shape: {
cx: node.x,
cy: node.y,
r: this.getRadius(node)
},
style: {
fill: this.getColor(node),
stroke: this.getColor(node, true),
textLineHeight: this.drawConfig.circleLineHeight,
fontSize: this.drawConfig.circleTextSize,
textFill: "#fff",
strokeNoScale: true,
transformText: true
},
})
const texts = {
text1: '',
text2: '',
text3: '',
text4: '',
}
const textIns = {};
const originText = node.name;
texts.text1 = originText.slice(0, 4)
texts.text2 = originText.slice(4, 8)
texts.text3 = originText.slice(8, 12)
texts.text4 = originText.slice(12, 16)
node.circle = circle;
node.textIns = textIns;
node.texts = texts;
this.groupMap.add(circle)
}
createAndUpdateLine(link) {
const x1 = link.source.x;
const y1 = link.source.y;
const x2 = link.target.x;
const y2 = link.target.y;
// 只有一条连线
if (link.size === 3 && link.linknum === 1 || link.size === 1) {
if (link.straightLine) {
let textOffset = [0, 0];
let x1 = link.source.x;
let y1 = link.source.y;
let x2 = link.target.x;
let y2 = link.target.y;
const { company_circle_r, man_circle_r } = this.drawConfig;
// 需要偏移的直线距离
const needOffset = (company_circle_r - man_circle_r) / 2;
// 需要偏移的关系(大小圆半径不同,视觉上需要特殊处理)
if (link.source.type === 'man' && link.target.type === 'company') {
// 获取角度
const a = Math.atan2((y2 - y1), (x2 - x1));
// 两点之间的直线长度
let totalLength = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
// 计算实际目标点的坐标
x2 = x1 + (totalLength - needOffset) * Math.cos(a)
y2 = y1 + (totalLength - needOffset) * Math.sin(a)
// 实际目标点-目标点=偏移长度
textOffset = [x2 - link.target.x, y2 - link.target.y]
}
link.straightLine?.line?.attr({
shape: {
x1: link.source.x,
y1: link.source.y,
x2: link.target.x,
y2: link.target.y,
},
style: {
textOffset,
opacity: link.opacity,
transformText: true
},
z: link.level || -1
})
link.straightLine?.arrow?.attr({
shape: {
x1: link.source.x,
y1: link.source.y,
x2: link.target.x,
y2: link.target.y,
},
z: link.level || -1,
style: { opacity: link.opacity || 1 }
})
return;
}
const line = new zrender.Line({
style: {
stroke: this.drawConfig.strokeColor,
fill: this.getArrowColor(link),
text: link.type,
textFill: this.drawConfig.relationTextColor,
fontSize: this.drawConfig.relationTextSize,
textBackgroundColor: '#fff'
},
shape: {
x1, y1, x2, y2,
},
z: link.level || -1
})
const Arrow = zrender.Path.extend({
type: 'fei',
shape: {
x: 0,
y: 0,
width: 0,
height: 0
},
buildPath: (ctx, shape) => {
const { x1, y1, x2, y2 } = shape;
this._drawArrow(ctx, x1, y1, x2, y2, this.getRadius(link.target), 10);
ctx.closePath();
this.tick();
}
});
// this._drawArrow(ctx, x1, y1, x2, y2, this.getRadius(link.target), 10);
// const polygon = new zrender.Polygon({
// shape: {
// points: [[100, 100], [600, 200], [300, 400]],
// number: [[0, 0], [400, 400]]
// }
// })
const arrow = new Arrow({
style: {
fill: this.getArrowColor(link),
opacity: link.opacity || 1
},
shape: {
x1: link.source.x,
y1: link.source.y,
x2: link.target.x,
y2: link.target.y,
},
z: -1
})
this.groupMap.add(line);
this.groupMap.add(arrow)
// link.line = line;
link.straightLine = {
line: line,
arrow: arrow
}
// link.arrow = arrow;
return;
}
// 曲线连接
// if (link.linknum > 0) {
// if (link.curve) {
// this.drawCurvedArrow({
// x1,
// y1,
// x2,
// y2,
// offset: this.getRadius(link.target),
// opacity: link.opacity,
// reverse: true,
// deg: Math.abs(link.linknum) * 10,
// level: link.level,
// fillColor: this.getArrowColor(link),
// }, link.curve)
// return;
// }
// link.curve = this.drawCurvedArrow({
// x1,
// y1,
// x2,
// y2,
// strokeColor: this.drawConfig.strokeColor,
// text: link.type,
// reverse: true,
// level: link.level,
// fillColor: this.getArrowColor(link),
// })
// }
// else if (link.linknum < 0) {
// if (link.curve) {
// this.drawCurvedArrow({
// x1,
// y1,
// x2,
// y2,
// offset: this.getRadius(link.target),
// opacity: link.opacity,
// reverse: false,
// deg: Math.abs(link.linknum) * 10,
// level: link.level,
// fillColor: this.getArrowColor(link),
// }, link.curve)
// return;
// }
// link.curve = this.drawCurvedArrow({
// x1,
// y1,
// x2,
// y2,
// offset: this.getRadius(link.target),
// strokeColor: this.drawConfig.strokeColor,
// reverse: false,
// fillColor: this.getArrowColor(link),
// text: link.type,
// level: link.level
// })
// }
}
tick() {
const { nodes, links } = this.data;
nodes.forEach(node => {
this.createAndUpdateCircle(node)
})
links.forEach(link => this.createAndUpdateLine(link))
}
// 拖拽
subject_from_event(e) {
const ex = this.zoomIdentity.invertX(d3.event.x),
ey = this.zoomIdentity.invertY(d3.event.y);
const node = this.simulation.find(ex, ey);
// 如果没有点击到目标点
if (!this.isContain(node.circle, ex, ey)) {
return;
}
// 重置层级
this.resetLevel();
// 提升拖拽相关的元素层级
this.data.links.forEach(link => {
if (link.source === node || link.target === node) {
link.source.level = 2;
link.target.level = 2;
link.level = 1;
}
})
return node;
}
drag_started() {
d3.event.subject.fx = d3.event.x;
d3.event.subject.fy = d3.event.y;
if (!d3.event.active) this.simulation.alphaTarget(0.3).restart();
d3.event.sourceEvent.stopPropagation();
}
dragged() {
const { x, y, k } = this.zoomIdentity;
d3.event.subject.fx += d3.event.dx / k;
d3.event.subject.fy += d3.event.dy / k;
}
drag_ended() {
if (!d3.event.active) this.simulation.alphaTarget(0);
this.resetLevel();
}
}
const data = {
nodes: [
{
currency: "万元",
id: "130790264",
img: "",
name: "兆协投资有限公司",
organ: false,
reg_amount: 0,
type: "company",
},
{
currency: "万元",
id: "115804450",
img: "",
name: "北京兆协食品有限公司上海销售分部",
organ: false,
reg_amount: 0,
type: "company",
isSelf: true
},
{
ex_com_id: 67,
id: "126",
img: "",
name: "翁祖明",
type: "man",
},
{
ex_com_id: "115804450",
id: "7065171",
name: "杨敬祖",
type: "man",
},
{
ex_com_id: 67932906,
id: "8032034",
name: "陈家协",
type: "man",
},
{
ex_com_id: 67932906,
id: "69542",
img: "",
name: "林杰",
type: "man",
},
{
ex_com_id: 67932906,
id: "630248",
img: "",
name: "陈钟峰",
type: "man",
},
{
currency: "万美元",
id: "67932906",
img: "",
name: "丹阳钱隆金属材料有限公司",
organ: false,
reg_amount: 2990,
type: "company",
},
{
ex_com_id: 67932906,
id: "17302430",
img: "",
name: "陈身秀",
type: "man",
},
{
ex_com_id: 67932906,
id: "17302431",
img: "",
name: "陈兴莺",
type: "man",
},
{
currency: "万美元",
id: "67",
img: "",
name: "北京兆协食品有限公司",
organ: false,
reg_amount: 12,
type: "company",
},
],
links: [
{
src: 67932906,
dst: 130790264,
type: "投资",
with: "company",
},
{
src: 130790264,
dst: 67932906,
type: "吃饭",
with: "company",
},
// {
// src: 130790264,
// dst: 67932906,
// type: "睡觉",
// with: "company",
// },
{
src: 126,
dst: 67,
type: "法定代表人",
with: "man",
},
{
src: 126,
dst: 130790264,
type: "投资",
with: "man",
},
{
src: 7065171,
dst: 115804450,
type: "法定代表人",
with: "man",
},
{
src: 7065171,
dst: 130790264,
type: "法定代表人",
with: "man",
},
{
src: 8032034,
dst: 67932906,
type: "董事",
with: "man",
},
{
src: 17302430,
dst: 67932906,
type: "法定代表人",
with: "man",
},
{
src: 17302431,
dst: 67932906,
type: "副董事长",
with: "man",
},
{
src: 69542,
dst: 67932906,
type: "投资",
with: "man",
},
{
src: 630248,
dst: 67932906,
type: "监事",
with: "man",
},
],
};
new RelationShip({ data, rootDom: document.querySelector('#main') })
</script>
</body>
</html>
企业级实例
力导图分堆需要调控force的力
转载自:https://juejin.cn/post/7183607189364768805