D3.js弧形文本换行对齐问题及弧形阴影实现求助
解决D3.js弧形文本对齐换行与阴影问题
我来帮你搞定这两个D3.js的问题——弧形段长文本的对齐换行,以及弧形阴影的最优实现方案。
一、弧形文本对齐与换行问题
你当前的问题主要出在文本旋转逻辑不准确和wrap函数没有适配弧形文本的坐标系上。下面是修正后的方案:
1. 修正文本旋转逻辑
原transformLabel函数的旋转计算有误,导致文本方向不对。我们需要基于弧形的中间角度计算旋转,并且在文本倒转的时候翻转180度,保证可读性:
function transformLabel(d){ const centroid = arc.centroid(d); const midAngle = (d.startAngle + d.endAngle) / 2; // 计算旋转角度,让文本沿弧形切线方向对齐 let rotation = midAngle * 180 / Math.PI; // 当角度在90°-270°之间时,翻转文本避免倒着显示 const flip = midAngle > Math.PI/2 && midAngle < 3*Math.PI/2 ? 180 : 0; rotation -= 90; // 调整基准旋转,让文本垂直于半径 return `translate(${centroid}) rotate(${rotation}) rotate(${flip})`; }
2. 适配弧形文本的wrap函数
原wrap函数是基于普通静态文本的x/y定位,而弧形文本已经经过translate和rotate,所以需要调整tspan的坐标逻辑,同时让多行文本居中:
function wrap(text, maxWidth) { text.each(function () { const textEl = d3.select(this); const words = textEl.text().split(/\s+/).reverse(); let word; let line = []; let lineNumber = 0; const lineHeight = 1.1; // 行高(em单位) const baseDy = parseFloat(textEl.attr("dy")) || 0; // 清空原有文本,创建第一个tspan(x设为0,因为已经通过translate定位) let tspan = textEl.text(null) .append("tspan") .attr("x", 0) .attr("y", 0) .attr("dy", baseDy + "em"); while (word = words.pop()) { line.push(word); tspan.text(line.join(" ")); if (tspan.node().getComputedTextLength() > maxWidth) { line.pop(); tspan.text(line.join(" ")); line = [word]; tspan = textEl.append("tspan") .attr("x", 0) .attr("y", 0) .attr("dy", ++lineNumber * lineHeight + baseDy + "em") .text(word); } } // 调整所有tspan的dy,让多行文本整体居中 const totalLines = lineNumber + 1; textEl.selectAll("tspan") .attr("dy", (_, i) => { return baseDy + (i - (totalLines - 1)/2) * lineHeight + "em"; }); }); }
3. 调用wrap函数
在添加子分类文本时,调用wrap并指定合适的最大宽度(根据你的弧形大小调整,这里用80作为示例):
g.append("text") .attr("transform", function(d) { return transformLabel(d); }) .attr("dy", ".35em") .text(function(d) { return d.data.subcategory; }) .call(wrap, 80); // 指定每行最大宽度
二、弧形阴影的最优方案
给弧形添加阴影最灵活可靠的方式是使用SVG滤镜(filter),可以精确控制阴影的模糊度、偏移、透明度等参数。
1. 定义阴影滤镜
在SVG的<defs>中添加滤镜定义(可以放在svg创建之后):
// 在创建svg之后添加滤镜 svg.append("defs") .append("filter") .attr("id", "arcShadow") .attr("x", "-20%") // 滤镜范围,确保阴影不会被裁剪 .attr("y", "-20%") .attr("width", "140%") .attr("height", "140%") .append("feGaussianBlur") .attr("in", "SourceAlpha") .attr("stdDeviation", 3); // 阴影模糊程度,数值越大越模糊 svg.select("#arcShadow") .append("feOffset") .attr("dx", 2) // 阴影水平偏移 .attr("dy", 2) // 阴影垂直偏移 .attr("result", "offsetblur"); svg.select("#arcShadow") .append("feComponentTransfer") .append("feFuncA") .attr("type", "linear") .attr("slope", 0.5); // 阴影透明度,0-1之间 svg.select("#arcShadow") .append("feMerge") .append("feMergeNode"); svg.select("#arcShadow feMerge") .append("feMergeNode") .attr("in", "SourceGraphic");
2. 应用阴影滤镜
给弧形path添加filter属性即可:
g.append("path") .attr("d", arc) .style("fill", function(d) { return color(d.data.category); }) .attr("filter", "url(#arcShadow)"); // 应用阴影滤镜
如果需要给不同层级的弧形(比如外层的arc和内层的wedge)设置不同阴影,可以创建多个滤镜(比如#wedgeShadow),分别应用即可。
修正后的完整代码
<!DOCTYPE html> <meta charset="utf-8"> <style> .arc text, .wedge text { font: 10px sans-serif; text-anchor: middle; } .arc path, .wedge path { stroke: #fff; } </style> <body> <script src="https://d3js.org/d3.v3.min.js"></script> <script> var width = 494, height = 504, radius = Math.min(width, height) / 2; var color = d3.scale.ordinal() .range(["#f38e36","#d79ae2","#dac2ad","#89e1ea"]) .domain(["Social", "Livability","Knowledge","Economy"]); var arc = d3.svg.arc() .outerRadius(radius - 0) .innerRadius(radius - 110); var wedge = d3.svg.arc() .outerRadius(radius - 110) .innerRadius(0); var labelWedge = d3.svg.arc() .outerRadius(radius-200) .innerRadius(radius-200); var pie = d3.layout.pie() .sort(null) .value(function(d) { return d.count; }); var svg = d3.select("body").append("svg") .attr("width", width) .attr("height", height) .append("g") .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); // 添加阴影滤镜 svg.append("defs") .append("filter") .attr("id", "arcShadow") .attr("x", "-20%") .attr("y", "-20%") .attr("width", "140%") .attr("height", "140%") .append("feGaussianBlur") .attr("in", "SourceAlpha") .attr("stdDeviation", 3); svg.select("#arcShadow") .append("feOffset") .attr("dx", 2) .attr("dy", 2) .attr("result", "offsetblur"); svg.select("#arcShadow") .append("feComponentTransfer") .append("feFuncA") .attr("type", "linear") .attr("slope", 0.5); svg.select("#arcShadow") .append("feMerge") .append("feMergeNode"); svg.select("#arcShadow feMerge") .append("feMergeNode") .attr("in", "SourceGraphic"); // 添加内层wedge的阴影滤镜 svg.append("defs") .append("filter") .attr("id", "wedgeShadow") .attr("x", "-20%") .attr("y", "-20%") .attr("width", "140%") .attr("height", "140%") .append("feGaussianBlur") .attr("in", "SourceAlpha") .attr("stdDeviation", 2); svg.select("#wedgeShadow") .append("feOffset") .attr("dx", 1) .attr("dy", 1) .attr("result", "offsetblur"); svg.select("#wedgeShadow") .append("feComponentTransfer") .append("feFuncA") .attr("type", "linear") .attr("slope", 0.4); svg.select("#wedgeShadow") .append("feMerge") .append("feMergeNode"); svg.select("#wedgeShadow feMerge") .append("feMergeNode") .attr("in", "SourceGraphic"); d3.csv("data.csv", type, function(error, data) { if (error) throw error; nested = [{"category":"Social","count":25},{"category":"Livability","count":25},{"category":"Knowledge","count":25},{"category":"Economy","count":25}]; data1= [{"category":"Social","subcategory":"Eduction and Employment","count":0.39}, {"category":"Social","subcategory":"Social Support","count":0.39}, {"category":"Social","subcategory":"Housing & Communities","count":0.39}, {"category":"Social","subcategory":"Civil Society","count":0.39}, {"category":"Livability","subcategory":"Activation and Calendaring","count":0.26}, {"category":"Livability","subcategory":"Mobility","count":0.26}, {"category":"Livability","subcategory":"Nature","count":0.26}, {"category":"Livability","subcategory":"Healthy and Sustainable Lifestyle","count":0.26}, {"category":"Livability","subcategory":"Marketing","count":0.26}, {"category":"Livability","subcategory":"Source of Distinctiveness","count":0.26}, {"category":"Knowledge","subcategory":"Vibrant Tech Companies","count":0.39}, {"category":"Knowledge","subcategory":"Tech Talent Attraction","count":0.39}, {"category":"Knowledge","subcategory":"Distinctive Tech Eduction","count":0.39}, {"category":"Knowledge","subcategory":"Full-Fledge R&D System","count":0.39}, {"category":"Economy","subcategory":"Business Environment Companiesetitiveness","count":0.39}, {"category":"Economy","subcategory":"Tourism","count":0.39}, {"category":"Economy","subcategory":"SME Support","count":0.39}, {"category":"Economy","subcategory":"Private Sector Incentivization","count":0.39}]; console.log("nested", nested); console.log("data1", data1); var g = svg.selectAll(".arc") .data(pie(data1)) .enter().append("g") .attr("class", "arc"); g.append("path") .attr("d", arc) .style("fill", function(d) { return color(d.data.category); }) .attr("filter", "url(#arcShadow)"); // 应用外层弧形阴影 g.append("text") .attr("transform", function(d) { return transformLabel(d); }) .attr("dy", ".35em") .text(function(d) { return d.data.subcategory; }) .call(wrap, 80); // 调用wrap函数,设置每行最大宽度80 var g = svg.selectAll(".wedge") .data(pie(nested)) .enter().append("g") .attr("class", "wedge"); g.append("path") .attr("d", wedge) .style("fill", function(d) { return color(d.data.category); }) .attr("filter", "url(#wedgeShadow)"); // 应用内层楔形阴影 g.append("text") .attr("transform", function(d) { return "translate(" + labelWedge.centroid(d) + ")"; }) .attr("dy", ".35em") .text(function(d) { return d.data.category; }); }); function type(d) { d.count = +d.count; return d; } function transformLabel(d){ const centroid = arc.centroid(d); const midAngle = (d.startAngle + d.endAngle) / 2; let rotation = midAngle * 180 / Math.PI; const flip = midAngle > Math.PI/2 && midAngle < 3*Math.PI/2 ? 180 : 0; rotation -= 90; return `translate(${centroid}) rotate(${rotation}) rotate(${flip})`; } function wrap(text, maxWidth) { text.each(function () { const textEl = d3.select(this); const words = textEl.text().split(/\s+/).reverse(); let word; let line = []; let lineNumber = 0; const lineHeight = 1.1; const baseDy = parseFloat(textEl.attr("dy")) || 0; let tspan = textEl.text(null) .append("tspan") .attr("x", 0) .attr("y", 0) .attr("dy", baseDy + "em"); while (word = words.pop()) { line.push(word); tspan.text(line.join(" ")); if (tspan.node().getComputedTextLength() > maxWidth) { line.pop(); tspan.text(line.join(" ")); line = [word]; tspan = textEl.append("tspan") .attr("x", 0) .attr("y", 0) .attr("dy", ++lineNumber * lineHeight + baseDy + "em") .text(word); } } const totalLines = lineNumber + 1; textEl.selectAll("tspan") .attr("dy", (_, i) => { return baseDy + (i - (totalLines - 1)/2) * lineHeight + "em"; }); }); } </script> </body> </html>
内容的提问来源于stack exchange,提问作者Abhishek Hingu
相关产品推荐
相关产品推荐

