用js写一个简单的网络图

1,193 阅读4分钟

引言

在工作中经常需要画一些网络图(也叫力导向图),之前都是在用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.xlink.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
    };
}

这样一个带拖动效果的简单网络图就做完了~

现在还很粗糙,后面还会继续完善~