Placing Texts on Arcs with D3.js

A tutorial showcasing the different uses of creating a bendable text

Posted: September 30, 2015-Likes: 0-Comments: 19-Categories: D3.js, Interactive, Tutorial-Tags: D3.js, Data Visualization, Donut chart
You are here: ...
Home / Blog / D3.js / Placing text on arcs with D3.js

In this tutorial I want to go into something I’ve learned while creating my “A Closer Look into the Division of Occupations & Age” interactive data visualisation: placing SVG text on arcs or paths. Because I’ve placed bar charts inside each circle in the occupation piece, I couldn’t just put the text in the middle of a circle as well, like the standard Zoomable Pack Layout has. Placing the titles of parent circles on the outer edge of the circle seemed like the most fitting alternative in my opinion. But circle packing is by far from the only case when placing text on a non-straight line could come in handy. Let me illustrate this with several examples

The Basics

Starting off with the bare necessities. You really don’t need to do much to place text on any kind of path that you want. The first thing that you need is the path itself; you need to tell the browser what the exact shape is along which the text needs to be placed. Once you have that figured out, you can append a textPath element to the SVG, which tells the browser that the text needs to be rendered along the shape of a path.

SVG paths can take a whole array of wonderful shapes. For a good tutorial on the different shapes they can take please look at this SVG path element tutorial. One random example of a path notation to create a wavy line path is M10,90 Q100,15 200,70 Q340,140 400,30. Below you can see the result of placing text on this SVG path.

Don’t worry too much about how this looks, in the following steps I’ll explain how to create a circular path notation which is very straightforward and in many of the later examples D3 easily calculates these paths for you.

Text on an SVG path in its simplest form

The code for the above image (with some unnecessary CSS styling lines taken out to keep the clutter low) can be seen down below. Create an SVG and append a path element to it where you supply the arc notation to the d command. It’s very important to give this path element a unique ID that you can reference later on when creating the textPath element.

Usually you want the path itself to be invisible and only see the text, but in the above example I chose to show the path line as well, so I only put the fill to none and not the stroke as well.

After the path element, you have to create a text element to which a textPath element can be attached. Just appending the textPath to the SVG will not work, it needs to be appended to a text element first.

Next comes the important path, you need to supply the ID of the path along which you want to place the text. This can be done with by providing the xlink:href statement with the ID. Afterwards you really only have to add a .text() statement and it will all work. The text would be placed on the arc and be positioned on the left starting point. To make the text center on the arc, just add the startOffset (a nice default attribute of a textPath element) and text-anchor lines that you can see in the code



//Create the SVG
var svg = d3.select("body").append("svg")
		.attr("width", 400)
		.attr("height", 120);
			
//Create an SVG path			
svg.append("path")
	.attr("id", "wavy") //very important to give the path element a unique ID to reference later
	.attr("d", "M 10,90 Q 100,15 200,70 Q 340,140 400,30") //Notation for an SVG path, from bl.ocks.org/mbostock/2565344
	.style("fill", "none")
	.style("stroke", "#AAAAAA");

//Create an SVG text element and append a textPath element
svg.append("text")
   .append("textPath") //append a textPath to the text element
	.attr("xlink:href", "#wavy") //place the ID of the path here
	.style("text-anchor","middle") //place the text halfway on the arc
	.attr("startOffset", "50%")		
	.text("Yay, my text is on a wavy path");

Creating an Arc

Most other tutorials or stackoverflow questions that I found while figuring this out myself stopped right about here. But I love creating (endlessly) long tutorials, so read on if you want to see what other interesting things you can do :)

Although you’re able to place text on any kind of path that you can think of with the M’s, Q’s, A’s and Z’s of the SVG path element, most of the time you’ll just want to place text along a circle. Therefore, I wanted to go a bit deeper into the path notation of an SVG arc (although you probably never have to write this yourself). If you’re not interested in knowing this, that’s fine too, just skip ahead to the next section. For those wanting to understand more, let me first show the different elements and afterwards explain them one by one.



