Text along circles in a D3 circle pack layout
Asked Answered
P

2

5

I am trying to label some circles in a circle pack layout with text that flows along the circle itself.

Here is one experimental jsfiddle:

enter image description here

As you can see, it is possible to render text along the circle, centered at its top. Though browser's rendering of curved SVG text is terrible. But let's say we don't care about it.

Here is another jsfiddle

enter image description here

I would like to place curved labels on this graph, under these conditions:

  • The circle represents a province (only depth==1) (BRITISH COLUMBIA, ALBERTA, and so forth)
  • The sum of sizes of all children (in other words, number of parliament seats allotted) of the province is greater than 5.
  • The name of the province should be all UPPERCASE.

You can see some of my attempts in the code itself. I have been trying for hours. My main problem is that circles in the circle are now somewhere in X Y space, whereas, in the first jsfiddle, all circles have centers in coordinate system origin.

Maybe you can help me by taking a fresh look at this.

Underlying data is based on this table:

enter image description here

(NOTE: This is somewhat related to the question 'Circle packs as nodes of a D3 force layout' I asked the other day, however this is an independent experiment.)

I decided to use regular SVG arcs instead of d3.svg.arc(). I still think it is a right decision. However, here is what I have now: :) jsfiddle

enter image description here

Pentadactyl answered 21/1, 2014 at 10:51 Comment(7)
One quick note: there is an exception being thrown because of assigning the cx and cy attributes to var arc. If you comment those two out, then the labels show up all bundled up in top left corner (coord issues you have mentioned). This is probably obvious and something you already know from experimenting with it, but wanted to point out as a sanity check.Plexiform
Yes, this is a kind of "development" example, so this is normal. :) I know about text all bundled in that corner. That's actually my problem - how to put it in the right place. Thanks for bringing it to my attention, now others will know too whats going on... :)Pentadactyl
In relation to the attr startOffset = "<length>", the docs say: If a <length> other than a percentage is given (which is what you have given it), then the ‘startOffset’ represents a distance along the path measured in the current user coordinate system. Following the docs, then one lands on distance along a path. At the moment, this looks like the only connection between the textPath element and the possibility of specifying x,y coords. Here hoping this is not some rabbit trailPlexiform
I think that here d3.svg.arc() is an obstacle. It can't be defined other than having center in (0,0). It looks to me that switching to regular SVG arc would work.Pentadactyl
@Plexiform - Also, there is a certain terminology mess here. d3.svg.arc() actually consists of two arcs (called "inner" and "outer"), and two straight lines. That's why I had to put .attr("startOffset",function(d,i){return "25%";}) , and not 50%.Pentadactyl
I quickly forked your latest fiddle before you fix it! I will store it for the possibility of graphing any future stories about the Brazilian Cangaço. Here is a famous picture of those guys :) Sorry, just a bit of levity...been working too hard...Plexiform
lol @Plexiform I never dreamt my graphs would have such an application! :) BTW, I managed to display labels as I wanted. Code is still very dirty.Pentadactyl
S
2

Just updating some of VividD's code.

The describeArc function was not working correctly for arcs with less than 180 degrees, and the start and end variables was flipped on the function and when calling it.

Here is an updated describeArc function that handles all alternatives of arcs, including small arcs and upside-down arcs:

function describeArc(x, y, radius, startAngle, endAngle) {
  var start = polarToCartesian(x, y, radius, startAngle);
  var end = polarToCartesian(x, y, radius, endAngle);
  var arcLength = endAngle - startAngle;
  if (arcLength < 0) arcLength += 360;
  var longArc = arcLength >= 180 ? 1 : 0;
  var d = [
    "M", start.x, start.y,
    "A", radius, radius, 0, longArc, 1, end.x, end.y
  ].join(" ");
  return d;
}

With this change the function should be called with reversed start and end values that make more sense:

describeArc(d.x, d.y, d.r, -160, 160);
Stopple answered 23/3, 2017 at 21:21 Comment(0)
P
8

NOTE (since I am answering my question): If some of you already spent time on this problem and found another solution, please post it, and I will accept your answer. Thanks to @FernOfTheAndes for participating in process of finding this solution, as it was filled with pain and misery of working with svg arcs.

Here is jsfiddle of the solution:

enter image description here

As mentioned in comments, the key part was generating arcs as plain vanilla svg arcs, not via d3.svg.arc().

