D3.js實現力向導圖的繪制教程詳解

力向導圖是繪圖的一種算法,實現瞭用以模擬粒子物理運動的 velocity Verlet 數值積分器。仿真思路如下: 它假設任意單位時間步長 Δt = 1,所有的粒子的單位質量常量 m = 1。作用在每個粒子上的合力 F 相當於在單位時間 Δt 內的恒定加速度 a。並且可以簡單的通過為每個粒子添加速度並計算粒子的位置來模擬仿真。

在二維或三維空間裡配置節點。節點之間用線連接,稱為連線。各連線的長度幾乎相等,且盡可能不相交。節點和連線都被施加瞭力的作用。力的大小是根據節點和連線的相對位置計算的。根據力的作用來計算節點和連線的運動軌跡,並不斷降低它們的能量,最終達到一種能量很低的穩定狀態

D3 Force 是一種模擬物理運動原理的繪圖算法,一開始給所有節點設置任意的初始值配置,接著根據配置的屬性,讓每個節點按屬性去運動——這就是每個節點之間的力。

力又分為斥力引力,每個節點之間的力就是斥力,而整個圖形又存在引力,就像人可以在地面上起跳,但是由於地心引力,我們不會跳的很高,最終也都會落回地面。節點在各種力的交互作用下,碰撞聚攏,逐漸收攏到一個穩定的位置,可以通過alpha屬性去設置整個過程的速度,還可以設置摩擦力velocityDecay去調整速度。

五種力

D3 一共給我們提供瞭五種力點擊查看Demo:

向心力

  • d3.forceCenter([x, y]) – 創建一個中心作用力.
  • center.x – 設置中心作用力的 x -坐標.
  • center.y – 設置中心作用力的 y -坐標.

向心力可以將所有的節點的中心統一整體的向指定的位置 ⟨x,y⟩ 移動。這種力強制修改每個節點的位置,但是不會修改速度,因為修改速度會造成節點在期望的位置附近抖動。這種力可以輔助保持所有的節點在視口中心。

碰撞力

節點之間相互碰撞的力,這個斥力會防止節點重合,可以使用 strength設置斥力的強弱。

  • d3.forceCollide – 創建一個圓形區域的碰撞檢測力模型.
  • collide.radius – 設置碰撞半徑.
  • collide.strength – 設置碰撞檢測力模型的強度.
  • collide.iterations – 設置迭代次數.

連接力(彈簧力)

使用d3.forceLink 將兩個節點添加連線到一起之後,就可以設置連接力瞭,它會根據兩節點之間的距離,拉近或推遠節點,力的強弱和兩節點間的距離成正比,就像彈簧一樣,所以也叫彈簧力。

  • d3.forceLink – 創建一個 link(彈簧) 作用力.
  • link.links – 設置彈簧作用力的邊.
  • link.id – 設置邊元素中節點的查找方式是索引還是 id 字符串.
  • link.distance – 設置 link 的距離.
  • link.strength – 設置 link 的強度.
  • link.iterations – 設置迭代次數.

電荷力

模擬所有節點之間的相互作用力,如果是正值,則相互吸引,如果是負值,則相互排斥。這樣就可以模擬電荷的吸引力,力的強弱也和節點間的距離有關。

  • d3.forceManyBody – 創建一個電荷作用力模型.
  • manyBody.strength – 設置電荷力模型的強度.
  • manyBody.theta – 設置 Barnes–Hut 算法的精度.
  • manyBody.distanceMin – 限制節點之間的最小距離.
  • manyBody.distanceMax – 限制節點之間的最大距離.

徑向力

設定一個圓,這樣所有的節點都會有一個指向圓心的力,這樣每個節點都會集中到圓上。

  • d3.forceRadial – 創建一個環形佈局的作用力.
  • radial.strength – 設置力強度.
  • radial.radius – 設置目標半徑.
  • radial.x – 設置環形作用力的目標中心 x -坐標.
  • radial.y – 設置環形作用力的目標中心 y -坐標.

五種力是可以疊加使用的!

力向導圖

在創建力學導圖時,我們需要先創建一個新的力學模型d3.forceSimulation,並指定節點nodes

1.d3.forceSimulation – 創建一個新的力學仿真.

2.simulation.restart – 重新啟動仿真的定時器.

3.simulation.stop – 停止仿真的定時器.

4.simulation.tick – 進行一步仿真模擬.

5.simulation.nodes – 設置仿真的節點.

每個 node 必須是一個對象類型,下面的幾個屬性將會被仿真系統添加:

  • index – 節點在 nodes 數組中的索引
  • x – 節點當前的 x-坐標
  • y – 節點當前的 y-坐標
  • vx – 節點當前的 x-方向速度
  • vy – 節點當前的 y-方向速度

5.simulation.alpha – 設置當前的 alpha 值.

6.simulation.alphaMin – 設置最小 alpha 閾值.

7.simulation.alphaDecay – 設置 alpha 衰減率.

為0的話,永遠都不會停

8.simulation.alphaTarget – 設置目標 alpha 值.

