Arash Howaida
Arash Howaida

Reputation: 2617

Modifying donut chart to "progress donut chart"

I am trying to develop a d3 donut chart visual that simply takes a user defined percentage -- anywhere from 0 to 100%. From this value I want the donut to have segments equal to the proportion. For instance, if 50% then exactly half of the donut chart segments would be appended. Likewise, if 100% then the whole donut chart would be drawn; all the segments would be appended. I couldn't figure out how to achieve this in an elegant fashion, but I did find a crude workaround which you can see below in the snippet.

var data =
[{'value':0,'interval':6.25},
{'value':6.25,'interval':6.25},
{'value':12.5,'interval':6.25},
{'value':18.75,'interval':6.25},
{'value':25,'interval':6.25},
{'value':31.25,'interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25}];


var width = 960,
    height = 500,
    radius = Math.min(width, height) / 2;

var color = d3.scale.linear()
        .range(["#0005fd","#00fe80"]).domain([0,35]);

var explode = function(x,index) {
  var offset = (index==5) ? 80:0;
  var angle = (x.startAngle + x.endAngle)/2;
  var xOff = Math.sin(angle)*offset;
  var yOff = -Math.cos(angle)*offset;
  return "translate(" +xOff+","+yOff+ ")";
}

var arc = d3.svg.arc()
    .outerRadius(radius - 10)
    .innerRadius(radius - 70);

var pie = d3.layout.pie()
    .padAngle(.05)
    .sort(null)
    .value(function(d) { return d.interval; });

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

  var g = svg.selectAll(".arc")
      .data(pie(data))
    .enter().append("g")
      .attr("class", "arc");

  g.append("path")
      .attr("d", arc)
      //.attr('transform', explode)
      .style('stroke','none')
      .style("fill", function(d) {
        if (d.data.value=='none') {
          return 'none'
        }
        return color(d.data.value); });

  g.append("text")
      .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
      .attr("dy", ".35em")
      //.text(function(d) { return d.data.age; });

svg.append('text')
    .text('37.5%') // magic number
    .attr('font-family','Play')
    .attr('font-size','140px')
    .attr('text-anchor','middle')
    .attr('x',0)
    .attr('y',40);


function type(d) {
  d.interval = +d.interval;
  return d;
}
<!DOCTYPE html>
<meta charset="utf-8">
<style>

</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>

In essence, what is happening is the proportion of entries with values to the entries reading "none" is 37.5% -- which is my magic number (6 values and 10 nones, thus 6/16 = 37.5%). Needless to say this is not scalable at all.

Question

Is there any built-in means or other less labor-intensive solutions at my particular juncture? I just want to be able to pass a number from 0 to 100 to a function then have that percentage of the donut segments drawn out. In my particular case I chose 6.25 because it seemed to be the most aesthetically pleasing.

Perhaps a custom fill with transparency to simulate the effect of having exploded segments? Seems too hacky...

Note: Versions are optional. That is to say I'm not opposed to d3.v5 solutions, I just used d3.v3 since I did not use donuts in v5 yet.

Upvotes: 2

Views: 594

Answers (1)

Andrew Reid
Andrew Reid

Reputation: 38151

The easiest way to do this would be to add another arc on top of the rest of the segments. This arc can be of arbitrary length, so it covers up all the segments that don't need to be shown. This could be done with:

  var percentage = .35;
  g.append("path")
    .attr("d", d3.svg.arc()
      .endAngle(Math.PI*2)
      .startAngle(percentage * Math.PI*2)
      .outerRadius(radius - 10)
      .innerRadius(radius - 70)
     )
     .attr("fill","white")

We start with the end angle, Math.PI*2, which is one full rotation, representing 100% complete. Then we move backwards with a smaller start angle, covering up everything between 100% and the percentage we have.

Depending on how you want to style it, you could even have it slightly transparent to show what portion is incomplete.

Here's an example:

var data = d3.range(16).map(function(d) {
  return { value: d*6.25, interval: 6.25 };
})

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

var color = d3.scale.linear()
        .range(["#0005fd","#00fe80"]).domain([0,35]);

var explode = function(x,index) {
  var offset = (index==5) ? 80:0;
  var angle = (x.startAngle + x.endAngle)/2;
  var xOff = Math.sin(angle)*offset;
  var yOff = -Math.cos(angle)*offset;
  return "translate(" +xOff+","+yOff+ ")";
}

var arc = d3.svg.arc()
    .outerRadius(radius - 10)
    .innerRadius(radius - 70);

var pie = d3.layout.pie()
    .padAngle(.05)
    .sort(null)
    .value(function(d) { return d.interval; });

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

  var g = svg.selectAll(".arc")
      .data(pie(data))
    .enter().append("g")
      .attr("class", "arc");

  g.append("path")
      .attr("d", arc)
      //.attr('transform', explode)
      .style('stroke','none')
      .style("fill", function(d) {return color(d.data.value); });
      
      
  g.append("text")
      .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
      .attr("dy", ".35em")
      .text(function(d) { return d.data.age; });
      
  // Extra arc:
  var percentage = .35;
  g.append("path")
    .attr("d", d3.svg.arc()
      .endAngle(Math.PI*2)
      .startAngle(percentage * Math.PI*2)
      .outerRadius(radius - 10)
      .innerRadius(radius - 70)
     )
     .attr("fill","white")


svg.append('text')
    .text(percentage * 100 + "%") // magic number
    .attr('font-family','Play')
    .attr('font-size','140px')
    .attr('text-anchor','middle')
    .attr('x',0)
    .attr('y',40);


function type(d) {
  d.interval = +d.interval;
  return d;
}
<!DOCTYPE html>
<meta charset="utf-8">
<style>

</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>

This approach also allows easy animation:

var data = d3.range(16).map(function(d) {
  return { value: d*6.25, interval: 6.25 };
})

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

var color = d3.scale.linear()
        .range(["#0005fd","#00fe80"]).domain([0,35]);

var explode = function(x,index) {
  var offset = (index==5) ? 80:0;
  var angle = (x.startAngle + x.endAngle)/2;
  var xOff = Math.sin(angle)*offset;
  var yOff = -Math.cos(angle)*offset;
  return "translate(" +xOff+","+yOff+ ")";
}

var arc = d3.svg.arc()
    .outerRadius(radius - 10)
    .innerRadius(radius - 70);

var pie = d3.layout.pie()
    .padAngle(.05)
    .sort(null)
    .value(function(d) { return d.interval; });

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

  var g = svg.selectAll(".arc")
      .data(pie(data))
    .enter().append("g")
      .attr("class", "arc");

  g.append("path")
      .attr("d", arc)
      //.attr('transform', explode)
      .style('stroke','none')
      .style("fill", function(d) {return color(d.data.value); });
      
      
  g.append("text")
      .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
      .attr("dy", ".35em")
      .text(function(d) { return d.data.age; });
      
  // Extra arc:
  var percentage = .35;

  var coverArc = g.append("path")
     .attr("fill","white")


var label = svg.append('text')
    .text(percentage * 100 + "%") // magic number
    .attr('font-family','Play')
    .attr('font-size','140px')
    .attr('text-anchor','middle')
    .attr('x',0)
    .attr('y',40);
    
function transition() {

  coverArc
    .transition()
    .tween("d", function(d) {
       var that = d3.select(this),
       i = d3.interpolateNumber(0, percentage);
       return function(t) { 
         that.attr("d", d3.svg.arc()
           .endAngle(Math.PI*2)
           .startAngle(i(t) * Math.PI*2)
           .outerRadius(radius - 10)
           .innerRadius(radius - 70)
           )
          label.text(Math.round(i(t) * 100) + "%");
        };
     })
     .duration(1000)
     // other way:
    .transition()
    .tween("d", function(d) {
       var that = d3.select(this),
       i = d3.interpolateNumber(percentage, 0);
       return function(t) { 
         that.attr("d", d3.svg.arc()
           .endAngle(Math.PI*2)
           .startAngle(i(t) * Math.PI*2)
           .outerRadius(radius - 10)
           .innerRadius(radius - 70)
           )
          label.text(Math.round(i(t) * 100) + "%");
        };
     })
     .duration(1000)
     .each("end",transition);

}

transition();




function type(d) {
  d.interval = +d.interval;
  return d;
}
<!DOCTYPE html>
<meta charset="utf-8">
<style>

</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>

Answer uses d3v3, there are some minor changes for d3v4+, relating to the arc specifically, d3.svg.arc is now d3.arc, d3.layout.pie is now d3.pie, and d3.scale.linear is now d3.scaleLinear, and for the animation used in the 2nd snippet, transition.each is now transition.on:

var data = d3.range(16).map(function(d) {
  return { value: d*6.25, interval: 6.25 };
})

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

var color = d3.scaleLinear()
        .range(["#0005fd","#00fe80"]).domain([0,35]);

var explode = function(x,index) {
  var offset = (index==5) ? 80:0;
  var angle = (x.startAngle + x.endAngle)/2;
  var xOff = Math.sin(angle)*offset;
  var yOff = -Math.cos(angle)*offset;
  return "translate(" +xOff+","+yOff+ ")";
}

var arc = d3.arc()
    .outerRadius(radius - 10)
    .innerRadius(radius - 70);

var pie = d3.pie()
    .padAngle(.05)
    .sort(null)
    .value(function(d) { return d.interval; });

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

  var g = svg.selectAll(".arc")
      .data(pie(data))
    .enter().append("g")
      .attr("class", "arc");

  g.append("path")
      .attr("d", arc)
      //.attr('transform', explode)
      .style('stroke','none')
      .style("fill", function(d) {return color(d.data.value); });
      
      
  g.append("text")
      .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
      .attr("dy", ".35em")
      .text(function(d) { return d.data.age; });
      
  // Extra arc:
  var percentage = .35;

  var coverArc = g.append("path")
     .attr("fill","white")


var label = svg.append('text')
    .text(percentage * 100 + "%") // magic number
    .attr('font-family','Play')
    .attr('font-size','140px')
    .attr('text-anchor','middle')
    .attr('x',0)
    .attr('y',40);
    
function transition() {

  coverArc
    .transition()
    .tween("d", function(d) {
       var that = d3.select(this),
       i = d3.interpolateNumber(0, percentage);
       return function(t) { 
         that.attr("d", d3.arc()
           .endAngle(Math.PI*2)
           .startAngle(i(t) * Math.PI*2)
           .outerRadius(radius - 10)
           .innerRadius(radius - 70)
           )
          label.text(Math.round(i(t) * 100) + "%");
        };
     })
     .duration(1000)
     // other way:
    .transition()
    .tween("d", function(d) {
       var that = d3.select(this),
       i = d3.interpolateNumber(percentage, 0);
       return function(t) { 
         that.attr("d", d3.arc()
           .endAngle(Math.PI*2)
           .startAngle(i(t) * Math.PI*2)
           .outerRadius(radius - 10)
           .innerRadius(radius - 70)
           )
          label.text(Math.round(i(t) * 100) + "%");
        };
     })
     .duration(1000)
     .on("end",transition);

}

transition();




function type(d) {
  d.interval = +d.interval;
  return d;
}
<!DOCTYPE html>
<meta charset="utf-8">
<style>

</style>
<body>
<script src="http://d3js.org/d3.v5.min.js"></script>

Upvotes: 2

Related Questions