「数据可视化 D3系列」入门第十章:饼图绘制详解与实现
// 配置参数
const config = {
margin: {top: 50, right: 30, bottom: 30, left: 30},
innerRadius: 60,
outerRadius: 120,
cornerRadius: 5,
padAngle: 0.02
};
// 准备数据
const data = [
{name: "类别A", value: 56},
{name: "类别B", value: 21},
{name: "类别C", value: 11},
{name: "类别D", value: 85},
{name: "类别E", value: 42},
{name: "类别F", value: 66}
];
// 初始化SVG
const svg = d3.select('svg');
const width = +svg.attr('width');
const height = +svg.attr('height');
const chartWidth = width - config.margin.left - config.margin.right;
const chartHeight = height - config.margin.top - config.margin.bottom;
const tooltip = d3.select('.tooltip');
// 创建图表容器
const g = svg.append('g')
.attr('transform', `translate(${config.margin.left + chartWidth/2}, ${config.margin.top + chartHeight/2})`);
// 添加标题
svg.append('text')
.attr('class', 'chart-title')
.attr('x', width/2)
.attr('y', 30)
.text('数据分布饼图');
// 颜色比例尺
const colorScale = d3.scaleOrdinal()
.domain(data.map(d => d.name))
.range(d3.schemeCategory10);
// 饼图布局
const pie = d3.pie()
.sort(null)
.value(d => d.value);
// 弧形生成器
const arc = d3.arc()
.innerRadius(config.innerRadius)
.outerRadius(config.outerRadius)
.cornerRadius(config.cornerRadius)
.padAngle(config.padAngle);
// 外环弧形(用于鼠标事件)
const outerArc = d3.arc()
.innerRadius(config.outerRadius * 1.02)
.outerRadius(config.outerRadius * 1.2);
// 生成饼图数据
const arcs = pie(data);
// 绘制扇形
const slices = g.selectAll('.slice')
.data(arcs)
.enter()
.append('g')
.attr('class', 'slice');
slices.append('path')
.attr('d', arc)
.attr('fill', (d,i) => colorScale(d.data.name))
.attr('stroke', '#fff')
.attr('stroke-width', 1)
.on('mouseover', function(d) {
d3.select(this)
.transition()
.duration(200)
.attr('opacity', 0.8)
.attr('stroke-width', 2);
tooltip.transition()
.duration(200)
.style('opacity', 1);
tooltip.html(`${d.data.name}: ${d.data.value} (${((d.endAngle - d.startAngle)/(2*Math.PI)*100).toFixed(1)}%)`)
.style('left', (d3.event.pageX + 10) + 'px')
.style('top', (d3.event.pageY - 28) + 'px');
})
.on('mouseout', function() {
d3.select(this)
.transition()
.duration(200)
.attr('opacity', 1)
.attr('stroke-width', 1);
tooltip.transition()
.duration(200)
.style('opacity', 0);
})
.on('click', function(d) {
alert(`点击了${d.data.name}分类,值为${d.data.value}`);
});
// 添加标签
slices.append('text')
.attr('transform', d => `translate(${arc.centroid(d)})`)
.attr('dy', '0.35em')
.attr('text-anchor', 'middle')
.text(d => d.data.value)
.style('fill', '#fff')
.style('font-size', '12px')
.style('pointer-events', 'none');
// 添加引导线
const polyline = slices.append('polyline')
.attr('points', function(d) {
const pos = outerArc.centroid(d);
pos[0] = config.outerRadius * 0.95 * (midAngle(d) < Math.PI ? 1 : -1);
return [arc.centroid(d), outerArc.centroid(d), pos];
})
.attr('stroke', '#999')
.attr('stroke-width', 1)
.attr('fill', 'none');
// 添加分类名称
slices.append('text')
.attr('transform', function(d) {
const pos = outerArc.centroid(d);
pos[0] = config.outerRadius * 0.99 * (midAngle(d) < Math.PI ? 1 : -1);
return `translate(${pos})`;
})
.attr('dy', '0.35em')
.attr('text-anchor', function(d) {
return midAngle(d) < Math.PI ? 'start' : 'end';
})
.text(d => d.data.name)
.style('font-size', '11px');
// 辅助函数:计算中间角度
function midAngle(d) {
return d.startAngle + (d.endAngle - d.startAngle)/2;
}