参考的例子:http://bl.ocks.org/robschmuecker/7880033
- 一、为什么选择d3.js
- 二、d3.js概述
- 三:树状图实现
- 1、创建svg
- 2、在svg元素里面画一个g标签,用于存放树结构
- 3、组织树结构,获取树结构原始数据,d3加工数据
- 4、data方式绑定数据生成数据结构,创建整棵树
- 5、创建树节点,设置样式并绑定事件
- 6、连接线生成器
- 7、过渡效果 transition
- 8、tooltip及编辑框
- 9、获取节点,更新节点方式
一、为什么选择d3.js
echarts绘制树状图的局限性:
1、echarts实现的树节点样式比较单一,如下图所示的树节点的样式无法实现,尤其是编辑图标。
2、echarts点击事件只能绑定在树节点的那个节点(下图的圈圈)上,而项目需要绑定树节点的悬浮事件,编辑图标的点击事件。
3、树的连接线与圆点分离,项目需要,用echarts基本无法实现。
根据以上业务需求,促使我们选择d3.js技术实现树状图。
二、d3.js概述
D3 (或者叫 D3.js )是一个基于 web 标准的 JavaScript 可视化库. D3 可以借助 SVG, Canvas 以及 HTML 将你的数据生动的展现出来. D3 结合了强大的可视化交互技术以及数据驱动 DOM 的技术结合起来, 让你可以借助于现代浏览器的强大功能自由的对数据进行可视化.
API地址: https://github.com/xswei/d3js_doc
三:树状图实现
1、创建svg
<svg ref={(r) => this.chartRef = r}></svg>
d3选择器选择svg元素赋值宽高
this.svg = d3.select(this.chartRef)
.attr('width', this.width)
.attr('height', this.height)
2、在svg元素里面画一个g标签,用于存放树结构
this.svgGroup = this.svg.append('g')
3、组织树结构,获取树结构原始数据,d3加工数据
树结构原始数据格式:
{
name:
'偿付能力',
field:
'root',
circleColor:
'white',
notEdit:
true,
children: [
{
name:
'经营和投资汇总',
field:
'jyhtzhz',
value:
40,
unit:
'亿元',
desc:
'销售现金回款+租金收入-土地购买支出-一般成本汇总-税务开支汇总',
circleColor:
'#09BE49',
children: [
{
name:
'销售现金回款',
field:
'xsxjhk',
fieldRelation:
'jyhtzhz,xsxjhk,zjsr,tdgmzc,ybcbhz,swkzhz',
funCalc:
'xsxjhk+zjsr-tdgmzc-ybcbhz-swkzhz',
value:
100,
unit:
'亿元',
desc:
'销售金额*回款率',
circleColor:
'#09BE49',
}
]
}
}
1.创建tree
// 对角线生成器,x和y对调则生成从左到右展开的树
this.diagonal = d3.svg.diagonal()
.projection((d) => { return [this.svgGWidth - d.y, d.x] })
// 创建tree
this.tree = d3.layout.tree()
.size([this.height, this.width])
2.加工数据
// 取最大节点数 乘以 固定高度 得到树的高度
let
newHeight =
d3.
max(
levelWidth) *
50
this.
tree =
this.
tree.
size([
newHeight,
this.
width])
// Compute the new tree layout.
let nodes = this.tree.nodes(this.root).reverse()
// Normalize for fixed-depth.
nodes.forEach((d) => { d.y = d.depth * 220 })
nodes就是加工后的数据
4、data方式绑定数据生成数据结构,创建整棵树
// 根据id绑定数据,返回update的节点,需要更新的节点
let node = this.svgGroup.selectAll('g.node')
.data(nodes, (d) => { return d.id || (d.id = ++this.i) })
// Enter any new nodes at the parent's previous position.
// 操作enter的节点,新增新的节点,这里的节点位置采用老的位置
let nodeEnter = node.enter().append('svg:g')
.attr('class', 'node')
.attr('transform', (d) => { return `translate(${(this.svgGWidth - source.y0)},${source.x0})` })
// Transition exiting nodes to the parent's new position.
// 退出节点过渡效果后删除
let nodeExit = node.exit().transition()
.duration(duration)
.attr('transform', (d) => { return `translate(${(this.svgGWidth - source.y)},${source.x})` })
.remove()
// update link 需要更新的连接线
let link = this.svgGroup.selectAll('path.link')
.data(this.tree.links(nodes), (d) => { return d.target.id })
// Enter any new links at the parent's previous position.
// 生成新的连接线,连接线位置是老的
link.enter().insert('svg:path', 'g')
.attr('class', 'link')
.style('fill', 'none')
.style('stroke', 'rgba(255,255,255,0.2)')
.style('stroke-width', '1px')
.attr('d', (d) => {
let o = { x: source.x0, y: source.y0 }
return this.diagonal({ source: o, target: o })
})
// Transition exiting nodes to the parent's new position.
// exit 过渡效果到新的位置然后移除
link.exit().transition()
.duration(duration)
.attr('d', (d) => {
let o = { x: source.x, y: source.y }
return this.diagonal({ source: o, target: o })
})
.remove()
5、创建树节点,设置样式并绑定事件
这个树节点其实是由 一个大的<g>元素,包了一个text元素,一个tspan元素,一个image元素,一个circle元素组成。
let text = nodeEnter.append('svg:text')
.on('mouseover', function (d) {
d3.select(this).style('fill', '#a5d5e4')
// 描述信息
})
.on('mouseout', function (d) {
d3.select(this).style('fill', '#FFFFFF')
})
text.append('svg:tspan')
.attr('dx', (d) => { return 10 })
.attr('text-anchor', (d) => { return 'start' })
.style('fill', 'rgba(255,255,255,0.5)')
.text((d) => {
return d.value != null ? d.value : ''
})
/**
* 编辑按钮,绑定了编辑事件
*/
nodeEnter.append('svg:image')
.on('click', function (d) {
_// 编辑事件
})
// 树节点上的圈圈
nodeEnter.append('svg:circle')
.attr('r', 1e-6)
.style('fill', (d) => { return d._children ? d.circleColor : '#0e1821' })
.style('stroke', (d) => {
return d.circleColor ? d.circleColor : 'steelblue'
})
.style('stroke-width', '1.5')
.style('cursor', 'pointer')
.on('click', (d) => { this.toggle(d); this.update(d) }) // 绑定的树节点展开和折叠的方法
问题:节点的text定位问题?连接线的定位问题?
需要计算节点上面文字的长度计算出具体的宽度像素值,这样就可以动态确定编辑图标的位置以及连接线的位置。
下图所示,编辑图标和连接线紧跟在文字后面就是这种方式实现的
6、连接线生成器
// 对角线生成器,x和y对调则生成从左到右展开的树
this.diagonal = d3.svg.diagonal()
.projection((d) => { return [this.svgGWidth - d.y, d.x] })
// 生成新的连接线,连接线位置是老的
link.enter().insert('svg:path', 'g')
.attr('class', 'link')
.style('fill', 'none')
.style('stroke', 'rgba(255,255,255,0.2)')
.style('stroke-width', '1px')
.attr('d', (d) => {
let o = { x: source.x0, y: source.y0 }
return this.diagonal({ source: o, target: o })
})
// Transition links to their new position.
// 过渡到新的位置
link.transition()
.duration(duration)
// .attr('d', this.diagonal)
.attr('d', (d) => {
d.target.y -= _this.calcWidth(d.target) + 25
return this.diagonal(d)
})
7、过渡效果 transition
// Transition nodes to their new position.
// 节点过渡效果,移动到新的位置
let nodeUpdate = node.transition()
.duration(duration)
.style('cursor', 'pointer')
.attr('transform', (d) => { return `translate(${(this.svgGWidth - d.y)},${d.x})` })
8、tooltip及编辑框
实际是悬浮的div
有个插件 d3-tip 做tooltip的,v3支持的不好,升级到v4时可以用。
9、获取节点,更新节点方式
/**
* 拿到指定元素
* @param {*} element 元素名称
* @param {*} field 叶子节点唯一字段
*/
getSvgElement (elementName, field) {
if (!elementName) {
elementName = 'tspan'
}
// 默认当前节点
if (!field) {
field = this.state.field
}
let arr = d3.selectAll(elementName)[0] //获取指定元素的所有节点
for (let obj of arr) {
if (obj.__data__ && obj.__data__.field && obj.__data__.field === field) {
return obj
}
}
return null
}
更新节点的值
getSvgElement (elementName, field)
.text(this.state.leftValue + this.state.unit)
.style('fill', color)