携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
开始绘制
引入D3模块
- 引入整个D3模块。
<!-- D3模块 -->
<script src="https://d3js.org/d3.v7.min.js"></script>
数据
- 自定义数据格式。
const dataTree = {
name: '太刀',
children: [
{
name: '矿石',
children: [
{
name: '结晶矿',
children: [
{ name: '蓝矿', num: 10 },
{ name: '黑铁矿', num: 3 },
{ name: '白灰矿', num: 4 }
]
}
]
},
{
name: '木材',
children: [
{
name: '稀木',
children: [
{ name: '钴木', num: 4 },
{ name: '黑木', num: 2 }
]
},
{
name: '水木',
children: [{ name: '蓝木', num: 4 }]
}
]
},
{
name: '宝石',
children: [
{
name: '太阳类',
children: [
{ name: '日金石', num: 6 },
{ name: '熔岩石', num: 1 }
]
},
{
name: '深海类',
children: [
{ name: '寒铁石', num: 2 },
{ name: '金晶石', num: 3 },
{ name: '玄冰结晶', num: 2 }
]
}
]
}
]
}
添加画布
- 初始化画布。
var width = 900
var height = 600
var margin = 30
var svg = d3
.select('.d3Chart')
.append('svg')
.attr('width', width)
.attr('height', height)
.style('background-color', '#1a3055')
// 图
var chart = svg.append('g').attr('transform', `translate(${2 * margin}, ${2 * margin})`)
创建比例尺
var colorScale = d3.scaleOrdinal(d3.schemeSet3)
- 颜色比例尺。
const rootTree = d3
.hierarchy(dataTree)
.sum((d) => d.num) // 计算绘图属性value的值 -求和 其子节点所有.num属性的和值
.sort((a, b) => a.value - b.value) // 根据 上面计算出的value属性 排序
- 转换数据为d3树形数据结构,并对每个层级排序。
// 创建一个新的树布局
// .size() 设置布局的尺寸
const treeData = d3.tree().size([width - 4 * margin, height - 4 * margin])
- 对树形结构数据,生成画布上的布局数据(每个节点在画布上的坐标信息)。
绘制树
// 绘制组
const linkChart = chart.append('g')
const rectChart = chart.append('g')
- 树节点和线条分开绘制,创建两个绘制组。
/**
* @name 处理结点 点击
* @param {Object} ev 事件
* @param {Object} d 数据
*/
function handle_node_click(ev, d) {
d.sourceX = d.x
d.sourceY = d.y
if (d.depth !== 0) {
if (d.children) {
d._children = d.children
d.children = undefined
draw()
} else if (d._children) {
for (let a of d._children) {
a.originX = a.parent.x
a.originY = a.parent.y
}
d.children = d._children
draw()
}
}
}
- 树图点击节点,可以收起子节点,再次点击,展开子节点。
- 创建点击方法对数据进行操作。
/**
* init 是否第一次加载
* */
function draw(init = false) {
...
}
draw(true)
- 为了实现点击节点树图变化的效果,每一次数据变换都需要重新绘制。绘制代码需要放在函数中实时调用。
function draw(init = false) {
// 为数据添加位置信息
let root = treeData(rootTree)
// 获取所有节点数据
let nodes = root.descendants()
const rectNode = rectChart
.selectAll('.node')
.data(nodes, (d) => d.data.name)
.join(
(enter) => {
/**
* 绘制节点 和 节点文本
* */
let $gs = enter.append('g').attr('transform', (d) => {
let x, y
if (d.originX) {
x = d.originX
delete d.originX
} else {
x = d.x
}
if (d.originY) {
y = d.originY
delete d.originY
} else {
y = d.y
}
return `translate(${x}, ${y})`
})
$gs
.append('circle')
.attr('r', 24)
.attr('cx', 0)
.attr('cy', 0)
.attr('fill', (d) => (d.children || d._children ? colorScale(1) : colorScale(2)))
$gs
.append('text')
.attr('class', 'text')
.text((d) => (d.data.name.length < 3 ? d.data.name : d.data.name.slice(0, 1) + '...'))
.attr('fill', '#000000')
.style('font-size', '12px')
.attr('dx', (d) => {
return -12
})
.attr('dy', function () {
return this.getBBox().height / 4
})
return $gs
},
(update) => update,
(exit) => {
// 删除多出 节点 添加动画
exit
.transition()
.duration(init ? 0 : 1000)
.attr('opacity', 0)
.attr('transform', (d) => `translate(${d.parent.x},${d.parent.y})`)
.remove()
}
)
.attr('class', 'node')
.style('cursor', 'pointer')
.on('click', handle_node_click)
// 节点加载 进行动画
.transition()
.duration(init ? 0 : 1000)
.attr('opacity', 1)
.attr('transform', (d) => `translate(${d.x}, ${d.y})`)
...
}
-
.join()
有三个参数,每个参数都是一个函数,创建、更新、删除。根据数据和节点绑定的key来判断。数据没有绑定节点,创建新的节点,执行创建函数。数据和节点同时存在且绑定过,执行更新函数。数据被删除,节点存在删除节点,执行删除函数。 -
监听点击事件,修改树图数据。
-
通过使用
.join()
函数,对节点加入和删除增加动画。 -
使用同样的方式绘制连线。
...
// 获取线条 位置信息
let links = root.links()
linkChart
.selectAll('.link')
.data(links, (d, i) => d.target.data.name)
.join(
(enter) =>
enter
.append('path')
.attr('class', 'link')
.attr('fill', 'none')
.attr('stroke', 'gray')
.attr('d', (d) => {
let s = d.source
let origin = `${s.sourceX || s.x},${s.sourceY || s.y}`
return `M ${origin} L ${origin}`
}),
(update) => update,
(exit) =>
exit
.transition()
.duration(init ? 0 : 1000)
.attr('d', (d) => {
let s = d.source
let origin = `${s.x},${s.y}`
return `M ${origin} L ${origin}`
})
.remove()
)
.transition()
.duration(init ? 0 : 1000)
.attr('d', (d) => {
let s = d.source
let t = d.target
return `M ${s.x},${s.y} L ${t.x},${t.y}`
})
...