d3.js製作連線動畫圖和編輯器

来源:https://www.cnblogs.com/eagle1098/archive/2019/08/29/11431679.html
-Advertisement-
Play Games

此文章為原創文章,原文地址: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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、背景定位 同一個標簽可以同時設置背景顏色和背景圖片,如果顏色和圖片同時存在,那麼圖片會覆蓋顏色 1.在CSS中有一個叫做background-position:屬性,就是專門用來控制背景圖片的位置 2.格式:background-position:值1 值2; 值1的取值範圍:left cent ...
  • 一、class載入方式 <div id="pop" class="easyui-droppable" style="width: 400px;height: 300px;background-color: powderblue"></div> 二、js載入方式 $("#pop").droppable ...
  • 1、 function randomMath(m,n){ if(n>=1000 && m<10000){ return parseInt(Math.random()*(m-n+1)+n); } else{ return "點擊刷新"; } } document.write(randomMath(10... ...
  • 正則是用來描述字元規則的,常常用來做表單驗證。先說下正則的創建和簡單的使用吧! / / 是正則表達式的標識符 創建 使用:不能直接使用,需配合方法使用 字元: 字元: str.match(reg) str.replace(reg) 正則: 正則: reg.test(str) 特點 篩選出符合條件的子 ...
  • 概況如下:1、SphereGeometry實現自轉的地球;2、THREE.ImageUtils.loadTexture載入地圖貼圖材質;3、THREE.Math.degToRad,Math.sin,Math.cos實現地圖經緯度與三位坐標x,y,z之間的轉換;4、軌跡中根據分段數與相應國家gdp值來 ...
  • Vue CLI Vue CLI 項目在pycharm中配置 第一步 第二步 第三步 ...
  • 簡單介紹BFC BFC 就是塊級格式化上下文,是頁面盒模型佈局中的一種 CSS 渲染模式,相當於一個獨立的容器,裡面的元素和外部的元素相互不影響。 創建 BFC 的方式有: 1.html的根元素 2.float浮動 3.絕對定位 4.overflow不為 visible 5.display為表格佈局 ...
  • 08.29自我總結 Vue中插槽指令 就是在組件里留著差值方便 而且由於插件是寫在父級中數據可以直接父級中傳輸而不需要傳子再傳父有些情況會減少寫代碼量 示例 html //4.創建個組件 //根據插槽的名稱創建插槽 //插槽裡面的內容 //1.創建組件 let msgTag = { template ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...