Li RUONAN


一枚行走在前端道路上的程序媛 ~~


d3force

既然立了flag,每周更新一篇博客,那么就从端午节开启~~

不知不觉,我成了部门专业写大屏代码的程序媛。于是,不得不经常与ECharts、D3、Mapbox打交道。Mapbox还未开始用,但之前了解到是做3D地图的框架。下次再画地图的时候,可以学习并玩玩啦。

  1. Echarts比较简单,大部分情况都很容易满足需求。记得三月份要做个可视化作品,当时用矩形树图去实现产品的要求,看了官网的demo和gallery,依然有个小功能无法实现,通读了几遍api文档,发现了link参数,这个参数就比较强大了,就意味着你可以对链接之后的对象做各种自定义操作,并且新的页面可以按需定制化。由于涉及敏感信息,此处就不贴项目demo和source了。感触就是,多看别人的项目,但是如果想要深入做项目,一定一定要多读文档,多思考多尝试。

  2. 说完ECharts,咱们说D3。D3其实我是不敢讲话的,毕竟这实在是万本(可视化)之源。D3的原理就是操作dom,所以对底层的开发要求比较高,并且全英文文档读起来确实困难些。浮躁如我,其实很早之前就知道D3,但也没能坚持读读官方文档。这次实在是需求导致不得不用D3去实现。

说正题~~

需求:基于D3实现消息传播路径图。
数据:nodes,links。nodes关键字段:id,name,group,links关键字段:source,target,value
看到links的字段太过于熟悉,做通信研究的时候,经常提到源节点和目的节点,所以用来做路径图是可行的。nodes的group字段一般是用来做节点分类的,比如:图例、颜色等。

问题:

  1. 如何画力导向图?
    力导向图,顾名思义,就是画受力分析图。做力导向图的时候,自己选择了随机分布,因此节点会自己找到力的平衡点。每一个节点的计算,都是在ticked回调函数中计算,每一步的算法可以依据当前画布的大小做适当调整。
1
2
3
4
5
6
7
# 在svg标签,先构建画布 #
const svg = d3
.select('svg')
.call(zoom)
.attr('width', width)
.attr('height', height)
.append('g');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 然后创建一个力导向模型,给定作用力中心#
let simulation = d3
.forceSimulation()
.force('link', d3.forceLink().id(d => d.id))
.force('center', d3.forceCenter(width / 2, height / 2));
# 给作用力赋值(引力or斥力),此处全部用斥力做处理,并且按照不同的节点数,分别给定不同大小的斥力和最大距离。
if (graph.nodes.length > 300) {
simulation.force('charge', d3.forceManyBody().strength(-100).distanceMax(250));
}
else if (graph.nodes.length > 200) {
simulation.force('charge', d3.forceManyBody().strength(-200).distanceMax(150));
} else {
simulation.force('charge', d3.forceManyBody().strength(-300).distanceMax(150));
}

…此处省略svg画节点和边的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 计算节点,构建力图 #
simulation.nodes(graph.nodes).on('tick', ticked);
simulation.force('link').links(graph.links).distance(20);
/** 更新连线坐标,对于每一个时间间隔 */
function ticked () {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);

node
.attr('cx', d => d.x)
.attr('cy', d => d.y);

graph.nodes.forEach(function (d, i) {
d.x = d.x - 20 / 2 < 0 ? 20 / 2 : d.x;
d.x = d.x + 20 / 2 > width ? width - 20 / 2 : d.x;
d.y = d.y - 20 / 2 < 0 ? 20 / 2 : d.y;
d.y = d.y + 20 / 2 > height ? height - 20 / 2 : d.y;
});
}
/** 绘制结束 */
  1. 如何处理细节问题?
  • 由于要限制于固定容器内,所以对不同节点数给了不同大小的斥力和最大距离,同时也应该设置不同scale,此处我的局部scale失效了,所以给了一个全局的scale。如果有朋友愿意交流细节,非常希望能够优化这部分功能。
  • 因为自己撒了个懒,对于异步请求的数据没有本地化保存,导致给领导演示的时候,路径图没有绘制出来,也是醉了。另外,因为我的路径图每隔10秒钟会重绘一遍,还好数据量不是很大。否则页面早就崩了。于是,赶紧做了状态保存,将异步请求的数据在sessionStorage保存了,这样一来,防止轮播过程中导致数据没有请求到无法绘制力图问题,同时也避免了多次请求导致页面崩溃。这个问题一开始写代码的时候就应该做好,当时因为将焦点集中于画图方面,忽略了此细节。无论何时,性能优化都很重要!!!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    getPathData().then( res => {
    this.pathdata = res.data;
    ## 存入sessionStorage的格式是json,本地获取时也需要解析
    sessionStorage.setItem(doc.id, pathdata);
    ## 一定要先保存,再画图。引入类型数据,画图时会自动将source和targe指向真实地址
    this.drawPath();
    }).catch(error => {
    console.log(error);
    })
  • 为了防止下次绘图的时候,与前一个图表重叠,每次画之前,对当前画布进行清除。第一次渲染的时候,建议也保证svg不存在。

    1
    2
    3
    4
    5
    # 移除dom节点,清除svg标签 #
    removeRender () {
    const svg = d3.select('svg');
    svg.selectAll('*').remove();
    }
  • 做设计的同事强烈要求,节点必须用渐变色,理由就是:渐变色有立体感,好看。这么喜欢渐变色的小哥哥我真的是第一次遇到,还这么强硬,对于强需求,作为“乙方”的“甲方”,我只能默默去实现了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 其实就是在svg上画linearGradient标签,通过id与节点绑定。每组渐变色都需要包含在defs #
    const linearColor1 = svg
    .append('defs')
    .append('linearGradient')
    .attr('id', 'blue')
    .attr('x1', '0%')
    .attr('y1', '0%')
    .attr('x2', '100%')
    .attr('y2', '100%');
    linearColor1.append('stop')
    .attr('offset', '0%')
    .style('stop-color', '#24b9ff');
    linearColor1.append('stop')
    .attr('offset', '100%')
    .style('stop-color', '#0b597d');
    # 对于多个节点,此处可以用函数封装起来。不做过多解释 #