利用d3.js制作连线动画图与编辑器
连线动画图
编辑器
效果如上图所示。
本项目使用主要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; } }); } }
以上函数的运行结果会产生一个对象,存储每个接入线段上‘挂载'的接入点,目的就是改变动画时方便判断。
// 更新线条动画 aniline.each(function(d, i){ var curline = d3.select(this); // 找到对应的动画line if (dd.name === curline.attr('tag')) { // 处理动画是否运行 if (dd.ani) { // 此线条动画运行 curline.style('animation-play-state', 'running'); curline.style('display', 'inline'); // 如果动画运行,则恢复原始动画路径 curline.attr('d', function(d){ return line(chartdata.eles[i].line); }); } else { // 此线条动画停止 // 先查找离本线段开始点最近的接入点 var acp = accesspoints; // 从accesspoints中找到本节点的接入点集合 var ap = []; acp.foreach(function(acd, aci){ if(acd.name === dd.name){ ap = acd.ap; } }); // 最近有动画接入点序号 var acindex = -1; // 找到最近的有动画接入点,远近按数组序号递增 for(var j=0;j<ap.length;j++){ // 复制所有子接入点数组 var allnames = ap[j].allnames.concat(); // 将接入点名称也加入 allnames.push(ap[j].name); // 判断此接入点树中是否有动画,如果1个有就可以 allnames.foreach(function(name,ani){ data.foreach(function(datad, datai){ if(datad.name === name){ if(datad.ani){ acindex = j; return; } } }); }); if(acindex != -1) { break; } } // 如果存在有动画接入点 if(acindex != -1){ curline.style('animation-play-state', 'running'); curline.style('display', 'inline'); curline.attr('d', function(d){ var accp = ap[acindex].ap; var curline = data.element[i].line.concat(); // 接入节点与开始点的距离 var disap = math.pow((accp[0] - curline[0][0]),2) + math.pow((accp[1] - curline[0][1]),2); // 如果当前线段中有离开始节点比接入点近的节点 // 则删除此节点 curline.foreach(function(curld, curli){ if(curli > 0){ var dis = math.pow((curld[0] - curline[0][0]),2) + math.pow((curld[1] - curline[0][1]),2); if(dis < disap){ // 删除此点 curline.splice(curli,1); } } }); // 从此接入点处开始动画 curline.splice(0,1,accp); // debugger; return line(curline); }); }else{ // 此线条动画停止 curline.style('animation-play-state', 'paused'); curline.style('display', 'none'); } } }
2.编辑器
由于本图表需要配置大量坐标,如果手动填写的话效率十分低下,所以需要开发一个编辑器用来修改图表。
编辑器的主要使用方法为,使用鼠标拖动图标,双击确定起始位置并开始实时画线状态,随着鼠标移动动态画出线段,单击确定临时终点,再单击确定下一个终点,右击结束动态画线状态。如果鼠标单击其他图标,则终点为该图标的起始坐标。本程序的实时画线部分进行了倾斜的约束,即左倾或右倾30度角。
编辑器比展示图要简单一些,复杂部分在事件处理。
// 拖动图标 var draging = d3.drag() .on('drag', function () { // 当长宽相同时,iconsize是图标大小[宽,高] var move = iconsize[0] / 2, movesubbg = [25, 53.5], movetitle = [25, 50]; var g = d3.select(this), eventx = d3.event.x - move, eventy = d3.event.y - move; // 设定图标位置 g.select('.image') .attr('x', eventx) .attr('y', eventy); }) // 拖拽结束 .on('end', function () { var g = d3.select(this); g.select('.subbg') .attr('transform', function (d, i) { // 对子标签的处理,自动符合字符串长度 var x = parsefloat(d3.select(this).attr('x')) + parsefloat(d3.select(this).attr('width')) / 2, // y没被缩放,所以不用处理 y = d3.select(this).attr('y'), dsl = (d.title.subtitle.text + '').length; var scalex = dsl * 5.5; return 'translate(' + x + ' ' + y + ') scale(' + scalex + ', 1) translate(' + -x + ' ' + -y + ')'; }); }); // 图标组增加拖动事件 imagegs.call(draging);
以上拖动事件,只是调用基本方法。
实时画线功能需要提前定义临时存储对象,用来存储鼠标移动时线段的终点坐标。
// 鼠标移动时,实时画线到鼠标当前位置,_bodyrect为主区域 _bodyrect.on('mousemove', function(){ // 如果不处于实时画线状态 if(!_chartdata.drawing){ return; } // 如果没有端点名称 if (!_chartdata.lineprepare.name) { return; } /* 实时画线 */ // 判断线段倾斜方向,lineprepare为线段临时存储 var prelines = lineprepare.lines; var mousepos = d3.mouse(_bodyrect.node()), beforepos = prelines[prelines.length - 1], newy, newpos = []; if((mousepos[0]>beforepos[0] && mousepos[1]>beforepos[1]) || (mousepos[0]<beforepos[0] && mousepos[1]<beforepos[1])){ // 向左倾斜\ 左上到右下:y = cy + 0.7*(x-cx) newy = beforepos[1] + 0.7 * (mousepos[0] - beforepos[0]); } else { // 向右倾斜/ 左下到右上:y = cy - 0.7*(cx-x) newy = beforepos[1] - 0.7 * (mousepos[0] - beforepos[0]); } newpos = [mousepos[0], newy]; // 移除旧线 if(_chartdata.templine.line){ _chartdata.templine.pos = []; _chartdata.templine.line.remove(); } // 画新线,templine为实时画线的临时存储 _chartdata.templine.line = _chartdata.linerootg.append('path') .attr('class', 'line-path') .attr('stroke', chartdata.line.color) .attr('stroke-width', chartdata.line.width) .attr('fill', 'none') .attr('d', function () { var newline = [ prelines[prelines.length - 1], newpos ]; _chartdata.templine.pos = newpos; return line(newline); }); // 当鼠标移入某个建筑图标范围时 _chartdata.imagegs.on('mouseenter', function(d, i){ // 移除旧线 if(_chartdata.templine.line){ _chartdata.templine.pos = []; _chartdata.templine.line.remove(); } // 得到图标中心点坐标 var posx = parsefloat(d3.select(this).select('.image').attr('x')) + _chartconf.basesize[0] / 2; var posy = parsefloat(d3.select(this).select('.image').attr('y')) + _chartconf.basesize[1] / 2; // 将此建筑图标的中心点坐标作为终点坐标画线 _chartdata.templine.line = _chartdata.linerootg.append('path') .attr('class', 'line-path') .attr('stroke', chartdata.line.color) .attr('stroke-width', chartdata.line.width) .attr('fill', 'none') .attr('d', function () { var newline = [ prelines[prelines.length - 1], [posx,posy] ]; _chartdata.templine.pos = [posx,posy]; return line(newline); }); }); // 当鼠标移出图标区域 _chartdata.imagegs.on('mouseleave', function(d, i){ // 移除旧线 if(_chartdata.templine.line){ _chartdata.templine.pos = []; _chartdata.templine.line.remove(); } }); // 对图标单击鼠标,保存线 _chartdata.imagegs.on('click', function (d, i) { // 保存临时线 drawline(); // 停止实时画线 exitdrawing(); }); }); // 点击鼠标右键,停止实时画线 _bodyrect.on('contextmenu', function(){ // 停止实时画线 exitdrawing(); d3.event.preventdefault(); }); }); }
在此只贴出部分代码,如果大家有任何建议和问题,还请留言,谢谢。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。
上一篇: viewPager+fragment刷新缓存fragment的方法
下一篇: 提高企业网站的转化率?