效果如上图所示。
本项目使用主要d3.jsv4制作,分两部分,一个是实际展示的连线动画图,另一个是管理人员使用鼠标编辑连线的页面。对于d3.js如何引入图片,如何画线等基础功能,这里就不再介绍了,大家可以找一些入门文章看一下。这里主要介绍一下重点问题。
1.连线动画图
此图的主要功能是每隔给定时间,通过ajax请求后台数据,并根据返回的数据动态改变每个图片下方的数值,动态改变连线上的动画流动方向和是否流动。
首先,确定图表中需要配置的内容,如各图片存储位置,连线和动画颜色,图片和连线的坐标等。这些数据需要在html中进行配置,最好写成object对象,赋值给我们自己的图表类的函数。比如:
var data = { element:[{ image: 'img/work.png', pos:[1,1], // 图片位置 linePoint:[], // 图片发出线段坐标数组 lineDir:0, // 线段动画方向 title: '工作' }], lineColor:'black', // 连线颜色 animateColor: 'red', // 动画颜色 }; var chart = new Myd3chart('#chart'); chart.lineChart(data);
其中图片发出的线段坐标数组,使用外部文件提供,此文件由之后介绍的编辑器生成。
在设计我们自己的图表函数时,最好把每个功能划分成独立的函数,这样方便以后的维护和扩展。
动画线段采用css的方式,有动画的线段添加此css即可:
.animate-line{ fill: none; stroke-width: 1; stroke-dasharray: 50 100; stroke-dashoffset: 0; animation: stroke 6s infinite linear; } @keyframes stroke { 100% { stroke-dashoffset: 500; /* 如果反向移动改为-500 */ } }
这个图表的难点在于动态改变连线上的流动动画,因为A线段的终点会连接到B线段上,如果B线段动画停止,则A线段上的动画仍然要从B上经过,而不能简单停止B线段上的动画。而且如果B线段上的接入点不止一个,还要判断接入点之间的顺序,只显示最靠近B起始点的接入点的动画。另外还要判断接入线段上是否有接入线段,层级关系里面如果有1个线段有动画,则此接入点就有动画流出。(这里说起来有点绕)
我的方法是:
1)统计每个线段上的所有接入点,这里就是图片名称,用于判断此线段是否有动画流出。
2)接收后台传来的数据时,判断每个线段是否有动画,如果有动画,则直接恢复其动画线段的起始点坐标;如果没有动画,则判断最靠近起始点的接入点是否有动画,如果有动画则将动画线段的起始点改为此接入点坐标。
// 统计接入点 function findAccessPoint() { var accessPoints = []; // 记录每个线段上的接入点,data为配置数据 data.eles.forEach(function(d, i){ if(d.line.length == 0){ return; } var acsp = { name: d.title.text, ap: [], // 接入点,按顺序排列,头部离开始点近 }; // 本线段上,每两相邻的点作为一个元素存入数组 var linePair = []; // 本线段起始点 var startPos = d.line[0]; d.line.forEach(function(dd, di){ if(d.line[di+1]){ var pair = { start: dd, end: d.line[di+1] }; linePair.push(pair); } }); // 对每两相邻的点,查找接入点 linePair.forEach(function(dd, di){ chartData.eles.forEach(function(ddd, ddi){ // 排除自己,查找自己线段上的接入点 if(i != ddi && ddd.line.length > 1){ // 得到此线段终点 var pos = ddd.line[ddd.line.length - 1]; // dd.start开始点,dd.end结束点 // 用x坐标计算在本线段上的y坐标,再和实际的y坐标比较 var computeY = dd.start[1] + (pos[0] - dd.start[0])*(dd.end[1] - dd.start[1])/(dd.end[0] - dd.start[0]); var dif = Math.abs(computeY - pos[1]); // 如果误差在2以内,并且此线终点在当前线起点和终点之间 // 认为此点为接入点 if(dif < 2 && ( ( ((pos[0] > dd.start[0]) && (pos[0] < dd.end[0])) || ((pos[0] < dd.start[0]) && (pos[0] > dd.end[0])) ) && ( ((pos[1] > dd.start[1]) && (pos[1] < dd.end[1])) || ((pos[1] < dd.start[1]) && (pos[1] > dd.end[1])) ) )) { var dis = Math.pow((pos[0] - startPos[0]),2) + Math.pow((pos[1] - startPos[1]),2); var ap = { name: ddd.title.text, ap: pos, distance: dis, // 距离起始点的距离 allNames: [], // 所有通过此接入点的站点名称 } acsp.ap.push(ap); } } }); }) accessPoints.push(acsp); }); //对所有的接入点,按与起始点的距离排序,并查找此接入点的上层站点 accessPoints.forEach(function(d, i){ // 按distance由小到大排序 d.ap.sort(function(a, b){ return a.distance - b.distance; }); // 查找每个接入点的上层站点 d.ap.forEach(function(dd, di){ findPoint(dd.name, dd.allNames); }); }); // name是接入点名称,arr是该接入点的allNames function findPoint(name, arr){ accessPoints.forEach(function(d, i){ // 在数组中找到指定名称的项 if(d.name === name){ if(d.ap.length>0){ // 把该项下面的ap中的名称加入给定arr d.ap.forEach(function(dd, di){ arr.push(dd.name); // 如果该点内的allNames已经有值则直接加入 if(dd.allNames.length>0){ dd.allNames.forEach(function(d, i){ arr.push(d); }); } else{ // 递归查找子接入点 findPoint(dd.name, arr); } }); } else { return; } }else{ return; } }); } }
以上函数的运行结果会产生一个对象,存储每个接入线段上‘挂载'的接入点,目的就是改变动画时方便判断。