前言
說到力導向可能很多小伙伴都只是會使用,不知道其中的實現原理,今天,我們一起來自己實現一套力導向算法,然后做一些技術相關的延伸。發散下思維。
什么是力導向算法?
根據百科的介紹:力導向算法是指通過對每個節點的計算,算出引力和排斥力綜合的合力,再由此合力來移動節點的位置。
通過力導向算法計算位置,繪制出對應的力導向圖,這樣的分配是最佳位置的分布圖。echarts和d3js里面也有力導向布局圖。首先來看一下力導向圖。

力導向算法是根據自然界中電子直接互相作用的原理來實現的,自然界中。兩個電子靠的太近會產生斥力,隔的太遠會產生引力,這樣保持一個平衡狀態,最終達到維持物體的形態的目的,這里就涉及到了一個庫侖定律(百科:是靜止點電荷相互作用力的規律。1785年法國科學家C,-A.de庫倫由實驗得出,真空中兩個靜止的點電荷之間的相互作用力同它們的電荷量的乘積成正比,與它們的距離的二次方成反比,作用力的方向在它們的連線上,同名電荷相斥,異名電荷相吸),這里就涉及到一個庫倫公式。
,如果假設電子q=1,那么 F=k/(r^2) * e(e為從q1到q2方向的矢徑;k為庫侖常數(靜電力常量))。那這里的F可以假設為某個方向的瞬間速度,e正好代表正負方向,有的力導向圖算法中加入了彈簧力,讓e有了緩動效果,但是,這里我們就不加入彈簧力了,主要是研究這個庫倫公式公式,如果進一步簡化,我們可以把F看做成一次函數的變化,這樣盡可能的簡化我們的代碼。復雜的問題簡單化,再慢慢深入。最終理解其原理。


