D3.js + Canvas 繪製組織結構圖

ssthouse發表於2018-06-01

使用 D3.js 預設的 svg 渲染

D3預設的樹狀圖畫圖使用的是svg, 比如這個來自D3作者的例子:

https://bl.ocks.org/mbostock/4339083

使用svg有好有壞:

  • 好處是方便操作dom元素, 新增使用者互動
  • 壞處是渲染效率不高, 在資料量較大時頁面易掉幀, 卡頓

在大多數資料量不是特別大情況下, 使用svg的好處是遠遠蓋過壞處的,但如果我們真的需要渲染大量的資料呢?

使用 D3.js + Canvas 渲染

source code

https://github.com/ssthouse/organization-chart

demo page

https://ssthouse.github.io/organization-chart/#/

demo gif

上面的demo就是使用 D3.js + Canvas 的方式實現的, 在組織的層數超過300時才會出現明顯的卡頓, 能滿足大部分的組織結構圖的資料.

思路

  1. 使用 D3.js的 Three 在 虛擬Dom 中畫好影象
  2. 使用Canvas繪圖 API將 虛擬Dom 中的資料 (座標 & 線的path) 等繪製到Canvas上
  3. 使用 Unique-color 的方式實現Canvas 的使用者互動
  4. 通過繪製一張和之前 Canvas資料相同的隱藏Canvas, 並給每一個 想要接受使用者互動的節點賦予唯一的顏色
  5. 通過監聽Canvas點選事件, 獲取點選畫素的顏色值來判斷點選的節點
  6. 該文章中有對該思路的詳細介紹: https://medium.com/@lverspohl/how-to-turn-d3-and-canvas-into-good-friends-b7a240a32915

1.使用 D3.js的 Three 在 虛擬Dom 中畫好影象

首先調使用D3建立 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()
複製程式碼

上面的變數 nodeslinks 現在就包含了結構圖中每個 組織節點連線線 的座標資訊.

2. 使用Canvas繪圖 API將 虛擬Dom 中的資料 (座標 & 線的path) 等繪製到Canvas上

在 drawShowCanvas中, 通過 d3.select拿到虛擬的dom節點, 再使用 Canvas的繪圖函式進行繪製, 這裡用到了一些 Util的工具方法, 具體實現請參考原始碼.

  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 的方式實現Canvas 的使用者互動

下圖中可以看到, 實際上是有兩張Canvas的, 其中下面的Canvas除了的節點顏色不同外, 和上面的Cavans繪製的資料完全相同.

  drawCanvas () {
    this.drawShowCanvas()
    this.drawHiddenCanvas()
  }
複製程式碼

unique color.png

在上面一張Canvas上監聽使用者點選事件, 通過象素的座標, 在下面一張圖中拿到使用者點選的節點 (注意: 顏色和節點的鍵值對 是在下面一張Canvas繪製的時候就已經建立好的.)

  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])
      }
    })
  }
複製程式碼

下面是建立 unique-color和節點的 鍵值對 的參考程式碼:

  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
複製程式碼

想繼續瞭解 D3.js ?

這裡是我的 D3.js資料視覺化 的github 地址, 歡迎 start & fork :tada:

D3-blog

如果覺得不錯的話, 不妨點選下面的連結關注一下 : )

github主頁

知乎專欄

掘金

相關文章