Matěj Zmítko
Matěj Zmítko

Reputation: 357

D3.js rotate axis labels around the middle point

I am working with D3.js now and even though I found very similar cases I am not really able to put them together and move on.

I have something similar as a radar chart and I would like to append to each ax I create (number of axes is not fixed could be 4, but also 40) text, which I already have, but rotate the text around center point and turn them as soon as they reach 180 degrees, actually 0 degrees.

The result should look like this:

enter image description here

What I have now is this:

enter image description here

I only know how to reach this within arc, which is also nicely shown here, but I did not really figure it out for my case.

This is my code snippet, where I append those text criteria:

//Write criterias
      axis.append("text")
        .attr("class","labels")
        .attr("font-size","12px")
        .attr("font-family","Montserrat")
        .attr("text-anchor","middle") 
        .attr("fill","white")
        .attr("x",function (d, i) {
          return radius * Math.cos(angleSlice * i - Math.PI/2)*options.circles.labelFactor;
        })
        .attr("y",function (d, i) {
          return radius * Math.sin(angleSlice * i - Math.PI/2)*options.circles.labelFactor;
        })
        .text(function (d) {
          return d;
        });

EDIT:

Here is my fiddle: https://jsfiddle.net/fsb47ndf/

Thank you for any advise

Upvotes: 3

Views: 2679

Answers (3)

altocumulus
altocumulus

Reputation: 21578

Like already mentioned by Gerardo Furtado in his answer life can get easier if you ditch your x and y attributes in favor of doing all positioning and rotation via the transform attribute. However, you can take his approach even a step further by letting the browser do all the trigonometry. All you need to do is specify a list of appropriate transform definitions.

.attr("transform", function(d, i) {
  var angleI = angleSlice  * i * 180 / Math.PI - 90;   // the angle to rotate the label
  var distance = radius * options.circles.labelFactor; // the distance from the center
  var flip = angleI > 90 ? 180 : 0;                    // 180 if label needs to be flipped

  return "rotate(" + angleI + ") translate(" + distance + ")" + "rotate(" + flip + ")");
  //       ^1.^                    ^2.^                           ^3.^
})

If you omit the x and y attributes, they will default to 0, which means, that the texts will all start off at the origin. From there it is easy to move and rotate them to their final position applying just three transformations:

  1. rotate the texts to the angle according to their position on the perimeter
  2. translate the rotated texts outwards to their final position
  3. Flip the texts on the left hand side of the circle using another rotate.

Have a look at the following snippet for a working demo:

data = [{
    name: 'DATA1',
    value: 22,
  },
  {
    name: 'DATA2',
    value: 50,
  },
  {
    name: 'DATA3',
    value: 0,
  },
  {
    name: 'DATA4',
    value: 24,
  },
  {
    name: 'DATA5',
    value: 22,
  },
  {
    name: 'DATA6',
    value: 30,
  },
  {
    name: 'DATA7',
    value: 20,
  },
  {
    name: 'DATA8',
    value: 41,
  },
  {
    name: 'DATA9',
    value: 31,
  },
  {
    name: 'DATA10',
    value: 30,
  },
  {
    name: 'DATA11',
    value: 30,
  },
  {
    name: 'DATA12',
    value: 30,
  },
  {
    name: 'DATA13',
    value: 30,
  },
  {
    name: 'DATA14',
    value: 30,
  },
];



var options = {

  width: 600,
  height: 600,

  margins: {
    top: 100,
    right: 100,
    bottom: 100,
    left: 100
  },

  circles: {
    levels: 6,
    maxValue: 100,
    labelFactor: 1.15,
    opacity: 0.2,
  },

};


var allAxis = (data.map(function(i, j) {
    return i.name
  })),
  total = allAxis.length,
  radius = Math.min(options.width / 2, options.height / 2),
  angleSlice = Math.PI * 2 / total,
  Format = d3.format('');

var rScale = d3.scale.linear()
  .domain([0, options.circles.maxValue])
  .range([50, radius]);

var svg = d3.select("body").append("svg")
  .attr("width", options.width + options.margins.left + options.margins.right)
  .attr("height", options.height + options.margins.top + options.margins.bottom);

var g = svg.append("g")
  .attr("transform", "translate(" + (options.width / 2 + options.margins.left) + "," + (options.height / 2 + options.margins.top) + ")");

var axisGrid = g.append("g")
  .attr("class", "axisWraper");

var axis = axisGrid.selectAll(".axis")
  .data(allAxis)
  .enter()
  .append("g")
  .attr("class", "axis")

//append them lines
axis.append("line")
  .attr("x1", 0)
  .attr("y1", 0)
  .attr("x2", function(d, i) {
    var tempX2 = radius * Math.cos(angleSlice * i - Math.PI / 2);
    return tempX2;
  })
  .attr("y2", function(d, i) {
    var tempY = radius * Math.sin(angleSlice * i - Math.PI / 2);
    return tempY;
  })
  .attr("class", "line")
  .attr("stroke", "black")
  .attr("fill", "none");

//Draw background circles
axisGrid.selectAll(".levels")
  .data([6, 5, 4, 3, 2, 1])
  .enter()
  .append("circle")
  .attr("class", "gridCircle")
  .attr("r", function(d, i) {
    return parseInt(radius / options.circles.levels * d, 10);
  })
  .attr("stroke", "black")
  .attr("fill-opacity", options.circles.opacity);

//Write data
axis.append("text")
  .attr("class", "labels")
  .attr("font-size", "12px")
  .attr("font-family", "Montserrat")
  .attr("text-anchor", "middle")
  .attr("fill", "black")
  .attr("dy", ".35em")
  .attr("transform", function(d, i) {
      var angleI = angleSlice * i * 180 / Math.PI - 90; // the angle to rotate the label
      var distance = radius * options.circles.labelFactor; // the distance from the center
      var flip = angleI > 90 ? 180 : 0; // 180 if label needs to be flipped

      return "rotate(" + angleI + ") translate(" + distance + ")" + "rotate(" + flip + ")"
  })
.text(function(d) {
  console.log(d);
  return d;
});
<script src="https://d3js.org/d3.v3.js"></script>

Upvotes: 2

Gerardo Furtado
Gerardo Furtado

Reputation: 102194

Some people find it difficult to rotate an SVG element, because the rotate function of the transform attribute rotates the element around the origin (0,0), not around its center:

If optional parameters and are not supplied, the rotate is about the origin of the current user coordinate system (source)

Thus, an easy option is dropping the x and the y attributes of the texts, and positioning them using transform. That way, we can easily calculate the rotation:

.attr("transform", function(d, i) {
    var rotate = angleSlice * i > Math.PI / 2 ?
        (angleSlice * i * 180 / Math.PI) - 270 :
        (angleSlice * i * 180 / Math.PI) - 90;
    return "translate(" + radius * Math.cos(angleSlice * i - Math.PI / 2) * options.circles.labelFactor +
        "," + radius * Math.sin(angleSlice * i - Math.PI / 2) * options.circles.labelFactor +
        ") rotate(" + rotate + ")"
})

Here is your code:

data = [{
   name: 'DATA1',
   value: 22,
 }, {
   name: 'DATA2',
   value: 50,
 }, {
   name: 'DATA3',
   value: 0,
 }, {
   name: 'DATA4',
   value: 24,
 }, {
   name: 'DATA5',
   value: 22,
 }, {
   name: 'DATA6',
   value: 30,
 }, {
   name: 'DATA7',
   value: 20,
 }, {
   name: 'DATA8',
   value: 41,
 }, {
   name: 'DATA9',
   value: 31,
 }, {
   name: 'DATA10',
   value: 30,
 }, {
   name: 'DATA11',
   value: 30,
 }, {
   name: 'DATA12',
   value: 30,
 }, {
   name: 'DATA13',
   value: 30,
 }, {
   name: 'DATA14',
   value: 30,
 }, ];



 var options = {

   width: 600,
   height: 600,

   margins: {
     top: 100,
     right: 100,
     bottom: 100,
     left: 100
   },

   circles: {
     levels: 6,
     maxValue: 100,
     labelFactor: 1.15,
     opacity: 0.2,
   },

 };


 var allAxis = (data.map(function(i, j) {
     return i.name
   })),
   total = allAxis.length,
   radius = Math.min(options.width / 2, options.height / 2),
   angleSlice = Math.PI * 2 / total,
   Format = d3.format('');

 var rScale = d3.scale.linear()
   .domain([0, options.circles.maxValue])
   .range([50, radius]);

 var svg = d3.select("body").append("svg")
   .attr("width", options.width + options.margins.left + options.margins.right)
   .attr("height", options.height + options.margins.top + options.margins.bottom);

 var g = svg.append("g")
   .attr("transform", "translate(" + (options.width / 2 + options.margins.left) + "," + (options.height / 2 + options.margins.top) + ")");

 var axisGrid = g.append("g")
   .attr("class", "axisWraper");

 var axis = axisGrid.selectAll(".axis")
   .data(allAxis)
   .enter()
   .append("g")
   .attr("class", "axis")

 //append them lines
 axis.append("line")
   .attr("x1", 0)
   .attr("y1", 0)
   .attr("x2", function(d, i) {
     var tempX2 = radius * Math.cos(angleSlice * i - Math.PI / 2);
     return tempX2;
   })
   .attr("y2", function(d, i) {
     var tempY = radius * Math.sin(angleSlice * i - Math.PI / 2);
     return tempY;
   })
   .attr("class", "line")
   .attr("stroke", "black")
   .attr("fill", "none");

 //Draw background circles
 axisGrid.selectAll(".levels")
   .data([6, 5, 4, 3, 2, 1])
   .enter()
   .append("circle")
   .attr("class", "gridCircle")
   .attr("r", function(d, i) {
     return parseInt(radius / options.circles.levels * d, 10);
   })
   .attr("stroke", "black")
   .attr("fill-opacity", options.circles.opacity);

 //Write data
 axis.append("text")
   .attr("class", "labels")
   .attr("font-size", "12px")
   .attr("font-family", "Montserrat")
   .attr("text-anchor", "middle")
   .attr("fill", "black")
   .attr("transform", function(d, i) {
     var rotate = angleSlice * i > Math.PI ? (angleSlice * i * 180 / Math.PI) - 270 : (angleSlice * i * 180 / Math.PI) - 90;
     return "translate(" + radius * Math.cos(angleSlice * i - Math.PI / 2) * options.circles.labelFactor + "," + radius * Math.sin(angleSlice * i - Math.PI / 2) * options.circles.labelFactor + ") rotate(" + rotate + ")"
   })
   .text(function(d) {
     return d;
   });
<script src="https://d3js.org/d3.v3.min.js"></script>

Upvotes: 3

sparta93
sparta93

Reputation: 3854

You can use something like this to rotate all the labels. You probably have to adjust the positioning and rotation angle based on exactly how you want it.

var angle = 180;

svg.selectAll(".labels")
   .attr("transform", "translate(300,0) rotate("+angle+")");

Upvotes: 0

Related Questions