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中進(jìn)行配置甚疟,最好寫成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ù)時黄痪,最好把每個功能劃分成獨(dú)立的函數(shù)紧帕,這樣方便以后的維護(hù)和擴(kuò)展。
動畫線段采用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)前線起點和終點之間
            // 認(rèn)為此點為接入點
            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ù)的運(yùn)行結(jié)果會產(chǎn)生一個對象蚕键,存儲每個接入線段上‘掛載’的接入點,目的就是改變動畫時方便判斷衰粹。

// 更新線條動畫
    aniLine.each(function(d, i){
        var curLine = d3.select(this);
        // 找到對應(yīng)的動畫line
        if (dd.name === curLine.attr('tag')) {
          // 處理動畫是否運(yùn)行
          if (dd.ani) {
            // 此線條動畫運(yùn)行
            curLine.style('animation-play-state', 'running');
            curLine.style('display', 'inline');
            // 如果動畫運(yùn)行锣光,則恢復(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;
            // 找到最近的有動畫接入點,遠(yuǎn)近按數(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)。本程序的實時畫線部分進(jìn)行了傾斜的約束惊科,即左傾或右傾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();
    });
   });
  }

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蚣旱,隨后出現(xiàn)的幾起案子碑幅,更是在濱河造成了極大的恐慌,老刑警劉巖塞绿,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沟涨,死亡現(xiàn)場離奇詭異,居然都是意外死亡异吻,警方通過查閱死者的電腦和手機(jī)裹赴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涧黄,“玉大人篮昧,你說我怎么就攤上這事∷裢祝” “怎么了懊昨?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長春宣。 經(jīng)常有香客問我酵颁,道長嫉你,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任躏惋,我火速辦了婚禮幽污,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘簿姨。我一直安慰自己距误,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布扁位。 她就那樣靜靜地躺著准潭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪域仇。 梳的紋絲不亂的頭發(fā)上刑然,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天,我揣著相機(jī)與錄音暇务,去河邊找鬼泼掠。 笑死,一個胖子當(dāng)著我的面吹牛垦细,可吹牛的內(nèi)容都是我干的择镇。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼蝠检,長吁一口氣:“原來是場噩夢啊……” “哼沐鼠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起叹谁,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤饲梭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后焰檩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體憔涉,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年兜叨,在試婚紗的時候發(fā)現(xiàn)自己被綠了国旷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖糙及,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情揣钦,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布匈庭,位于F島的核電站夭拌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏桶现。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一唤衫、第九天 我趴在偏房一處隱蔽的房頂上張望页藻。 院中可真熱鬧份帐,春花似錦废境、人聲如沸巴元。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至纹安,卻和暖如春尤辱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背厢岂。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工光督, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人咪笑。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓可帽,卻偏偏與公主長得像,于是被迫代替她去往敵國和親窗怒。 傳聞我的和親對象是個殘疾皇子映跟,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容