在圆形内部包裹文本

13
我正在使用d3绘制UML图,并且希望在用d3绘制的形状内换行文本。我已经完成了以下代码,但找不到解决方案使文本“适应”我的形状(见下面的图片)。
var svg =  d3.select('#svg')
    .append('svg')
        .attr('width', 500)
        .attr('height', 200);

var global = svg.append('g');

global.append('circle')
      .attr('cx', 150)
      .attr('cy', 100)
      .attr('r', 50);

global.append('text')
  .attr('x', 150)
  .attr('y', 100)
  .attr('height', 'auto')
  .attr('text-anchor', 'middle')
  .text('Text meant to fit within circle')
  .attr('fill', 'red');

result


我曾经也遇到过这个问题。据我所知,D3没有任何方式可以自动完成这项任务,你需要通过编程将单词分开成它们自己的tspan并在每行调整它们的dy属性来实现。然后将其居中于圆形上,计算半径,并在必要时缩小以适应。我没有此功能代码,因为我可以将每个单词分成自己的一行,但这就是基本思路。 - Alexander O'Mara
我有一种感觉,这是一个需要非常好的答案的非常好的问题。 - VividD
1
这里有一个类似的问题:(https://dev59.com/dHTYa4cB1Zd3GeqPwZV8)...但是,没有什么令人满意的答案! - VividD
5个回答

10

这是我所能做的最好的翻译。

输入图像描述

我想在SVG中使文本居中并包裹在圆形或矩形内。 无论文本长度如何,文本都应保持居中(水平/垂直)。

svg {
    width: 600px;
    height: 200px;
    background-color: yellow;
}
.circle {
    background-color: blue;
    height: 100%;
    border-radius: 100%;
    text-align: center;
    line-height: 200px;
    font-size: 30px;
}
.circle span {
    line-height: normal;
    display:inline-block;
    vertical-align: middle;
    color: white;
    text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
}
<svg>
    <foreignObject width="200" height="200" x="100" y="100" transform="translate(-100,-100)">
        <div class="circle">
            <span>Here is a</span>
        </div>
    </foreignObject>

    <foreignObject width="200" height="200" x="300" y="100" transform="translate(-100,-100)">
        <div class="circle">
            <span>Here is a paragraph</span>
        </div>
    </foreignObject>

    <foreignObject width="200" height="200" x="500" y="100" transform="translate(-100,-100)">
        <div class="circle">
            <span>Here is a paragraph that requires word wrap</span>
        </div>
    </foreignObject>
</svg>

transform属性不是必需的,我使用了一个translate(-r, -r),使得foreignObject的(x,y)位置与SVG圆的(cx,cy)位置相似,而且width和height均等于2*r,其中r为半径。

我这样做是为了在D3力学布局中使用作为节点。我把将此代码段转换为javascript D3的风格作为练习留给你了。


9

SVG不提供文字环绕功能,但是使用foreignObject可以实现类似的效果。假设radius是圆的半径,我们可以计算出一个适合圆内的盒子的尺寸:

var side = 2 * radius * Math.cos(Math.PI / 4),
    dx = radius - side / 2;

var g = svg.append('g')
    .attr('transform', 'translate(' + [dx, dx] + ')');

g.append("foreignObject")
    .attr("width", side)
    .attr("height", side)
    .append("xhtml:body")
    .html("Lorem ipsum dolor sit amet, ...");

为了使文本居中,应该将组件移动一小段距离。我知道这不完全是要求的内容,但它可能有所帮助。我写了一个小的演示。结果会像这样:

enter image description here


1
遗憾的是,foreignObject 元素无法在垂直方向上居中。如果文本太短,它将对齐到框的顶部。 - Pablo Navarro
3
请记住,foreignObject 不与 IE 兼容。 - Matt

5
如果您将内容放在SVG形状下面的

这真的很好,但它在长单词上会出现问题。我们可以截断/连字它们,但最大可能长度取决于该单词在圆圈内的哪一行。 - Olivvv

1

虽然不是最理想的,但 @Pablo.Navarro 的回答启发了我找到了以下方法。

var svg =  d3.select('#svg')
  .append('svg')
    .attr('width', 500)
    .attr('height', 200);

var radius = 60,
    x      = 150,
    y      = 100,
    side   = 2 * radius * Math.cos(Math.PI / 4),
    dx     = radius - side / 2;

var global = svg.append('g')
  .attr('transform', 'translate(' + [ dx, dx ] + ')');

global.append('circle')
  .attr('cx', x)
  .attr('cy', y)
  .attr('r', radius);

global.append('foreignObject')
  .attr('x', x - (side/2))
  .attr('y', y - (side/2))
  .attr('width', side)
  .attr('height', side)
  .attr('color', 'red')
  .append('xhtml:p')
    .text('Text meant to fit within circle')
    .attr('style', 'text-align:center;padding:2px;margin:2px;');

结果

result


是的,但你需要一个相当大的圆。换句话说,这种方法没有有效地利用圆形。 - VividD
同意,这不是理想的解决方案,但它给出了我想要的结果。仍然希望正确的答案出现。最终,文本和圆之间需要建立关系,并且它们需要相应地缩放。 - vladiim
你说得对。顺便说一句,令人惊讶的是这么多年过去了,竟然没有人解决它。非常好的问题! - VividD
我添加了我的答案,希望它更好一些。如果文本过长,您仍然需要注意字体大小。 - bformet

1

对我来说,这是目前为止最好的解决方案。

// based on code: https://observablehq.com/@mbostock/fit-text-to-circle

function createChart(lines, lineHeight) {
      const width = 180;
      const height = width;
      const radius = Math.min(width, height) / 2 - 4;

      const svg = d3
        .select("#graph")
        .append("svg")
        .style("font", "10px sans-serif")
        .style("width", "500px")
        .style("height", "500px")
        .attr("text-anchor", "middle");

      svg
        .append("circle")
        .attr("cx", width / 2)
        .attr("cy", height / 2)
        .attr("fill", "#ccc")
        .attr("r", radius);

      svg
        .append("text")
        .attr(
          "transform",
          `translate(${width / 2},${height / 2}) scale(${
            radius / textRadius(lines, lineHeight)
          })`
        )
        .selectAll("tspan")
        .data(lines)
        .enter()
        .append("tspan")
        .attr("x", 0)
        .attr("y", (d, i) => (i - lines.length / 2 + 0.8) * lineHeight)
        .text((d) => d.text);

      return svg.node();
    }

    function textRadius(lines, lineHeight) {
      let radius = 0;
      for (let i = 0, n = lines.length; i < n; ++i) {
        const dy = (Math.abs(i - n / 2 + 0.5) + 0.5) * lineHeight;
        const dx = lines[i].width / 2;
        radius = Math.max(radius, Math.sqrt(dx ** 2 + dy ** 2));
      }
      return radius;
    }

    function createWords(text) {
      const words = text.split(/\s+/g); // To hyphenate: /\s+|(?<=-)/
      if (!words[words.length - 1]) words.pop();
      if (!words[0]) words.shift();
      return words;
    }

    function createLines(words) {
      let line;
      let lineWidth0 = Infinity;
      const lines = [];
      for (let i = 0, n = words.length; i < n; ++i) {
        let lineText1 = (line ? line.text + " " : "") + words[i];
        let lineWidth1 = measureWidth(lineText1);

        if ((lineWidth0 + lineWidth1) / 2 < targetWidth) {
          line.width = lineWidth0 = lineWidth1;
          line.text = lineText1;
        } else {
          lineWidth0 = measureWidth(words[i]);
          line = { width: lineWidth0, text: words[i] };
          lines.push(line);
        }
      }
      return lines;
    }

    function measureWidth(text) {
      const ctx = document.createElement("canvas").getContext("2d");
      return ctx.measureText(text).width;
    }

    const text =
      "Hello! This notebookshows how to wrap andfit text inside a circle. Itmight be useful forlabelling a bubble chart.You can edit the textbelow, or read the notesand code to learn howit works! ";

    const lineHeight = 12;
    const targetWidth = Math.sqrt(measureWidth(text.trim()) * lineHeight);
    const lines = createLines(createWords(text));

    createChart(lines, lineHeight);
 <head>
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.1.1/d3.min.js"
      crossorigin="anonymous"
      referrerpolicy="no-referrer"
    ></script>
  </head>  

<body>
    <div id="graph"></div>
  </body>


为什么这是最佳解决方案?你是如何做到的?你能通过编辑你的回答来解释一下吗 :) - Elikill58

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接