「数据可视化 D3系列」入门第十章:饼图绘制详解与实现

时间:2025-04-21 17:21:39
// 配置参数 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; }