D3 js multi level pie chart animation - javascript

I created a multi-level pie chart but i am having trouble animate it on load.
Here is the JS that i tryied.The animation works fine on the first circle of the chart , but it hides the other 2.
Any help would be appreciated.Thanks:)
<script>
var dataset = {
final: [7000],
process: [1000, 1000, 1000, 7000],
initial: [10000],
};
var width = 660,
height = 500,
cwidth = 75;
var color = d3.scale.category20();
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("class","wrapper")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
var gs = svg.selectAll("g.wrapper").data(d3.values(dataset)).enter()
.append("g")
.attr("id",function(d,i){
return Object.keys(dataset)[i];
});
var gsLabels = svg.selectAll("g.wrapper").data(d3.values(dataset)).enter()
.append("g")
.attr("id",function(d,i){
return "label_" + Object.keys(dataset)[i];
});
var count = 0;
var path = gs.selectAll("path")
.data(function(d) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d, i, j) {
if(Object.keys(dataset)[j] === "final"){
return arc.innerRadius(cwidth*j).outerRadius(cwidth*(j+1))(d);
}
else{
return arc.innerRadius(10+cwidth*j).outerRadius(cwidth*(j+1))(d);
}
})
.transition().delay(function(d, i, j) {
return i * 500;
}).duration(500)
.attrTween('d', function(d,x,y) {
var i = d3.interpolate(d.startAngle+0.1, d.endAngle);
return function(t) {
d.endAngle = i(t);
return arc(d);
}
});
</script>

The main problem is that you're using the same arc generator for all of the different pie segments. That means that after the transition, all the segments will have the same inner and outer radii -- they are there, you just can't see them because they're obscured by the outer blue segment.
To fix this, use different arc generators for the different levels. You also need to initialise the d attribute to zero width (i.e. start and end angle the same) for the animation to work properly.
I've implemented a solution for this here where I'm saving an arc generator for each pie chart segment with the data assigned to that segment. This is a bit wasteful, as a single generator for each level would be enough, but faster to implement. The relevant code is below.
var path = gs.selectAll("path")
.data(function(d) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d, i, j) {
d._tmp = d.endAngle;
d.endAngle = d.startAngle;
if(Object.keys(dataset)[j] === "final"){
d.arc = d3.svg.arc().innerRadius(cwidth*j).outerRadius(cwidth*(j+1));
}
else{
d.arc = d3.svg.arc().innerRadius(10+cwidth*j).outerRadius(cwidth*(j+1));
}
return d.arc(d);
})
.transition().delay(function(d, i, j) {
return i * 500;
}).duration(500)
.attrTween('d', function(d,x,y) {
var i = d3.interpolate(d.startAngle, d._tmp);
return function(t) {
d.endAngle = i(t);
return d.arc(d);
}
});

Related

First d3 transition on update inside interval is not transitioning

