打個(gè)小廣告:
如果你想獲取更多前端干貨、鵝廠工程師的前端面試指南籽腕,
歡迎關(guān)注我的個(gè)人微信公眾號(hào):
前端夜談
D3.js + Canvas 繪制組織結(jié)構(gòu)圖
使用 D3.js 默認(rèn)的 svg 渲染
D3默認(rèn)的樹狀圖畫圖使用的是svg, 比如這個(gè)來自D3作者的例子:
https://bl.ocks.org/mbostock/4339083
使用svg有好有壞:
- 好處是方便操作dom元素, 添加用戶交互
- 壞處是渲染效率不高, 在數(shù)據(jù)量較大時(shí)頁面易掉幀, 卡頓
在大多數(shù)數(shù)據(jù)量不是特別大情況下, 使用svg的好處是遠(yuǎn)遠(yuǎn)蓋過壞處的,但如果我們真的需要渲染大量的數(shù)據(jù)呢?
使用 D3.js + Canvas 渲染
source code
https://github.com/ssthouse/organization-chart
demo page
https://ssthouse.github.io/organization-chart/#/
上面的demo就是使用 D3.js + Canvas 的方式實(shí)現(xiàn)的, 在組織的層數(shù)超過300時(shí)才會(huì)出現(xiàn)明顯的卡頓, 能滿足大部分的組織結(jié)構(gòu)圖的數(shù)據(jù).
思路
- 使用 D3.js的 Three 在
虛擬Dom
中畫好圖像 - 使用Canvas繪圖 API將
虛擬Dom
中的數(shù)據(jù) (坐標(biāo) & 線的path) 等繪制到Canvas上 - 使用
Unique-color
的方式實(shí)現(xiàn)Canvas 的用戶交互 - 通過繪制一張和之前 Canvas數(shù)據(jù)相同的隱藏Canvas, 并給每一個(gè) 想要接受用戶交互的節(jié)點(diǎn)賦予唯一的顏色
- 通過監(jiān)聽Canvas點(diǎn)擊事件, 獲取點(diǎn)擊像素的顏色值來判斷點(diǎn)擊的節(jié)點(diǎn)
- 該文章中有對(duì)該思路的詳細(xì)介紹: https://medium.com/@lverspohl/how-to-turn-d3-and-canvas-into-good-friends-b7a240a32915
1.使用 D3.js的 Three 在 虛擬Dom
中畫好圖像
首先調(diào)使用D3創(chuàng)建 Tree的虛擬Dom:
this.data = this.d3.hierarchy(data)
this.treeGenerator = this.d3.tree()
.nodeSize([this.nodeWidth, this.nodeHeight])
let nodes = this.treeData.descendants()
let links = this.treeData.links()
上面的變量 nodes
和 links
現(xiàn)在就包含了結(jié)構(gòu)圖中每個(gè) 組織節(jié)點(diǎn)
和 連接線
的坐標(biāo)信息.
2. 使用Canvas繪圖 API將 虛擬Dom
中的數(shù)據(jù) (坐標(biāo) & 線的path) 等繪制到Canvas上
在 drawShowCanvas中, 通過 d3.select拿到虛擬的dom節(jié)點(diǎn), 再使用 Canvas的繪圖函數(shù)進(jìn)行繪制, 這里用到了一些 Util的工具方法, 具體實(shí)現(xiàn)請(qǐng)參考源碼.
drawShowCanvas () {
this.context.clearRect(-50000, -10000, 100000, 100000)
let self = this
// draw links
this.virtualContainerNode.selectAll('.link')
.each(function () {
let node = self.d3.select(this)
let linkPath = self.d3.linkVertical()
.x(function (d) {
return d.x
})
.y(function (d) {
return d.y
})
.source(function () {
return {x: node.attr('sourceX'), y: node.attr('sourceY')}
})
.target(function () {
return {x: node.attr('targetX'), y: node.attr('targetY')}
})
let path = new Path2D(linkPath())
self.context.stroke(path)
})
this.virtualContainerNode.selectAll('.orgUnit')
.each(function () {
let node = self.d3.select(this)
let treeNode = node.data()[0]
let data = treeNode.data
self.context.fillStyle = '#3ca0ff'
let indexX = Number(node.attr('x')) - self.unitWidth / 2
let indexY = Number(node.attr('y')) - self.unitHeight / 2
// draw unit outline rect (if you want to modify this line ===> please modify the same line in `drawHiddenCanvas`)
Util.roundRect(self.context, indexX, indexY, self.unitWidth, self.unitHeight, 4, true, false)
Util.text(self.context, data.name, indexX + self.unitPadding, indexY + self.unitPadding, '20px', '#ffffff')
// Util.text(self.context, data.title, indexX + self.unitPadding, indexY + self.unitPadding + 30, '20px', '#000000')
let maxWidth = self.unitWidth - 2 * self.unitPadding
Util.wrapText(self.context, data.title, indexX + self.unitPadding, indexY + self.unitPadding + 24, maxWidth, 20)
})
}
3. 使用 Unique-color
的方式實(shí)現(xiàn)Canvas 的用戶交互
下圖中可以看到, 實(shí)際上是有兩張Canvas的, 其中下面的Canvas除了的節(jié)點(diǎn)顏色不同外, 和上面的Cavans繪制的數(shù)據(jù)完全相同.
drawCanvas () {
this.drawShowCanvas()
this.drawHiddenCanvas()
}
在上面一張Canvas上監(jiān)聽用戶點(diǎn)擊事件, 通過象素的坐標(biāo), 在下面一張圖中拿到用戶點(diǎn)擊的節(jié)點(diǎn) (注意: 顏色和節(jié)點(diǎn)的鍵值對(duì) 是在下面一張Canvas繪制的時(shí)候就已經(jīng)創(chuàng)建好的.)
setClickListener () {
let self = this
this.canvasNode.node().addEventListener('click', function (e) {
let colorStr = Util.getColorStrFromCanvas(self.hiddenContext, e.layerX, e.layerY)
let node = self.colorNodeMap[colorStr]
if (node) {
// let treeNodeData = node.data()[0]
// self.hideChildren(treeNodeData, true)
self.toggleTreeNode(node.data()[0])
self.update(node.data()[0])
}
})
}
下面是創(chuàng)建 unique-color和節(jié)點(diǎn)的 鍵值對(duì) 的參考代碼:
addColorKey () {
// give each node a unique color
let self = this
this.virtualContainerNode.selectAll('.orgUnit')
.each(function () {
let node = self.d3.select(this)
let newColor = Util.randomColor()
while (self.colorNodeMap[newColor]) {
newColor = Util.randomColor()
}
node.attr('colorKey', newColor)
node.data()[0]['colorKey'] = newColor
self.colorNodeMap[newColor] = node
})
}
其他
To draw your own nested data
please replace the data in /src/base/data-generator
with your own nested data.
please add your data drawing logic in /src/components/org-chart.js #drawShowCanvas
Want to develop locally ?
source code
if you like it , welcome to star and fork :tada:
https://github.com/ssthouse/organization-chart
# install dependencies
npm install
# serve with hot reload at localhost
npm run dev
# build for production with minification (build to ./docs folder, which can be auto servered by github page ??)
npm run build
想繼續(xù)了解 D3.js ?
這里是我的 D3.js 乾蓬、 數(shù)據(jù)可視化 的github 地址, 歡迎 start & fork :tada: