D3 力導向圖和 WebGL 的整合使用

D3 是目前最流行的數據可視化庫,WebGL 是目前 Web 端最快的繪製技術。因爲性能問題的侷限,將二者結合的嘗試愈來愈多(如),本文將嘗試用 D3 的力導向圖Three.jsPixiJS 結合。全文閱讀完大概 5 分鐘,由於你重點應該看代碼node

作數據可視化時,必然會考慮性能的問題。早前數據可視化都是用 Qt 等 GUI,後來逐漸遷移到了迅猛發展的瀏覽器上展現,Web 的性能問題成了大多數可視化的侷限,尤爲是在三維可視化,或數據量特別大的時候。如今主流的 Web 可視化技術爲三種:SVG、Canvas 和 WebGL,難易程度和性能以下圖:git

Web visualization tech

SVG 的優勢不少,編輯簡單,交互便捷,靈活性極高,業內成熟的可視化工具(如 d3)都是用的 SVG。可是每一個 SVG 都是一個 DOM 元素,隨着它的數量上來以後,交互開始慢的難以忍受。這是由於每當修改一個 DOM 對象,只要這個對象在文檔裏,接着在瀏覽器裏就會發生兩個動做,一個叫 Reflow(重排,就是從新排版),另外一個叫 Repaint(重繪,就是從新渲染頁面)。這兩個動做不必定都會發生,但若是被修改的 DOM 當前可見的話,那麼就會先重排,後重繪。繪製性能上 canvas 和 SVG(DOM 元素)應該差很少,但前者能夠省掉重排過程,所以性能更高。然而,WebGL 的性能更勝一籌,由於 WebGL 使用 GPU 加速渲染,GPU 在大規模計算方面有絕對優點(圖像處理、深度學習都在用,顯卡已經賣瘋了)。例子:用 WebGL 繪製 200000 個點的動畫(http://rickyreusser.com/smoot...github

WebGL 雖然威力無窮,可是寫起來比較痛苦,畫個三角形大體要 100 行代碼。因此不少人對 WebGL 進行了封裝。上面圖中提到的兩個 Three.jsPixiJS 是目前最流行的兩款 WebGL 庫,固然還有新興的 regl 在今年的 OpenVis 上大放異彩。本文嘗試用前二者和 d3-force 結合(項目代碼在此),後面若是有時間的話,我會把使用 regl 和原生 WebGL 的例子也補充進去(我知道這是個 flag)。web

正文

首先咱們要知道什麼是力導向圖和如何使用 d3-force。d3 4.0 以後,做者將其模塊化,force 這個模塊是基於 velocity Verlet 實現了物理粒子之間的做用力的仿真,經常使用於網絡或關係結構數據。即你把網絡中的節點想象成一個個粒子,它們之間互相有做用力,因此不停的拉扯,直到趨於一個穩定狀態,具體能夠看我 demo 中可視化出來的樣子。canvas

Demo 效果圖

仔細看 demo 中的源碼能夠發現,用 three.js 和用 pixi.js 實現起來很是相似,其中有關力導向圖的關鍵代碼是下面幾句:數組

const simulation = d3.forceSimulation() // 建立一個做用力的仿真,但此時還沒啓動
  .force('link', d3.forceLink().id((d) => d.id)) // 爲邊之間添加 Link 型做用力
  .force('charge', d3.forceManyBody()) // 指定節點間的做用力類型爲 Many-Body 型
  .force('center', d3.forceCenter(width / 2, height / 2)) // Centering 做用力指定佈局圍繞的中心

d3-force 提供了五種做用力,分別是 Centering、Collision、Links、Many-Body、Positioning。此時咱們已經建立好帶有各類力的仿真器了,接下來須要啓動它:瀏覽器

simulation
  .nodes(data.nodes) // 根據 data.nodes 數組來計算點之間的做用力,至關於不停計算節點的 xy 座標
  .on('tick', ticked) // 每次 tick 調用 ticked

simulation.force('link')
  .links(data.links) // 根據 data.links 數據計算邊之間的做用力

至此一個力導向圖的仿真就開始了,那麼怎麼把這些節點和邊顯示出來呢?讓咱們繼續看源碼,以 three.js 爲例:網絡

const scene = new THREE.Scene()
const camera = new THREE.OrthographicCamera(0, width, height, 0, 1, 1000)
const renderer = new THREE.WebGLRenderer({alpha: true})
renderer.setSize(width, height)
container.appendChild(renderer.domElement) // container 這裏是 document.body

在 Three.js 中展現場景須要具有三要素:場景、照相機、渲染器。照相機就至關於咱們的眼睛,它對着渲染好的場景就至關於把場景成像到了相機中,這裏的照相機咱們用的是平行投影相機,渲染器咱們使用的是 WebGL 渲染器。設置好渲染器的大小,把它添加到頁面的元素上,至關於添加了一個 <canvas> 元素。接下來,咱們生成每一個節點和邊的樣子:app

data.nodes.forEach((node) => {
  node.geometry = new THREE.CircleBufferGeometry(5, 32)
  node.material = new THREE.MeshBasicMaterial({ color: colour(node.id) })
  node.circle = new THREE.Mesh(node.geometry, node.material)
  scene.add(node.circle)
})

data.links.forEach((link) => {
  link.material = new THREE.LineBasicMaterial({ color: 0xAAAAAA })
  link.geometry = new THREE.Geometry()
  link.line = new THREE.Line(link.geometry, link.material)
  scene.add(link.line)
})

套路都同樣,都是先建一個幾何體,而後設置材質的樣式,添加到場景中就行了。接下來只要在剛纔提到的 ticked 這個回調函數中把節點和邊的座標更新一下就行了:dom

function ticked () {
  data.nodes.forEach((node) => {
    const { x, y, circle } = node
    circle.position.set(x, y, 0)
  })

  data.links.forEach((link) => {
    const { source, target, line } = link
    line.geometry.verticesNeedUpdate = true
    line.geometry.vertices[0] = new THREE.Vector3(source.x, source.y, -1)
    line.geometry.vertices[1] = new THREE.Vector3(target.x, target.y, -1)
  })

  render(scene, camera)
}

是否是比想象的簡單多了?若是以上有什麼地方看不懂,說明你可能對 Three.js 不是很瞭解,不過不要緊,它的文檔寫的很好,入門很快。但願這篇文章能給你帶來一些幫助,作了點微小的貢獻,很慚愧 :)


本做品採用知識共享 署名-非商業性使用-禁止演繹 4.0 國際 許可協議進行許可。

相關文章
相關標籤/搜索