利用d3.js制作连线动画图与编辑器的方法实例


Posted in Javascript onSeptember 05, 2019

利用d3.js制作连线动画图与编辑器的方法实例

连线动画图

利用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();
 });
 });
 }

在此只贴出部分代码,如果大家有任何建议和问题,还请留言,谢谢。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
JS查看对象功能代码
Apr 25 Javascript
Mootools 1.2教程 Fx.Morph、Fx选项和Fx事件
Sep 15 Javascript
ajax更新数据后,jquery、jq失效问题
Mar 16 Javascript
JS事件Event元素(兼容IE,Firefox,Chorme)
Nov 01 Javascript
js中回调函数的学习笔记
Jul 31 Javascript
利用Javascript裁剪图片并存储的简单实现
Mar 13 Javascript
基于JavaScript实现带数据验证和复选框的表单提交
Aug 23 Javascript
node.js自动上传ftp的脚本分享
Jun 16 Javascript
解决koa2 ctx.render is not a function报错问题
Aug 07 Javascript
微信小程序scroll-view实现滚动穿透和阻止滚动的方法
Aug 20 Javascript
微信小程序第三方框架对比 之 wepy / mpvue / taro
Apr 10 Javascript
Typescript类型系统FLOW静态检查基本规范
May 25 Javascript
javascript之分片上传,断点续传的实际项目实现详解
Sep 05 #Javascript
layui 实现table翻页滚动条位置保持不变的例子
Sep 05 #Javascript
vue中如何实现后台管理系统的权限控制的方法步骤
Sep 05 #Javascript
Vue 动态路由的实现及 Springsecurity 按钮级别的权限控制
Sep 05 #Javascript
layui 对弹窗 form表单赋值的实现方法
Sep 04 #Javascript
layui-table对返回的数据进行转变显示的实例
Sep 04 #Javascript
layui table数据修改的回显方法
Sep 04 #Javascript
You might like
PHP 利用AJAX获取网页并输出的实现代码(Zjmainstay)
2012/08/31 PHP
php基础设计模式大全(注册树模式、工厂模式、单列模式)
2015/08/31 PHP
PHP验证终端类型是否为手机的简单实例
2017/02/07 PHP
JavaScript国旗变换效果代码
2008/08/13 Javascript
基于jquery的地址栏射击游戏代码
2011/03/10 Javascript
jquery实现的随机多彩tag标签随机颜色和字号大小效果
2014/03/27 Javascript
JavaScript生成福利彩票双色球号码
2015/05/15 Javascript
js实现简易的单数字随机抽奖(0-9)
2020/03/19 Javascript
Bootstrap每天必学之前端开发框架
2015/11/19 Javascript
JavaScript File API文件上传预览
2016/02/02 Javascript
使用postMesssage()实现跨域iframe页面间的信息传递方法
2016/03/29 Javascript
基于JavaScript实现带缩略图的轮播效果
2017/01/12 Javascript
Javascript实现信息滚动效果
2017/05/18 Javascript
浅谈Node Inspector 代理实现
2017/10/19 Javascript
解决在Bootstrap模糊框中使用WebUploader的问题
2018/03/22 Javascript
node.js中fs文件系统模块的使用方法实例详解
2020/02/13 Javascript
微信小程序订阅消息(java后端实现)开发
2020/06/01 Javascript
vue中重定向redirect:‘/index‘,不显示问题、跳转出错的完美解决
2020/09/28 Javascript
Python原始字符串(raw strings)用法实例
2014/10/13 Python
Python数据分析之双色球中蓝红球分析统计示例
2018/02/03 Python
查找python项目依赖并生成requirements.txt的方法
2018/07/10 Python
python使用Matplotlib画饼图
2018/09/25 Python
Python爬虫之正则表达式的使用教程详解
2018/10/25 Python
解决Jupyter NoteBook输出的图表太小看不清问题
2020/04/16 Python
Eclipse配置python默认头过程图解
2020/04/26 Python
纯CSS3实现扇形动画菜单(简化版)实例源码
2017/01/17 HTML / CSS
捷科时代的软件测试笔试题
2015/11/09 面试题
学期自我鉴定
2013/11/04 职场文书
护理人员的自我评价分享
2014/03/15 职场文书
党的群众路线教育实践活动个人剖析材料
2014/10/07 职场文书
幼儿园教师节活动总结
2015/03/23 职场文书
涨工资申请书应该怎么写?
2019/07/08 职场文书
员工保密协议范本,您一定得收藏!很有用!
2019/08/08 职场文书
高端收音机+蓝牙音箱,JBL TUNER FM带收音蓝牙音箱评测
2021/04/24 无线电
深入详解JS函数的柯里化
2021/06/09 Javascript
Linux系统下安装PHP7.3版本
2021/06/26 PHP