此文章为原创文章,原文地址:https://www.cnblogs.com/eagle1098/p/11431679.html

连线动画图

编辑器

效果如上图所示。
本项目使用主要d3.jsv4制作,分两部分,一个是实际展示的连线动画图,另一个是管理人员使用鼠标编辑连线的页面。对于d3.js如何引入图片,如何画线等基础功能,这里就不再介绍了,大家可以找一些入门文章看一下。这里主要介绍一下重点问题。

1.连线动画图

此图的主要功能是每隔给定时间,通过ajax请求后台数据,并根据返回的数据动态改变每个图片下方的数值,动态改变连线上的动画流动方向和是否流动。
首先,确定图表中需要配置的内容,如各图片存储位置,连线和动画颜色,图片和连线的坐标等。这些数据需要在html中进行配置,最好写成object对象,赋值给我们自己的图表类的函数。比如:

  1. var data = {
  2. element:[{
  3. image: 'img/work.png',
  4. pos:[1,1], // 图片位置
  5. linePoint:[], // 图片发出线段坐标数组
  6. lineDir:0, // 线段动画方向
  7. title: '工作'
  8. }],
  9. lineColor:'black', // 连线颜色
  10. animateColor: 'red', // 动画颜色
  11. };
  12. var chart = new Myd3chart('#chart');
  13. chart.lineChart(data);

其中图片发出的线段坐标数组,使用外部文件提供,此文件由之后介绍的编辑器生成。
在设计我们自己的图表函数时,最好把每个功能划分成独立的函数,这样方便以后的维护和扩展。
动画线段采用css的方式,有动画的线段添加此css即可:

  1. .animate-line{
  2. fill: none;
  3. stroke-width: 1;
  4. stroke-dasharray: 50 100;
  5. stroke-dashoffset: 0;
  6. animation: stroke 6s infinite linear;
  7. }
  8. @keyframes stroke {
  9. 100% {
  10. stroke-dashoffset: 500; /* 如果反向移动改为-500 */
  11. }
  12. }