9.simulation.velocityDecay – 設置速度衰減率.

10.simulation.force – 添加或移除一個力模型.

11.simulation.find – 根據指定的位置找出最近的節點.

12.simulation.on – 添加或移除事件監聽器.

  • tick – 仿真內部定時器每次 tick 之後。
  • end – 當 alpha < alphaMin 時仿真內部定時器停止。
const simulation = d3.forceSimulation(nodes) 
// 連接力
.force('link', d3.forceLink()) 
// 在 y軸方向上施加一個力 
.force('y', d3.forceY().strength(0.025)) 
// 電荷力 
.force('charge', d3.forceManyBody()) 
// 碰撞力
.force('collision', d3.forceCollide().radius(d => 4)) 
//  向心力
.force('center', d3.forceCenter(width / 2, height / 2))

接下來我們繪制一下文章開頭的那張關系圖點擊查看Demo:

創建模擬數據

const nodes = [
    {name: "張三"}
    ...
]

const links = [
    { source: 0, target: 1, relation: "關系1"}
    ...
]

創建力模型

let simulation = d3.forceSimulation(nodes)
    .force("link", d3.forceLink(links).distance(100)) // 連接力

繪制節點和連線

// 畫線
function drawLine() {
    let lines = svg.append("g")
        .selectAll(".force-line")
        .data(links)
        .enter()
        .append("line")
        .attr("class", "line")
        .attr("stroke", "#999")
        .attr("stroke-width", "1px");
    return lines;
}
let lines = drawLine();

// 畫節點節點盒子

function drawCircle() {
    let nodeGroups = svg.append("g")
        .attr("class", "nodes-box")
        .selectAll(".force-node")
        .data(nodes)
        .enter()
        .append("g")
        .attr("class", "force-circle")

    nodeGroups.append("circle")
        .attr("class", "force-circle")
        .attr("r", 20)
        .style("fill",(d, i) => color(i));

    nodeGroups.append("text")
        .attr("class", "force-text")
        .attr("dy", ".33em")
        .attr("font-size", "12px")
        .attr("text-anchor", "middle")
        .style("fill", "#eee")
        .text(d => d.name);
    return nodeGroups;
}
let nodesCircle = drawCircle();

到這一步呀,就隻能看到畫佈的左上角,原點位置 有circle圖形,因為力學模型,是動態計算節點和連線的位置,所以我們需要動態的去更新它們的位置<x, y>,此時我們需要監聽的就是tick.

監聽 tick

let simulation = d3.forceSimulation(nodes)
    .force("link", d3.forceLink(links).distance(100));
    .on("tick", ticked);
    
function ticked() 
    lines
        .attr("x1", (d) => d.source.x)
        .attr("y1", (d) => d.source.y)
        .attr("x2", (d) => d.target.x)
        .attr("y2", (d) => d.target.y);

    // 這裡就不適合 去改變circle的圓心位置瞭,因為有文字存在,改變整個circleGroup的transform
    nodesCircle.attr("transform", function (d) {
        // d.fx=d.x;d.fy=d.y; 固定位置
        return "translate(" + d.x + ", " + d.y + ")";
    });
}

我們可以添加一個向心力,讓整個圖形出現在畫佈中心。

添加向心力

let simulation = d3.forceSimulation(nodes)
    .force("center", d3.forceCenter(width / 2, height / 2)) // 用指定的x坐標和y坐標創建一個居中力
    .force("link", d3.forceLink(links).distance(100)) //
    .on("tick", ticked);

到這裡,我們可以發現,節點出現瞭重合的現象,我們可以給節點添加一個碰撞力,讓它們分開。

添加碰撞力

let simulation = d3.forceSimulation(nodes)
    .force("charge", d3.forceManyBody().strength(-200)) // 電荷力 相互之間的作用力
    .force("center", d3.forceCenter(width / 2, height / 2)) // 用指定的x坐標和y坐標創建一個居中力
    .force("link", d3.forceLink(links).distance(100)) //
    .on("tick", ticked);

添加拖拽效果

d3.drag後面再詳細介紹,本章就不深入瞭。

let nodeGroups = svg.append("g")
    ...
    .call(
        d3.drag().on("start", started).on("drag", dragged).on("end", ended)
    );
// 拖拽
function started(event) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    event.subject.fx = event.subject.x;
    event.subject.fy = event.subject.y;
}

function dragged(event) {
    event.subject.fx = event.x;
    event.subject.fy = event.y;
}

function ended(event) {
    if (!event.active) simulation.alphaTarget(0);
    event.subject.fx = null;
    event.subject.fy = null;
}

整個拖拽的過程中:

  • 連接力:拖拽任意節點,其餘節點都會同向運動
  • 碰撞力:在拖拽的過程中,各節點之間不會重合
  • 向心力:拖拽不能拖到任意位置,由於向心力的存在,最後還是會向中心靠攏

以上就是D3.js實現力向導圖的繪制教程詳解的詳細內容,更多關於D3.js力向導圖的資料請關註WalkonNet其它相關文章!

推薦閱讀: