Search code examples
javascriptd3.jssunburst-diagram

Rotate Labels in d3 sunburst v4


I try to use the d3 v4 sunburst to visulize some data from a .json I can display the sunburst diagramm with the labels i want, but the labels on the left side are upside down. I tried various methode, which i found on so or other sources but nothing works for me.

Heres my coding

<!DOCTYPE html>
<meta charset="utf-8">
<style>

path {
stroke: #fff;
}

text {
font-family: Arial, sans-serif;
font-size: 18px;
}

</style>
<body>
<script src="d3.min.js"></script>
<script src="data.js" charset="utf-8"></script>

<script language="javascript">

var width = 1200,
height = 1000,
radius = Math.min(width, height) / 2;

var x = d3.scaleLinear()
.range([0, 2 * Math.PI]);

var y = d3.scaleLinear()
.range([0, radius]);

var formatNumber = d3.format(",d");

var color = d3.scaleOrdinal(d3.schemeCategory20c);



var partition = d3.partition();

var arc = d3.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, 
 x(d.x0))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x1))); 
 })
.innerRadius(function(d) { return Math.max(0, y(d.y0)); })
.outerRadius(function(d) { return Math.max(0, y(d.y1)); });

 var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");

 root = d3.hierarchy(root);
root.sum(function(d) { return d.size; });
svg.selectAll("path")
    .data(partition(root).descendants())
    .enter().append("g").attr("class", "node");

 path = svg.selectAll(".node")
  .append("path")
  .attr("d", arc)
  .style("fill", function(d) { return color((d.children ? d : 
 d.parent).data.name); })
  .on("click", click);

text = svg.selectAll(".node")
   .append("text")
      .attr("transform", function(d) { 
         return  "rotate(" + computeTextRotation(d) + ")"; 
      })

      .attr("x", function(d) { 
         return y(d.y0); 
      })
      .attr("dx", "6") // margin
      .attr("dy", ".35em") // vertical-align
      .text(function(d) { 
          return d.data.name === "root" ? "" : d.data.name
      });


  function click(d) {
  //Hide text while Sunburst transitions
  text.transition().attr("opacity", 0);

  svg.transition()
  .duration(750)
  .tween("scale", function() {
    var xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
        yd = d3.interpolate(y.domain(), [d.y0, 1]),
        yr = d3.interpolate(y.range(), [d.y0 ? 20 : 0, radius]);
    return function(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); };
   })
  .selectAll("path")
  .attrTween("d", function(d) { return function() { return arc(d); }; })
  .on("end", function(e, i) {
      // Check if the animated element's data e lies within the visible 
      // angle span given in d:
      if (e.x0 >= d.x0 && e.x0 < d.x1) {
          // get a selection of the associated text element
          var arcText = d3.select(this.parentNode).select("text");
          // fade in the text element and recalculate positions
          arcText.transition().duration(750)
              .attr("opacity", 1)

              .attr("transform", function() { return "rotate(" + 
  computeTextRotation(e) + ")" })
              .attr("x", function(d) { return y(d.y0); })
              .text(function(d) { 
                  return d.data.name === "root" ? "" : d.data.name
              });
      }
  });
  }

  d3.select(self.frameElement).style("height", height + "px");

 // Interpolate the scales!
 function arcTween(d) {
  var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
  yd = d3.interpolate(y.domain(), [d.y, 1]),
  yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
  return function(d, i) {
 return i
    ? function(t) { return arc(d); }
    : function(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); return 
 arc(d); };
  };
 }

 function computeTextRotation(d) {
  var angle = (x((d.x0 + d.x1)/2) - Math.PI / 2) / Math.PI * 180;
  return (angle >  90 || angle < 270) ?  angle : 180 + angle ;

  }



  </script>

and this is the data.json:

var root={
"id":"00000001", "name": "",
"children": [
{
"id":"00000003", "name":"#",
"children": [
{
"id":"00000003", "name":"2017",
"children": [
{
"id":"00000002", "name":"Nov","size":           12608.00},
] },
] },
{
"id":"00000010", "name":"A",
"children": [
{
"id":"00000010", "name":"2017",
"children": [
{
"id":"00000003", "name":"Dez","size":          119215.80},
{
"id":"00000004", "name":"Jul","size":            5000.00},
{
"id":"00000005", "name":"Jun","size":           45536.00},
{
"id":"00000006", "name":"Mai","size":           18500.00},
{
"id":"00000007", "name":"Nov","size":           20107.31},
{
"id":"00000008", "name":"Okt","size":           70303.00},
{
"id":"00000009", "name":"Sep","size":           11240.00},
] },
] },
{
"id":"00000018", "name":"DIR",
"children": [
{
"id":"00000018", "name":"2017",
"children": [
{
"id":"00000010", "name":"Aug","size":          705110.07},
{
"id":"00000011", "name":"Dez","size":          667101.28},
{
"id":"00000012", "name":"Jul","size":          684326.04},
{
"id":"00000013", "name":"Jun","size":          975615.11},
{
"id":"00000014", "name":"Mai","size":          625832.83},
{
"id":"00000015", "name":"Nov","size":          488444.60},
{
"id":"00000016", "name":"Okt","size":          578924.89},
{
"id":"00000017", "name":"Sep","size":          755968.14},
] },
] },
{
"id":"00000020", "name":"EU",
"children": [
{
"id":"00000020", "name":"2017",
"children": [
{
"id":"00000018", "name":"Nov","size":          505400.00},
{
"id":"00000019", "name":"Sep","size":          505400.00},
] },
] },
{
"id":"00000028", "name":"NAT",
"children": [
{
"id":"00000028", "name":"2017",
"children": [
{
"id":"00000020", "name":"Aug","size":         1882688.00},
{
"id":"00000021", "name":"Dez","size":          268861.33},
{
"id":"00000022", "name":"Jul","size":         1174708.67},
{
"id":"00000023", "name":"Jun","size":         3860969.90},
{
"id":"00000024", "name":"Mai","size":          917468.75},
{
"id":"00000025", "name":"Nov","size":         2233213.25},
{
"id":"00000026", "name":"Okt","size":         2340277.41},
{
"id":"00000027", "name":"Sep","size":         1667464.09},
] },
] },
{
"id":"00000036", "name":"X",
"children": [
{
"id":"00000036", "name":"2017",
"children": [
{
"id":"00000028", "name":"Aug","size":          249939.37},
{
"id":"00000029", "name":"Dez","size":          289363.70},
{
"id":"00000030", "name":"Jul","size":           98847.32},
{
"id":"00000031", "name":"Jun9","size":            7799.00},
{
"id":"00000032", "name":"Mai","size":           19520.00},
{
"id":"00000033", "name":"Nov","size":         1177309.62},
{
"id":"00000034", "name":"Okt","size":          224970.85},
{
"id":"00000035", "name":"Sep","size":          167309.57},
] },
] },
] }

This is how it looks like at the moment
]

as you can see the left side is upside down


Solution

  • The idea is to rotate by an additional 180° text labels which have an angle between 90° and 270° (left part of the graph).

    Here is the updated demo:

    <meta charset="utf-8">
    <style>
    
    path {
    stroke: #fff;
    }
    
    text {
    font-family: Arial, sans-serif;
    font-size: 18px;
    }
    
    </style>
    <body>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    
    <script language="javascript">
    
    var root={
    "id":"00000001", "name": "",
    "children": [
    {
    "id":"00000003", "name":"#",
    "children": [
    {
    "id":"00000003", "name":"2017",
    "children": [
    {
    "id":"00000002", "name":"Nov","size":           12608.00},
    ] },
    ] },
    {
    "id":"00000010", "name":"A",
    "children": [
    {
    "id":"00000010", "name":"2017",
    "children": [
    {
    "id":"00000003", "name":"Dez","size":          119215.80},
    {
    "id":"00000004", "name":"Jul","size":            5000.00},
    {
    "id":"00000005", "name":"Jun","size":           45536.00},
    {
    "id":"00000006", "name":"Mai","size":           18500.00},
    {
    "id":"00000007", "name":"Nov","size":           20107.31},
    {
    "id":"00000008", "name":"Okt","size":           70303.00},
    {
    "id":"00000009", "name":"Sep","size":           11240.00},
    ] },
    ] },
    {
    "id":"00000018", "name":"DIR",
    "children": [
    {
    "id":"00000018", "name":"2017",
    "children": [
    {
    "id":"00000010", "name":"Aug","size":          705110.07},
    {
    "id":"00000011", "name":"Dez","size":          667101.28},
    {
    "id":"00000012", "name":"Jul","size":          684326.04},
    {
    "id":"00000013", "name":"Jun","size":          975615.11},
    {
    "id":"00000014", "name":"Mai","size":          625832.83},
    {
    "id":"00000015", "name":"Nov","size":          488444.60},
    {
    "id":"00000016", "name":"Okt","size":          578924.89},
    {
    "id":"00000017", "name":"Sep","size":          755968.14},
    ] },
    ] },
    {
    "id":"00000020", "name":"EU",
    "children": [
    {
    "id":"00000020", "name":"2017",
    "children": [
    {
    "id":"00000018", "name":"Nov","size":          505400.00},
    {
    "id":"00000019", "name":"Sep","size":          505400.00},
    ] },
    ] },
    {
    "id":"00000028", "name":"NAT",
    "children": [
    {
    "id":"00000028", "name":"2017",
    "children": [
    {
    "id":"00000020", "name":"Aug","size":         1882688.00},
    {
    "id":"00000021", "name":"Dez","size":          268861.33},
    {
    "id":"00000022", "name":"Jul","size":         1174708.67},
    {
    "id":"00000023", "name":"Jun","size":         3860969.90},
    {
    "id":"00000024", "name":"Mai","size":          917468.75},
    {
    "id":"00000025", "name":"Nov","size":         2233213.25},
    {
    "id":"00000026", "name":"Okt","size":         2340277.41},
    {
    "id":"00000027", "name":"Sep","size":         1667464.09},
    ] },
    ] },
    {
    "id":"00000036", "name":"X",
    "children": [
    {
    "id":"00000036", "name":"2017",
    "children": [
    {
    "id":"00000028", "name":"Aug","size":          249939.37},
    {
    "id":"00000029", "name":"Dez","size":          289363.70},
    {
    "id":"00000030", "name":"Jul","size":           98847.32},
    {
    "id":"00000031", "name":"Jun9","size":            7799.00},
    {
    "id":"00000032", "name":"Mai","size":           19520.00},
    {
    "id":"00000033", "name":"Nov","size":         1177309.62},
    {
    "id":"00000034", "name":"Okt","size":          224970.85},
    {
    "id":"00000035", "name":"Sep","size":          167309.57},
    ] },
    ] },
    ] }
    
    var width = 1200,
    height = 1000,
    radius = Math.min(width, height) / 2;
    
    var x = d3.scaleLinear()
    .range([0, 2 * Math.PI]);
    
    var y = d3.scaleLinear()
    .range([0, radius]);
    
    var formatNumber = d3.format(",d");
    
    var color = d3.scaleOrdinal(d3.schemeCategory20c);
    
    var partition = d3.partition();
    
    var arc = d3.arc()
    .startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, 
     x(d.x0))); })
    .endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x1))); 
     })
    .innerRadius(function(d) { return Math.max(0, y(d.y0)); })
    .outerRadius(function(d) { return Math.max(0, y(d.y1)); });
    
     var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height)
    .append("g")
    .attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");
    
     root = d3.hierarchy(root);
    root.sum(function(d) { return d.size; });
    svg.selectAll("path")
        .data(partition(root).descendants())
        .enter().append("g").attr("class", "node");
    
     path = svg.selectAll(".node")
      .append("path")
      .attr("d", arc)
      .style("fill", function(d) { return color((d.children ? d : 
     d.parent).data.name); })
      .on("click", click);
    
    text = svg.selectAll(".node")
       .append("text")
          .attr("transform", function(d) {
             var rotation = computeTextRotation(d);
             var translation = y(d.y0);
             if (rotation > 90 && rotation < 270) {
               rotation = rotation + 180;
               translation = -translation - 13;
             }
             return (
               "rotate(" + rotation + ")" +
               "translate(" + translation + ",0)"
             );
          })
          .attr("text-anchor", function(d) {
            var rotation = computeTextRotation(d);
            return (rotation > 90 && rotation < 270) ? "end" : "start";
          })
          .attr("dx", function(d) {
            var rotation = computeTextRotation(d);
            return (rotation > 90 && rotation < 270) ? -6 : 6;
          })
          .attr("dy", ".35em") // vertical-align
          .text(function(d) { 
              return d.data.name === "root" ? "" : d.data.name
          });
    
    
      function click(d) {
      //Hide text while Sunburst transitions
      text.transition().attr("opacity", 0);
    
      svg.transition()
      .duration(750)
      .tween("scale", function() {
        var xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
            yd = d3.interpolate(y.domain(), [d.y0, 1]),
            yr = d3.interpolate(y.range(), [d.y0 ? 20 : 0, radius]);
        return function(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); };
       })
      .selectAll("path")
      .attrTween("d", function(d) { return function() { return arc(d); }; })
      .on("end", function(e, i) {
          // Check if the animated element's data e lies within the visible 
          // angle span given in d:
          if (e.x0 >= d.x0 && e.x0 < d.x1) {
              // get a selection of the associated text element
              var arcText = d3.select(this.parentNode).select("text");
              // fade in the text element and recalculate positions
              arcText.transition().duration(750)
                  .attr("opacity", 1)
    
                  .attr("transform", function() { return "rotate(" + 
      computeTextRotation(e) + ")" })
                  .attr("x", function(d) { return y(d.y0); })
                  .text(function(d) { 
                      return d.data.name === "root" ? "" : d.data.name
                  });
          }
      });
      }
    
      d3.select(self.frameElement).style("height", height + "px");
    
     // Interpolate the scales!
     function arcTween(d) {
      var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
      yd = d3.interpolate(y.domain(), [d.y, 1]),
      yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
      return function(d, i) {
     return i
        ? function(t) { return arc(d); }
        : function(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); return 
     arc(d); };
      };
     }
    
     function computeTextRotation(d) {
      var angle = (x((d.x0 + d.x1)/2) - Math.PI / 2) / Math.PI * 180;
      return (angle >  90 || angle < 270) ?  angle : 180 + angle ;
    
      }
    
      </script>

    Let's go step by step:

    First we'll apply this conditional additional rotation:

    .attr("transform", function(d) {
      var rotation = computeTextRotation(d);
      if (rotation > 90 && rotation < 270)
        rotation = rotation + 180;
      return "rotate(" + rotation + ")";
    })
    

    But then all labels are on the right part of the graph...

    This means we then have to reverse the translation for left labels:

    .attr("transform", function(d) {
      var rotation = computeTextRotation(d);
      var translation = y(d.y0);
      if (rotation > 90 && rotation < 270) {
        rotation = rotation + 180;
        translation = -translation;
      }
      return (
        "rotate(" + rotation + ")" +
        "translate(" + translation + ",0)"
      );
    })
    

    Here I've prefered to perform the translate using the transform operator rather than the x attribute the text element.

    But then the text is not exactly at the right spot in its shape...

    This can be fixed by modifying the text-anchor of labels for left elements to be at the end of the text. We can do this by using this attribute:

    .attr("text-anchor", function(d) {
      var rotation = computeTextRotation(d);
      return (rotation > 90 && rotation < 270) ? "end" : "start";
    })
    

    And finally, it's still not perfect since we'd like the same margin for labels on the left than on the right. We can thus adjust our translation for left items:

    .attr("dx", function(d) {
      var rotation = computeTextRotation(d);
      return (rotation > 90 && rotation < 270) ? -6 : 6;
    })
    

    You can replicate this method to adapt the click function as well.