說到力導向可能不少小夥伴都只是會使用,不知道其中的實現原理,今天,咱們一塊兒來本身實現一套力導向算法,而後作一些技術相關的延伸。發散下思惟。javascript
根據百科的介紹:力導向算法是指經過對每一個節點的計算,算出引力和排斥力綜合的協力,再由此協力來移動節點的位置。java
經過力導向算法計算位置,繪製出對應的力導向圖,這樣的分配是最佳位置的分佈圖。echarts和d3js裏面也有力導向佈局圖。首先來看一下力導向圖。
力導向算法是根據天然界中電子直接互相做用的原理來實現的,天然界中。兩個電子靠的太近會產生斥力,隔的太遠會產生引力,這樣保持一個平衡狀態,最終達到維持物體的形態的目的,這裏就涉及到了一個庫侖定律(百科:是靜止點電荷相互做用力的規律。1785年法國科學家C,-A.de庫倫由實驗得出,真空中兩個靜止的點電荷之間的相互做用力同它們的電荷量的乘積成正比,與它們的距離的二次方成反比,做用力的方向在它們的連線上,同名電荷相斥,異名電荷相吸),這裏就涉及到一個庫倫公式。,若是假設電子q=1,那麼 F=k/(r^2) * e(e爲從q1到q2方向的矢徑;k爲庫侖常數(靜電力常量))。那這裏的F能夠假設爲某個方向的瞬間速度,e正好表明正負方向,有的力導向圖算法中加入了彈簧力,讓e有了緩動效果,可是,這裏咱們就不加入彈簧力了,主要是研究這個庫倫公式公式,若是進一步簡化,咱們能夠把F看作成一次函數的變化,這樣儘量的簡化咱們的代碼。複雜的問題簡單化,再慢慢深刻。最終理解其原理。node
若是要用代碼去實現簡化後的力導向圖的佈局,咱們須要幾個步驟。算法
重複執行4操做N次,獲得想要的力導向圖形。在執行力算法的時候,這裏咱們把庫倫公式簡化成了一次函數,因此,要麼減一個數,要麼加一個數去改變點的座標。理解起來就很容易了,固然,實際上咱們應該加上電子做用力(庫倫公式)和彈簧力(胡克定律),讓力導向的效果更接近天然界的做用結果。canvas
原理圖:瀏覽器
設置數據
/** * @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);
最終生成的效果:echarts
這裏,咱們設置了最大的位移maxOffset,以及最小的位移minOffset。若是沒有達到平衡點(兩點之間距離爲maxInterval)的時候,會互相靠近或者遠離,距離變化咱們來的比較暴力,固然,實際上咱們應該加上電子做用力(庫倫公式)和彈簧力(胡克定律),讓力導向的效果更接近天然界的做用結果。dom
知識延伸一下:這裏咱們是對nodes兩兩比較。若是咱們只對兩個連接點進行兩兩比較,又會是這樣的結果呢,改動以下?函數
獲得圖形:佈局
這個代碼只是爲了讓你們入門學習使用,真正的力導向算法比這個複雜的多,還能夠作不少優化,好比最新版本的d3js裏面的力導向算法就用四叉樹算法對其進行了優化,拋磚引玉到此爲止,歡迎你們指正!