實現邏輯
如果要用代碼去實現簡化后的力導向圖的布局,我們需要幾個步驟。
- 設置點數據nodes, 鏈接數據links。
- 對點進行隨機定位。
- 渲染視圖
- 執行力算法計算位置,渲染視圖
重復執行4操作N次,得到想要的力導向圖形。在執行力算法的時候,這里我們把庫倫公式簡化成了一次函數,所以,要么減一個數,要么加一個數去改變點的坐標。理解起來就很容易了,當然,實際上我們應該加上電子作用力(庫倫公式)和彈簧力(胡克定律),讓力導向的效果更接近自然界的作用結果。
代碼實現
原理圖:
設置數據
/**
* @desc 模擬數據
*/
function getData(num, exLink) {
const data = { nodes: new Array(num).fill(1), links: [] };
data.nodes = data.nodes.map((d, id) => {
return {
id,
name: d,
position: [0, 0],
childs: []
}
});
data.nodes.forEach((d, i) => {
// 都和0相連
if (d.id !== 0) {
data.links.push({
source: 0,
target: d.id,
sourceNode: data.nodes[0],
targetNode: d
});
}
});
// 隨機抽取其中2個相連
const randomLink = () => {
data.nodes.sort(() => 0.5 - Math.random());
data.links.push({
source: data.nodes[0].id,
target: data.nodes[1].id,
sourceNode: data.nodes[0],
targetNode: data.nodes[1]
});
}
for (let i = 0; i < exLink; i++) {
randomLink();
};
// 添加數據。childs
const obj = {};
data.nodes.forEach(d => {
if (!obj[d.id]) {
obj[d.id] = d;
}
});
data.links.forEach(d => {
obj[d.source].childs.push(d.targetNode);
obj[d.target].childs.push(d.sourceNode);
});
return data;
}
隨機定位
/**
* @desc 獲取隨機數
*/
function getRandom(min, max) {
return Math.floor(min + Math.random() * (max - min));
}
/**
* @desc 打亂順序定位
* @param data 數據
* @param size 畫布大小
*/
function randomPosition(data, size) {
const { nodes, links } = data;
nodes.forEach(d => {
let x = getRandom(0, size);
let y = getRandom(0, size);
d.position = [x, y];
});
}
渲染視圖
/**
* @desc 繪制
* @param ctx canvas上下文
* @param data 數據
* @param size 畫布大小
*/
function render(ctx, data, size) {
ctx.clearRect(0, 0, size, size); //清空所有的內容
const box = 20;
ctx.fillStyle = '#FF0000';
data.links.forEach(d => {
let { sourceNode, targetNode } = d;
let [x1, y1] = sourceNode.position;
let [x2, y2] = targetNode.position;
ctx.beginPath(); //新建一條path
ctx.moveTo(x1, y1); //把畫筆移動到指定的坐標
ctx.lineTo(x2, y2); //繪制一條從當前位置到指定坐標(200, 50)的直線.
ctx.closePath();
ctx.stroke(); //繪制路徑。
});
data.nodes.forEach(d => {
let [x, y] = d.position;
ctx.fillText(d.id, x, y + box);
ctx.fillRect(x - box / 2, y - box / 2, box, box);
});
}
模擬作用力計算位置
/**
* @desc 力算法
*/
function force(data, ctx, size) {
const { nodes, links } = data;
// 需要參數
const maxInterval = 300; // 平衡位置間距
const maxOffset = 10; // 最大變化位移
const minOffset = 0; // 最小變化位移
const count = 100; // force次數
const attenuation = 40; // 力衰減
const doforce = () => {
// 計算開始
nodes.forEach(d => {
let [x1, y1] = d.position;
nodes.forEach(e => {
if (d.id === e.id) {
return;
}
let [x2, y2] = e.position;
// 計算兩點距離
let interval = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
// console.log('interval', d.id + '-' + e.id, interval);
// 力衰減變量
let forceOffset = 0;
let x3, y3;
// 如果大於平橫間距,靠攏,如果小於平衡間距,排斥。這里計算第三點的坐標用到了相似三角形原理
if (interval > maxInterval) {
forceOffset = (interval - maxInterval) / attenuation; // 力衰減
forceOffset = forceOffset > maxOffset ? maxOffset : forceOffset;
forceOffset = forceOffset < minOffset ? minOffset : forceOffset;
forceOffset += e.childs.length / attenuation;
// console.log('如果大於平橫間距,靠攏', interval, d.id + '-' + e.id, ~~forceOffset);
let k = forceOffset / interval;
x3 = k * (x1 - x2) + x2;
y3 = k * (y1 - y2) + y2;
} else if (interval < maxInterval && interval > 0) { // 如果小於平橫間距,分開
forceOffset = (maxInterval - interval) / attenuation; // 力衰減
forceOffset = forceOffset > maxOffset ? maxOffset : forceOffset;
forceOffset = forceOffset < minOffset ? minOffset : forceOffset;
forceOffset += e.childs.length / attenuation;
// console.log('如果小於平橫間距,分開', interval, d.id + '-' + e.id, ~~forceOffset);
let k = forceOffset / (interval + forceOffset);
x3 = (k * x1 - x2) / (k - 1);
y3 = (k * y1 - y2) / (k - 1);
} else {
x3 = x2;
y3 = y2;
}
// 邊界設置
x3 > size ? x3 -= 10 : null;
x3 < 0 ? x3 += 10 : null;
y3 > size ? y3 -= 10 : null;
y3 < 0 ? y3 += 10 : null;
e.position = [x3, y3];
});
})
}
let countForce = 0;
const forceRun = () => {
setTimeout(() => {
countForce++;
if (countForce > count) {
return;
}
doforce();
render(ctx, data, size);
forceRun();
}, 1000 / 30)
// requestAnimationFrame(forceRun);
}
forceRun();
}
main 函數
/*
<canvas class="force-map" id="forceMap" width="800" height="800">
您的瀏覽器不支持
</canvas>
*/
const size = 800;
// 1.獲取數據
const data = getData(30, 0);
// 2.隨機定位
randomPosition(data, size);
// 3.渲染
let cav = document.getElementById('forceMap');
let ctx = cav.getContext('2d');
render(ctx, data, size);
// 4.執行力算法
force(data, ctx, size);
最終生成的效果:

知識延伸
這里,我們設置了最大的位移maxOffset,以及最小的位移minOffset。如果沒有達到平衡點(兩點之間距離為maxInterval)的時候,會互相靠近或者遠離,距離變化我們來的比較暴力,當然,實際上我們應該加上電子作用力(庫倫公式)和彈簧力(胡克定律),讓力導向的效果更接近自然界的作用結果。
知識延伸一下:這里我們是對nodes兩兩比較。如果我們只對兩個鏈接點進行兩兩比較,又會是這樣的結果呢,改動如下?

得到圖形:

這個代碼只是為了讓大家入門學習使用,真正的力導向算法比這個復雜的多,還可以做很多優化,比如最新版本的d3js里面的力導向算法就用四叉樹算法對其進行了優化,拋磚引玉到此為止,歡迎大家指正!
本文轉載於:猿2048➩https://www.mk2048.com/blog/blog.php?id=h0c01c21c2j