//Creating an Arc path
M start-x, start-y A radius-x, radius-y, x-axis-rotation, large-arc-flag, sweep-flag, end-x, end-y

First we need to supply the location of the starting position. This is done with the M (from move) command followed by the x and y coordinate of this location on the screen. This is not unique to arcs,  you need to supply this for every kind of SVG path.

We can draw an Arc with the A command followed by the radius of the arc in the x and y direction. For circles both of these numbers are the same.

The third parameter is the x-axis-rotation. This sets the rotation of the arc’s x-axis. Leave it at 0

The large-arc-flag & sweep-flag are needed because just supplying the start & end position together with the radii still leaves you with 4 possible arcs that can fit that description (for ellipses). I found that w3 gives the best explanation and image.

  • Of the four candidate arc sweeps, two will represent an arc sweep that is > 180 degrees (the “large-arc”), and two will represent an arc sweep that is < 180 degrees (the “small-arc”). If large-arc-flag is 1, then one of the two larger arc sweeps will be chosen; otherwise, if large-arc-flag is 0 one of the smaller arc sweeps will be chosen.
  • If sweep-flag is 1, then the arc will be drawn in a positive-angle/clockwise direction. A value of 0 causes the arc to be drawn in a negative-angle/counter-clockwise direction.

Visual explanation of the large-arc-flag and the sweep-flag

And finally the position of the ending x and y coordinate are supplied to the arc notation.

To get the image below, we can use the script supplied in the previous section and replace the arc notation by M 0,300 A 200,200 0 0,1 400,300.  Which will become a half circle, starting at [0,300] pixels with a radius of 200 pixels (a circle), no x-axis-rotation. Since I made an exact half circle, the large-arc-flag doesn’t actually have any impact and both 0 and 1 give the same result. I want the line to run clockwise from left to right, thus the sweep-flag has to be 1 and finally the path ends at the location of [400,300] pixels

SVG text on a circular arc

Simple Animations with Arcs

Creating a transition between two different arcs is actually quite easy. You only have to update the original path element itself and the text will move with it automatically. So all you have to do to go from the bigger circle in the section above to a somewhat smaller circle is add the following code to the script. It calls a transition of the path and supplies an arc notation of a smaller circle.



//Transition from the original arc path to a new arc path
svg.selectAll("path")
	.transition().duration(2000).delay(2000)
	.attr("d", "M75,300 A125,125 0 0,1 325,300");

It’s also possible to transition between more complex paths, but usually you can’t use the default .transition() statement and supply the new arc notation afterwards (I’ve tried). Luckily Mike Bostock has supplied a nice example of a function that is capable of providing an interpolation between any kind of SVG path. Using the pathTween function from the example, you can transition between the path of the very first image in this blog to the second image in this blog. And again, you only have to transition the path element, the textPath will follow whatever happens

Adding Arcs to Donut Charts

One option for placing texts along arcs is when creating donut charts (sort of the same the arcs of chord diagrams). In this instance we can use the arc notations that the d3.svg.arc() command creates for us as paths to stick a textPath to.

For this example I want to create an arc where each slice is a month in a year. I’ve created a dataset that supplies the starting day number (in the year) and ending day number for each month. I won’t go into the first part on how to make a donut chart (you can find the full code in the links in the last section of this blog), but once you create the slices, you have to supply one extra command, and that is giving each slice that unique ID



//Draw the arcs themselves
svg.selectAll(".monthArc")
	.data(pie(monthData))
   .enter().append("path")
	.attr("class", "monthArc")
	.attr("id", function(d,i) { return "monthArc_"+i; }) //Give each slice a unique ID
	.attr("d", arc);

It can be as simple as using the iterator i to create unique IDs. Next append the text and textPath elements to the svg



//Append the month names to each slice
svg.selectAll(".monthText")
	.data(monthData)
   .enter().append("text")
	.attr("class", "monthText")
   .append("textPath")
	.attr("xlink:href",function(d,i){return "#monthArc_"+i;})
	.text(function(d){return d.month;});	

You can see the resulting image below. The text begins at the starting position of each arc and follows its contours

Text along a donut chart without supplying an extra x and y attribute

But I want the text to be placed inside each arc and a bit away from the starting line. To do this, I can add an x attribute to move it along the arc and a dy attribute to move it above or below the arc



//Append the month names within the arcs
svg.selectAll(".monthText")
	.data(monthData)
   .enter().append("text")
	.attr("class", "monthText")
	.attr("x", 5)   //Move the text from the start angle of the arc
	.attr("dy", 18) //Move the text down
   .append("textPath")
	.attr("xlink:href",function(d,i){return "#monthArc_"+i;})
	.text(function(d){return d.month;});	

Which results in the following image. Pretty easy right? We didn’t even have to do anything with actual arc notations to create this

Text along a donut chart with a non-zero x and dy attribute

Centering Text along Donut Charts

You might wonder why centering the text from the previous example along the arc, instead of placing at the start would need its own section. Why not use the text-anchor and startOffset like before?

The problem is that the actual arc of each donut slice is the entire outline. So an arc section on top at the outer radius, a line segment, another arc section for the inner radius and then it closes the path, which happens to look like another line segment

The 4 sections that make up a donut slice path

So if you set the startOffset to 50% like we did before, you might now expect the kind of image that results:

Setting the startOffset to 50% is not a good idea in this case

Luckily, we can do something about it :) It doesn’t take many lines, but if you’re new to regular expressions some parts of the code might seem a bit odd to you.

To change things up a bit I’ve created a completely random dataset about animals (I leave it up to you to imagine what the length of each slice might represent ;) ) and turned this into a donut chart. I already added the label names, put placed them along the start of each slice

One other change is that I made this donut chart start at -90 degrees instead of at 0 degrees. It makes things easier to explain later on. Changing the starting angle can be achieved in the d3.layout.pie() statement with the startAngle and endAngle



//Turn the pie chart 90 degrees counter clockwise, so it starts at the left	
var pie = d3.layout.pie()
	.startAngle(-90 * Math.PI/180)
	.endAngle(-90 * Math.PI/180 + 2*Math.PI)
	.value(function(d) { return d.value; })
	.padAngle(.01)
	.sort(null);

Starting with the labels at the beginning of each Donut slice

We can inspect the total arc statement for the Cheetah slice with arc(pie(donutData)[2]), which returns the following path



//The total arc statement of the Cheetah slice	
M-52.2173956570654,-195.6517150213447A202.5,202.5 0 0,1 138.3415227728006,-147.87789921723495L117.70250731088419,-126.10459853919384A172.5,172.5 0 0,0 -44.2910698933094,-166.71697912242166Z

That’s a rather incomprehensible piece of text. But if we look closer, we can detect that the first part follows that notation for an arc path as explained before. Rounding it off a bit and placing some spaces, the first section reads  M -52.2,-195.7 A 202.5, 202.5 0 0,1 138.3,-147.9 an arc starting at [-52.2,-195.7] with a radius of 202.5, running clockwise to [138.8,-147,9].

To use the startOffset at 50% again to center the labels, we need to extract this first arc statement from the total arc path that d3.svg.arc() gives us and then create a second set of (invisible) arc paths that run only alongside the outer radius of each slice. Let me first show you the code that does exactly this and explain it piece by piece next.



