本文靈感來源於Mike Bostock 的一個 demo 頁面javascript
原 demo 基於 D3.js v3 開發, 筆者將其使用 D3.js v5 進行重寫, 並改成使用 ES6 語法.css
源碼: github前端
在線演示 : demojava
能夠看到, 上圖左上角爲圖例, 中間爲各個手機公司之間的專利關係圖.node
圖例中有三種線段:git
下面讓咱們看看如何一步步實現上圖的效果.github
[
{ source: 'Microsoft', target: 'Amazon', type: 'licensing' },
{ source: 'Microsoft', target: 'HTC', type: 'licensing' },
{ source: 'Samsung', target: 'Apple', type: 'suit' },
{ source: 'Motorola', target: 'Apple', type: 'suit' },
{ source: 'Nokia', target: 'Apple', type: 'resolved' },
{ source: 'HTC', target: 'Apple', type: 'suit' },
{ source: 'Kodak', target: 'Apple', type: 'suit' },
{ source: 'Microsoft', target: 'Barnes & Noble', type: 'suit' },
{ source: 'Microsoft', target: 'Foxconn', type: 'suit' },
...
]
能夠看到, 每一條數據都是由如下幾部分組成:數組
source
: 訴訟方的公司名稱target
: 被訴訟方的公司名稱type
: 當前訴訟狀態須要注意的是: 有一些公司 (如 Apple, Microsoft ) 同時參與了多起訴訟案件, 但咱們在數據可視化時只會爲每個公司分配一個節點, 而後用連線表示各個公司之間的關係.前端框架
數據可視化最重要的就是數據和圖像之間的映射關係, 本例中咱們的可視化的邏輯爲:網絡
公司 ==> 圓形節點
訴訟關係 ==> 連線
要實現能夠拖動, 自動佈局的網絡圖, 本 demo 用到了 D3.js 中的 d3-force 和 d3-drag , 固然還有最基礎的 d3-selection.
(爲了方便搭建用戶界面, 使用了 Vue 做爲前端框架. 但 Vue 並不對數據可視化邏輯產生影響, 不使用也不會對咱們的實現形成影響.)
如今讓咱們進入代碼部分, 首先咱們畫出每一個公司表明的圓形節點:
上面說到了, 原始數據中, 有部分公司屢次出如今不一樣的訴訟關係中, 而咱們要爲每一個公司畫出惟一的節點, 因此咱們要對數據進行一些處理:
initData() {
this.links = [
{ source: 'Microsoft', target: 'Amazon', type: 'licensing' },
{ source: 'Microsoft', target: 'HTC', type: 'licensing' },
{ source: 'Samsung', target: 'Apple', type: 'suit' },
{ source: 'Motorola', target: 'Apple', type: 'suit' },
{ source: 'Nokia', target: 'Apple', type: 'resolved' },
...
] // 這裏省略了一些數據
this.nodes = {}
// Compute the distinct nodes from the links.
this.links.forEach(link => {
link.source =
this.nodes[link.source] ||
(this.nodes[link.source] = { name: link.source })
link.target =
this.nodes[link.target] ||
(this.nodes[link.target] = { name: link.target })
})
console.log(this.links)
}
上面這段代碼的邏輯是, 遍歷全部的 links, 將其中的 source 和 target 做爲 key 放置到 nodes 中, 這樣咱們就獲得了不含重複節點的數據 nodes:
細心的讀者可能已經發現了, 上面的數據中有許多 x, y 的座標數據, 這些數據是從哪裏來的呢? 答案就是 d3-force, 由於咱們要實現的是模擬物理做用力的分佈圖, 因此咱們使用了 d3-force 來模擬並幫助咱們計算出每一個節點的位置, 調用方法以下:
this.force = this.d3
.forceSimulation(this.d3.values(this.nodes))
.force('charge', this.d3.forceManyBody().strength(50))
.force('collide', this.d3.forceCollide().radius(50))
.force('link', forceLink)
.force(
'center',
this.d3
.forceCenter()
.x(width / 2)
.y(height / 2)
)
.on('tick', () => {
if (this.path) {
this.path.attr('d', this.linkArc)
this.circle.attr('transform', transform)
this.text.attr('transform', transform)
}
})
這裏咱們爲 d3-force 添加了三種做用力:
.force('charge', this.d3.forceManyBody().strength(50))
爲每一個節點添加互相之間的吸引力.force('collide', this.d3.forceCollide().radius(50))
爲每一個節點添加剛體碰撞效果.force('link', forceLink)
添加節點之間的鏈接力執行上面的代碼後, d3-force 就會爲每個節點計算好座標並將其 做爲 x, y 屬性賦予每一個節點.
處理好了數據, 讓咱們將其映射到頁面上的 svg ==> circle 元素:
this.circle = this.svgNode // svgNode 爲頁面中的 svg節點 (d3.select('svg'))
.append('g')
.selectAll('circle')
.data(this.d3.values(this.nodes)) // d3.values() 將對象數據 Object{}轉換爲數組數據 Array[]
.enter()
.append('circle')
.attr('r', 10)
.style('cursor', 'pointer')
.call(this.enableDragFunc())
注意到這裏咱們在最後調用了 .call(this.enableDragFunc())
, 這點代碼是爲了實現 circle 節點的拖拽功能, 咱們在後面再進一步講解.
上面這段代碼邏輯爲: 將 nodes 數據映射爲 circle 元素, 並設置 circle 元素的屬性:
執行以上代碼後的效果:
畫出表明公司的圓形節點後, 再畫出公司名稱就很簡單了. 只須要將 x, y 座標進行必定偏移便可.
這裏咱們將公司名稱放在圓形節點的右方:
this.text = this.svgNode
.append('g')
.selectAll('text')
.data(this.d3.values(this.nodes))
.enter()
.append('text')
.attr('x', 12)
.attr('y', '.31em')
.text(d => d.name)
上面的代碼只是將 text 元素放置在了 (12 , 0 ) 的位置, 咱們在 d3-force 的每個 tick 週期中, 對其 text 進行位置的偏移, 這樣就達到了 text 元素在 circle 元素右側 12 個像素的效果:
this.force = this.d3
...
.on('tick', () => {
if (this.path) {
this.path.attr('d', this.linkArc)
this.circle.attr('transform', transform)
this.text.attr('transform', transform)
}
})
效果如圖:
接下來咱們將有訴訟關係的節點鏈接起來. 由於連線不是規則的圖形, 因此咱們使用 svg 的 path 元素來實現.
this.path = this.svgNode
.append('g')
.selectAll('path')
.data(this.links)
.enter()
.append('path')
.attr('class', function(d) {
return 'link ' + d.type
})
.attr('marker-end', function(d) {
return 'url(#' + d.type + ')'
})
咱們使用 'link ' + d.type
爲不一樣的訴訟關係連線賦予不一樣的 class, 而後經過 css 對不一樣 class 的連線添加不一樣的樣式(紅色實線, 藍色虛線, 綠色實線).
path 的 d 屬性咱們一樣在 d3-force 的 tick 週期中設置:
this.force = this.d3
...
.on('tick', () => {
if (this.path) {
this.path.attr('d', this.linkArc)
this.circle.attr('transform', transform)
this.text.attr('transform', transform)
}
})
linkArc(d) {
const dx = d.target.x - d.source.x
const dy = d.target.y - d.source.y
const dr = Math.sqrt(dx * dx + dy * dy)
return (
'M' +
d.source.x +
',' +
d.source.y +
'A' +
dr +
',' +
dr +
' 0 0,1 ' +
d.target.x +
',' +
d.target.y
)
}
這裏咱們直接用字符串拼接了一小段 svg 的指令, 效果是畫出一條圓弧曲線, 完成上面的代碼後, 咱們獲得的效果是:
如今咱們已經基本完成了預期的效果, 可是圖中缺乏圖例, 訪問者會不理解不一樣顏色的曲線分別表明着什麼含義, 因此咱們在畫面的左上角添加圖例.
圖例的實現方法大體上面步驟相同, 可是有兩個區別:
咱們構造一下圖例的數據:
const sampleData = [
{
source: { name: 'Nokia', x: xIndex, y: yIndex },
target: { name: 'Qualcomm', x: xIndex + 100, y: yIndex },
title: 'Still in suit:',
type: 'suit'
},
{
source: { name: 'Qualcomm', x: xIndex, y: yIndex + 100 },
target: { name: 'Nokia', x: xIndex + 100, y: yIndex + 100 },
title: 'Already resolved:',
type: 'resolved'
},
{
source: { name: 'Microsoft', x: xIndex, y: yIndex + 200 },
target: { name: 'Amazon', x: xIndex + 100, y: yIndex + 200 },
title: 'Locensing now:',
type: 'licensing'
}
]
const nodes = {}
sampleData.forEach((link, index) => {
nodes[link.source.name + index] = link.source
nodes[link.target.name + index] = link.target
})
按照一樣的步驟, 咱們畫出圖例:
sampleContainer
.selectAll('path')
.data(sampleData)
.enter()
.append('path')
.attr('class', d => 'link ' + d.type)
.attr('marker-end', d => 'url(#' + d.type + ')')
.attr('d', this.linkArc)
sampleContainer
.selectAll('circle')
.data(this.d3.values(nodes))
.enter()
.append('circle')
.attr('r', 10)
.style('cursor', 'pointer')
.attr('transform', d => `translate(${d.x}, ${d.y})`)
sampleContainer
.selectAll('.companyTitle')
.data(this.d3.values(nodes))
.enter()
.append('text')
.style('text-anchor', 'middle')
.attr('x', d => d.x)
.attr('y', d => d.y + 24)
.text(d => d.name)
sampleContainer
.selectAll('.title')
.data(sampleData)
.enter()
.append('text')
.attr('class', 'msg-title')
.style('text-anchor', 'end')
.attr('x', d => d.source.x - 30)
.attr('y', d => d.source.y + 5)
.text(d => d.title)
最終效果:
使用 D3.js 進行這樣的數據可視化很是簡單, 並且很是靈活. 只是在使用 d3-force 時須要多調整一下參數來達到理想的效果, 實際實現的代碼並不長, 邏輯代碼放在這個文件中: graphGenerator.js, 感興趣的讀者不妨直接看看源碼.
這裏是我關於 D3.js 、 數據可視化 博客 的github 地址, 歡迎 start & fork :tada: