Math, Radars & D3.js


g.selectAll(".nodes")
  .data(y, function(j, i){
    dataValues.push([
      cfg.w/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)
      	*cfg.factor*Math.sin(i*cfg.radians/total)),
      cfg.h/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)
      	*cfg.factor*Math.cos(i*cfg.radians/total))
    ]);
  });

Let's start with:



  
  
  
  




  
    
    
    
    
  



  
    
    
    
    
  

How do we calculate this point?

Or any other point along this line?

Trigonometry!

soh

cah

toa

sin(𝛩) = opposite/hypotenuse

cos(𝛩) = adjacent/hypotenuse

tan(𝛩) = opposite/adjacent



  
  	
    
    
    
    
    
  

Let's try in D3

SETUP


var chart = d3.select('#chart');
var width = 900, height = 600;
var padding = 60;
var radius = d3.min([width - padding, height - padding])/2;

var svg = chart.selectAll("svg")
  .data([{}]).enter()
  .append("svg")
  .attr("height", height)
  .attr("width", width);

var centerCoords = [width/2, height/2];
var radar = svg.append('g')
  .attr('transform',"translate(" + centerCoords[0] + "," + centerCoords[1] + ")");

var angles = d3.range(0,360,45);
// [0, 45, 90, 135, 180, 225, 270, 315]
radar.append('circle')
  .attr('cx',0)
  .attr('cy',0)
  .attr('r', radius);

radar.selectAll('line')
  .data(angles).enter()
  .append('line')
  .attr('x0',0)
  .attr('y0',0)
  .attr('x1',function(d) { return Math.cos(d) * radius})
  .attr('y1',function(d) { return Math.sin(d) * radius});

Radians!

What are Radians?

Degrees to Radians

radians = degrees * ∏/180


var angles = d3.range(0,360,45)
  .map(function(a) { return a * Math.PI / 180});
// [0, 0.7853981633974483, 1.5707963267948966, 2.356194490192345,
//  3.141592653589793, 3.9269908169872414, 4.71238898038469,
//  5.497787143782138]

radar.selectAll('line')
  .data(angles).enter()
  .append('line')
  .attr('x0',0)
  .attr('y0',0)
  .attr('x1',function(d) { return Math.cos(d) * radius})
  .attr('y1',function(d) { return Math.sin(d) * radius});

Now let's build a radar


var data = [
{attribute: 'Bulk Apperception', value: 14},
{attribute: 'Candor', value: 19},
{attribute: 'Vivacity', value: 17},
{attribute: 'Coordination', value: 10},
{attribute: 'Meekness', value: 2},
{attribute: 'Humility', value: 3},
{attribute: 'Cruelty', value: 1},
{attribute: 'Self-Preservation', value: 10},
{attribute: 'Patience', value: 3},
{attribute: 'Decisiveness', value: 14},
{attribute: 'Imagination', value: 13},
{attribute: 'Curiosity', value: 8},
{attribute: 'Aggression', value: 5},
{attribute: 'Loyalty', value: 16},
{attribute: 'Empathy', value: 9},
{attribute: 'Tenacity', value: 17},
{attribute: 'Courage', value: 15},
{attribute: 'Sensuality', value: 18},
{attribute: 'Charm', value: 18},
{attribute: 'Humor', value: 9}
]

var angleInRadians = (360 / data.length) * Math.PI / 180;

radar.selectAll('line.axis')
  .data(data).enter()
  .append('line')
  .classed('axis',true)
  .attr('x0',0)
  .attr('y0',0)
  .attr('x1',function(d,i) {
    return Math.cos(angleInRadians * i) * radius
  })
  .attr('y1',function(d,i) {
    return Math.sin(angleInRadians * i) * radius
  })

With a Scale


var angle = d3.scaleLinear()
    .domain([0, data.length])
    .range([0, 2 * Math.PI])

radar.selectAll('line.axis')
  .data(data).enter()
  .append('line')
  .classed('axis',true)
  .attr('x0',0)
  .attr('y0',0)
  .attr('x1',function(d,i) {
    return Math.cos(angle(i)) * radius
  })
  .attr('y1',function(d,i) {
    return Math.sin(angle(i)) * radius
  })

Axis & Labels


radar.selectAll('line.axis')
  .data(data).enter()
  .append('line')
  .classed('axis',true)
  .attr('x0',0)
  .attr('y0',0)
  .attr('x1',function(d,i) { return X(i)})
  .attr('y1',function(d,i) { return Y(i)})

radar.selectAll('text.label')
  .data(data).enter()
  .append('text')
  .classed('label',true)
  .attr('x', function(d,i) { return X(i) })
  .attr('y', function(d,i) { return Y(i) })
  .text(function(d) {
    return d.attribute.toUpperCase() + " [" + d.value +"]";
  })


var labelAnchor = function(d, i) {
  var topAngle = 0 - Math.PI/2;
  var bottomAngle = 0 + Math.PI/2;
  var the_angle = angle(i) - RADIAN_OFFSET;
  var anchor = "";
  if (the_angle == topAngle || the_angle == bottomAngle)
    anchor = "middle"
  else if(the_angle > topAngle && the_angle < bottomAngle)
    anchor = "start"
  else
    anchor = "end"
  return anchor;
}

ALIGNING LABELS


radar.selectAll('text.label')
  .data(data).enter()
  .append('text')
  .classed('label',true)
  .attr('x', function(d,i) { return X(i, 20)})
  .attr('y', function(d,i) { return Y(i, 20)})
  .style("text-anchor", labelAnchor)
  .text(function(d) {
    return d.attribute.toUpperCase() + " [" + d.value +"]";
  })

RINGS


var measureScale = d3.scaleLinear()
    .domain([0, 20])
    .range([0, radius])

radar.selectAll('circle.ring')
  .data(data).enter()
  .append('circle')
  .classed('ring',true)
  .attr('cx',0)
  .attr('cy',0)
  .attr('r', function(d,i) {
    return measureScale(i + 1);
  })

AREA


radar.selectAll('polygon.area')
  .data([data]).enter()
  .append('polygon')
  .classed('area',true)
  .attr('points', function(d) {
    return d.map(function(d,i) {
      var rad = measureScale(d.value);
      return [X(i, rad), Y(i, rad)];
    }).join(' ');
  })

POINTS


radar.selectAll('circle.point')
  .data(data).enter()
  .append('circle')
  .classed('point',true)
  .attr('cx',function(d,i) { 
    return X(i,measureScale(d.value))})
  .attr('cy',function(d,i) { 
    return Y(i,measureScale(d.value))})
  .attr('r', 7)

Animated areas


var areas = radar.selectAll('polygon.area')
  .data([data]).enter()

areas.append('polygon')
  .classed('area',true)
  .attr('points', function(d) {
    return d.map(function() {
      return [0,0]
    }).join(' ')
  })
.merge(areas)
  .transition()
  .duration(2500)
  .attr('points', function(d) {
    return d.map(function(d,i) {
      var rad = measureScale(d.value);
      return [X(i, rad), Y(i, rad)];
    }).join(' ');
  })

Animated points


var points = radar.selectAll('circle.point')
  .data(data).enter()

points.append('circle')
  .classed('point',true)
  .attr('r', 7)
  .attr('cx',0)
  .attr('cy',0)
.merge(points)
  .transition()
  .duration(2500)
  .attr('cx',function(d,i) {
    return X(i,measureScale(d.value))
  })
  .attr('cy',function(d,i) {
    return Y(i,measureScale(d.value))
  })

Once Again...

Thanks

jsancha@carto.com

@jorgesancha

One last thing...

WE ARE HIRING!!

https://carto.com/jobs