2

I am having a great deal of trouble getting my pie/donut chart to update data dynamically. I have it configured so that the user slides a range input to select which month of data he/she wants to see, then the data is passed to my d3 visual. For the sake of simplicity I have hard-coded the data in my example. You may view the snippet below:

var width = 450;
var height = 350;
var margins = { left: 0, top: 0, right: 0, bottom: 0 };

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

var period = ['JAN 2016','FEB 2016','MAR 2016', 'APR 2016', 'MAY 2016', 'JUN 2016',
'JUL 2016', 'AUG 2016', 'SEP 2016', 'OCT 2016', 'NOV 2016', 'DEC 2016'];

d3.select('#timeslide').on('input', function() {
 update(+this.value);
});

function update(value) {
 document.getElementById('range').innerHTML=period[value];
 create_pie(period[value]);
}

var pie_data = {
 'JAN2016': [16,4,1,30],
 'FEB2016': [17,4,0,30],
 'MAR2016': [16,5,1,29],
 'APR2016': [17,4,1,29],
 'MAY2016': [17,4,1,29],
 'JUN2016': [17,4,2,28],
 'JUL2016': [18,3,2,28],
 'AUG2016': [18,3,2,28],
 'SEP2016': [18,3,2,28],
 'OCT2016': [18,3,3,27],
 'NOV2016': [18,3,3,27],
 'DEC2016': [18,3,3,27]
}

function create_pie(month) {

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

  var color = d3.scale.ordinal().range(['darkblue','steelblue','blue',  'lightblue']);

  var arc = d3.svg.arc()
  .innerRadius(radius - 50)
  .outerRadius(radius - 20);

  var sMonth = String(month).replace(' ','');
  var pData = pie_data[sMonth];

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

  var pieG = svg.append('g')
    .attr('transform', 'translate(' + 100 +',' + 75 + ')');

  var Ppath = pieG.datum(pData).selectAll(".pie")
   .data(pie);

 Ppath
  .enter().append("path").attr('class','pie');

 Ppath
   .attr("fill", function(d, i) { return color(i); })
   .attr("d", arc)
   .each(function(d) {
    this.x0 = d.x;
    this.dx0 = d.dx;
   });

 Ppath
   .transition()
   .duration(650)
   .attrTween("d", arcTweenUpdate);

 Ppath
   .exit().remove();

function arcTweenUpdate(a) {
 var i = d3.interpolate({x: this.x0, dx: this.dx0}, a);
 return function(t) {
  var b = i(t);
  this.x0 = b.x;
  this.dx0 = b.dx;
  return arc(b);
 };
}
}

create_pie('JAN 2016');
    
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="//d3js.org/d3.v3.min.js"></script>
</head>
<body>
 <div id='sliderContainer'>
  <input id='timeslide' type='range' min='0' max='11' value='0' step='1'/><br>
  <span id='range'>JAN 2016</span>
 </div>

The good news is the pie chart is getting the new data each time the month is updated, because it looks like the pie chart is indeed moving. The bad news is the pie chart is looking very jerky and it seems my .transition().duration(650) is not working at all. Actually I am started to think that the pie chart is being drawn again and again on top of itself, because the pie chart looks more blurry with each update of the data. I'm not sure why that would be since I was extra careful to include the Ppath.exit().remove(); at the presumably correct place. Ultimately, I'm left feeling like my understanding of how to dynamically update pie data is fundamentally flawed.

I soon realized I wasn't the first to have some issues with the pie transitions. The trickiest thing seems to be the arcTween part. I looked at http://stackoverflow.com/questions/22312943/d3-sunburst-transition-given-updated-data-trying-to-animate-not-snap and followed along as closely as I could. I used that implementation of the arcTweenUpdate, but unfortunately my situation did not improve. You may notice from the snippet that the colored arcs are moving around but the empty spaces that "divide" or "slice" up the pie are static. That's not what I want, it should all be transitioning, nice and smooth. There should not be static parts or awkwardly transitioning parts like it is currently.

Question: How can I keep the dynamic nature of the visual (access pie_data in its current format via my function create_pie) but also have a smooth, clean looking transition like M. Bostock's classic donut?

Note: M. Bostock's block uses a change() function to update the pie chart. I prefer an answer that corrects/augments/adds to my existing code structure (i.e. Ppath.enter()... Ppath.transition() ... Ppath.exit().remove()) However, I would be willing to accept a change() function similar to M. Bostock's orginial if someone can explain explicitly why my method as per this post is impossible / highly impracticle.

Edit

Another unforseen issue when I try to update the data dynamically is concerning radio buttons. As per Karan3112's formatData() function:

function formatData(data) {
    if (document.getElementById("radioButton1").checked) {
        return data[Object.keys(data)[0]].slice(0,4).map(function(item, index) {
            let obj = {};
            Object.keys(data).forEach(function(key) {
                obj[key] = data[key][index] //JAN2016 : [index]
            })
            return obj;
        })
    }
     else if (document.getElementById("radioButton2").checked) {
        return data[Object.keys(data)[0]].slice(5,8).map(function(item, index) {
            let obj = {};
            Object.keys(data).forEach(function(key) {
                obj[key] = data[key][index] //JAN2016 : [index]
            })
            return obj;
        })
    }
}

Basically, in my real data, each month has an array of length 8 like this:

'JAN2016': [17,4,1,29,7,1,1,42],

So depending on which radio button is checked, I want to have the pie be drawn according to either the first 4 items in the array for radioButton1 or the last 4 items in the array for radioButton2.

I initially omitted this part of the task for my OP because I figured it would be simple enough to adapt, but after trying for a good while with little progress, I have reconsidered. My slices don't seem to be working. I think it is because the formatData function is only called once. I tried putting a formatData call inside the change() function, but that didnt work either.

