溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

利用d3.js制作連線動畫圖與編輯器的方法實例

發(fā)布時間:2020-10-15 11:45:15 來源:腳本之家 閱讀:188 作者:eagle1098 欄目:web開發(fā)

利用d3.js制作連線動畫圖與編輯器的方法實例

連線動畫圖

利用d3.js制作連線動畫圖與編輯器的方法實例

編輯器

效果如上圖所示。

本項目使用主要d3.jsv4制作,分兩部分,一個是實際展示的連線動畫圖,另一個是管理人員使用鼠標(biāo)編輯連線的頁面。對于d3.js如何引入圖片,如何畫線等基礎(chǔ)功能,這里就不再介紹了,大家可以找一些入門文章看一下。這里主要介紹一下重點問題。

1.連線動畫圖

此圖的主要功能是每隔給定時間,通過ajax請求后臺數(shù)據(jù),并根據(jù)返回的數(shù)據(jù)動態(tài)改變每個圖片下方的數(shù)值,動態(tài)改變連線上的動畫流動方向和是否流動。

首先,確定圖表中需要配置的內(nèi)容,如各圖片存儲位置,連線和動畫顏色,圖片和連線的坐標(biāo)等。這些數(shù)據(jù)需要在html中進行配置,最好寫成object對象,賦值給我們自己的圖表類的函數(shù)。比如:

var data = {
 element:[{
 image: 'img/work.png',
 pos:[1,1], // 圖片位置
 linePoint:[], // 圖片發(fā)出線段坐標(biāo)數(shù)組
 lineDir:0, // 線段動畫方向
 title: '工作'
 }],
 lineColor:'black', // 連線顏色
 animateColor: 'red', // 動畫顏色
};
var chart = new Myd3chart('#chart');
chart.lineChart(data);

其中圖片發(fā)出的線段坐標(biāo)數(shù)組,使用外部文件提供,此文件由之后介紹的編輯器生成。

在設(shè)計我們自己的圖表函數(shù)時,最好把每個功能劃分成獨立的函數(shù),這樣方便以后的維護和擴展。

動畫線段采用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 */
 }
}

這個圖表的難點在于動態(tài)改變連線上的流動動畫,因為A線段的終點會連接到B線段上,如果B線段動畫停止,則A線段上的動畫仍然要從B上經(jīng)過,而不能簡單停止B線段上的動畫。而且如果B線段上的接入點不止一個,還要判斷接入點之間的順序,只顯示最靠近B起始點的接入點的動畫。另外還要判斷接入線段上是否有接入線段,層級關(guān)系里面如果有1個線段有動畫,則此接入點就有動畫流出。(這里說起來有點繞)

我的方法是:

1)統(tǒng)計每個線段上的所有接入點,這里就是圖片名稱,用于判斷此線段是否有動畫流出。

2)接收后臺傳來的數(shù)據(jù)時,判斷每個線段是否有動畫,如果有動畫,則直接恢復(fù)其動畫線段的起始點坐標(biāo);如果沒有動畫,則判斷最靠近起始點的接入點是否有動畫,如果有動畫則將動畫線段的起始點改為此接入點坐標(biāo)。

// 統(tǒng)計接入點
 function findAccessPoint() {
 var accessPoints = [];
 // 記錄每個線段上的接入點,data為配置數(shù)據(jù)
 data.eles.forEach(function(d, i){
 if(d.line.length == 0){
 return;
 }
 var acsp = {
 name: d.title.text,
 ap: [], // 接入點,按順序排列,頭部離開始點近
 };
 // 本線段上,每兩相鄰的點作為一個元素存入數(shù)組
 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結(jié)束點
  // 用x坐標(biāo)計算在本線段上的y坐標(biāo),再和實際的y坐標(biāo)比較
  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以內(nèi),并且此線終點在當(dāng)前線起點和終點之間
  // 認為此點為接入點
  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){
 // 在數(shù)組中找到指定名稱的項
 if(d.name === name){
  if(d.ap.length>0){
  // 把該項下面的ap中的名稱加入給定arr
  d.ap.forEach(function(dd, di){
  arr.push(dd.name);
  // 如果該點內(nèi)的allNames已經(jīng)有值則直接加入
  if(dd.allNames.length>0){
  dd.allNames.forEach(function(d, i){
   arr.push(d);
  });
  } else{
  // 遞歸查找子接入點
  findPoint(dd.name, arr);
  }
  });
  } else {
  return;
  }
 }else{
  return;
 }
 });
 }
 }

以上函數(shù)的運行結(jié)果會產(chǎn)生一個對象,存儲每個接入線段上‘掛載'的接入點,目的就是改變動畫時方便判斷。

// 更新線條動畫
 aniLine.each(function(d, i){
 var curLine = d3.select(this);
 // 找到對應(yīng)的動畫line
 if (dd.name === curLine.attr('tag')) {
  // 處理動畫是否運行
  if (dd.ani) {
  // 此線條動畫運行
  curLine.style('animation-play-state', 'running');
  curLine.style('display', 'inline');
  // 如果動畫運行,則恢復(fù)原始動畫路徑
  curLine.attr('d', function(d){
  return line(chartData.eles[i].line);
  });
  } else {
  // 此線條動畫停止
  // 先查找離本線段開始點最近的接入點
  var acp = accessPoints;
  // 從accessPoints中找到本節(jié)點的接入點集合
  var ap = [];
  acp.forEach(function(acd, aci){
  if(acd.name === dd.name){
  ap = acd.ap;
  }
  });  
  // 最近有動畫接入點序號
  var acIndex = -1;
  // 找到最近的有動畫接入點,遠近按數(shù)組序號遞增
  for(var j=0;j<ap.length;j++){
  // 復(fù)制所有子接入點數(shù)組
  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();
  // 接入節(jié)點與開始點的距離
  var disAp = Math.pow((accp[0] - curLine[0][0]),2) +
  Math.pow((accp[1] - curLine[0][1]),2);
  // 如果當(dāng)前線段中有離開始節(jié)點比接入點近的節(jié)點
  // 則刪除此節(jié)點
  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.編輯器

由于本圖表需要配置大量坐標(biāo),如果手動填寫的話效率十分低下,所以需要開發(fā)一個編輯器用來修改圖表。

編輯器的主要使用方法為,使用鼠標(biāo)拖動圖標(biāo),雙擊確定起始位置并開始實時畫線狀態(tài),隨著鼠標(biāo)移動動態(tài)畫出線段,單擊確定臨時終點,再單擊確定下一個終點,右擊結(jié)束動態(tài)畫線狀態(tài)。如果鼠標(biāo)單擊其他圖標(biāo),則終點為該圖標(biāo)的起始坐標(biāo)。本程序的實時畫線部分進行了傾斜的約束,即左傾或右傾30度角。

編輯器比展示圖要簡單一些,復(fù)雜部分在事件處理。

// 拖動圖標(biāo)
 var draging = d3.drag()
 .on('drag', function () {
 // 當(dāng)長寬相同時,iconSize是圖標(biāo)大小[寬,高]
 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;
 // 設(shè)定圖標(biāo)位置
 g.select('.image')
  .attr('x', eventX)
  .attr('y', eventY);
 })
 // 拖拽結(jié)束
 .on('end', function () {
 var g = d3.select(this);
 g.select('.subBg')
  .attr('transform', function (d, i) {
  // 對子標(biāo)簽的處理,自動符合字符串長度
  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 + ')';
  });
 });
 // 圖標(biāo)組增加拖動事件
 imageGs.call(draging);

以上拖動事件,只是調(diào)用基本方法。

實時畫線功能需要提前定義臨時存儲對象,用來存儲鼠標(biāo)移動時線段的終點坐標(biāo)。

// 鼠標(biāo)移動時,實時畫線到鼠標(biāo)當(dāng)前位置,_bodyRect為主區(qū)域
 _bodyRect.on('mousemove', function(){
 // 如果不處于實時畫線狀態(tài)
 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);
 });

 // 當(dāng)鼠標(biāo)移入某個建筑圖標(biāo)范圍時
 _chartData.imageGs.on('mouseenter', function(d, i){
 // 移除舊線
 if(_chartData.tempLine.line){
  _chartData.tempLine.pos = [];
  _chartData.tempLine.line.remove();
 }
 // 得到圖標(biāo)中心點坐標(biāo)
 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;
 // 將此建筑圖標(biāo)的中心點坐標(biāo)作為終點坐標(biāo)畫線
 _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);
  });
 });
 // 當(dāng)鼠標(biāo)移出圖標(biāo)區(qū)域
 _chartData.imageGs.on('mouseleave', function(d, i){
 // 移除舊線
 if(_chartData.tempLine.line){
  _chartData.tempLine.pos = [];
  _chartData.tempLine.line.remove();
 }
 });
 // 對圖標(biāo)單擊鼠標(biāo),保存線
 _chartData.imageGs.on('click', function (d, i) {
 // 保存臨時線
 drawLine();
 // 停止實時畫線
 exitDrawing();
 });
 });
 // 點擊鼠標(biāo)右鍵,停止實時畫線
 _bodyRect.on('contextmenu', function(){
 // 停止實時畫線
 exitDrawing();
 d3.event.preventDefault();
 });
 });
 }

在此只貼出部分代碼,如果大家有任何建議和問題,還請留言,謝謝。

總結(jié)

以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,謝謝大家對億速云的支持。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI