亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 編程 > JavaScript > 正文

利用d3.js力導布局繪制資源拓撲圖實例教程

2019-11-19 12:18:40
字體:
來源:轉載
供稿:網友

前言

最近公司業務服務老出bug,各路大佬盯著鏈路圖找問題找的頭昏眼花。某天大佬丟了一張圖過來“我們做一個資源拓撲圖吧,方便大家找bug”。

就是這個圖,應該是馬爸爸家的

好吧,來仔細瞧瞧這個需求咋整呢。一圈資源圍著一個中心的一個應用,用曲線連接起來,曲線中段記有應用與資源間的調用信息。emmm 這個看起來很像女神在遛一群舔狗... 啊不,是 d3.js 力導向圖!

d3.js 力導向圖

d3.js 是著名的數據可視化基礎工具,他提供了基本的將數據映射至網頁元素的能力,同時封裝了大量實用的數據操作函數與圖形算法。其中力導向圖(Force-Directed Graph)是 d3.js 提供的一種十分經典的繪圖算法。通過在二維空間里配置節點和連線,在各種各樣力的作用下,節點間相互碰撞和運動并在這個過程中不斷地降低能量,最終達到一種能量很低的安定狀態,形成一種穩定的力導向圖。

d3.js 力導向圖中默認提供了 5 種作用力(以最新的 5.x 為準):

中心力(Centering)

中心力作用于所有的節點而不是某些單獨節點,可以將所有的節點的中心一致的向指定的位置移動,而且這種移動不會修改速度也不會影響節點間的相對位置。

碰撞力(Collision)

碰撞力將每個節點視為一個具有一定半徑的圓,這個力會阻止代表節點的這個圓相互重疊,即兩個節點間會相互碰撞,可以通過設置 strength 設置這個碰撞力的強度。

彈簧力(Links)

當兩個節點通過設置 link 連接到一起后,可以設置彈簧力,這個力將根據兩個節點間的距離將兩個節點拉近或推遠,力的強度和這個距離成比例就和彈簧一樣。

電荷力(Many-Body)

通過設置 strength 來模擬所有節點間的相互作用力,如果為正節點間就會相互吸引,可以用來模擬電荷吸引力,如果為負節點間就會相互排斥。這個力的大小也和節點間的距離有關。

定位力(Positioning)

這個力可以將節點沿著指定的維度推向一個指定位置,比如通過設置 forceX 和 forceY 就可以在 X軸 和 Y軸 方向推或者拉所有的節點,forceRadial 則可以形成一個圓環把所有的節點都往這個圓環上相應的位置推。

回到這個需求上,其實可以把應用、所有的資源與調用信息都看成節點,資源之間通過一個較弱的彈簧力與調用信息連接起來,同時如果應用與資源間的調用有來有往,則在這兩個調用信息之間加上一個較強的彈簧力。

ok說干就干

// 所有代碼基于 typescript,省略部分代碼type INode = d3.SimulationNodeDatum & {  id: string label: string; isAppNode?: boolean;};type ILink = d3.SimulationLinkDatum<INode> & {  strength: number;};const nodes: INode[] = [...]; const links: ILink[] = [...];const container = d3.select('container');const svg = container.select('svg')  .attr('width', width) .attr('height', height);const html = container.append('div')  .attr('class', styles.HtmlContainer);// 創建一個彈簧力,根據 link 的 strength 值決定強度const linkForce = d3.forceLink<INode, ILink>(links)  .id(node => node.id) // 資源節點與信息節點間的 strength 小一點,信息節點間的 strength 大一點 .strength(link => link.strength);const simulation = d3.forceSimulation<INode, ILink>(nodes)  .force('link', linkForce) // 在 y軸 方向上施加一個力把整個圖形壓扁一點 .force('yt', d3.forceY().strength(() => 0.025))  .force('yb', d3.forceY(height).strength(() => 0.025)) // 節點間相互排斥的電磁力 .force('charge', d3.forceManyBody<INode>().strength(-400)) // 避免節點相互覆蓋 .force('collision', d3.forceCollide().radius(d => 4)) .force('center', d3.forceCenter(width / 2, height / 2)) .stop();// 手動調用 tick 使布局達到穩定狀態for (let i = 0, n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay())); i < n; ++i) {  simulation.tick();}const nodeElements = svg.append('g')  .selectAll('circle') .data(nodes) .enter().append('circle') .attr('r', 10) .attr('fill', getNodeColor);const labelElements = svg.append('g')  .selectAll('text') .data(nodes) .enter().append('text') .text(node => node.label) .attr('font-size', 15);const pathElements = svg.append('g')  .selectAll('line') .data(links) .enter().append('line') .attr('stroke-width', 1) .attr('stroke', '#E5E5E5');const render = () => {  nodeElements .attr('cx', node => node.x!) .attr('cy', node => node.y!); labelElements .attr('x', node => node.x!) .attr('y', node => node.y!); pathElements .attr('x1', link => link.source.x) .attr('y1', link => link.source.y) .attr('x2', link => link.target.x) .attr('y2', link => link.target.y);}render(); 

效果如下:

ok 已經基本實現啦,那就這樣啦,等后臺同學實現一下接口就可以上線啦,日均UV兩位數的產品要啥自行車,有的看就不錯了(手動二哈)。

當然不行了,有這么一個都市傳說,中臺產品的好用與否與離職率高低成相關關系。本來需要打開資源拓撲圖就是一件很🤢的事了,再看到這么一款體驗極差的產品,感覺分分鐘就要離職了。為了給我司年交易額兩萬億的長遠目標添磚加瓦,我們來看看有啥需要改進的地方。

至少字給我居中吧

注意到我們的字都是左下角定位到節點中心的,這是因為我們使用的是 svg 的 text 元素,默認情況下給 text 元素設置的 x 和 y 代表了 text 元素 baseLine 的起始位置。當然我們可以通過直接設置 dx 與 dy 設置一個偏移量來完成居中的問題,但考慮到 svg 元素相比普通的 html 元素畢竟還是有所限制,并不方便將來的擴展啥的,所以我們索性把所有的圓點與文字都換成 html 元素。

...const nodeElements = html.append('div')  .selectAll('div') .data(nodes.filter(node => node.isAppNode)) .enter().append('div') // css modules .attr('class', styles.NodeItem) .html((node: INode) => { return `<p>${node.id}</p>`; });const labelElements = html.append('div')  .selectAll('div') .data(nodes.filter(node => !node.isAppNode)) .enter().append('div') // css modules .attr('class', styles.LabelItem) .html(node => ` <p>${node.label}</p> <p>Avada Kedavra!</p> `);...const render = () => {  nodeElements .attr('style', (node) => { return `transform: translate3d(calc(${node.x}px - 50%), calc(${node.y}px - 50%), 0);`; }); labelElements .attr('style', (node) => { return `transform: translate3d(calc(${node.x}px - 50%), calc(${node.y}px - 50%), 0);`; });}

效果如下:

字都居中了!

這個線怎么跟激光似的,一點也不像在遛舔狗

再來看看這個線,我們一開始是把所有代表彈簧力的線段當成直線就畫上去了,但這樣看起來很生硬效果很差。實際上我們需要的是一條自然的曲線把資源節點和應用節點連接起來,同時穿過信息節點,所以問題就變成了如何穿過三個點畫一條曲線。

要畫曲線自然要用到 svg 的 path 元素和他的 d 繪制指令,關于怎么用 path 畫曲線,這里MDN上都有很詳細的教程。在具體實際項目應用中,一般來說貝塞爾曲線會比較難把控也比較難獲得較好的效果,所以我們使用 A 指令來畫這個弧線。

使用 A 指令畫弧線,需要知道的元素有:x軸半徑,y軸半徑,弧形旋轉角度,角度大小flag,弧線方向flag,弧形的終點。那在已知三個點坐標的情況下,怎么求出這些元素呢?是時候復習一波三角函數了。