这个图表的难点在于动态改变连线上的流动动画,因为A线段的终点会连接到B线段上,如果B线段动画停止,则A线段上的动画仍然要从B上经过,而不能简单停止B线段上的动画。而且如果B线段上的接入点不止一个,还要判断接入点之间的顺序,只显示最靠近B起始点的接入点的动画。另外还要判断接入线段上是否有接入线段,层级关系里面如果有1个线段有动画,则此接入点就有动画流出。(这里说起来有点绕)
我的方法是:
1)统计每个线段上的所有接入点,这里就是图片名称,用于判断此线段是否有动画流出。
2)接收后台传来的数据时,判断每个线段是否有动画,如果有动画,则直接恢复其动画线段的起始点坐标;如果没有动画,则判断最靠近起始点的接入点是否有动画,如果有动画则将动画线段的起始点改为此接入点坐标。

  1. // 统计接入点
  2. function findAccessPoint() {
  3. var accessPoints = [];
  4. // 记录每个线段上的接入点,data为配置数据
  5. data.eles.forEach(function(d, i){
  6. if(d.line.length == 0){
  7. return;
  8. }
  9. var acsp = {
  10. name: d.title.text,
  11. ap: [], // 接入点,按顺序排列,头部离开始点近
  12. };
  13. // 本线段上,每两相邻的点作为一个元素存入数组
  14. var linePair = [];
  15. // 本线段起始点
  16. var startPos = d.line[0];
  17. d.line.forEach(function(dd, di){
  18. if(d.line[di+1]){
  19. var pair = {
  20. start: dd,
  21. end: d.line[di+1]
  22. };
  23. linePair.push(pair);
  24. }
  25. });
  26. // 对每两相邻的点,查找接入点
  27. linePair.forEach(function(dd, di){
  28. chartData.eles.forEach(function(ddd, ddi){
  29. // 排除自己,查找自己线段上的接入点
  30. if(i != ddi && ddd.line.length > 1){
  31. // 得到此线段终点
  32. var pos = ddd.line[ddd.line.length - 1];
  33. // dd.start开始点,dd.end结束点
  34. // 用x坐标计算在本线段上的y坐标,再和实际的y坐标比较
  35. var computeY = dd.start[1] +
  36. (pos[0] - dd.start[0])*(dd.end[1] - dd.start[1])/(dd.end[0] - dd.start[0]);
  37. var dif = Math.abs(computeY - pos[1]);
  38. // 如果误差在2以内,并且此线终点在当前线起点和终点之间
  39. // 认为此点为接入点
  40. if(dif < 2 && (
  41. (
  42. ((pos[0] > dd.start[0]) && (pos[0] < dd.end[0])) ||
  43. ((pos[0] < dd.start[0]) && (pos[0] > dd.end[0]))
  44. ) && (
  45. ((pos[1] > dd.start[1]) && (pos[1] < dd.end[1])) ||
  46. ((pos[1] < dd.start[1]) && (pos[1] > dd.end[1]))
  47. )
  48. )) {
  49. var dis = Math.pow((pos[0] - startPos[0]),2) + Math.pow((pos[1] - startPos[1]),2);
  50. var ap = {
  51. name: ddd.title.text,
  52. ap: pos,
  53. distance: dis, // 距离起始点的距离
  54. allNames: [], // 所有通过此接入点的站点名称
  55. }
  56. acsp.ap.push(ap);
  57. }
  58. }
  59. });
  60. })
  61. accessPoints.push(acsp);
  62. });
  63.  
  64. //对所有的接入点,按与起始点的距离排序,并查找此接入点的上层站点
  65. accessPoints.forEach(function(d, i){
  66. // 按distance由小到大排序
  67. d.ap.sort(function(a, b){
  68. return a.distance - b.distance;
  69. });
  70. // 查找每个接入点的上层站点
  71. d.ap.forEach(function(dd, di){
  72. findPoint(dd.name, dd.allNames);
  73. });
  74. });
  75. // name是接入点名称,arr是该接入点的allNames
  76. function findPoint(name, arr){
  77. accessPoints.forEach(function(d, i){
  78. // 在数组中找到指定名称的项
  79. if(d.name === name){
  80. if(d.ap.length>0){
  81. // 把该项下面的ap中的名称加入给定arr
  82. d.ap.forEach(function(dd, di){
  83. arr.push(dd.name);
  84. // 如果该点内的allNames已经有值则直接加入
  85. if(dd.allNames.length>0){
  86. dd.allNames.forEach(function(d, i){
  87. arr.push(d);
  88. });
  89. } else{
  90. // 递归查找子接入点
  91. findPoint(dd.name, arr);
  92. }
  93. });
  94. } else {
  95. return;
  96. }
  97. }else{
  98. return;
  99. }
  100. });
  101. }
  102. }