Arash Howaida
  • 2,575
  • 2
  • 19
  • 50
  • I guess there is some issue in your approach. Have a look at this example [here](https://bl.ocks.org/mbostock/1346410). On slider change you will have to just update the data of the pie, instead of re drawing everything. – karan3112 Feb 26 '18 at 06:40
  • @karan3112 I know that block, it was part of my inspiration, it's very nifty. I'm not sure why but I couldn't get the `change()` function to work with my monthly data. It would not throw any errors, but no animation was triggered. I think I pointed everything correctly, so I went for this approach. I hope there is a way to salvage what I have in my post here, and then I can share my `change()` function issue at a later date. But I'm not sure, maybe others will say `change()` is the only way to make it work, I'll have to see. – Arash Howaida Feb 26 '18 at 06:52
  • Also it's not supposed to redraw everything. It's supposed to be drawn once during `.enter()` and then `transition()` if called again. But yea, for some reason it's redrawing things. – Arash Howaida Feb 26 '18 at 07:02
  • The data format in the example is such that they have generated all the paths for each data on init `d3.tsv("data.tsv"`. But the specific path is shown only when the change is called. However in your code you are generating the path on change of the slider. I guess if you update that logic in your code it will work fine. – karan3112 Feb 26 '18 at 07:28
  • @karan3112 I'm not sure what you mean by specific path. You mean when I set `var pData = pie_data[sMonth];` ? I'm not sure if there is a way to have the paths generated on init and still have a dynamic visual. I could be wrong though. – Arash Howaida Feb 27 '18 at 05:59

1 Answers1

4

Following the example by Mike Bostock have updated your code as follows.

  • Added a data format function which will return the data in a {label : value} format.
  • Updated the code logic from loading/redrawing the data onUpdate to updating the pie value.

var width = 450;
  height = 350;
    radius = Math.min(width, height) / 4;

var color = d3.scale.ordinal().range(['darkblue','steelblue','blue',  'lightblue']);

var pie = d3.layout.pie()
    .value(function(d) { return d['JAN2016']; })
    .sort(null);

var arc = d3.svg.arc()
    .innerRadius(radius - 50)
 .outerRadius(radius - 20);

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

var period = ['JAN 2016','FEB 2016','MAR 2016', 'APR 2016', 'MAY 2016', 'JUN 2016',
'JUL 2016', 'AUG 2016', 'SEP 2016', 'OCT 2016', 'NOV 2016', 'DEC 2016'];
var pie_data = {
  'JAN2016': [16,4,1,30],
  'FEB2016': [17,4,0,30],
  'MAR2016': [16,5,1,29],
  'APR2016': [17,4,1,29],
  'MAY2016': [17,4,1,29],
  'JUN2016': [17,4,2,28],
  'JUL2016': [18,3,2,28],
  'AUG2016': [18,3,2,28],
  'SEP2016': [18,3,2,28],
  'OCT2016': [18,3,3,27],
  'NOV2016': [18,3,3,27],
  'DEC2016': [18,3,3,27]
};

var path = svg.datum(formatData(pie_data)).selectAll("path")
    .data(pie)
  .enter().append("path")
    .attr("fill", function(d, i) { return color(i); })
    .attr("d", arc)
    .each(function(d) { this._current = d; }); // store the initial angles
    
d3.select('#timeslide').on('input', function() {
  change(this.value);
});

function change(key) {
 var value =  period[key].replace(' ','');
  document.getElementById('range').innerHTML=period[key];
  pie.value(function(d) { return d[value]; }); // change the value function
  path = path.data(pie); // compute the new angles
  path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
}

function arcTween(a) {
  var i = d3.interpolate(this._current, a);
  this._current = i(0);
  return function(t) {
    return arc(i(t));
  };
}

function formatData(data){
  return data[Object.keys(data)[0]].map(function(item, index){
    let obj = {};
    Object.keys(data).forEach(function(key){
      obj[key] = data[key][index] //JAN2016 : [index]
    })
    return obj;
  })
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<div id='sliderContainer'>
  <input id='timeslide' type='range' min='0' max='11' value='0' step='1'/><br>
  <span id='range'>JAN 2016</span>
 </div>
karan3112
  • 1,867
  • 14
  • 20
  • You comments in the code were very helpful. Sometimes it's hard to figure out what the `var path` and `var pie` were doing. Now it makes more sense. I'm a little bummed that my original way doesn't seem to work, but you have demonstrated how to adapt the M. Bostock way very nicely here. I would have never thought to put a `formatData` function and use it with `var path = svg.datum()`. – Arash Howaida Feb 28 '18 at 16:21
  • Maybe this is my fault for not including more code; I just wanted to put a simple snippet down to illustrate my dynamic data update issue. So anyway, the problem now is I can't figure out how to add another dimension to the data. In addition to the slide interface, I also have two radio buttons (similar to the M. Bostock example). Depending on which button is clicked, I want to slice the array at different parts. I will update the question to show you. – Arash Howaida Mar 01 '18 at 06:04
  • @ArashHowaida The data is loaded in the pie only once, i.e on init. I don't think you can reduce the no. of items in a pie dynamically (first 4 and last 4 depending on radio value). Just try to keep it simple and create 2 different pie charts for each radio. Sometimes it is important to understand that we cannot achieve everything in a single function. – karan3112 Mar 01 '18 at 07:20
  • I feel like I'm pretty close now. I went with the approach you suggested where I create a new pie chart on radio select. I did this by calling a `drawPie()` function on radio select. I am also using the modified `formatData()` function in the **Edit** section of my post that has radio select if clauses to return either `.slice(0,4)` or `.slice(5,8)`. The strange thing is both are returning the same data: `slice(0,4)`. If you could help me figure out the right way to slice in the `formatData()` function you created, then everything will be set. – Arash Howaida Mar 01 '18 at 15:42