I'm trying to animate a pie chart using D3, meaning animate the start angle from 0 to some X angle. I have the following code:
var path = svg.append("g").selectAll("path").data(pie([0, 0]))
.enter()
.append("path")
.attr("fill", function(d, i) { return "#314965" })
.attr("d", arc.endAngle(colorCounter == 0 ? 2 * Math.PI : 0.5))
path = d3.select(path[0][0]);
setInterval(function() {
path.transition()
.duration(750)
.call(arcTween, Math.random() * 0.314);
}, 1500);
//Animation tween function
function arcTween(transition, newAngle) {
transition.attrTween("d", function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return arc(d);
};
});
}
This animates fine, however, the arc that animates doesn't have the same inner or outer radius as the original one. I'm suspecting that the problem is when I return arc(d), the radii got reset to default. If that's the case, I'm not sure how to set them. Please help! Thank you.
Related
I am pretty new to D3 but i have a basic knowledge of HTML, CSS and JavaScript. And i was tasked to create a dashboard for our team in MS Sharepoint (used to be in Excel lol).
Right now, i only need 6 Arcs to be visually presented in the site page. I can just extract the data lists from sharepoint and compute it via javascript then store it in a variable to throw it in the D3 arc. Here is my current code:
<body>
<div class="container" id="graph_container1">
<svg id="svg1"></svg>
<svg id="svg2"></svg>
<svg id="svg3"></svg>
</div>
<script>
var canvas = d3.select("#svg1")
.attr("width", 400)
.attr("height", 400);
var group = canvas.append("g")
.attr("transform", "translate(150, 150)");
var r = 100;
var p = Math.PI * 2;
var score = 70;
var finalScore = p * (70/100);
var arc = d3.arc()
.innerRadius(r)
.outerRadius(80)
.startAngle(0)
.endAngle(finalScore);
group.append("path").attr("d", arc)
.attr("fill", "orange")
.transition()
.ease(d3.easeLinear)
.duration(2000)
.attrTween("d", pieTween);
function pieTween(b) {
b.innerRadius = 0;
var i = d3.interpolate( {startAngle: 0, endAngle: 0}, b );
return function(t) { return arc(i(t));};
}
</script>
</body>
I have 3 main problems right now:
Putting the score text in the middle of the Arc graph
Making a simple animation for the Arc graph like filling up the graph until to the final score
Creating 5 more graphs just like those
I am copying this animation: https://www.youtube.com/watch?v=kK5kKA-0PUQ. I tried its code but it's not working.
Since in a pie/donut chart there is normally one group translated to the center of the chart (which is the case here), just append a text using text-anchor as middle (here, using just 2 decimal places):
group.append("text")
.attr("text-anchor", "middle")
.text(d3.format(".2f")(finalScore))
Your pieTween function has a parameter (b), but there is no argument being passed, since there is no data bound. Besides that, the arc generator has finalScore as the end angle, and because of that no transition is possible.
Change the arc generator and the pieTween function accordingly:
var arc = d3.arc()
.innerRadius(r)
.outerRadius(80)
.startAngle(0);
function pieTween() {
var i = d3.interpolate({
endAngle: 0
}, {
endAngle: finalScore
});
return function(t) {
return arc(i(t));
};
}
Too broad for S.O., sounds like a request. Try it yourself and, if you can't, ask another question (sharing the non-working code).
Here is the code with those changes:
<body>
<div class="container" id="graph_container1">
<svg id="svg1"></svg>
<svg id="svg2"></svg>
<svg id="svg3"></svg>
</div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
var canvas = d3.select("#svg1")
.attr("width", 400)
.attr("height", 400);
var group = canvas.append("g")
.attr("transform", "translate(150, 150)");
var r = 100;
var p = Math.PI * 2;
var score = 70;
var finalScore = p * (70 / 100);
var arc = d3.arc()
.innerRadius(r)
.outerRadius(80)
.startAngle(0);
group.append("path")
.attr("fill", "orange")
.transition()
.ease(d3.easeLinear)
.duration(2000)
.attrTween("d", pieTween);
group.append("text")
.attr("text-anchor", "middle")
.text(d3.format(".2f")(finalScore))
function pieTween() {
var i = d3.interpolate({
endAngle: 0
}, {
endAngle: finalScore
});
return function(t) {
return arc(i(t));
};
}
</script>
</body>
I'd like the arc to grow on mouseover. My code is:
.on("mousemove", function(d) {
d3.select(this)
.transition()
.duration(200)
.attr("d", arcOver);
}
var arcOver = d3.svg.arc()
.outerRadius(radius + 10);
What am I doing wrong? Thank you!
Here's my code on JSFiddle.
A couple of thing, you should remove .outerRadius(radius - 10) from the arc and add it like this:
.each(function(d) { d.outerRadius = outerRadius - 10; });
to your pie, after that you can use d3.interpolate on .attrTween
to increase the arc size through a transition like this:
d3.select(this).transition().duration(200).delay(0).attrTween("d", function(d) {
var i = d3.interpolate(d.outerRadius, outerRadius);
return function(t) { d.outerRadius = i(t); return arc(d); };
});
Here's a fork of your code: Plunker
I am trying to implement an update function for my D3 sunburst diagram where i can change the data that is displayed. I am able to successfully add or remove nodes.
However, i can't seem to be able to modify the existing data.
For example, if half of my chart is removed, i would like for the other half to fill the space leaved by the delete. Same thing goes when new data get added, i would like for the existing data to shrink and take less space
Here is my update function :
function update(newData) {
root = newData;
node = root;
g = svg.datum(root)
.selectAll("g")
.data(partition.nodes(root));
var newG = g.enter()
.append("g");
g.exit().selectAll("path").transition().duration(5000).attrTween("d", arcTween(0)).remove();
path = g.selectAll("path").transition().duration(5000).attr("d", arc);
newG.append("path")
.attr("d", arc);
};
Here is how the chart is built :
function render(data) {
root = data;
node = root;
width = $(".burst-chart-container").height();
height = ($(".burst-chart-container").width() / 2);
radius = Math.min(width, height) / 2;
x = d3.scale.linear().range([0, 2 * Math.PI]);
y = d3.scale.linear().range([0, radius]);
rad = Math.min(width, height) / Math.PI - 25;
partition = d3.layout.partition().sort(null).value(function (d) { return d.size; });
arc = d3.svg.arc()
.startAngle(function (d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function (d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function (d) { return Math.max(0, y(d.y)); })
.outerRadius(function (d) { return Math.max(0, y(d.y + d.dy)); });
svg = d3.select(element[0]).append('svg')
.attr("width", width).attr("height", height)
.attr("class", "svgDashboard")
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");
g = svg.datum(root).selectAll("g")
.data(partition.nodes)
.enter()
.append("g")
path = g.append("path")
.attr("d", arc)
}
I do know that path.attr("d",arc) should update the visual, but it doesn't work in my case.
I think that it has something to do with the partition layout who dosen't tell the existing arcs that they need to change, or the way that I do the selection to update the data, but I might be wrong.
Any help would be appreciated.
I found out that the path and text data were never updated. My update did only change g element data. To correct it, I simply take the parent data and put into into his child after I updated it.
g = svg.datum(root)
.selectAll("g")
.data(partition.nodes(root));
//Add this part to update child elements
g.selectAll("path").each(function () {
d3.select(this).datum(d3.select(this.parentNode).datum());
});
g.selectAll("text").each(function () {
d3.select(this).datum(d3.select(this.parentNode).datum());
});
With those changes, i am able to call my attrTween function which update the visual with the new data.
I am trying to work out how to tween multiple arcs to new positions, while also changing the size of the arc in terms of its inner and outer radius.
Meaning it will move from its current position on the canvas to its new position while also morphing to its new shape based on changes in new data compared to the old data.
I have read many examples such as http://bl.ocks.org/mbostock/5100636 which are all great however I cannot adapt any example to my particular need. Most examples that Ive found do not involve moving the arc on the canvas.
here is how I am creating the arcs in the first place which is working fine, and also animating the arc from 0 to its end position according to the data.
// THIS IS CALLED ON LOAD OF THE GRAPH
function createLargeArcs(graph,data){
var arc = d3.svg.arc()
.innerRadius(function (data) { return (data.r * 5) + 5; })
.outerRadius(function (data) { return data.r * 5 })
.startAngle(0);
var endAngleFunction = function (data) {
return (data.percentage) * ((2 * Math.PI) / 180);
};
// a global variable
arcGroup = graph.append("g")
.attr("class", "arcsGroup");
var arcs = arcGroup.selectAll("path")
.data(data)
.enter()
.append("svg:path")
.attr("transform", function (d) { return "translate(" + xScale(d.u) + "," + yScale(d.t) + ")"; })
.style("fill", "red");
arcs.transition()
.duration(750)
.call(arcTween);
// http://bl.ocks.org/mbostock/5100636
function arcTween(transition, newAngle) {
transition.attrTween("d", function (d) {
var interpolate = d3.interpolate(0, endAngleFunction(d));
return function (t) {
d.endAngle = interpolate(t);
return arc(d);
};
});
}
}
all of the above works just fine
But its the process of working out how to tween the arcs to their new positions and their new size based on new data values that I am struggling with.
so far I have a refresh function which accepts a new/updated data set and within the refresh function this is where I am targeting the tween of the arc elements.
I have achieved the moving of all of the arcs to their new positions with the following :
// THIS IS CALLED LATER ON WHEN DATA CHANGES - ITS HERE I NEED TO IMPLEMENT
function refresh(data){
var arcs = arcGroup.selectAll("path")
.data(newData)
arcs.transition()
.duration(1000)
.attr("transform", function(d) { return "translate("+ xScale(d.users) + "," + yScale(d.traffic) + ")"; });
}
Any help would be much appreciated. Thanks
Okay, so I finally worked out what seems the best approach for this use case. So as you can see from the above example I was able to create the arcs with no issue, but the task of transitioning position, radius and arc radians at the same time across multiple arcs (10 in this case) it started to get a little tricky.
Most of the examples such as http://bl.ocks.org/mbostock/5100636 demonstrate how to transition the arc radians based on changing data efficiently, however I could not find an example that achieved this and also moved the arc to a new position on the canvas while also transitioning the radius of the arc. Perhaps there is an example out there but I could not find it. So here is how I managed to achieve all 3 transitions.
// A helper function to convert a percentage value to radians
var endAngleFunction = function (d) {
return (d['percentage-value']) * ((2 * Math.PI) / 180);
};
// create an arc object but without setting the "endAngle"
var arc = d3.svg.arc()
.innerRadius(function (data) { return (d.r * 5) + 5; }) // calculating the radius based on a value within the data (different for each arc)
.outerRadius(function (data) { return d.r * 5 })
.startAngle(0);
// selecting all svg paths (arcs) within arcGroup, a global variable referencing the group each arc gets appended to when created
var arcs = arcGroup.selectAll("path")
.data(data);
// transition the position of the arc to its new position
arcs.transition("position")
.duration(1000)
.attr("transform", function(d) { return "translate("+ xScale(d.u) + "," + yScale(d.t) + ")"; });
// transition the arc radians to new value
arcs.transition("newArc")
.duration(1000)
.attr("endAngle", function(d) { return endAngleFunction(d); }) // binding the new end Angle to the arc for later reference to work out what previous end angle was
.transition()
.delay(500)
.duration(1000)
.call(arcTween);
// A function to tween the arc radians from a value to a new value - inspired by http://bl.ocks.org/mbostock/5100636
function arcTween(transition, newAngle) {
transition.attrTween("d", function (d) {
var interpolate = d3.interpolate(d3.select(this).attr("endAngle"), endAngleFunction(d));
return function (t) {
d.endAngle = interpolate(t);
return arc(d);
};
});
}
Note : The big thing here for me was working out how to get the previous angle and transition to the new angle, which I achieved by binding the end angle to the arc object on creation and/or modification so i could then pull it in later and use for the transition.
And this seemed to work for me just fine. However as I am new to D3 there could be a more efficient way of achieving this or even structuring the code or the order of how things are called. From here I will try tighten this up and work out a more efficient way.
I hope this might be of help to someone at some stage as god knows I really struggled with this for some time.
Thank you to all who helped on this.
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);
}
});