Reputation: 1960
I'm using d3 to draw a UML diagram and would like to wrap text within the shapes drawn with d3. I've gotten as far as the code below and can't find a solution to make the text 'fit' within my shape (see image below).
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');
Upvotes: 13
Views: 14574
Reputation: 72
For me is the best solution so far.
// 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>
Upvotes: 1
Reputation: 19127
If you add your content inside a <text>
element immediately below the SVG shape, then you can use D3plus' .textwrap()
function to do exactly this. I quote from the documentation:
Using
d3plus.textwrap
, SVG<text>
elements can be broken into separate<tspan>
lines, as HTML does with<div>
elements.... D3plus automatically detects if there is a<rect>
or<circle>
element placed directly before the<text>
container element in DOM, and uses that element's shape and dimensions to wrap the text. If it can't find one, or that behavior needs to be overridden, they can manually be specified using.shape( )
,.width( )
, and.height( )
.
I've created a codepen to better illustrate this since the examples in the documentation can be a little confusing: http://codepen.io/thdoan/pen/rOPYxE
Upvotes: 5
Reputation: 14191
Here is the best I could do.
I want to center and wrap a text inside a circle or rect in SVG. The text should remain centered (horizontal/vertical) whatever the text length.
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>
The transform attribute is not mandatory, I'm using a translate(-r, -r) so that the (x,y) of the foreignObject is like the (cx, cy) of the SVG circle, and width, height = 2*r with r the radius.
I did this to use as nodes within a D3 force layout. I leave as an exercise to translate this snippet into javascript D3's style.
Upvotes: 10
Reputation: 1960
It's not ideal, but @Pablo.Navarro's answer led me to the following.
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;');
Upvotes: 1
Reputation: 8264
SVG doesn't provide text wrapping, but using foreignObject
you can achieve a similar effect. Assuming that radius
is the radius of the circle, we can compute the dimensions of a box that will fit inside the circle:
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, ...");
The group should be displaced a small amount to have the text centered. I know that this is not exactly what is asked, but it can be helpful. I wrote a small fiddle. The result will look like this:
Upvotes: 9