SVG rules for defining arcs are clear, but a little hard to manage. Here is an interactive explorer of svg syntax for arcs.

Also, these two functions helped me during this process of defining the right arcs:

function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
    var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
    return {
        x: centerX + (radius * Math.cos(angleInRadians)),
        y: centerY + (radius * Math.sin(angleInRadians))
    };
}

function describeArc(x, y, radius, startAngle, endAngle){
    var start = polarToCartesian(x, y, radius, endAngle);
    var end = polarToCartesian(x, y, radius, startAngle);
    var arcSweep = endAngle - startAngle <= 180 ? "0" : "1";
    var d = [
        "M", start.x, start.y, 
        "A", radius, radius, 0, 1, 1, end.x, end.y
    ].join(" ");
    return d;       
}

This is the code that actually directly generates curved labels:

var arcPaths = vis.append("g")
    .style("fill","navy");
var labels = arcPaths.append("text")
    .style("opacity", function(d) {
        if (d.depth == 0) {
            return 0.0;
        }
        if (!d.children) {
            return 0.0;
        }
        var sumOfChildrenSizes = 0;
        d.children.forEach(function(child){sumOfChildrenSizes += child.size;});
        //alert(sumOfChildrenSizes);
        if (sumOfChildrenSizes <= 5) {
            return 0.0;
        }
        return 0.8;
    })
    .attr("font-size",10)
    .style("text-anchor","middle")
    .append("textPath")
    .attr("xlink:href",function(d,i){return "#s"+i;})
    .attr("startOffset",function(d,i){return "50%";})
    .text(function(d){return d.name.toUpperCase();})

Fortunately, centering text on an arc was just a matter of setting the right property.

Pentadactyl answered 21/1, 2014 at 16:10 Comment(7)
I'll post this here. Much the same as yours, various other fiddling around. To get the text inside the circle, while still being right way around, I made the path radius a little bit smaller than the circle radius. I was trying to use the "textLength" property to squish text to fit, but it doesn't seem to work on <textpath> elements. jsfiddle.net/2hM3s/4Quebec
@Quebec I just changed .style("font-size",12) to .attr("font-size",12), and the result is: (jsfiddle.net/VividD/vCG6w) (the error was there all the time from my example)Pentadactyl
@Quebec I think you should post this as an answer, its good for future readers to be able to see two approaches.Pentadactyl
@Quebec I meant to say that now with lets say .attr("font-size",8) one can make text as small as he/she wishes: updated (jsfiddle.net/VividD/vCG6w)Pentadactyl
And my tiny contribution to @VividD's latest fiddle: moving the labels according to the different user choices. Can be improved with transitions and other touch-ups, but you get the idea.Plexiform
The main difference between .style("font-size",12) and .attr("font-size",12) is that attribute styles can be over-ridden by CSS. Also, it seems that the first is interpretted as a point size and the second as a pixel size (if you don't specify units). Doesn't change the way long names are getting cut off in smaller circles -- for text and tspan elements, you can force the text to squish to a given length by setting the textLength attribute, regardless of how font is sized. However, you could compute the path length and text length and use that to directly set font size and kerning.Quebec
P.S. Playing around made me realize that SVG path arcs are nearly as confusing as Bezier curves. So I made up a reference image/tutorial: codepen.io/AmeliaBR/full/kAIzfQuebec
S
2

Just updating some of VividD's code.

The describeArc function was not working correctly for arcs with less than 180 degrees, and the start and end variables was flipped on the function and when calling it.

Here is an updated describeArc function that handles all alternatives of arcs, including small arcs and upside-down arcs:

function describeArc(x, y, radius, startAngle, endAngle) {
  var start = polarToCartesian(x, y, radius, startAngle);
  var end = polarToCartesian(x, y, radius, endAngle);
  var arcLength = endAngle - startAngle;
  if (arcLength < 0) arcLength += 360;
  var longArc = arcLength >= 180 ? 1 : 0;
  var d = [
    "M", start.x, start.y,
    "A", radius, radius, 0, longArc, 1, end.x, end.y
  ].join(" ");
  return d;
}

With this change the function should be called with reversed start and end values that make more sense:

describeArc(d.x, d.y, d.r, -160, 160);
Stopple answered 23/3, 2017 at 21:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.