d3.js力导向图使用详解

在这里插入图片描述
创建一个力导向图需要三个东西:

  • 仿真模拟系统
  • 节点

当然,一般我们也会创建links(边)来连接两个节点,例如上图

仿真模拟系统中存在多个节点和多种类型的力,通过力控制节点的运动,每个节点都在多个力的作用下不断发生移动,直到系统趋于平衡。中间会发生多次tick事件,每次tick,仿真系统都会更新节点的位置,且系统的能量(alpha)也会逐渐降低,直到达到某个数值(alphaMin),整个图表就停止运动了。

节点

节点是一个对象数组,对象的属性没有限制,你可以添加多种信息来控制图表的渲染(例如颜色大小等),每次tick都会更新节点的位置(x,y)和速度velocity(vx,vy)。

力驱动着整个系统运动,你可以给系统添加力,控制节点的运动。

常见的几种力:

  • d3.forceCenter([x, y]):中心力,将所有的节点都推向图表的中心(给定的一个点),默认坐标是[0,0],施加力时,所有节点的相对位置保持不变
  • d3.forceCollide([radius]):collision,碰撞力,使两个节点接触时像弹簧球一样弹开
  • d3.forceLink([links]): 连接力,拉动节点相互连接,好像节点之间有一个弹簧
  • d3.forceManyBody():排斥力,类似带电电子的排斥方式,推动所有节点彼此远离
  • d3.forceX,d3.forceY:定位力,将节点推向期望的点(x,y),不同于forceCenter,它们会改变节点的相对位置

同时可以访问这个demo,测试下不同力的作用。

links

定义了节点之间的关系,通过节点间的连线定义。同时links也是创建连接力(forceLink)必不可少的东西。

example:

var nodes = [
    {"id": "Alice"},
  	{"id": "Bob"},
  	{"id": "Carol"}
];

var links = [
  	{"source": 0, "target": 1}, // Alice → Bob
  	{"source": 1, "target": 2} // Bob → Carol
];

每个对象必须包含source target属性,表示边的起点和终点,属性的值是节点的id,默认是节点在数组中的索引,同时也可以自定义id getter

d3.forceLink().id(d => d.id)

创建一个力导向图

通过渲染一个树结构的数据来展示力导向图的使用

数据:json

1、根据这份数据分析出我们需要的所有节点和边:

const root = d3.hierarchy(data) 
const nodes = root.descendants()
const links = root.links()

2、创建svg容器

const svg = d3.select('body')
    .append('svg')
    .attr('width', width)
    .attr('height', height)
    .attr('class', 'chart')

3、创建仿真系统

const simulation = d3.forceSimulation(nodes)
    .force('charge', d3.forceManyBody())
    .force('link', d3.forceLink(links))
    .force('x', d3.forceX(width / 2))
    .force('y', d3.forceY(height / 2))

4、设置排斥力和连接力的部分属性

simulation.alphaDecay(0.05) // 衰减系数,值越大,图表稳定越快
simulation.force('charge')
    .strength(-50) // 排斥力强度,正值相互吸引,负值相互排斥
simulation.force('link')
    .id(d => d.id) // set id getter
    .distance(0) // 连接距离
    .strength(1) // 连接力强度 0 ~ 1
    .iterations(1) // 迭代次数

5、绘制边

边的绘制需要先进行,因为svg中没有类似z-index这样的属性来设置层级,后绘制的会覆盖先绘制的

const simulationLinks = svg.append('g')
    .selectAll('line')
    .data(links)
    .enter()
    .append('line')
    .attr('stroke', d => '#c2c2c2')

6、绘制节点并设置拖动事件

每次拖动开始,设置alphaTarget并重启仿真系统,alpha的值会从alphaTarget递减到alphaMin,所以如果你将alphaTarget的值设置的比alphaMin小,就会卡住,不会继续更新。

 const simulationNodes = svg.append('g')
    .attr('fill', '#fff')
    .attr('stroke', '#000')
    .attr('stroke-width', 1.5)
    .selectAll('circle')
    .data(nodes)
    .enter()
    .append('circle')
    .attr('r', 3.5)
    .attr('fill', d => d.children ? null : '#000') // 叶子节点黑底白边,父节点白底黑边
    .attr('stroke', d => d.children ? null : '#fff')
    .call(d3.drag()
        .on('start', started)
        .on('drag', dragged)
        .on('end', ended)
    )

function started(d) {
    if (!d3.event.active) {
        simulation.alphaTarget(.2).restart()
    }
    d.fx = d.x 
    // fx fy 表示下次节点被固定的位置
    // 每次tick结束node.x都会被设置为node.fx,node.vx设置为0
    d.fy = d.y
}

function dragged(d) {
    d.fx = d3.event.x
    d.fy = d3.event.y
}

function ended(d) {
    if (!d3.event.active) {
        // 设置为0直接停止,如果大于alphaMin则会逐渐停止
        simulation.alphaTarget(0)
    }
    d.fx = null
    d.fy = null
}

7、最后设置tick事件

虽然仿真系统会更新节点的位置(只是设置了nodes对象的x y属性),但是它不会转为svg内部元素的坐标表示,这需要我们自己来操作

simulation.on('tick', ticked) 
function ticked() {
    simulationLinks.attr('x1', d => d.source.x )
        .attr('y1', d => d.source.y )
        .attr('x2', d => d.target.x )
        .attr('y2', d => d.target.y )

    simulationNodes.attr('cx', d => d.x )
        .attr('cy', d => d.y )
}

最后,这里有完整的代码,和在线demo

参考文章

发布了80 篇原创文章 · 获赞 31 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/juzipidemimi/article/details/100787059