D3.js - 调整文本大小以适应任何多边形

D3.js - Resize text to fit any polygon

如何调整文本大小以适应 D3js 中的任何给定多边形?

我需要如图所示的东西:

我发现类似 topics 但没有可用的分辨率:同样 old/deprecated/examples 不工作。

这个问题本质上归结为在多边形内找到一个最大的矩形,在这种情况下与水平轴对齐并具有固定的纵横比,这由文本给出。

以高效的方式找到这个矩形不是一件容易的事,但是有可用的算法。例如d3plus-library. The details of this algorithm (which finds a good but not an optimal rectangle) are described in this blog post.

中的largestRect方法

利用矩形的坐标,您可以变换文本,使其包含在矩形中,i。 e.

  • 转换到矩形的左下角并
  • 按矩形宽度与文本宽度的比例缩放。

如果您不想在您的依赖列表中添加额外的库,并且您正在考虑的多边形(几乎)是凸的并且不是高度不规则的,您可以尝试自己找到一个“令人满意的矩形”。下面,我对以多边形质心为中心的矩形进行了二进制搜索。在每次迭代中,我使用 d3-polygond3.polygonContains 方法检查四个角是否在多边形内部。生成的矩形为绿色以供比较。当然,这只是一个起点。

const dim = 500;
const svg = d3.select("svg").attr("width", dim).attr("height", dim);
const text = svg.append("text").attr("x", 0).attr("y", 0);
const polygon = svg.append("polygon").attr("fill", "none").attr("stroke", "blue");
const rectangle = svg.append("polygon").attr("fill", "none").attr("stroke", "red");
const rectangle2 = svg.append("polygon").attr("fill", "none").attr("stroke", "green");

d3.select("input").on("change", fitText);
d3.select("button").on("click", drawPolygon);

// Draw random polygon
function drawPolygon() {
  const num_points = 3 + Math.ceil(7 * Math.random());
  points = [];
  for (let i = 0; i < num_points; i++) {
    const angle = 2 * Math.PI / num_points * (i + 0.1 + 0.8 * Math.random());
    const radius = dim / 2 * (0.1 + 0.9 * Math.random());
    points.push([
      radius * Math.cos(angle) + dim / 2,
      radius * Math.sin(angle) + dim / 2,
    ])
  }
  polygon.attr("points", points.map(d => d.join()).join(' '));
  fitText();
}

function fitText() {
  // Set text to input value and reset transform.
  text.text(d3.select("input").property("value")).attr("transform", null);
  // Get dimensions of text
  const text_dimensions = text.node().getBoundingClientRect();
  const ratio = text_dimensions.width / text_dimensions.height;
  // Find largest rectangle
  const rect = d3plus.largestRect(points, {angle: 0, aspectRatio: ratio}).points;
  // transform text
  const scale = (rect[1][0] - rect[0][0]) / text_dimensions.width;
  text.attr("transform", `translate(${rect[3][0]},${rect[3][1]}) scale(${scale})`);
  rectangle.attr("points", rect.map(d => d.join()).join(' '));
  // alternative
  const rect2 = satisfyingRect(ratio);
  rectangle2.attr("points", rect2.map(d => d.join()).join(' '));
}

function satisfyingRect(ratio) {
    // center rectangle around centroid
    const centroid = d3.polygonCentroid(points);
  let minWidth = 0;
  let maxWidth = d3.max(points, d => d[0]) - d3.min(points, d => d[0]);
  let rect;
  for (let i = 0; i < 20; i++) {
    const width = 0.5 * (maxWidth + minWidth);
    rect = [
      [centroid[0] - width, centroid[1] - width / ratio],
      [centroid[0] + width, centroid[1] - width / ratio],
      [centroid[0] + width, centroid[1] + width / ratio],
      [centroid[0] - width, centroid[1] + width / ratio]
    ]
    if (rect.every(d => d3.polygonContains(points, d)))
      minWidth = width;
    else
      maxWidth = width;
   }
   return rect;
}

let points;
drawPolygon();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.3.0/d3.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/d3plus-shape@1"></script>

<div>
<input type="text" value="lorem ipsum dolor">
<button>New polygon</button>
</div>
<svg></svg>