引言
在工作中经常需要画一些网络图(也叫力导向图),之前都是在用Echarts和D3来实现,闲着没事儿看了一些D3的源码,想着自己尝试从0写一个,锻炼一下。 先上图:
正文
写在前面
一个网络图存在两类信息:
- 节点信息
- 边的信息
一个节点至少要有三个信息:
- id 用于区分各个节点
- x、y 节点在图中的坐标
一个边至少要有两个信息(两端节点的id):
- source
- target
因为可以通过source和target找到节点,也就可以找到对应节点的x和y,所以边上不需要带有坐标信息。
数据的格式就是这个样子:
let nodes = [{
id: 1,
x: 100,
y: 100
}, {
id: 2,
x: 200,
y: 200
}];
let links = [{
source: 1,
target: 2
}]
数据有了我们怎么画图呢?上canvas,canvas相关知识戳这里。
// 画个弧形
// context.arc(x, y, r, sAngle, eAngle)
// x, y 坐标,r 半径,sAngle 起始角,eAngle 结束角
// 起始角是0,结束角是2PI,所以是个圆形
context.beginPath()
context.arc(50, 40, 30, 0, 2 * Math.PI)
// 填充圆形
context.fill()
context.stroke()
context.beginPath()
context.arc(50, 160, 30, 0, 2 * Math.PI)
// 填充圆形
context.fill()
context.stroke()
效果图:
这个时候在画上一条线context.beginPath()
context.strokeStyle = '#ff0000'
context.moveTo(50, 40)
context.lineTo(50, 160)
context.stroke()
context.stroke()
效果图:
*注意:虽然绘制连线的代码是在后面介绍的,在写的时候,其实是放在绘制节点代码的前面,不然效果会是这样: 数据格式、绘制网络图的代码已经介绍了,哪如何把两部分连接起来呢?开始实现
这部分很多代码时照着d3的d3-force实现的,大牛们还是看d3-force吧,不用看我拙劣的操作,哈哈哈~
首先,我们需要把数据处理一下——把node的坐标信息共享给link
// 通过node的id在键值对中找到node
function findNode(nodeById, nodeId) {
let node = nodeById.get(nodeId)
if (!node) throw Error('missing ' + nodeId)
return node
}
function initialize(nodes, links) {
// 将node的数组进行进一步处理,得到Map类型的数据(保存键值对,
// 键——node的id,值——node本身)
let nodeById = new Map(nodes.map((d, i) => [d.name, d]))
for (let i = 0; i < links.length; i++) {
let link = links[i]
// 通过source和target上存储的id信息,在键值对中找到node
// 然后,把node浅拷贝到link的source和target属性上
link.source = findNode(nodeById, link.source)
link.target = findNode(nodeById, link.target)
}
}
initialize(nodes, links)
通过上一步处理,我们每一次就可以通过link.source.x
和link.source.y
(target同理)得到link的起始坐标了。
得到坐标后,我们现在需要实现绘制节点和线的函数了
function drawNode(d) {
context.beginPath()
context.strokeStyle = '#000000'
context.arc(d.x, d.y, 30, 0, 2 * Math.PI)
context.fill()
context.stroke()
// 绘制文本,目前展示的是node带有的坐标信息
context.beginPath()
context.textAlign = 'center'
context.textBaseline = 'middle'
context.font = "20px Georgia";
context.fillStyle = '#ff0000'
context.fillText('(' + d.x + ',' + d.y + ')', d.x, d.y)
context.stroke()
context.fillStyle = '#000000'
}
function drawLink(d) {
context.beginPath()
context.strokeStyle = '#ff0000'
context.moveTo(d.source.x, d.source.y)
context.lineTo(d.target.x, d.target.y)
context.stroke()
}
let canvas = document.getElementsByTagName('canvas')[0]
let context = canvas.getContext('2d')
function draw(nodes, links) {
context.clearRect(0, 0, 1000, 1000)
context.save()
links.forEach(drawLink)
nodes.forEach(drawNode)
context.restore()
}
draw(nodes, links)
大体已经完成了,需要准备数据了,计算在一个网络图的布局这部分目前我的能力还不够实现,目前节点的坐标都是通过一些公式计算为固定的,没有使用模拟力的一些算法来实现。
let x0 = 500,
y0 = 500,
R = 100,
sin = Math.sin,
cos = Math.cos
let nodes = [{
name: 1,
x: x0,
y: y0
}];
for (let i = 0; i < 6; i++) {
let radius = 2 * Math.PI / 6
nodes.push({
name: i + 2,
x: x0 + Math.floor(R * cos(radius * i)),
y: y0 + Math.floor(R * sin(radius * i))
})
}
let links = [];
for (let i = 0; i < 6; i++) {
links.push({
source: 1,
target: i + 2
})
}
效果如图所示:
在日常网络图的使用中,有一个最常用的功能就是拖动,后面来实现拖动的效果,简单分析拖动效果需要怎么实现:- 鼠标按下时,如何判定选中了一个节点? 获取到鼠标点击时的坐标,计算和每一个节点的距离,如果距离小于节点半径
- 拖动效果如何实现的? 每次拖动时,将选中的节点的坐标设置为鼠标的坐标,然后重绘canvas 代码实现:
// 通过鼠标的坐标信息,找到选中的节点,找不到返回null
function find(x, y, r) {
r *= r
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i]
let dx = x / scale - node.x,
dy = y / scale - node.y,
d2 = dx * dx + dy * dy
if (d2 < r) {
return node
}
}
return null
}
canvas.onmousedown = function (e) {
// e.layerX,e.layerY 相对于触发对象的坐标,这里就是canvas
let node = find(e.layerX, e.layerY, 30)
if (!node) return
canvas.onmousemove = function (e) {
node.x = e.layerX;
node.y = e.layerY;
draw(nodes, links)
};
canvas.onmouseup = function () {
canvas.onmousemove = null
canvas.onmouseup = null
node = null
};
}
这样一个带拖动效果的简单网络图就做完了~
现在还很粗糙,后面还会继续完善~