已知 A、B、C 坐標(xaya、xbyb、xcyc),則可求得 a、b、c 長度(Math.sqrt((x1-x2)2 - (y1-y2)2),再根據余弦定理可求得∠C,再根據正弦定理可得r,具體參看代碼:

type IVisualLink = {  id: string; start: number[]; middle: number[]; end: number[]; arcPath: string; hasReverseVisualLink: boolean;};const visualLinks: IVisualLink[] = [...];function dist(a: number[], b: number[]) {  return Math.sqrt( Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2));}...const pathElements = svg.append('g')  .selectAll('path') .data(visualLinks) .enter().append('path') .attr('fill', 'none') .attr('stroke-width', 1) .attr('stroke', '#E5E5E5');...const render = () => {  ... nodes // 過濾出所有的信息節點 .filter(node => !node.isAppNode) .forEach((node) => { ... // 根據信息節點的信息得到對應的 visualLink 對象 index const idx = findVisualLinkIndex(node) visualLinks[idx].start = [source.x!, source.y!]; visualLinks[idx].middle = [node.x!, node.y!]; visualLinks[idx].end = [target.x!, target.y!]; const A = visualLinks[idx].start; const B = visualLinks[idx].end; const C = visualLinks[idx].middle; const a = dist(B, C); const b = dist(C, A); const c = dist(A, B); // 余弦定理求得∠C const angle = Math.acos((a * a + b * b - c * c) / (2 * a * b)); // 正弦定理求得外接圓半徑 const r = _.round(c / Math.sin(angle) / 2, 4); // 角度大小flag,因為我們要的是條弧線而不是一個殘缺的圓,所以恒為0 const laf = 0; // 弧線方向flag,根據AB的斜率判斷C在AB線的那一邊,再確定弧線方向 const saf = +((B[0] - A[0]) * (C[1] - A[1]) - (B[1] - A[1]) * (C[0] - A[0]) < 0); const arcPath = ['M', A, 'A', r, r, 0, laf, saf, B].join(' '); visualLinks[idx].arcPath = arcPath; }); pathElements .attr('d', (link) => { return link.arcPath; });}

效果如下:

這些線一對A都沒有,分不清正反啊

應用與資源間的關系,是有方向的,大部分情況下是應用調用資源,也有情況會有雙向的調用,除了文字意外,我們還需要加上箭頭來表明是誰在調用誰。怎么加這個箭頭呢?svg 的 path 元素有一個 marker-end 屬性,通過設置這個屬性可以可以將一個 svg 元素繪制到 path 元素最后的向量上。

// 在 svg 元素中添加一個 marker 元素<svg>  <marker id="arrow" viewBox="-10 -10 20 20" markerWidth="20" markerHeight="20" orient="auto" > <path d="M-6.75,-6.75 L 0,0 L -6.75,6.75" fill="none" stroke="#E5E5E5" /> </marker></svg>...const pathElements = svg.append('g')  .selectAll('path') .data(visualLinks) .enter().append('path') .attr('fill', 'none') // 設置 marker-end 屬性 .attr('marker-end', 'url(#arrow)') .attr('id', link => link.id) .attr('stroke-width', 1) .attr('stroke', '#E5E5E5');...

但直接這樣寫的話,效果會很差,為啥呢?因為我們 path 元素的起點與終點是節點的中心點,直接這樣的話箭頭都在節點上面,如圖:

看到中間那朵菊花沒

所以我們沒法直接通過加這個屬性來加上箭頭,我們需要對 path 做一些處理,對 path 線段去頭去尾。那怎么做呢?還好有巨佬已經實現了一種算法,算出兩個 path 元素之間的交點,因此我們可以在算出原 arcPath 后,再算出這條弧線與節點外一個大一點的圓的交點,再把原 arcPath 的起點與終點移到這兩個點上。

import intersect from 'path-intersection';const render = () => {  ... nodes // 過濾出所有的信息節點 .filter(node => !node.isAppNode) .forEach((node) => { ... // 根據信息節點的信息得到對應的 visualLink 對象 index const idx = findVisualLinkIndex(node) visualLinks[idx].start = [source.x!, source.y!]; visualLinks[idx].middle = [node.x!, node.y!]; visualLinks[idx].end = [target.x!, target.y!]; const A = visualLinks[idx].start; const B = visualLinks[idx].end; const C = visualLinks[idx].middle; const a = dist(B, C); const b = dist(C, A); const c = dist(A, B); // 余弦定理求得∠C const angle = Math.acos((a * a + b * b - c * c) / (2 * a * b)); // 正弦定理求得外接圓半徑 const r = _.round(c / Math.sin(angle) / 2, 4); // 角度大小flag,因為我們要的是條弧線而不是一個殘缺的圓,所以恒為0 const laf = 0; // 弧線方向flag,根據AB的斜率判斷C在AB線的那一邊,再確定弧線方向 const saf = +((B[0] - A[0]) * (C[1] - A[1]) - (B[1] - A[1]) * (C[0] - A[0]) < 0); const origArcPath = ['M', A, 'A', r, r, 0, laf, saf, B].join(' '); const raidus = NODE_RADIUS; const startCirclePath = [ 'M', A, 'm', [-raidus, 0], 'a', raidus, raidus, 0, 1, 0, [raidus * 2, 0], 'a', raidus, raidus, 0, 1, 0, [-raidus * 2, 0], ].join(' '); const endCirclePath = [ 'M', B, 'm', [-raidus, 0], 'a', raidus, raidus, 0, 1, 0, [raidus * 2, 0], 'a', raidus, raidus, 0, 1, 0, [-raidus * 2, 0], ].join(' '); const startIntersection = intersect(origArcPath, startCirclePath)[0]; const endIntersection = intersect(origArcPath, endCirclePath)[0]; const arcPath = [ 'M', [startIntersection.x, startIntersection.y], 'A', r, r, 0, laf, saf, [endIntersection.x, endIntersection.y], ].join(' '); visualLinks[idx].arcPath = arcPath; }); pathElements .attr('d', (link) => { return link.arcPath; }); ...}

效果已經很接近了!

字疊到一起啦,臣妾看不清啊

到這一步整體效果其實已經差不多了,但追求完美的我們怎么可能到此為止呢?仔細看看這個圖,因為調用信息是一個方盒而不是原型的節點,如果應用和資源間有來有往,那這個字很容易疊到一起??梢試L試調整碰撞力(Collision)和彈簧力(Links)來讓他們別疊到一起,不過試下來發現調整這兩個系數很容易把整個圖弄得亂七八糟的。那咋辦呢?我們就要到此為止了嗎?不妨換個思路,如果應用與資源間有來有往,則這個連接信息就不放到中間點,而是放到開始三分之一處。

說的挺好,我咋知道開始三分之一處在哪?

還好這種「復雜」的數學問題,前人已經幫我們探索的差不多了。svg 標準里定義了 SVGGeometryElement.getTotalLength SVGGeometryElement.getPointAtLength 兩個方法,通過這兩個方法我們可以獲得 path 路徑的全長,和某一長度時點的位置。不過這兩個方法都是附在 DOM 元素上的,直接調用有點麻煩,還好有PureJS 的實現:

import { svgPathProperties } from 'svg-path-properties';...render = () => {  ... labelElements .attr('style', (link) => { const properties = svgPathProperties(link.arcPath); const totalLength = properties.getTotalLength(); const point = properties.getPointAtLength( link.hasReverseVisualLink ? totalLength / 3 : totalLength / 2, ); return `transform: translate3d(calc(${point.x}px - 50%), calc(${point.y}px - 50%), 0);`; }); ...}

最終效果:

還差一點

效果做到這已經差不多了,不過還有一些不完美的地方

  • 各種力的系數,在數據不同時不能通用,還必須根據數據不同試出來一個相對通用的系數函數。
  • 不能保證所有的節點都在方框內且不重疊

感覺這兩個問題都算是力導布局的固有缺陷,可能那張圖的實現根本和力導布局沒啥關系呢😂。不過我們使用力導布局也可以實現不錯的效果,這種 edge case 可以慢慢來解決了就。

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對武林網的支持。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
欧美床上激情在线观看| 中国china体内裑精亚洲片| 亚洲激情在线视频| 欧美日韩国产色视频| 精品久久久久久久久久国产| 国产精品成人观看视频国产奇米| 欧美日韩国产黄| 国产一区二区三区久久精品| 俺去亚洲欧洲欧美日韩| 日韩人体视频一二区| 国产香蕉一区二区三区在线视频| 97热精品视频官网| 中文字幕精品一区久久久久| 久久影院在线观看| 国产精品欧美一区二区三区奶水| 久久久欧美精品| 色综合伊人色综合网站| 国产精品美女在线| 久久精品成人欧美大片古装| 久久人人爽人人爽爽久久| 亚洲美女性生活视频| 欧美在线性视频| 亚洲色图综合网| 国产精品入口免费视| 欧美国产一区二区三区| 中文字幕日韩欧美在线| 国产欧美日韩精品在线观看| 亚洲电影免费在线观看| 欧美日韩国产综合新一区| 国自在线精品视频| 精品久久久久国产| 国产成人精品日本亚洲专区61| 97碰碰碰免费色视频| 98精品国产高清在线xxxx天堂| 97人人模人人爽人人喊中文字| 成人黄色大片在线免费观看| 在线观看日韩欧美| 九九热在线精品视频| 欧美日韩久久久久| 国产亚洲精品久久| 日韩在线视频网站| 日韩精品视频三区| 久久久久久亚洲精品不卡| 欧美成人免费va影院高清| 69**夜色精品国产69乱| 亚洲影院色在线观看免费| 国产精品专区第二| 国产精品一久久香蕉国产线看观看| 国产精品日韩在线播放| 日本一区二区三区在线播放| 欧美日韩在线观看视频| 久久理论片午夜琪琪电影网| 国产日韩在线亚洲字幕中文| 上原亚衣av一区二区三区| 亚洲欧美国产日韩天堂区| 久久理论片午夜琪琪电影网| 国色天香2019中文字幕在线观看| 国产视频久久久| 中文字幕一区电影| 欧美精品18videosex性欧美| 91在线观看免费| 在线亚洲男人天堂| 69精品小视频| 欧美麻豆久久久久久中文| 欧美性xxxx极品hd欧美风情| 欧美人与性动交a欧美精品| 一本一本久久a久久精品牛牛影视| 国产一区二区三区在线播放免费观看| 国产91精品久久久| 亚洲国产精品福利| 亚洲精品视频免费| 国产综合久久久久久| 人人做人人澡人人爽欧美| 97在线看免费观看视频在线观看| 一区二区国产精品视频| 日韩精品视频中文在线观看| 日韩在线观看免费全集电视剧网站| 热久久视久久精品18亚洲精品| 18性欧美xxxⅹ性满足| 日韩在线观看免费高清| 热re99久久精品国产66热| 一个人看的www欧美| 亚洲**2019国产| 久久精品国产v日韩v亚洲| 亚洲男人天堂古典| 一个色综合导航| 韩国精品美女www爽爽爽视频| 日韩视频在线免费观看| 国产99久久久欧美黑人| 黄网站色欧美视频| 国产综合在线视频| 久久亚洲国产成人| 日本一区二区在线免费播放| 91成品人片a无限观看| 国产精品久久久久久久久久久新郎| 美女福利视频一区| 久久视频中文字幕| 92国产精品视频| 国产va免费精品高清在线观看| 色偷偷av一区二区三区| 久久精品国产96久久久香蕉| 亚洲韩国欧洲国产日产av| 国产精品女主播视频| 日韩视频在线一区| 欧美性受xxxx白人性爽| 欧美一级在线播放| 亚洲欧美一区二区三区四区| 在线播放精品一区二区三区| 午夜美女久久久久爽久久| 久久成人精品一区二区三区| 97视频在线观看免费高清完整版在线观看| 欧美精品在线播放| 亚洲日韩欧美视频| 久久免费视频在线观看| 欧美日韩国产一区二区| 97超碰国产精品女人人人爽| 91精品国产综合久久香蕉| 91在线免费观看网站| 欧美极品美女电影一区| 国产+成+人+亚洲欧洲| 欧美日韩精品在线视频| 91chinesevideo永久地址| 91精品国产91久久久久福利| 欧美黑人一级爽快片淫片高清| 中文字幕日韩综合av| 久久久久久久97| 色综合久久久888| 日韩中文在线不卡| 日韩亚洲欧美中文在线| 精品欧美一区二区三区| 欧美日韩中文在线观看| 久久婷婷国产麻豆91天堂| 色婷婷久久一区二区| 91香蕉嫩草影院入口| 欧美激情精品久久久久久变态| 国产在线视频欧美| 欧美怡春院一区二区三区| 日韩av在线免播放器| 性金发美女69hd大尺寸| 欧美激情三级免费| 日韩人体视频一二区| 亚洲天堂网在线观看| 91香蕉电影院| 欧美激情精品久久久久久蜜臀| 欧美激情亚洲自拍| 国产精品视频中文字幕91| 久久91亚洲人成电影网站| 欧美成人免费全部观看天天性色| 亚洲激情在线视频| 国产精品三级在线| 久久影院资源站| 亚洲欧美日韩中文视频| 成人羞羞国产免费| 色一情一乱一区二区| 精品久久久av| 欧美视频在线看| 欧美日韩亚洲视频一区| 国产精品一区二区久久久| 9.1国产丝袜在线观看| 一区二区三区精品99久久| 久久精品在线视频| 亚洲大胆人体视频| 亚洲视频一区二区三区| 伊人久久久久久久久久久久久|