//Create the donut slices and also the invisible arcs for the text 
svg.selectAll(".donutArcSlices")
	.data(pie(donutData))
  .enter().append("path")
	.attr("class", "donutArcSlices")
	.attr("d", arc)
	.style("fill", function(d,i) { return colorScale(i); })
	.each(function(d,i) {
		//A regular expression that captures all in between the start of a string (denoted by ^) 
		//and the first capital letter L
		var firstArcSection = /(^.+?)L/; 

		//The [1] gives back the expression between the () (thus not the L as well) 
		//which is exactly the arc statement
		var newArc = firstArcSection.exec( d3.select(this).attr("d") )[1];
		//Replace all the comma's so that IE can handle it -_-
		//The g after the / is a modifier that "find all matches rather than stopping after the first match"
		newArc = newArc.replace(/,/g , " ");
						
		//Create a new invisible arc that the text can flow along
		svg.append("path")
			.attr("class", "hiddenDonutArcs")
			.attr("id", "donutArc"+i)
			.attr("d", newArc)
			.style("fill", "none");
	});
	
//Append the label names on the outside
svg.selectAll(".donutText")
	.data(donutData)
   .enter().append("text")
	.attr("class", "donutText")
	.attr("dy", -13)
   .append("textPath")
	.attr("startOffset","50%")
	.style("text-anchor","middle")
	.attr("xlink:href",function(d,i){return "#donutArc"+i;})
	.text(function(d){return d.name;});

The first six lines are normal when creating a donut chart, we’ve even removed the line with .attr(“id”, ) since we are going to create new arcs for the textPaths. Next it goes into an .each() statement which will execute the code within for each data element and where we can reference the data with the normal d and i. In this .each() section the arc notation of the outer radius is extracted.

By looking at that horrible arc path that arc(pie(donutData)[2]) returns, we would like to save the section from the start of the result until we get to the capital L (but not including the L) into a new arc path.  This can be done by using a regular expression. A regular expression, or regex, is “a sequence of characters that define a search pattern”. If you’re serious about doing data analytics or visualization, you’re bound to run into these at some point.

The variable firstArcSection saves the regex needed for the extraction. It will capture all in between the start of a string (denoted by ^) and the first capital letter L (The letter L in an SVG path denotes the start of a line segment). The “all in between” is denoted by the .+? where the . is a regular expression for “match any single character except the newline character”. The + means “match the preceding expression 1 or more times” (since the . precedes it, it means any single character 1 or more times). The ? has to be added to make sure that it stops at the first L it finds, not the last L. It thus makes sure that the idea of ^.*L matches the fewest possible characters. For more information on regular expressions see this link

The firstArcSection is applied to the total arc (given by d3.select(this).attr(“d”)) and the result is saved in newArc, which is only the path section that runs along the outer radius of the donut slice (the arc) and is what we want to link the textPath to. Therefore a new (invisible) path using newArc is added to the SVG in the last section of the .each() with, of course, a unique ID.

Afterwards, the labels are created just like before with a reference to the hidden paths. Now we can safely use the startOffset and text-anchor styles again

(The code above is based on an answer that AmeliaBR gave on stackoverflow, but I can’t seem to find it again, sorry…)

The resulting image looks as follows

The text arcs are now nicely centered along the donut slices

Flipping the Text on the Bottom Half

You could already feel like it’s finished with that look. But I find those labels along the bottom half, that are upside down, rather hard to read. I’d prefer it if those labels were flipped, so I can read them from left to right again.

To accomplish this, we need to switch the start and end coordinates of the current arc paths along the bottom half so they are drawn from left to right. Furthermore, the sweep-flag has to be set to 0 to get the arc that runs in a counterclockwise fashion from left to right

So for the final act, let’s add a few more lines of code to the .each() statement



