D3.js + Canvas 繪制組織結(jié)構(gòu)圖

打個(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 gif

上面的demo就是使用 D3.js + Canvas 的方式實(shí)現(xiàn)的, 在組織的層數(shù)超過300時(shí)才會(huì)出現(xiàn)明顯的卡頓, 能滿足大部分的組織結(jié)構(gòu)圖的數(shù)據(jù).

思路

  1. 使用 D3.js的 Three 在 虛擬Dom 中畫好圖像
  2. 使用Canvas繪圖 API將 虛擬Dom 中的數(shù)據(jù) (坐標(biāo) & 線的path) 等繪制到Canvas上
  3. 使用 Unique-color 的方式實(shí)現(xiàn)Canvas 的用戶交互
  4. 通過繪制一張和之前 Canvas數(shù)據(jù)相同的隱藏Canvas, 并給每一個(gè) 想要接受用戶交互的節(jié)點(diǎn)賦予唯一的顏色
  5. 通過監(jiān)聽Canvas點(diǎn)擊事件, 獲取點(diǎn)擊像素的顏色值來判斷點(diǎn)擊的節(jié)點(diǎn)
  6. 該文章中有對(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()

上面的變量 nodeslinks 現(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()
  }
unique color.png

在上面一張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:

D3-blog

如果覺得不錯(cuò)的話, 不妨點(diǎn)擊下面的鏈接關(guān)注一下 : )

github主頁

知乎專欄

掘金

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末河劝,一起剝皮案震驚了整個(gè)濱河市壁榕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赎瞎,老刑警劉巖牌里,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異务甥,居然都是意外死亡牡辽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門敞临,熙熙樓的掌柜王于貴愁眉苦臉地迎上來态辛,“玉大人,你說我怎么就攤上這事挺尿∽嗪冢” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵编矾,是天一觀的道長(zhǎng)熟史。 經(jīng)常有香客問我,道長(zhǎng)窄俏,這世上最難降的妖魔是什么蹂匹? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮凹蜈,結(jié)果婚禮上限寞,老公的妹妹穿的比我還像新娘。我一直安慰自己仰坦,他們只是感情好履植,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著悄晃,像睡著了一般静尼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上传泊,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天鼠渺,我揣著相機(jī)與錄音,去河邊找鬼眷细。 笑死拦盹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的溪椎。 我是一名探鬼主播普舆,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼校读!你這毒婦竟也來了沼侣?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤歉秫,失蹤者是張志新(化名)和其女友劉穎蛾洛,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體雁芙,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡轧膘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了兔甘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谎碍。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖洞焙,靈堂內(nèi)的尸體忽然破棺而出蟆淀,到底是詐尸還是另有隱情,我是刑警寧澤澡匪,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布熔任,位于F島的核電站,受9級(jí)特大地震影響仙蛉,放射性物質(zhì)發(fā)生泄漏笋敞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一荠瘪、第九天 我趴在偏房一處隱蔽的房頂上張望夯巷。 院中可真熱鬧,春花似錦哀墓、人聲如沸趁餐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至勉抓,卻和暖如春藕筋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背掰茶。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工盐碱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留甸各,地道東北人焰坪。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像诫尽,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子酣藻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容