以上函数的运行结果会产生一个对象,存储每个接入线段上‘挂载’的接入点,目的就是改变动画时方便判断。

  1. // 更新线条动画
  2. aniLine.each(function(d, i){
  3. var curLine = d3.select(this);
  4. // 找到对应的动画line
  5. if (dd.name === curLine.attr('tag')) {
  6. // 处理动画是否运行
  7. if (dd.ani) {
  8. // 此线条动画运行
  9. curLine.style('animation-play-state', 'running');
  10. curLine.style('display', 'inline');
  11. // 如果动画运行,则恢复原始动画路径
  12. curLine.attr('d', function(d){
  13. return line(chartData.eles[i].line);
  14. });
  15. } else {
  16. // 此线条动画停止
  17. // 先查找离本线段开始点最近的接入点
  18. var acp = accessPoints;
  19. // 从accessPoints中找到本节点的接入点集合
  20. var ap = [];
  21. acp.forEach(function(acd, aci){
  22. if(acd.name === dd.name){
  23. ap = acd.ap;
  24. }
  25. });
  26. // 最近有动画接入点序号
  27. var acIndex = -1;
  28. // 找到最近的有动画接入点,远近按数组序号递增
  29. for(var j=0;j<ap.length;j++){
  30. // 复制所有子接入点数组
  31. var allNames = ap[j].allNames.concat();
  32. // 将接入点名称也加入
  33. allNames.push(ap[j].name);
  34. // 判断此接入点树中是否有动画,如果1个有就可以
  35. allNames.forEach(function(name,ani){
  36. data.forEach(function(datad, datai){
  37. if(datad.name === name){
  38. if(datad.ani){
  39. acIndex = j;
  40. return;
  41. }
  42. }
  43. });
  44. });
  45. if(acIndex != -1) {
  46. break;
  47. }
  48. }
  49. // 如果存在有动画接入点
  50. if(acIndex != -1){
  51. curLine.style('animation-play-state', 'running');
  52. curLine.style('display', 'inline');
  53. curLine.attr('d', function(d){
  54. var accp = ap[acIndex].ap;
  55. var curLine = data.element[i].line.concat();
  56. // 接入节点与开始点的距离
  57. var disAp = Math.pow((accp[0] - curLine[0][0]),2) +
  58. Math.pow((accp[1] - curLine[0][1]),2);
  59. // 如果当前线段中有离开始节点比接入点近的节点
  60. // 则删除此节点
  61. curLine.forEach(function(curld, curli){
  62. if(curli > 0){
  63. var dis = Math.pow((curld[0] - curLine[0][0]),2) +
  64. Math.pow((curld[1] - curLine[0][1]),2);
  65. if(dis < disAp){
  66. // 删除此点
  67. curLine.splice(curli,1);
  68. }
  69. }
  70. });
  71. // 从此接入点处开始动画
  72. curLine.splice(0,1,accp);
  73. // debugger;
  74. return line(curLine);
  75. });
  76. }else{
  77. // 此线条动画停止
  78. curLine.style('animation-play-state', 'paused');
  79. curLine.style('display', 'none');
  80. }
  81. }
  82. }

此文章为原创文章,原文地址:https://www.cnblogs.com/eagle1098/p/11431679.html

2.编辑器

由于本图表需要配置大量坐标,如果手动填写的话效率十分低下,所以需要开发一个编辑器用来修改图表。
编辑器的主要使用方法为,使用鼠标拖动图标,双击确定起始位置并开始实时画线状态,随着鼠标移动动态画出线段,单击确定临时终点,再单击确定下一个终点,右击结束动态画线状态。如果鼠标单击其他图标,则终点为该图标的起始坐标。本程序的实时画线部分进行了倾斜的约束,即左倾或右倾30度角。
编辑器比展示图要简单一些,复杂部分在事件处理。

  1. // 拖动图标
  2. var draging = d3.drag()
  3. .on('drag', function () {
  4. // 当长宽相同时,iconSize是图标大小[宽,高]
  5. var move = iconSize[0] / 2,
  6. moveSubBg = [25, 53.5], moveTitle = [25, 50];
  7. var g = d3.select(this),
  8. eventX = d3.event.x - move,
  9. eventY = d3.event.y - move;
  10. // 设定图标位置
  11. g.select('.image')
  12. .attr('x', eventX)
  13. .attr('y', eventY);
  14. })
  15. // 拖拽结束
  16. .on('end', function () {
  17. var g = d3.select(this);
  18. g.select('.subBg')
  19. .attr('transform', function (d, i) {
  20. // 对子标签的处理,自动符合字符串长度
  21. var x = parseFloat(d3.select(this).attr('x')) + parseFloat(d3.select(this).attr('width')) / 2,
  22. // y没被缩放,所以不用处理
  23. y = d3.select(this).attr('y'),
  24. dsl = (d.title.subTitle.text + '').length;
  25. var scaleX = dsl * 5.5;
  26. return 'translate(' + x + ' ' + y + ') scale(' + scaleX + ', 1) translate(' + -x + ' ' + -y + ')';
  27. });
  28. });
  29. // 图标组增加拖动事件
  30. imageGs.call(draging);

以上拖动事件,只是调用基本方法。
实时画线功能需要提前定义临时存储对象,用来存储鼠标移动时线段的终点坐标。

  1. // 鼠标移动时,实时画线到鼠标当前位置,_bodyRect为主区域
  2. _bodyRect.on('mousemove', function(){
  3. // 如果不处于实时画线状态
  4. if(!_chartData.drawing){
  5. return;
  6. }
  7. // 如果没有端点名称
  8. if (!_chartData.linePrePare.name) {
  9. return;
  10. }
  11. /* 实时画线 */
  12. // 判断线段倾斜方向,linePrePare为线段临时存储
  13. var preLines = linePrePare.lines;
  14. var mousePos = d3.mouse(_bodyRect.node()),
  15. beforePos = preLines[preLines.length - 1], newy,
  16. newPos = [];
  17. if((mousePos[0]>beforePos[0] && mousePos[1]>beforePos[1]) || (mousePos[0]<beforePos[0] && mousePos[1]<beforePos[1])){
  18. // 向左倾斜\ 左上到右下:y = cy + 0.7*(x-cx)
  19. newy = beforePos[1] + 0.7 * (mousePos[0] - beforePos[0]);
  20. } else {
  21. // 向右倾斜/ 左下到右上:y = cy - 0.7*(cx-x)
  22. newy = beforePos[1] - 0.7 * (mousePos[0] - beforePos[0]);
  23. }
  24. newPos = [mousePos[0], newy];
  25. // 移除旧线
  26. if(_chartData.tempLine.line){
  27. _chartData.tempLine.pos = [];
  28. _chartData.tempLine.line.remove();
  29. }
  30. // 画新线,tempLine为实时画线的临时存储
  31. _chartData.tempLine.line = _chartData.lineRootG.append('path')
  32. .attr('class', 'line-path')
  33. .attr('stroke', chartData.line.color)
  34. .attr('stroke-width', chartData.line.width)
  35. .attr('fill', 'none')
  36. .attr('d', function () {
  37. var newLine = [
  38. preLines[preLines.length - 1],
  39. newPos
  40. ];
  41. _chartData.tempLine.pos = newPos;
  42. return line(newLine);
  43. });
  44.  
  45. // 当鼠标移入某个建筑图标范围时
  46. _chartData.imageGs.on('mouseenter', function(d, i){
  47. // 移除旧线
  48. if(_chartData.tempLine.line){
  49. _chartData.tempLine.pos = [];
  50. _chartData.tempLine.line.remove();
  51. }
  52. // 得到图标中心点坐标
  53. var posX = parseFloat(d3.select(this).select('.image').attr('x')) + _chartConf.baseSize[0] / 2;
  54. var posY = parseFloat(d3.select(this).select('.image').attr('y')) + _chartConf.baseSize[1] / 2;
  55. // 将此建筑图标的中心点坐标作为终点坐标画线
  56. _chartData.tempLine.line = _chartData.lineRootG.append('path')
  57. .attr('class', 'line-path')
  58. .attr('stroke', chartData.line.color)
  59. .attr('stroke-width', chartData.line.width)
  60. .attr('fill', 'none')
  61. .attr('d', function () {
  62. var newLine = [
  63. preLines[preLines.length - 1],
  64. [posX,posY]
  65. ];
  66. _chartData.tempLine.pos = [posX,posY];
  67. return line(newLine);
  68. });
  69. });
  70. // 当鼠标移出图标区域
  71. _chartData.imageGs.on('mouseleave', function(d, i){
  72. // 移除旧线
  73. if(_chartData.tempLine.line){
  74. _chartData.tempLine.pos = [];
  75. _chartData.tempLine.line.remove();
  76. }
  77. });
  78. // 对图标单击鼠标,保存线
  79. _chartData.imageGs.on('click', function (d, i) {
  80. // 保存临时线
  81. drawLine();
  82. // 停止实时画线
  83. exitDrawing();
  84. });
  85. });
  86. // 点击鼠标右键,停止实时画线
  87. _bodyRect.on('contextmenu', function(){
  88. // 停止实时画线
  89. exitDrawing();
  90. d3.event.preventDefault();
  91. });
  92. });
  93. }

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

原文地址:https://www.cnblogs.com/eagle1098/p/11431679.html

d3.js制作连线动画图和编辑器的更多相关文章

  1. D3.js 制作中国地图 .net 公共基础类

    D3.js 制作中国地图 from:  http://d3.decembercafe.org/pages/map/index.html GeoJSON is a format for encoding ...

  2. d3.js制作蜂巢图表带动画效果

    以上是效果图,本图表使用d3.js v4制作.图表主要功能是在六边形格子中显示数据,点击底部图标可以切换指定格子高亮显示,图表可以随浏览器任意缩放. 1.图表的主体结构是由正六边形组成,使用d3生成六 ...

  3. d3.js 教程 模仿echarts折线图

    今天我们来仿echarts折线图,这个图在echarts是折线图堆叠,但是我用d3改造成了普通的折线图,只为了大家学习(其实在简单的写一个布局就可以).废话不多说商行代码. 1 制作 Line 类 c ...

  4. d3.js制作条形时间范围选择器

    此文章为原创文章,原文地址:https://www.cnblogs.com/eagle1098/p/12146688.html 效果如上图所示. 本项目使用主要d3.js v4制作,可以用来选择两年的 ...

  5. 【D3.js】Focus + Context 折线图

    利用D3.js库实现Focus+Context的折线图.读取data.tsv文件数据 index.html <!DOCTYPE html> <meta charset="u ...

  6. d3.js 制作简单的俄罗斯方块

    d3.js是一个不错的可视化框架,同时对于操作dom也是十分方便的.今天我们使用d3.js配合es6的类来制作一个童年小游戏--俄罗斯方块.话不多说先上图片. 1. js tetris类 由于方法拆分 ...

  7. d3.js 制作简单的贪吃蛇

    d3.js是一个不错的可视化框架,同时对于操作dom也是十分方便的.今天我们使用d3.js配合es6的类来制作一个童年小游戏–贪吃蛇.话不多说先上图片. 1. js snaker类 class Sna ...

  8. D3.js系列——布局:打包图和地图

    一.打包图 打包图( Pack ),用于表示包含与被包含的关系,也可表示各对象的权重,通常用一圆套一圆来表示前者,用圆的大小来表示后者. 1.布局(数据转换) var pack = d3.layout ...

  9. D3.js系列——布局:弦图和集群图/树状图

    一.弦图 1.弦图是什么 弦图(Chord),主要用于表示两个节点之间的联系的图表.两点之间的连线,表示谁和谁具有联系. 2.数据 初始数据为: var city_name = [ "北京& ...

随机推荐

  1. PTA 打印沙漏

    https://pintia.cn/problem-sets/17/problems/260 #include <bits/stdc++.h> using namespace std; i ...

  2. [leetcode] 87. Scramble String (Hard)

    题意: 判断两个字符串是否互为Scramble字符串,而互为Scramble字符串的定义: 字符串看作是父节点,从字符串某一处切开,生成的两个子串分别是父串的左右子树,再对切开生成的两个子串继续切开, ...

  3. Windows系统配置java环境

    1:下载jdk  网址:http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 2:下载 ...

  4. FTP文件传输服务器原理

    FTP服务器,全称File Transfer Protocol Server,是在互联网上提供文件存储和访问服务的计算机,它们依照FTP协议提供服务.FTP,文件传输协议(File Transfer ...

  5. django第三次(转自刘江)

    所有的模型字段都可以接收一定数量的参数,比如CharField至少需要一个max_length参数.下面的这些参数是所有字段都可以使用的,并且是可选的. null 该值为True时,Django在数据 ...

  6. PageHelper分页实战(SSM整合)

    步骤一:引入SSM相关的jar包,包列表如下: 步骤二:创建或修改配置文件,配置文件清单如下: applicationContext.xml <?xml version="1.0&qu ...

  7. isMemberOfClass、isKindOfClass原理分析

    isMemberOfClass - 调用者必须是传入的类的实例对象才返回YES- 判断调用者是否是传入对象的实例,别弄反了,如 [s1 isMemberOfClass:p1] ,意思是s1是否是p1的 ...

  8. How to check all timestamps of a file

    A friend of mine she asked me how to check all timestamps of a file on an NTFS volume. She did not h ...

  9. MemCached的工具类。获取cached中的所有key

    package com.ibs.auth.controller; import java.io.UnsupportedEncodingException; import java.util.Date; ...

  10. Redis优化建议

    优化的一些建议 1.尽量使用短的key 当然在精简的同时,不要完了key的"见名知意".对于value有些也可精简,比如性别使用0.1. 2.避免使用keys * keys *, ...