//Create the new invisible arcs and flip the direction for those labels on the bottom half
.each(function(d,i) {
	//Search pattern for everything between the start and the first capital L
	var firstArcSection = /(^.+?)L/; 	

	//Grab everything up to the first Line statement
	var newArc = firstArcSection.exec( d3.select(this).attr("d") )[1];
	//Replace all the commas so that IE can handle it
	newArc = newArc.replace(/,/g , " ");
	
	//If the end angle lies beyond a quarter of a circle (90 degrees or pi/2) 
	//flip the end and start position
	if (d.endAngle > 90 * Math.PI/180) {
		var startLoc 	= /M(.*?)A/,		//Everything between the capital M and first capital A
			middleLoc 	= /A(.*?)0 0 1/,	//Everything between the capital A and 0 0 1
			endLoc 		= /0 0 1 (.*?)$/;	//Everything between the 0 0 1 and the end of the string (denoted by $)
		//Flip the direction of the arc by switching the start and end point (and sweep flag)
		var newStart = endLoc.exec( newArc )[1];
		var newEnd = startLoc.exec( newArc )[1];
		var middleSec = middleLoc.exec( newArc )[1];
		
		//Build up the new arc notation, set the sweep-flag to 0
		newArc = "M" + newStart + "A" + middleSec + "0 0 0 " + newEnd;
	}//if
	
	//Create a new invisible arc that the text can flow along
	svg.append("path")
		.attr("class", "hiddenDonutArcs")
		.attr("id", "donutArc"+i)
		.attr("d", newArc)
		.style("fill", "none");
});

The only thing that has changed since the previous section is the addition of the if statement. To flip the start and end positions, we can use a few more regular expressions. The current starting x and y location is given by everything in between the capital M and the capital A. The current radius is denoted by everything in between the capital A and the 0 0 1 of the x-axis rotation, large-arc flag and sweep flag. Finally the end location is given by all in between the 0 0 1 and the end of the string (denoted by a $ in regex).

So we save all the pieces in different variables and build/replace up the newArc using the final line in the if statement which has switched the start and end position.

The textPath section needs a small change. For the bottom half arcs, the dy attribute shouldn’t raise the labels above the arc paths, but lower the labels below the arc paths. So we need a small if statement which will result in two different dy values.
(To be able to use the d.endAngle in the if statement I replaced the donutData by pie(donutData) in the .data() step. You can still reference the data itself by using d.data instead of just d, which you can see in the .text() line of code.)



//Append the label names on the outside
svg.selectAll(".donutText")
	.data(pie(donutData))
   .enter().append("text")
	.attr("class", "donutText")
	//Move the labels below the arcs for those slices with an end angle greater than 90 degrees
	.attr("dy", function(d,i) { return (d.endAngle > 90 * Math.PI/180 ? 18 : -11); })
   .append("textPath")
	.attr("startOffset","50%")
	.style("text-anchor","middle")
	.attr("xlink:href",function(d,i){return "#donutArc"+i;})
	.text(function(d){return d.data.name;});

And that’s how we end up with the final image below

The labels on the bottom half have been flipped

The Code

I hope the examples above showed you enough variation to start using bendable texts in your own D3 visualization as well.  To help you on your way, you can find full workable examples of all of the examples discussed in these links

And that’s it for the tutorial that originated from my “A Closer Look at the division of Occupation” visual. I hope you enjoyed it :)

PS: as with most of my tutorials, the code above is only one way to do things. I wouldn’t be surprised if it’s not the most efficient way to do it, but it’s a way to accomplish things

