效果如上圖所示。
本項目使用主要d3.jsv4製做,分兩部分,一個是實際展現的連線動畫圖,另外一個是管理人員使用鼠標編輯連線的頁面。對於d3.js如何引入圖片,如何畫線等基礎功能,這裏就再也不介紹了,你們能夠找一些入門文章看一下。這裏主要介紹一下重點問題。css
此圖的主要功能是每隔給定時間,經過ajax請求後臺數據,並根據返回的數據動態改變每一個圖片下方的數值,動態改變連線上的動畫流動方向和是否流動。
首先,肯定圖表中須要配置的內容,如各圖片存儲位置,連線和動畫顏色,圖片和連線的座標等。這些數據須要在html中進行配置,最好寫成object對象,賦值給咱們本身的圖表類的函數。好比:html
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便可:node
.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)接收後臺傳來的數據時,判斷每一個線段是否有動畫,若是有動畫,則直接恢復其動畫線段的起始點座標;若是沒有動畫,則判斷最靠近起始點的接入點是否有動畫,若是有動畫則將動畫線段的起始點改成此接入點座標。ajax
// 統計接入點 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'); } } }
因爲本圖表須要配置大量座標,若是手動填寫的話效率十分低下,因此須要開發一個編輯器用來修改圖表。
編輯器的主要使用方法爲,使用鼠標拖動圖標,雙擊肯定起始位置並開始實時畫線狀態,隨着鼠標移動動態畫出線段,單擊肯定臨時終點,再單擊肯定下一個終點,右擊結束動態畫線狀態。若是鼠標單擊其餘圖標,則終點爲該圖標的起始座標。本程序的實時畫線部分進行了傾斜的約束,即左傾或右傾30度角。
編輯器比展現圖要簡單一些,複雜部分在事件處理。app
// 拖動圖標 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);
以上拖動事件,只是調用基本方法。
實時畫線功能須要提早定義臨時存儲對象,用來存儲鼠標移動時線段的終點座標。curl
// 鼠標移動時,實時畫線到鼠標當前位置,_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(); }); }); }
在此只貼出部分代碼,若是你們有任何建議和問題,還請留言,謝謝。編輯器