I have a donut chart that is being used as a way to show progression. I don't have a way to show you the donut chart, but the code is simple enough to copy and paste.
I added the code to show you an example. I've tried various unreasonable methods to make the transition work the first time. But for some reason it's still not working. All examples online are pretty similar so I'm not really sure why this is happening.
var data = [95, 5];
var pie = d3.pie().sort(null);
var svg = d3.select("svg"),
width = svg.attr("width"),
height = svg.attr("height"),
radius = Math.min(width, height) / 2;
var arc = d3.arc()
.innerRadius(60)
.outerRadius(radius);
function createdonut() {
g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
//Inner SVG Circle
svg.append("svg:circle")
.attr("cx", width / 2)
.attr("cy", height / 2)
.attr("r", 60)
.style("fill", "#ead4d4")
.append("g");
var color = d3.scaleOrdinal(['#4daf4a', '#377eb8', '#ff7f00', '#984ea3', '#e41a1c']);
//Generate groups
var arcs = g.selectAll("arc")
.data(pie(data))
.enter()
.append("g")
.attr("class", "arc")
//Draw arc paths
arcs.append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc)
.on('mouseover', mouseover);
function mouseover(d, i) {
$('#percentage').html(i.data + ' units');
}
}
function updateDoNut(update) {
data[0] = data[0] - update;
data[1] = data[1] + update;
var path = d3.select("svg").selectAll("path").data(pie(data));
/*path.enter().append("path")
.attr("fill", function (d, i) {
return color[i];
})
.attr("d", arc);*/
path.transition().duration(100).attrTween("d", arcTween);
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function (t) {
return arc(i(t));
};
}
createdonut();
//updateDoNut(0);
var inter = setInterval(function () { updateDoNut(5); }, 3000);
<script src="https://d3js.org/d3.v6.min.js"></script>
<div id="my_dataviz">
<svg width="300" height="200"> </svg>
<div id="percentage">0 units</div>
</div>
If we look at your tween function we'll see a problem:
var i = d3.interpolate(this._current, a);
this._current = i(0);
this.current is undefined when you first start transtioning - so how is D3 to interpolate between undefined and an object contianing arc properties? It doesn't. Resulting in the non-transition you are seeing. Set this._current when appending the arcs:
arcs.append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc)
.each(function(d) {
this._current = d;
})
.on('mouseover', mouseover);
Now when you update the circle, there is a valid start point for the interpolator and you should see a transition:
var data = [95, 5];
var pie = d3.pie().sort(null);
var svg = d3.select("svg"),
width = svg.attr("width"),
height = svg.attr("height"),
radius = Math.min(width, height) / 2;
var arc = d3.arc()
.innerRadius(60)
.outerRadius(radius);
function createdonut() {
g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
//Inner SVG Circle
svg.append("svg:circle")
.attr("cx", width / 2)
.attr("cy", height / 2)
.attr("r", 60)
.style("fill", "#ead4d4")
.append("g");
var color = d3.scaleOrdinal(['#4daf4a', '#377eb8', '#ff7f00', '#984ea3', '#e41a1c']);
//Generate groups
var arcs = g.selectAll("arc")
.data(pie(data))
.enter()
.append("g")
.attr("class", "arc")
//Draw arc paths
arcs.append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc)
.each(function(d) {
this._current = d;
})
.on('mouseover', mouseover);
function mouseover(d, i) {
$('#percentage').html(i.data + ' units');
}
}
function updateDoNut(update) {
data[0] = data[0] - update;
data[1] = data[1] + update;
var path = d3.select("svg").selectAll("path").data(pie(data));
path.transition().duration(2000).attrTween("d", arcTween);
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function (t) {
return arc(i(t));
};
}
createdonut();
//updateDoNut(0);
var inter = setInterval(function () { updateDoNut(5); }, 3000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<div id="my_dataviz">
<svg width="300" height="200"> </svg>
<div id="percentage">0 units</div>
</div>
Why doesn't this interpolation between undefined and an object generate an error? Well D3-interpolate will try to interpolate very hard. In this case, between undefined and an object, it'll use d3-interpolateObject, which will interpolate as follows:
For each property in b, if there exists a corresponding property in a,
a generic interpolator is created for the two elements using
interpolate. If there is no such property, the static value from b is
used in the template. (docs)
So, as there are no properties in undefined, the interpolator just uses a static value for every point in the interpolation, hence the lack of a transition on the first update: every interpolated point is the same: the end point values.

Label names are not displayed from input data in pie transition chart of d3.js

I am using d3.js to draw a pie transition chart. But when labels are placed in the data array as show below:
data = [{"label":"sector1", "value":25}, {"label":"sector2", "value":45}]
The pie chart won't be displayed. Instead "NaN" will be printed.
The complete code is pasted below:
var w = 400,
h = 400,
r = Math.min(w, h) / 2,
data = [{"label":"sector1", "value":25}, {"label":"sector2", "value":45}], // Data with label-value pairs
color = d3.scale.category20(),
arc = d3.svg.arc().outerRadius(r),
donut = d3.layout.pie();
var vis = d3.select("body").append("svg") // Place the chart in 'pie-chart-div'
.data([data])
.attr("width", w)
.attr("height", h);
var arcs = vis.selectAll("g.arc")
.data(donut)
.enter().append("g")
.attr("class", "arc")
.attr("transform", "translate(" + r + "," + r + ")");
var paths = arcs.append("path")
.attr("fill", function(d, i) { return color(i); });
var labels = arcs.append("text")
.attr("transform", function(d) { d.innerRadius = 120; return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.value; });
paths.transition()
.ease("bounce")
.duration(2000)
.attrTween("d", tweenPie);
paths.transition()
.ease("elastic")
.delay(function(d, i) { return 2000 + i * 50; })
.duration(750)
.attrTween("d", tweenDonut);
function tweenPie(b) {
b.innerRadius = 0;
var i = d3.interpolate({startAngle: 0, endAngle: 0}, b);
return function(t) {
return arc(i(t));
};
}
function tweenDonut(b) {
b.innerRadius = r * .6;
var i = d3.interpolate({innerRadius: 0}, b);
return function(t) {
return arc(i(t));
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
How to display the label names along with the values in the chart ?
You need to call donut with your data inside like that :
data2 = data.map(function(d) { return d.value}) // [25, 45]
...
.data(donut(data2))
and then call the label ;
.text(function(d, i) { return data[i].label; });
See http://jsfiddle.net/980f0cdj/1/

Multiple d3 charts in the same row

The code below draws 1 pie chart and a legend on the left side of the screen. Right now, I am trying to draw another pie chart with legend right next to the one on the left (same row). I've tried using multiple divs in the html to make this work, but I want a more pure d3 solution in which the duplication happens in the d3 code rather than in the html or css.
var w = 200;
var h = 200;
var r = h / 2;
var color = d3.scale.category20c();
var vis = d3.select(divId).append("svg:svg").data([descArray]).attr("width",w).attr("height", h).append("svg:g").attr("transform", "translate(" + r + "," + r + ")");
var pie = d3.layout.pie().value(function (d, i) {
return countArray[i];
});
// declare an arc generator function
var arc = d3.svg.arc().outerRadius(r);
// select paths, use arc generator to draw
var arcs = vis.selectAll("g.slice").data(pie).enter().append("svg:g").attr("class", "slice");
arcs.append("svg:path")
.on("click", function(d) {//clicking on individual arcs
arcs.selectAll("path").style("opacity", 1);//resets all arcs' opacity to 1
d3.select(this).style("opacity", 0.5);//sets clicked arc's opacity down
alert(d.data + " " + d.value);
})
.style("fill", function(d,i) { return color(i); })
.transition().delay(function(d, i) { return i * 100; }).duration(1000)
.attrTween('d', function(d) {
var i = d3.interpolate(d.startAngle+0.7, d.endAngle);
return function(t) {
d.endAngle = i(t);
return arc(d);
}
})
.attr("fill", function (d, i) {
return color(i);
});
var legend = d3.select(divId).append("svg")
.attr("class", "legend")
.attr("width", r * 4)
.attr("height", r * 4)
.selectAll("g")
.data(color.domain().slice().reverse())
.enter().append("g")
.attr("transform", function(d, i) { return "translate(230," + i * 27 + ")"; });
legend.append("rect")
.on("click", function(d) {
alert(d.data + " " + d.value);
})
.attr("width", 18)
.attr("height", 18)
.style("fill", function (d, i) {
return color(i);
})
put them in seperate divs but in the same SVG element
Presuming vis is your svgElement:
var firstChart = vis.append(div). // then put your first chart here
var secondChart = vis.append(div). // then put your second chart here

arctween and dataset change in donut chart with d3,js

With the click of a button, I want to add a new dataset to my doughnut chart and have it transition the new dataset. The code I've written sort of does that but it runs into an issue when the number of individual data within the dataset is different from the previous i.e. going from [1,2] to [1,2,3,4].
I think the issue is that I need to create a new path whenever there the new dataset has more data, and remove paths whenever it has less. However, when I try to append data in my click function, it will append it without removing the old paths and will overlap on the chart.
Here is a version without appending, where the arctween will work but there will be empty pie arcs because I don't append path (arctween works half the time):
http://jsfiddle.net/njrPF/1/
var pieW = 500;
var pieH = 500;
var innerRadius = 100;
var outerRadius = 200;
var results_pie = d3.layout.pie()
.sort(null);
var pie_arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var svg_pie = d3.select("#pieTotal")
.attr("width", pieW)
.attr("height", pieH)
.append("g")
.attr("transform", "translate(" + pieW / 2 + "," + pieH / 2 + ")")
.attr("class", "piechart");
var pie_path = svg_pie.selectAll("path").data(results_pie([1, 2]))
.enter().append("path")
.attr("d", pie_arc)
.each(function (d) {
this._current = d;
}) // store the initial values
.attr("class", "vote_arc")
.attr("value", function (d, i) {
return (i - 1);
});
var pie_votes = [1, 2];
var pie_colors = ["#0f0", "#f00"];
$(svg_pie).bind("monitor", worker);
$(svg_pie).trigger("monitor");
function worker(event) {
pie_path = pie_path.data(results_pie(pie_votes))
.attr("fill", function (d, i) {
return pie_colors[i];
});
pie_path.transition().duration(500).attrTween("d", arcTween).each('end', function (d) {
if (d.value <= 0) {
this.remove();
}
});
setTimeout(function () {
$(svg_pie).trigger("monitor");
}, 500);
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function (t) {
return pie_arc(i(t));
};
}
$('button').click(function () {
pie_votes = [];
pie_colors = [];
for (var i = 0; i < Math.floor(Math.random() * 6); i++) {
//sets new values on pie arcs
pie_votes.push(Math.floor(Math.random() * 10));
pie_colors.push("#" + (Math.floor(Math.random() * 16777215)).toString(16));
}
pie_path = pie_path.data(results_pie(pie_votes))
.attr("fill", function (d, i) {
return pie_colors[i]
});
pie_path.transition().duration(500).attrTween("d", arcTween).each('end', function (d) {
if (d.value <= 0) {
this.remove();
}
});
});
Here is a version where I try to append new paths but they overlap:
http://jsfiddle.net/njrPF/3/
var pieW = 500;
var pieH = 500;
var innerRadius = 100;
var outerRadius = 200;
var results_pie = d3.layout.pie()
.sort(null);
var pie_arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var svg_pie = d3.select("#pieTotal")
.attr("width", pieW)
.attr("height", pieH)
.append("g")
.attr("transform", "translate(" + pieW / 2 + "," + pieH / 2 + ")")
.attr("class", "piechart");
var pie_path = svg_pie.selectAll("path").data(results_pie([1, 2]))
.enter().append("path")
.attr("d", pie_arc)
.each(function (d) {
this._current = d;
}) // store the initial values
.attr("class", "vote_arc")
.attr("value", function (d, i) {
return (i - 1);
});
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function (t) {
return pie_arc(i(t));
};
}
$('button').click(function () {
pie_votes = [];
pie_colors = [];
for (var i = 0; i < Math.floor(Math.random() * 10); i++) {
//sets new values on pie arcs
pie_votes.push(Math.floor(Math.random() * 10));
pie_colors.push("#" + (Math.floor(Math.random() * 16777215)).toString(16));
}
pie_path = pie_path.data(results_pie(pie_votes))
.enter().append("path")
.attr("d", pie_arc)
.each(function (d) {
this._current = d; }) // store the initial values
.attr("class", "vote_arc")
.attr("value", function (d, i) {
return (i - 1);
});
pie_path.attr("fill", function (d, i) {
return pie_colors[i]
});
pie_path.transition().duration(500).attrTween("d", arcTween).each('end', function (d) {
if (d.value <= 0) {
this.remove();
}
});
});
Thanks in advance.
You need to handle the enter and exit selections as well as the update selection. See for example this tutorial. The relevant code in your case would be
pie_path = pie_path.data(results_pie(pie_votes));
pie_path.enter().append("path")
.attr("d", pie_arc)
.each(function (d) {
this._current = d;
}) // store the initial values
.attr("class", "vote_arc")
.attr("value", function (d, i) {
return (i - 1);
});
pie_path.exit().remove();
Complete example here.

d3.js dynamic data with labels

I've managed to get the following working (pie chart changing dynamically based on a slider value):
var x = function() {
return $("#slider").val();
}
var data = function() {
return [x(), ((100-x())/2), ((100-x())/2)];
}
var w = 100,
h = 100,
r = 50,
color = d3.scale.category20(),
pie = d3.layout.pie().sort(null),
arc = d3.svg.arc().outerRadius(r);
var svg = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")");
var arcs = svg.selectAll("path")
.data(pie(data()))
.enter().append("svg:path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { this._current = d; });
var redraw = function() {
newdata = data(); // swap the data
arcs = arcs.data(pie(newdata)); // recompute the angles and rebind the data
arcs.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
};
// Store the currently-displayed angles in this._current.
// Then, interpolate from this._current to the new angles.
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
I can't seem to modify it to include dynamic labels (use an object for data rather than an array {'label': 'label1', 'value': value}. How would I modify the above code to add labels?
This should work, keeping the labels and the pie chart as separate entities:
var x = function() {
return $("#slider").val();
}
var data = function() {
return [x(), ((100-x())/2), ((100-x())/2)];
}
var labels = function() {
var label1 = "LABEL 1: " + x();
var label2 = "LABEL 2: " + ((100-x())/2);
var label3 = "LABEL 3: " + ((100-x())/2);
return [label1, label2, label3];
}
var w = 200,
h = 100,
r = 50,
color = d3.scale.category20(),
pie = d3.layout.pie().sort(null),
arc = d3.svg.arc().outerRadius(r);
var svg = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + 50 + "," + h / 2 + ")");
var arcs = svg.selectAll("path")
.data(pie(data()))
.enter().append("svg:path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { this._current = d; });
var label_group = svg.selectAll("text")
.data(labels())
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("x", 60)
.attr("y", function(d, i) { return (i * 20) - 16; });
var redraw = function() {
newdata = data(); // swap the data
arcs = arcs.data(pie(newdata)); // recompute the angles and rebind the data
arcs.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
label_group = label_group.data(labels());
label_group.transition().delay(300).text(function(d) {
return d;
});

Categories

Resources