力導向演算法從入門到放棄!
前言
說到力導向可能很多小夥伴都只是會使用,不知道其中的實現原理,今天,我們一起來自己實現一套力導向演算法,然後做一些技術相關的延伸。發散下思維。
什麼是力導向演算法?
根據百科的介紹:力導向演算法是指通過對每個節點的計算,算出引力和排斥力綜合的合力,再由此合力來移動節點的位置。
通過力導向演算法計算位置,繪製出對應的力導向圖,這樣的分配是最佳位置的分佈圖。echarts和d3js裡面也有力導向佈局圖。首先來看一下力導向圖。
力導向演算法是根據自然界中電子直接互相作用的原理來實現的,自然界中。兩個電子靠的太近會產生斥力,隔的太遠會產生引力,這樣保持一個平衡狀態,最終達到維持物體的形態的目的,這裡就涉及到了一個庫侖定律(百科:是靜止點電荷相互作用力的規律。1785年法國科學家C,-A.de庫倫由實驗得出,真空中兩個靜止的點電荷之間的相互作用力同它們的電荷量的乘積成正比,與它們的距離的二次方成反比,作用力的方向在它們的連線上,同名電荷相斥,異名電荷相吸),這裡就涉及到一個庫倫公式。
實現邏輯
如果要用程式碼去實現簡化後的力導向圖的佈局,我們需要幾個步驟。
- 設定點資料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裡面的力導向演算法就用四叉樹演算法對其進行了優化,拋磚引玉到此為止,歡迎大家指正!