Prev / Next Post
Comments (19)
  • Davo Galavotti - October 6, 2015 - Reply

    Thanks Nadieh, your tutorials are getting better and better. I remember almost 2 years ago trying to solve the rotation of labels on the lower 50% of a similar visualization and giving up poorly.

    • Nadieh
      (Author) Nadieh - October 9, 2015 - Reply

      Thank you Davo! Good to hear that you also found rotating the lower half a good idea :)

  • Julien Renaux - October 13, 2015 - Reply

    #respect awesome post!

  • Alex - October 23, 2015 - Reply

    Just what I’ve been trying to solve for ages, thanks! However, I can’t seem to make it work in my example (using sunburst diagram): http://codepen.io/znak/pen/MaOdgG
    It seems arc sections are not being copied to new paths for some reason?

  • Tom - December 10, 2015 - Reply

    This is a really great post, thanks for sharing!

  • Edo - January 14, 2016 - Reply

    Your blog is really cool, thanks :)

  • gzaraza - February 4, 2016 - Reply

    i like this tutorial, it’s vital, i wanna ask if i rotate the pie how would be done the recalculation of the flipped labels? :)

  • Daniel - March 5, 2016 - Reply

    Great tutorial, Nadieh.

    I’ve used some of the concepts to one of my projects.
    But I’ve some labels wich are longer than the respective arcs, resulting in ugly overlapping letters at the end of the labels.
    Does anyone have an angle about how to resolve this?

    This would work for bar charts, but I am unable to adapt it to Donuts.

    Anyway, awesome blog!

    • BobDylan - June 11, 2017 - Reply

      I ran across this problem too and only came up with this not too elegant solution. Maybe it’ll give you some ideas.

      – Render your pie chart paths as normal

      – Create a second invisible pie with the same coodinates as the original but make it a single segment taking up the full 360 degrees.

      – Collect the start points of all your pie slices from the main chart.

      – Then create new paths combining each start point and the angle data of the full arc.

      – Link your text to those new paths.

      Do you see? You should get a text that begins at each part of the slice but that will continue over a full circle so it doesn’t wrap around.
      They might overlap if you have two adjacent small sections, but you can just transform the text or something for the exceptions.

  • Venkat Ram Reddy Kunta - July 22, 2016 - Reply

    How do we display Ellipsis when the text on a section doesn’t fit in the arc section.

    For Details check my #Fiddle
    http://jsfiddle.net/venkatramreddykunta/uk67p1md/3/

  • Hugo - August 23, 2016 - Reply

    Thanks for this tutorial, it helped me a lot as I am only getting started with D3. I think I may have found a much simpler way of centering the text, without the regex complexity.
    The main idea is to draw the hidden arc with the same value for inner and outer radius. That way you can set offset to 25% on the upper half, and 75% on the lower half.
    After that I just gave the text a dy of half line-height, and all labels were perfectly aligned.

  • ben - November 2, 2016 - Reply

    Thanks for the tutorial, this is very concise and well-explained! One issue though: if one of the arc segments is larger than 180′ the regex doesn’t work. Additionally, you should include a space before the `0 0 1` regex so it doesn’t match numbers after the A which end in 0, and check for starting angles for large arcs. Here’s the issue and solution I found for these: http://stackoverflow.com/questions/40370212/error-when-providing-svg-v1-path-parameter-d/40388397#40388397

  • philippe - November 16, 2016 - Reply

    Hi,

    Thanks for this very clear tutorial. I have finally understood many things about d3 and svg. However I have the same problem as Alex ( October 23, 2015) with a sunburst diagram. Do you have a solution ?

    Cheers

  • Leena - January 11, 2017 - Reply

    Thank you so much for this tutorial Nadieh.

  • Laszlo - January 24, 2017 - Reply

    Thank you for this tutorial, it’s well explained, and very helpful! :)

  • George E Stout - January 30, 2017 - Reply

    My donut label starting point must be at the top of the chart in the 0 (zero) position. My label data is the months of the year.

    Setting the chart to not rotate -90 degrees but instead start at the zero position the last three months label are upsidedown.
    //Turn the pie chart 90 degrees counter clockwise, so it starts at the left
    var pie = d3.layout.pie()
    .startAngle(0 * Math.PI / 180)
    .endAngle(0 * Math.PI / 180 + 2 * Math.PI)
    .value(function (d) { return d.endDateID – d.startDateID; })
    .padAngle(.01)
    .sort(null);

    Seeking help, please.

  • minun - February 24, 2017 - Reply

    how do you change the font color? i am a beginner and i dont really understand how

    • minun - February 24, 2017 - Reply

      on the animated one i mean

  • Jonathan Hinkle - June 9, 2017 - Reply

    This is a fantastically useful and well put together post. Thanks so much for this!

Add comment

seven + 19 =