持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情
开始绘制
引入D3模块
- 这里引入的是整个D3模块。需要按模块引入可以在
github
上查找模块CDN地址。
<script src="https://d3js.org/d3.v7.min.js"></script>
数据
- 自定义数据格式,主要方便后面使用。
var dataArr = [
{
label: '1月',
value: 10.5
},
{
label: '2月',
value: 70.5
},
{
label: '3月',
value: 60.5
},
{
label: '4月',
value: 10.5
},
{
label: '5月',
value: 20.5
},
{
label: '6月',
value: 30.5
}
]
添加画布
- 初始化画布。
var width = 700
var height = 700
var margin = 60
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(${margin}, ${margin})`)
创建比例尺
- 根据需求创建比例迟。
// 序数比例尺 - 颜色
let colorScale = d3.scaleOrdinal().domain(d3.range(0, dataArr.length)).range(d3.schemeCategory10)
- 一个简单的饼图只需要颜色比例尺,计算每个扇形颜色。
d3.schemeCategory10
数组,十个D3内置颜色。
绘制扇形
// 半径
var radius = (Math.min(width, height) * 0.5) / 2
// .pie() 是shape中的 饼图(pie)生成器 计算图所需要的角度信息
// .startAngle() 起始角度设置
// .endAngle() 终止角度设置
// .padAngle() 饼图扇形之间的间隔设置
let drawData = d3
.pie()
.value(function (d) {
return d.value
})
.startAngle(0)
.endAngle(Math.PI * 2)
// .arc() 是shape中的 弧形生成器
// innerRadius() 设置内半径
// outerRadius() 设置外半径
// cornerRadius() 设置拐角圆滑
let arc = d3.arc().innerRadius(0).outerRadius(100)
- 设置好饼图的半径。
d3.pie()
创建饼图生成器。把数据转换为饼图数据。d3.arc()
创建弧形生成器,根据饼图数据绘制扇形。
const arcs = chart
.append('g')
.attr('transform', 'translate( ' + (radius * 2 - margin) + ', ' + (radius * 2 - margin) + ' )')
- 创建饼图绘制组
g
,居中绘制扇形。
arcs
.selectAll()
.data(drawData(dataArr))
.enter()
.append('path')
.attr('class', 'pieArc')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1)
.attr('fill', function (d) {
return colorScale(d.index)
})
.attr('d', function (d, i) {
// 根据 pie 数据 计算路径
return arc(d)
})
.transition()
.duration(1000)
.attrTween('d', function (d) {
// 初始加载时的过渡效果
let interpolate = d3.interpolate(
{
endAngle: d.startAngle
},
d
)
return function (t) {
return arc(interpolate(t))
}
})
d3.interpolate()
插值器。可以简单理解为一个线性比例尺,输入范围是0 ~ 1,输出为传入的两个对象中,对应字段之间的值。如上,第一个对象就一个字段endAngle
,第二个对象是饼图数据,在第二个对象中存在endAngle
字段。输出的范围对应两个endAngle
字段之间的值。
- 使用创建好的饼图生成器
drawData(dataArr)
,转换数据并绑定到饼图绘制组g
上。 - 在生成
d
属性时,使用弧形生成器。绘制扇形。 - 添加动画,使用
d3.interpolate()
使弧形长度,从开始值变化到结束值。
绘制标签
//
const arc2 = d3
.arc()
.outerRadius(100 * 3.5)
.innerRadius(0)
/*
* 计算文本水平偏移
**/
const textOffsetM = 10
const scaleTextDx = d3
.scaleLinear()
.domain([0, Math.PI / 2])
.range([textOffsetM, textOffsetM * 3])
function computeTextDx(d) {
// 计算文本水平偏移
const middleAngle = (d.endAngle + d.startAngle) / 2
let dx = ''
if (middleAngle < Math.PI) {
dx = scaleTextDx(Math.abs(middleAngle - Math.PI / 2))
} else {
dx = -scaleTextDx(Math.abs(middleAngle - (Math.PI * 3) / 2))
}
return dx
}
- 创建新的弧形生成器,扩大弧形半径。用于后面获取文本位置。
- 创建文本偏移计算方法。对绘制文本影响不大,可不使用。
arcs
.selectAll()
.data(drawData(dataArr))
.enter()
.append('text')
.attr('text-anchor', (d) => {
return (d.endAngle + d.startAngle) / 2 > Math.PI ? 'end' : 'start'
})
.attr('stroke', 'steelblue')
.attr('dy', '0.35em')
.attr('dx', computeTextDx)
.attr('transform', (d) => {
return 'translate(' + arc2.centroid(d) + ')'
})
.text((d) => d.data.label + ': ' + d.data.value)
arc.centroid()
获取扇形的中心点。
- 这里需要注意,绘制文本需要在(
arcs
)饼图绘制组(g
)上,文本的位移才是正确的。 - 使用
translate(' + arc2.centroid(d) + ')
获取扩大扇形的中心点,平移文本到对应位置。 - 通过饼图数据,获取文本信息组合绘制。
绘制连线
const linePoints = drawData(dataArr).map((d) => {
const line = []
const tempPoint = arc2.centroid(d)
const tempDx = computeTextDx(d)
const dx = tempDx > 0 ? tempDx - textOffsetM : tempDx + textOffsetM
line.push(arc.centroid(d))
line.push(tempPoint)
line.push([tempPoint[0] + dx, tempPoint[1]])
return line
})
- 循环饼图数据,得到
arc.centroid(d)
饼图中扇形中心点,tempPoint
文本所在位置,和文本偏移后的位置。组合为线数据组。
const generateLine = d3
.line()
.x((d) => d[0])
.y((d) => d[1])
arcs
.selectAll()
.data(linePoints)
.enter()
.insert('path', ':first-child')
.classed('line', true)
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('d', generateLine)
- 创建线生成器,绘制线。
添加交互
/**
* 弧形动画
* */
function arcTween(outerRadius) {
return function () {
d3.select(this)
.transition()
.attrTween('d', function (d) {
let interpolate = d3.interpolate(100, 100 + outerRadius)
return function (t) {
let arcT = d3.arc().outerRadius(interpolate(t)).innerRadius(0)
return arcT(d)
}
})
}
}
d3.selectAll('.pieArc').on('mouseover', arcTween(20)).on('mouseout', arcTween(0))
- 创建动画函数,控制扇形的放大缩小。
- 代码地址