此文章為原創文章,原文地址:https://www.cnblogs.com/eagle1098/p/11431679.html 連線動畫圖 編輯器 效果如上圖所示。本項目使用主要d3.jsv4製作,分兩部分,一個是實際展示的連線動畫圖,另一個是管理人員使用滑鼠編輯連線的頁面。對於d3.js如何引入圖 ...
此文章為原創文章,原文地址: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