前言|force佈局
筆者在fastVG產品圖視覺化佈局中force佈局採用D3-force-layout,因此介紹下該佈局的一些演算法邏輯和基礎使用規則。
本文預期收穫:
- 對於佈局演算法有更深入的瞭解。
- 在使用d3 & d3-force的時候 有調參規則的經驗。
- 可結合其他渲染庫進行獨立使用。
演算法邏輯簡介
演算法說明
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欄位 :
A
,B
或C
)
在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 個物件組成的簡單陣列,並新增了兩個力函式forceManyBody
和forceCenter
。(其中第一個使元素相互排斥,而第二個將元素吸引到中心點。)
每次模擬迭代時,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
});
}
在codepen中嘗試編輯上面示例
force simulations(力模擬) 的強大和靈活集中在 force functions(力函式) 上,這些函式可以調整元素的位置和速度,以實現吸引、排斥和碰撞檢測等多種效果。
D3 內建了很多有用的函式:
forceCenter
(用於設定系統的重心)forceManyBody
(用於使元素相互吸引或排斥)forceCollide
(用於防止元素重疊)forceX
和forceY
(用於將元素吸引到給定點)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))
在建立網路圖時,通常配置元素相互排斥。但對於元素聚集在一起的需求,則需要配置元素的吸引(引力)。
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
}))
在codepen中嘗試編輯上面示例
forceManyBody
將所有節點聚集到一起,並將節點保持在容器的中心 ,forceCollide
避免節點重疊。
forceX 和 forceY
forceX和forceY設定元素吸引到指定的位置。我們可以對所有元素使用一箇中心,也可以為每個元素的基礎上新增。同時使用 .strength()
配置引力,進行配合。
例如,假設您有許多元素,每個元素都有一個category
具有 value0
或1
的屬性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;
}));
forceManyBody
將所有節點聚集到一起,然後forceX
將節點吸引到特定的 x 座標。forceCollide
避免(組織)節點相交。
如果我們的資料具有相關座標資訊,當然也可以同時使用forceX
或forceY
去定位元素。
...
.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));
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();
最後
本文只是針對一個庫的使用介紹,無合適時機引申物理模型相關知識體系。下篇打算針對於d3-force原始碼:力模型(Force Model), 多種力型別的實現, 多體系統求解[Barnes-Hut 演算法] 迭代/約束 事件處理等方面進行深入探討/交流。
感謝您的閱讀,有問題隨時請聯絡溝通。