d3-force怎麼使用?該演算法是怎麼實現的?

發表於2023-09-19

前言|force佈局

筆者在fastVG產品圖視覺化佈局中force佈局採用D3-force-layout,因此介紹下該佈局的一些演算法邏輯和基礎使用規則。

本文預期收穫:

  1. 對於佈局演算法有更深入的瞭解。
  2. 在使用d3 & d3-force的時候 有調參規則的經驗。
  3. 可結合其他渲染庫進行獨立使用。

演算法邏輯簡介

演算法說明

D3-force-layout (力佈局)模組利用velocity Verlet演算法 實現了一個用於模擬粒子上物理力的數值積分器。當然內部的模擬做了簡化, 假設每個step(的時間單位步長\_Δt = 1 ,所有粒子質量 m = 1。因此,作用在粒子上的力 F 等效於在時間間隔 Δ t上的恆定加速度 a,可以透過簡單的方式將其與粒子的速度相加來模擬,然後將其新增到粒子的位置。

通俗簡單來說D3-force-layout基於一定的物理規則來定位視覺化元素(nodes and edges)。

演算法過程

D3 的力佈局使用基於物理的模擬器來定位視覺元素。

可以在元素之間設定force(力),例如:

  • elements(所有元素) 都可以配置為與其他元素相互排斥
  • elements(所有元素)可以被吸引到center(物理中也稱為重心,可理解為中心), 通俗來說就是所有節點的平均位置靠近。
  • linked elements (連結元素) 可以設定為fixed distance(固定距離)
  • 利用collision detection(碰撞檢測), elements(元素)可以配置為避免相互交叉.

透過配置, force-layout從而幫助我們以特定方式來進行定位元素。

本文主要講如何使用D3-force-layout以及如何使用它來建立**網路視覺化(network visualisations),叢集(clusters)**展示。

請看下面這個force-layout的例子:假設我們有許多circle, 且這些circles分為3類(透過category欄位區分) ,然後我們新增forces

  • circles之間相互吸引(將circles聚集在一起)
  • 碰撞檢測(避免circles重疊)
  • circles被三個重心之一吸引(category欄位 :ABC

image.png

在codepen中嘗試編輯上面示例

force-layout比其他佈局演算法需要更多的計算量,因為演算法內部的實現是迭代式的。逐步達到最優效果。

演算法結論/效果

force simulation

一般來說,設定力模擬有 4 個步驟:

  • 建立物件陣列(nodes and edges)
  • 呼叫forceSimulation,傳入物件陣列 (nodes)
  • 新增一個或多個force functions(力函式)(例如forceManyBody, forceCenter
  • 設定回撥函式, each tick (每次迭代)後更新元素的位置。

看個簡單的例子:

let width = 300, height = 300  
let nodes = \[{}, {}, {}, {}, {}\]  
​  
let simulation = d3.forceSimulation(nodes)  
  .force('charge', d3.forceManyBody())  
  .force('center', d3.forceCenter(width / 2, height / 2))  
  .on('tick', ticked);

我們在這裡建立了一個由 5 個物件組成的簡單陣列,並新增了兩個力函式forceManyBodyforceCenter。(其中第一個使元素相互排斥,而第二個將元素吸引到中心點。)

每次模擬迭代時,ticked都會呼叫該函式。此函式將nodes陣列連線到circle元素並更新它們的位置:

function ticked() {  
  var u = d3.select('svg')  
    .selectAll('circle')  
    .data(nodes)  
    .join('circle')  
    .attr('r', 5)  
    .attr('cx', function(d) {  
      return d.x  
    })  
    .attr('cy', function(d) {  
      return d.y  
    });  
}


image.png

在codepen中嘗試編輯上面示例

force simulations(力模擬) 的強大和靈活集中在 force functions(力函式) 上,這些函式可以調整元素的位置和速度,以實現吸引、排斥和碰撞檢測等多種效果。

D3 內建了很多有用的函式:

  • forceCenter(用於設定系統的重心)
  • forceManyBody(用於使元素相互吸引或排斥)
  • forceCollide(用於防止元素重疊)
  • forceXforceY(用於將元素吸引到給定點)
  • forceLink(用於在連線元素之間建立固定距離)

透過.force()將**force functions (力函式)**新增到模擬中,第一個引數是定義的 id,第二個引數是force functions(力函式)

simulation.force('charge', d3.forceManyBody())

下面我們展開看一下內建的force functions(力函式)

forceCenter

forceCenter對於將元素作為一個整體圍繞centering居中是有用的。如果不設定預設座標是 [0, 0]。

可以直接設定位置[x,y]初始化:

d3.forceCenter(100, 100)

或使用配置功能.x().y()

d3.forceCenter().x(100).y(100)

然後使用以下方法將其新增到模擬中:

simulation.force('center', d3.forceCenter(100, 100))

forceManyBody

forceManyBody使所有元素相互吸引或排斥。可以設定吸引或排斥的強度,.strength()其中正值導致元素相互吸引,而負值將導致元素相互排斥。預設值為-30

simulation.force('charge', d3.forceManyBody().strength(-20))

image.png

在建立網路圖時,通常配置元素相互排斥。但對於元素聚集在一起的需求,則需要配置元素的吸引(引力)。

在codepen中嘗試編輯上面示例

forceCollide

forceCollide用於避免元素(此處是circle)重疊,並且可以將circle“聚集”在一起。

元素的半徑r是透過將訪問器函式.radius方法來傳遞給forceCollide'的,。此函式的第一個引數d是用來data join,可以從中得到半徑r

例如:

let numNodes = 100  
let nodes = d3.range(numNodes).map(function(d) {  
  return {radius: Math.random() \* 25}  
})  
​  
let simulation = d3.forceSimulation(nodes)  
  .force('charge', d3.forceManyBody().strength(5))  
  .force('center', d3.forceCenter(width / 2, height / 2))  
  .force('collision', d3.forceCollide().radius(function(d) {  
    return d.radius  
  }))

image.png

在codepen中嘗試編輯上面示例

forceManyBody將所有節點聚集到一起,並將節點保持在容器的中心 ,forceCollide避免節點重疊。

forceX 和 forceY

forceX和forceY設定元素吸引到指定的位置。我們可以對所有元素使用一箇中心,也可以為每個元素的基礎上新增。同時使用 .strength() 配置引力,進行配合。

例如,假設您有許多元素,每個元素都有一個category具有 value01的屬性2。您可以新增一個forceX力函式基於元素的category分別將元素吸引到 x 座標100,300或500的地方:

let xCenter = \[100, 300, 500\];

let simulation = d3.forceSimulation(nodes)
  .force('charge', d3.forceManyBody().strength(5))
  .force('x', d3.forceX().x(function(d) {
    return xCenter\[d.category\];
  }))
  .force('collision', d3.forceCollide().radius(function(d) {
    return d.radius;
  }));

image.png

在codepen中嘗試編輯上面示例

forceManyBody將所有節點聚集到一起,然後forceX將節點吸引到特定的 x 座標。forceCollide避免(組織)節點相交。

如果我們的資料具有相關座標資訊,當然也可以同時使用forceXforceY去定位元素。

...
.force('x', d3.forceX().x(function(d) {
    return d.x;
  }))
.force('y', d3.forceY().y(function(d) {
   return d.y;
}))
...

forceLink

forceLink將連結的元素移動到一個固定的距離(distance)。它需要links(一組連結)來指定將哪些元素連結在一起。每個連結物件指定一個source(源)元素和target(目標)元素,其中值是元素的標識id (如果沒有id可以用陣列的索引):

let links = d3.range(nodes.length - 1).map(function(i) {
    return {
        source: Math.floor(Math.sqrt(i)),
        target: i + 1,
    };
});
let links = \[
  {source: 0, target: 1},
  ...
]

然後,使用.links()方法將links(連結陣列)傳遞給forceLink函式:

let simulation = d3.forceSimulation(nodes)
  .force('charge', d3.forceManyBody().strength(-100))
  .force('center', d3.forceCenter(width / 2, height / 2))
  .force('link', d3.forceLink().links(links));

image.png

在codepen中嘗試編輯上面示例

forceManyBody將節點分開,forceCenter使節點與畫布容器保持居中,forceLink保持連結節點之間的固定距離。

演算法聚類group webgl渲染效果

d3.forceSimulation(nodes)
    .force("charge", d3.forceManyBody())
    // defaults strength: Math.min(count(link.source), count(link.target));
    // default distance 30
    .force("link", d3.forceLink(layout_links))
    .force('x', d3.forceX().x(function(d) {  // 給定座標進行節點聚類 group分組
      return groups.indexOf(d.group) * 1200;
    }))
    .force("y", d3.forceY().y(function(d){
      return Math.floor(groups.indexOf(d.group) / 3) * 100;
    }))
    .stop();

image.png

最後

本文只是針對一個庫的使用介紹,無合適時機引申物理模型相關知識體系。下篇打算針對於d3-force原始碼:力模型(Force Model), 多種力型別的實現, 多體系統求解[Barnes-Hut 演算法] 迭代/約束 事件處理等方面進行深入探討/交流。

感謝您的閱讀,有問題隨時請聯絡溝通。

相關文章