最近工作需要画一个关系层级比较深的树,由定制化的东西比较多,小白浅试 D3js 顺便记录一下学习过程
什么是D3js
D3js 是一个可以基于数据来操作文档的JavaScript库。可以帮助你使用 HTML、CSS、SVG 以及 Canvas来展示数据。D3 的全称是 Data-Driven Documents(数据驱动文档)
怎么理解呢?可以把 D3 当成是 svg 的 jQuery ,D3 里面提供了数据驱动DOM和一系列的 api 可供创建/ 选择 svg。
先说数据驱动,D3 里面有一些列的方法可供开发者梳理数据,再根据数据去操作 DOM,这一点与 react 的思想背道而驰D3 对DOM 的操作全是是真实 DOM react 是 VDOM;再来谈谈 SVG DOM, D3 建立了一整套数据到SVG属性的计算框架,开发者可以通过 jQuery 式操作便捷的生成一个定制化的 svg,减少了开发人员学习 SVG 成本,也不用再为生成 一条 path 而耗费精力。
如何使用
如果你是一个初学者 那么最好的方法就是手撸 demo ,来画一颗有箭头的树,先上图 展示的效果是这样的:
初始化
定义画布 以及初始化
createTreeRoot (treeData) {
// d3.hierarchy(data[, children]) 构造有根节点的tree
this.$root = d3.hierarchy(treeData)
this.$root.x0 = this.nodeConfig.dx
this.$root.y0 = this.nodeConfig.dy
// 从当前节点开始返回其后代节点数组
this.$root.descendants().forEach((d, i) => {
d.id = d.data.id || getUUID()
d.data.id = d.id
})
// 节点间横向和纵向的距离
d3.tree().nodeSize([this.nodeConfig.nodeDis, this.nodeConfig.layerDis])(this.$root)
}
画一个圆角矩形
const nodeEnter = node.enter().append('g')
.attr('class', this.nodeClassName)
.attr('id', d => d.id)
.attr('transform', d => {
return diagonal({ x: source.x0, y: source.y0 })
})
// 画变暖给
nodeEnter.append('rect')
.attr('fill', d => !d.data.isRoot ? this.nodeConfig.fill : this.nodeConfig.stroke)
.attr('stroke', d => !d.data.isRoot ? this.nodeConfig.stroke : 'none')
.attr('stroke-width', d => !d.data.isRoot ? 1 : 0)
.attr('width', this.nodeConfig.width)
.attr('height', this.nodeConfig.height)
.attr('rx', this.nodeConfig.radius)
.attr('ry', this.nodeConfig.radius)
.attr('x', (this.nodeConfig.width / 2) * -1)
.attr('y', (this.nodeConfig.height / 2) * -1)
.attr('text-anchor', 'middle')
const fo = nodeEnter.append('foreignObject')
.attr('width', this.nodeConfig.width)
.attr('height', this.nodeConfig.height)
.attr('x', (this.nodeConfig.width / 2) * -1)
.attr('y', (this.nodeConfig.height / 2) * -1)
.on('mouseenter', (e, d) => {
console.log('mouseenter', e, d)
})
.on('mouseleave', (e, d) => {
console.log('mouseleave', e, d)
})
fo.append('xhtml:div')
.style('width', `${this.nodeConfig.width}px`)
.style('height', `${this.nodeConfig.height}px`)
.style('font-size', `${this.nodeConfig.text.fontSize}px`)
.style('color', d => !d.data.isRoot ? this.nodeConfig.text.fill : '#fff')
.style('padding', '4px 6px')
.style('text-align', 'center')
.style('display', 'flex')
.style('align-items', 'center')
.style('justify-content', 'center')
.attr('title', d => d.data.name)
.html(d => `${d.data.name.substr(0, 20)}${d.data.name.length > 20 ? '...' : ''}`)
画线
const linkEnter = link.enter().append('g')
.attr('class', 'link')
linkEnter.append('path')
.classed('link', true)
.attr('id', d => `${d.source.id}-${d.target.id}`)
.attr('stroke', this.linkConfig.stroke)
.attr('stroke-width', this.linkConfig.strokeWidth)
.attr('fill', 'none')
.attr('d', d => {
const o = { x: source.x0, y: source.y0 }
return diagonal({ source: o, target: o })
})
画箭头
linkEnter.append('path')
.classed('arrow', true)
.attr('fill', this.linkConfig.arrow.fill)
.attr('stroke-width', 0)
.attr('d', d => {
const o = { x: source.x0, y: source.y0 }
return arrowDiagonal({ source: o, target: o }, this.linkConfig.arrow.size)
})
致次一个 tree 就画完啦~