携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情
引入D3模块
- 引入整个D3模块。
<!-- D3模块 -->
<script src="https://d3js.org/d3.v7.min.js"></script>
数据
const data = {
name: 'A',
children: [
{
name: 'a',
children: [
{
name: 'a-a',
children: [
{
name: 'a-aa',
value: 2
}
]
},
{
name: 'a-b',
value: 4
}
]
},
{
name: 'b',
children: [
{
name: 'b-a',
children: [
{
name: 'b-aa',
value: 2
}
]
}
]
},
{
name: 'c',
children: [
{
name: 'c-a',
children: [
{
name: 'c-aa',
value: 2
},
{
name: 'c-ab',
value: 5
},
{
name: 'c-ac',
children: [
{
name: 'c-aca',
value: 2
},
{
name: 'c-acb',
value: 5
}
]
}
]
}
]
}
]
}
添加画布
- 初始化画布。
// 画布
const width = 500
const height = 500
const svg = d3
.select('.d3Chart')
.append('svg')
.attr('width', width)
.attr('height', height)
.style('background-color', '#1a3055')
// 图
const chart = svg.append('g')
比例尺和配置信息
// 创建颜色比例尺
const color = d3.scaleOrdinal(d3.quantize(d3.interpolateRainbow, data.children.length + 1))
d3.interpolateRainbow(t)
返回一串RGB颜色。t: 取值范围是[0,1]。d3.quantize(interpolator, n)
返回数组。通过插值器函数(interpolator
),返回传入的样本数(n)。
- 颜色比例尺,参数相同返回相同颜色。
const root = d3
.hierarchy(data)
.sum((d) => d.value)
.sort((a, b) => b.value - a.value)
- 使用
.hierarchy()
构造根节点数据。如节点层级、节点总值等。
// 分区图布局
const partition = (newData) => {
// d3.partition() 递归地将节点树 分区为旭日图或者冰柱图
return d3.partition().size([2 * Math.PI, newData.height + 1])(newData)
}
d3.partition()
分区图布局,对数据在画布上进行布局。
- 通过
.size()
设置布局为极坐标系,以弧度和层级进行布局。
绘制扇形
const slices = chart.append('g').attr('transform', `translate(${width / 2},${height / 2})`)
- 创建绘制组。
// 中心点 标识
let currentRoot = 'chart'
- 处理交互时的唯一标识。图中间圆标识。
function handle_node_click(e, d) {
if (d.data.name === currentRoot) {
// 点击中心节点回退
if (d.parent) {
const newD = d.parent.copy()
newD.parent = d.parent.parent
draw(newD)
}
} else {
// 点击其余节点下钻
// .copy() 复制层次结构
const newD = d.copy()
newD.parent = d.parent
draw(newD)
}
}
.copy()
继承d3.hierarchy
,复制当前节点信息。
- 绘图节点点击,修改数据重新绘制图新形成交互。
.copy()
只复制节点当前节点信息,手动添加父节点信息。
function draw(newData) {
// 获取展示数据
const nodes = partition(newData)
nodes.each((d) => (d.current = d))
const radius = width / 2 / (nodes.height + 2)
// 中心节点
currentRoot = nodes.data.name
}
draw(root)
- 创建绘图函数
draw()
。 - 对数据添加布局信息,根据节点层级计算半径。设置当前数据中心节点标识。
function draw(newData) {
...
const path = slices
.selectAll('.node')
.data(nodes.descendants(), (d) => d.data.name)
.join(
(enter) => {
let $gs = enter.append('path')
$gs.append('title').text((d) => `${d.data.name}: ${d.value}`)
return $gs
},
(update) => update,
(exit) => {
// 多出的节点 删除
exit.transition().duration(100).attr('opacity', 0).remove()
}
)
.attr('class', 'node')
.style('cursor', 'pointer')
.attr('fill', (d) => {
while (d.depth > 1) d = d.parent
return color(d.data.name)
})
.on('click', handle_node_click)
.transition()
.duration(300)
.attrTween('d', arcTween)
// path
function arcTween(item) {
/**
* 弧度计算
* */
let currentArc = this._current
if (!currentArc) {
currentArc = { startAngle: 0, endAngle: 0 }
}
const interpolateArc = d3.interpolate(
//对弧度插值
currentArc,
{
startAngle: item.x0,
endAngle: item.x1
}
)
this._current = interpolateArc(1)
/**
* 半径计算
* */
// 不同层级 内半径不同
const innerRadiusR = item.y0 * radius
let currentRadius = this._currentR
if (!currentRadius) {
currentRadius = innerRadiusR
}
const outerRadiusR = d3.interpolate(currentRadius, item.y1 * radius - 1)
this._currentR = outerRadiusR(1)
return function (t) {
const arc = d3
.arc()
.padAngle(Math.min((item.x1 - item.x0) / 2, 0.005))
.innerRadius(innerRadiusR)
.outerRadius(outerRadiusR(t))
return arc(interpolateArc(t))
}
}
...
}
- 添加了交互动画,需要对节点设置唯一标识
node
。对绑定数据设置唯一标识.data(nodes.descendants(), (d) => d.data.name)
。 - 第二次通过标识
node
获取已存在的节点。d3
会通过数据的标识对节点进行修改删除。 - 使用
.attrTween('d', arcTween)
加载动画。这里对弧度和半径进行了过度动画设置。
...
// 文本位置计算
function labelTransform(d) {
const x = (((d.x0 + d.x1) / 2) * 180) / Math.PI
let y = ((d.y0 + d.y1) / 2) * radius
if (d.y0 === 0) {
return `translate(0,0)`
}
return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`
}
const label = slices
.selectAll('text')
.data(nodes.descendants(), (d) => d.data.name)
.join(
(enter) => {
let $gs = enter.append('text')
return $gs
},
(update) => update,
(exit) => {
// 多出的节点删除
exit.transition().duration(100).attr('opacity', 0).remove()
}
)
.attr('text-anchor', 'middle')
.attr('pointer-events', 'none')
.style('user-select', 'none')
.attr('dy', '0.35em')
.attr('transform', (d) => labelTransform(d.current))
.text((d) => d.data.name)
...
- 创建文本位置计算函数,根据弧度值和层级计算出文本的旋转角度和位移值。
- 和弧形一样的,通过数据标识来控制节点。