持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情
开始绘制
引入D3模块
- 引入整个D3模块。
<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 = 120
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)
// 线性比例尺 根据值 获取扇形的半径长度
let scaleRadius = d3
.scaleLinear()
.domain([0, d3.max(dataArr.map((d) => d.value))])
.range([0, d3.min([width - 4 * margin, height - 4 * margin]) * 0.5])
- 创建线性比例,根据值获取对应的半径长度。
绘制扇形
// 饼图(pie)生成器 计算图所需要的角度信息
let drawData = d3
.pie()
.value(function (d) {
return d.value
})
.startAngle(0)
.endAngle(Math.PI * 2)(dataArr)
- 创建饼图生成器,得到每个扇形的图形数据。
// 图形绘制组
const arcs = chart
.append('g')
.attr('class', 'pie')
.attr('transform', 'translate(' + (width - 2 * margin) / 2 + ',' + (height - 2 * margin) / 2 + ')')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1)
// 扇形
arcs
.selectAll()
.data(drawData)
.enter()
.append('path')
.attr('class', (d, i) => 'arc arc-' + i)
.attr('fill', (d, i) => colorScale(i))
.transition()
.duration(1000)
.attrTween('d', arcTween)
function arcTween(d) {
// 半径插值
const interpolate = d3.interpolate(0, scaleRadius(d.value))
// 弧度插值
let fn = d3.interpolate(
{
endAngle: d.startAngle
},
d
)
return function (t) {
let arc = d3.arc().outerRadius(interpolate(t)).innerRadius(0)
return arc(fn(t))
}
}
- 创建饼图绘制组
g
,设置好位置和样式。 - 绑定饼图数据,绘制扇形。
- 在动画函数中,通过比例尺获取对应值的半径。
- 在动画函数中创建弧形生成器,保证每个扇形都使用自己的弧形生成器。
绘制标签
const textOffsetH = 10
// 文本
// 线性比例尺 文本 根据弧度获取偏移值
const scaleTextDx = d3
.scaleLinear()
.domain([0, Math.PI / 2])
.range([textOffsetH, textOffsetH * 3])
/**
* 根据 扇形弧度
* 计算文本水平偏移
*
* @param d (pie)生成器的 数据
* @returns 文本偏移值
*/
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
}
- 创建文本偏移计算函数。这个函数是为了优化文本显示,非必要函数。
/**
* @param outerRadius 半径
* @param d (pie)生成器的 数据
* @param averageLength 是否放大半径
* @returns 扇形 中心点
*/
function getArcCentorid(outerRadius, d, averageLength) {
if (averageLength) outerRadius = Math.sqrt(outerRadius * 600)
return d3.arc().outerRadius(outerRadius).innerRadius(0).centroid(d)
}
- 因为每个扇形的半径不同,创建中心点获取函数。用于获取扇形中心点或获取文本所在中心点。放大半径为文本所在位置。
// 文本
arcs
.selectAll()
.data(drawData)
.enter()
.append('text')
.attr('text-anchor', (d) => {
return (d.endAngle + d.startAngle) / 2 > Math.PI ? 'end' : 'start'
})
.attr('dy', '0.35em')
.attr('dx', computeTextDx)
.attr('transform', (d) => {
return 'translate(' + getArcCentorid(scaleRadius(d.value), d, true) + ')'
})
.text((d) => d.data.label + ': ' + d.data.value)
- 在饼图绘制组下绘制文本标签。
- 在设置位置时,先使用
scaleRadius()
获取当前扇形对象的半径,在调用中心点获取方法。
// 生成连线的点
const linePoints = drawData.map((d) => {
const line = []
// 文本位置点
const tempPoint = getArcCentorid(scaleRadius(d.value), d, true)
const tempDx = computeTextDx(d)
const dx = tempDx > 0 ? tempDx - textOffsetH : tempDx + textOffsetH
line.push(getArcCentorid(scaleRadius(d.value) * 2, d))
line.push(tempPoint)
line.push([tempPoint[0] + dx, tempPoint[1]])
return line
})
const generateLine = d3
.line()
.x((d) => d[0])
.y((d) => d[1])
arcs
.selectAll()
.data(linePoints)
.enter()
.insert('path', ':first-child')
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('d', generateLine)
- 通过饼图数据,获取扇形对象的连线点数组。第一个点:扇形对象的中心点扩大两倍。第二个点:文本未偏移的位置。第二个点:文本偏移后的位置。当不计算文本偏移时连线就只需要两个点。
- 创建线生成器,在饼图绘制组下绘制连线。
添加交互
/**
* 弧形动画
* */
function arcTweenMouse(type) {
// 设置缓动函数,为鼠标事件使用
return function () {
d3.select(this)
.transition()
.attrTween('d', function (d) {
let interpolate = null
if (type) {
interpolate = d3.interpolate(scaleRadius(d.value), scaleRadius(d.value + 20))
} else {
interpolate = d3.interpolate(scaleRadius(d.value + 20), scaleRadius(d.value))
}
return function (t) {
let arc = d3.arc().outerRadius(interpolate(t)).innerRadius(0)
return arc(d)
}
})
}
}
d3.selectAll('.arc').on('mouseover', arcTweenMouse(true)).on('mouseout', arcTweenMouse(false))
- 创建弧形动画函数,控制扇形的放大缩小。
- 代码地址