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

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

总结

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

(0)

相关推荐

  • 利用d3.js实现蜂巢图表带动画效果

    以上是效果图,本图表使用d3.js v4制作.图表主要功能是在六边形格子中显示数据,点击底部图标可以切换指定格子高亮显示,图表可以随浏览器任意缩放. 1.图表的主体结构是由正六边形组成,使用d3生成六边形可以使用d3-hexbin.js,生成六边形比较方便,只要给定中心点坐标和半径即可生成六边形路径,例如: var r = 10;// 六边形半径 var pos = [[5,5],[10,10]]; // 六边形中心点坐标数组 var hexbin = d3.hexbin() // 使用hexb

  • 利用d3.js制作连线动画图与编辑器

    连线动画图 编辑器 效果如上图所示. 本项目使用主要d3.jsv4制作,分两部分,一个是实际展示的连线动画图,另一个是管理人员使用鼠标编辑连线的页面.对于d3.js如何引入图片,如何画线等基础功能,这里就不再介绍了,大家可以找一些入门文章看一下.这里主要介绍一下重点问题. 1.连线动画图 此图的主要功能是每隔给定时间,通过ajax请求后台数据,并根据返回的数据动态改变每个图片下方的数值,动态改变连线上的动画流动方向和是否流动. 首先,确定图表中需要配置的内容,如各图片存储位置,连线和动画颜色,图

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

    连线动画图 编辑器 效果如上图所示. 本项目使用主要d3.jsv4制作,分两部分,一个是实际展示的连线动画图,另一个是管理人员使用鼠标编辑连线的页面.对于d3.js如何引入图片,如何画线等基础功能,这里就不再介绍了,大家可以找一些入门文章看一下.这里主要介绍一下重点问题. 1.连线动画图 此图的主要功能是每隔给定时间,通过ajax请求后台数据,并根据返回的数据动态改变每个图片下方的数值,动态改变连线上的动画流动方向和是否流动. 首先,确定图表中需要配置的内容,如各图片存储位置,连线和动画颜色,图

  • 利用Three.js制作一个新闻联播开头动画

    目录 这里才是引言 技术选型 场景分解 代码逻辑分解 创建背景图和背景音乐 背景图 背景音乐 在线体验地址:点我预览 代码地址:点我github 这里才是引言 五一居家隔离,闲着也是闲着,想着整个活儿,于是就有了这个项目. 项目本身不是很难,但是中间确实是遇到了一些小问题,断断续续也是花费了三四天时间才写完,还有一些需要优化的地方,后续有时间再整. 我会从脚手架开始,按照场景中出现的物体顺序逐条进行讲解制作,每个物体将分为独立的一篇文章,方便理解.Go. 技术选型 选用vite作为构建工具: 选

  • 利用Vue.js制作一个拼图华容道小游戏

    目录 游戏介绍 核心思路 核心代码 html games 类 生成随机图片数量 移动图片 键盘事件 拼图完成 结语 游戏介绍 先看看界面 这是一个拼图游戏,可以自选难度和自选闯关图片 游戏开始后根据不同难度,生成与所选主图 对应的 不同张数的 随机顺序的小图,然后只要把乱序的小图片还原成完整的图片就闯关成功 游戏区域有一个空白位置,可以用鼠标点击空白位相邻的图片完成替换,也就是移动,也可以用键盘上下左右操作 游戏好玩,可不要贪杯哦,学习也不能落下,不管什么游戏都一样 这个虽然用到的技术很一般很简

  • 利用pixi.js制作简单的跑酷小游戏

    目录 前言 项目地址 demo地址 初始化项目 主要逻辑 useParkour useScene useHurdle Player 前言 此项目使用pixi.js和vue实现,部分素材来自爱给网,本项目仅作用于 pixi.js 学习用途,侵权立删. 项目地址 shellingfordly/pixi-games demo地址 pixi-games 初始化项目 使用vite初始化项目 pnpm create vite my-vue-app 安装pixi.js和pixi-tweener pixi-tw

  • 利用D3.js实现最简单的柱状图示例代码

    首先把效果图放出来: 具备了一个柱状图的基础元素:柱形,坐标轴,刻度,数值等. 不得不说,d3.js比直接用的echarts更麻烦,但是确实更自由. 来看看如何实现吧. //确定画布的大小 var width = 400; var height = 400; //在 body 里添加一个 SVG 画布 var svg = d3.select("body") .append("svg") .attr("width", width) .attr(&q

  • 利用d3.js力导布局绘制资源拓扑图实例教程

    前言 最近公司业务服务老出bug,各路大佬盯着链路图找问题找的头昏眼花.某天大佬丢了一张图过来"我们做一个资源拓扑图吧,方便大家找bug". 就是这个图,应该是马爸爸家的 好吧,来仔细瞧瞧这个需求咋整呢.一圈资源围着一个中心的一个应用,用曲线连接起来,曲线中段记有应用与资源间的调用信息.emmm 这个看起来很像女神在遛一群舔狗... 啊不,是 d3.js 力导向图! d3.js 力导向图 d3.js 是著名的数据可视化基础工具,他提供了基本的将数据映射至网页元素的能力,同时封装了大量实

  • 利用ECharts.js画K线图的方法示例

    前言 最近有一个统计的项目要做,在前端的数据需要用图表的形式展示.网上搜索了一下,发现有几种统计图库. MSChart 这个是Visual Studio里的自带控件,使用比较简单,不过数据这块需要在后台绑定. ichartjs 是一款基于HTML5的图形库.使用纯javascript语言, 利用HTML5的canvas标签绘制各式图形. 支持饼图.环形图.折线图.面积图.柱形图.条形图等. Chart.js 也是一款基于HTML5的图形库和ichartjs整体类似.不过Chart.js的教程文档

  • 使用D3.js制作图表详解

    D3是用于数据可视化的Javascript库.使用SVG,Canvas和HTML.结合强大的可视化技术和数据驱动的DOM操作方法. D3与JQuery的区别 D3是数据驱动的,JQuery不是:我们使用JQuery直接操纵元素:但是使用D3 时我们需要通过D3专有的data(),enter()和exit()方法提供数据和回调,然后D3操作元素. D3通常用于数据可视化:JQuery用于创建Web应用.D3有很多数据可视化扩展:JQuery有很多Web应用插件.两者都是Javascript DOM

随机推荐