How to resize the text to fit in any given polygon in D3js ?
I need something like in the picture:
I found similar topics but no usable resolutions: too old/deprecated/examples not working.
CodePudding user response:
This question essentially boils down to finding a maximal rectangle inside a polygon, in this case aligned with the horizontal axis and of fixed aspect ratio, which is given by the text.
Finding this rectangle in an efficient way is not an easy task, but there are algorithms available. For example, the largestRect
method in the d3plus-library. The details of this algorithm (which finds a good but not an optimal rectangle) are described in this blog post.
With the coordinates of the rectangle, you can transform the text such that it is contained in the rectangle, i. e.
- translate to the bottom left point of the rectangle and
- scale by the ratio of the width of the rectangle and the width of the text.
If you don't want to add an additional library to your dependency list and the polygons you are considering are (almost) convex and not highly irregular, you could try to find a "satisfying rectangle" by yourself. Below, I did a binary search on rectangles centered around the centroid of the polygon. In each iteration I check wether the four corners are inside the polygon using the d3.polygonContains
method of d3-polygon. The resulting rectangle is green for comparison. Of course, this would just be a starting point.
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>