You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 &amp; 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&amp;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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 09:15:59