I was using the example at this link to draw my pie chart: http://blog.stephenboak.com/2011/08/07/easy-as-a-pie.html
I made two modification to the original code:
1 only three arcs are allowed
function update() {
arraySize = 3;// it is total number of sectors
streakerDataAdded = d3.range(arraySize).map(fillArray); // value is fed into streakerDataAdded
oldPieData = filteredPieData;
pieData = donut(streakerDataAdded);//piedata is donut and value fed into
2 I manually set the value for the arc
var totalOctets = 0;
filteredPieData = pieData.filter(filterData);
filteredPieData[0].value=4000;
filteredPieData[1].value=8000;
filteredPieData[2].value=16000;
totalOctets=filteredPieData[0].value+filteredPieData[1].value+filteredPieData[2].value;
filteredPieData[0].name="Silver";
filteredPieData[1].name="Gold";
filteredPieData[2].name="Copper";
//angle
filteredPieData[0].startAngle=0;
filteredPieData[0].endAngle=filtere
dPieData[0].value/totalOctets*2*Math.PI;
filteredPieData[1].startAngle=filteredPieData[0].endAngle;
filteredPieData[1].endAngle=filteredPieData[1].startAngle+filteredPieData[1].value/totalOctets*2*Math.PI;
filteredPieData[2].startAngle=filteredPieData[1].endAngle;
filteredPieData[2].endAngle=filteredPieData[2].startAngle+filteredPieData[2].value/totalOctets*2*Math.PI;
The complete code is here:http://jsfiddle.net/jsXC5/
So far, so good. The desired function would be that if I click on one of the gold, a function is called and the value for the gold arc increase by 1000 while the values for silver and copper both decrease by 500.
I tried by doing:
paths = arc_group.selectAll("path").data(filteredPieData);
paths.enter().append("svg:path")
.attr("stroke", "white")
.attr("stroke-width", 0.5)
.attr("fill", function(d, i) { return color(i); })
.transition()
.duration(tweenDuration)
.attrTween("d", pieTween)
.on("click", function(d,i){
filteredPieData[1].value=filteredPieData[1].value+1000;
filteredPieData[0].value=filteredPieData[0].value-500;
filteredPieData[2].value=filteredPieData[2].value-500;
});
But it does not work. Help please!
just solved it. The onlick function should be added directly below .attr instead of .attrTween
Related
I am trying to animate a set of points along with a path. Essentially I want to draw the path as the point appears, is there a way for them to have the same delay? I tried doing some calculations but the points seem to be plotted before the line gets to it. Here is a fiddle:
http://jsfiddle.net/Y62Hq/1/
And an excerpt of my code:
var totalLength = path.node().getTotalLength();
var duration = 7000;
var interval = Math.round(duration/data.length);
path
.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength)
.transition()
.duration(duration)
.ease("linear")
.attr("stroke-dashoffset", 0);
var circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("fill", "blue");
circles
.transition()
.delay(function (d, i) { return i * interval;})
.ease("linear")
.attr({
cx: function (d) { return yearScale(d.year); },
cy: function (d) { return window.innerHeight - numberScale(d.number)},
r: 4,
fill: "blue",
// /stroke: "#78B446",
"stroke-width": 4
});
Hmm, intriguing. The problem is that not all of your line segments are the same length. While they all cover the same x distance, the y distance is different. Therefore, the amount of time the different line segments take to reach the next point differs. So you can't use a constant delay.
One way of doing this is to add and measure the different line segments, record their lengths and base the delay of the circles appearing on those lengths relative to the total length and the total duration.
var segments = [0];
for(var i = 1; i < data.length; i++) {
var tmp = svg.append("path")
.datum([data[i-1], data[i]])
.attr("d", line);
segments.push(segments[i-1] + tmp.node().getTotalLength());
tmp.remove();
}
This code adds each line segment (from the first point to the second, second to third and so on), measures its length and removes it immediately afterwards. The result is accumulated in the array segments, which gives the length of the total path up to this point (this is needed for the delay). This is why the previous length is added to the current length.
Then all you need to do to compute the delay for the points is
.delay(function (d, i) { return segments[i]*duration/totalLength;})
Complete demo here. This should work for any number of points/type of line.
To the best of my understanding it is not possible to include a transition on the entering elements in the standard enter/append/merge chain, since doing so would replace the entering element selection with a transition that cannot be merged with the update selection. (See here on the distinction between selections and transitions).
(Question edited in response to comment)
If the desired effect is sequenced transitions, one before and one after the merge, it can be accomplished as follows:
// Join data, store update selection
circ = svg.selectAll("circle")
.data(dataset);
// Add new circle and store entering circle selection
var newcirc = circ.enter().append("circle")
*attributes*
// Entering circle transition
newcirc
.transition()
.duration(1000)
*modify attributes*
.on("end", function () {
// Merge entering circle with existing circles, transition all
circ = newcirc.merge(circ)
.transition()
.duration(1000)
*modify attributes*
});
jsfiddle
I would like to know if there is a way to do this without breaking up the enter/append/merge chain.
There can be no doubt, that you have to have at least one break in your method chaining since you need to keep a reference to the update selection to be able to merge it into the enter selection later on. If you are fine with that, there is a way to keep the chain intact after that initial break.
I laid out the basic principal for this to work in my answer to "Can you chain a function after a transition without it being a part of the transition?". This uses transition.selection() which allows you to break free from the current transition and get access to the underlying selection the transition was started on. Your code is more complicated, though, as the chained transition adds to the complexity.
The first part is to store the update selection like you did before:
// Join data, store update selection
const circUpd = svg.selectAll("circle")
.data(dataset);
The second, uninterrupted part goes like this:
const circ = circUpd // 2. Store merged selection from 1.
.enter().append("circle")
// .attr()...
.transition()
// .duration(1000)
// .attr()...
.on("end", function () {
circ.transition() // 3. Use merged selection from 2.
// .duration(1000)
// .attr()...
})
.selection().merge(circUpd); // 1. Merge stored update into enter selection.
This might need some further explanations for the numbered steps above:
The last line is the most important one—after kicking off the transition, the code uses .selection() to get a hold of the selection the transition was based on, i.e. the enter selection, which in turn can be used to easily merge the stored update selection into it.
The merged selection comprising both the enter and the update selection is the result of the entire chain and is then stored in circ.
This is the tricky part! It is important to understand, that the function provided to .on("end", function() {...}) is a callback which is not executed before the transition ends. Although this line comes before the merging of the selections, it is actually executed after that merge. By referring to circ, however, it closes over—captures, if you will—the reference to circ. That way, when the callback is actually executed, circ will already refer to the previously merged selection.
Have a look at the following working snippet:
var w = 250;
var h = 250;
// Create SVG
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
// Create background rectangle
svg.append("rect")
.attr("x", "0")
.attr("y", "0")
.attr("width", w)
.attr("height", h)
.attr("fill", "aliceblue");
var dataset = [170, 220, 40, 120, 0, 300];
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.range([0, w]);
var yScale = d3.scaleLinear()
.domain([0, 300])
.range([75, 200])
var rad = xScale.bandwidth()/2
// Join data
var circ = svg.selectAll("circle")
.data(dataset);
// Create initial circles
circ.enter().append("circle")
.attr("cx", (d, i) => xScale(i)+rad)
.attr("cy", d => yScale(d))
.attr("r", rad)
.attr("fill", "blue");
// Trigger update on click
d3.select("h3")
.on("click", function () {
// Create new data value
var newvalue = Math.floor(Math.random()*300);
dataset.push(newvalue);
xScale.domain(d3.range(dataset.length));
rad = xScale.bandwidth()/2;
// Join data, store update selection
const circUpd = svg.selectAll("circle")
.data(dataset);
// Add new circle and store entering circle selection
const circ = circUpd // 2. Store merged selection from 1.
.enter().append("circle")
.attr("cx", "0")
.attr("cy", "25")
.attr("r", rad)
.attr("fill", "red")
.transition()
.duration(1000)
.attr("cx", (d, i) => xScale(i)+rad)
.on("end", function () {
circ.transition() // 3. Use merged selection from 2.
.duration(1000)
.attr("cx", (d, i) => xScale(i)+rad)
.attr("cy", d => yScale(d))
.attr("r", rad)
.attr("fill", "blue");
})
.selection().merge(circUpd); // 1. Merge stored update into enter selection.
});
<script src="https://d3js.org/d3.v4.js"></script>
<h3>Add a circle</h3>
I'm stuck at trying to bind two-dimensional data in d3. I want to display a matrix of green squares. I'm building a matrix like this:
var size = 10;
dataset = [];
for(var y = 0; y<size; y++){
var tempData = [size];
for(var x = 0; x<size; x++){
tempData[x] = 5;
};
dataset.push(tempData);
};
I'm not sure how to bind the data correctly. I sort of understand Mike Bostock's tutorial on nested selections, but he's binding a matrix of fixed size to already existing elements. How would I use enter() to create new rectangles? This is how I tried to apply the tutorial's advice to first bind the outer, then the inner arrays.. not surprised that it doesn't work but I also don't know where to go from here..
svg.selectAll("rect")
.data(dataset)
.selectAll("rect")
.data(function (d,i) {return d;})
.enter()
.append("rect")
.attr("x", function(d,i){
return i*20})
.attr("y", function(d,i){
return i*20;})
.attr("height", 15)
.attr("width", 15)
.attr("fill", "green");
There are two problems. First, you have the second .selectAll() immediately after the first .data(), which means that you'll be operating on the update selection. This is empty as there are no elements in the DOM to start with. You need to operate on the enter selection instead (and it's good practice to use g elements here for the first level):
svg.selectAll("rect")
.data(dataset)
.enter()
.append("g")
.selectAll("rect")
.data(function (d,i) {return d;})
Second, you're putting the rectangles along the diagonal (same x and y coordinates), so even though the correct number of rect elements is there, you don't see all of them because they overlap. To fix, you need to take the index in the parent group into account for one of the coordinates (using the secret third argument):
.append("rect")
.attr("x", function(d,i){
return i*20;
})
.attr("y", function(d, i, j){
return j*20;
})
Complete demo here.
I'm rolling through Mike Bostock's unbelievable collection of examples, and I am currently trying to get my own version of Pie Update II working (http://bl.ocks.org/mbostock/1346410). I've co-mingled with code shown here (http://jonsadka.com/blog/how-to-create-adaptive-pie-charts-with-transitions-in-d3/) to help me better understand what's actually going on. By this, I mean I've been tweaking the mbostock example by individually adding bits of code from the other example to see what works and what doesn't.
Regardless, I have a simple HTML page set up with radio buttons declared like so:
<form>
<label><input type="radio" name="dataset" value="photoCounts2" checked> Photos</label>
<label><input type="radio" name="dataset" value="videoCounts2"> Videos</label>
</form>
I have an array of integers of equal size (10), named videoCounts2 and photoCounts2. I'm trying to be able to click on a radio button and have the pie chart adjust to the corresponding dataset. I don't require a smooth transition just yet, I'd first like to get the initial transition working. Here is the JavaScript:
/* ----------------------------------------------------------------------- */
// Global vars
/* ----------------------------------------------------------------------- */
var width = 960,
height = 500,
radius = Math.min(width, height) / 2;
/* ----------------------------------------------------------------------- */
// Data Agnostic - Setup page elements
/* ----------------------------------------------------------------------- */
var color = d3.scale.category20();
var pie = d3.layout.pie()
.value(function(d) { return d; })
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 20);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
/* ----------------------------------------------------------------------- */
// Data Dependent
/* ----------------------------------------------------------------------- */
var path = g.datum(photoCounts2).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.selectAll("input")
.on("change", change);
function change() {
console.log("made it to change");
data = ((this.value === "photoCounts2") ? photoCounts2 : videoCounts2); // This needs to be changed if the dataset is different than video count.
console.log(this.value);
// Transition the pie chart
g.datum(data).selectAll("path")
.data(pie)
.transition()
.attr("d", arc);
// Add any new data
g.datum(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
// Remove any unused data
g.datum(data).selectAll("path")
.data(pie)
.exit()
.remove();
}
I actually get the desired functionality, and nothing visually breaks, but I continually get the following error printed to the console when I switch between datasets (by choosing a different radio input):
Error: Invalid value for <path> attribute d="M1.4083438190194563e-14,-230A230,230 0 0.000006912,1 217.6110300447649,-74.45790482200897L141.92023698571626,-48.559503144788465A150,150 0 0.000006912,0 9.184850993605149e-15,-150Z"
(anonymous function) # d3.js:8717
tick # d3.js:8914
(anonymous function) # d3.js:8906
d3_timer_mark # d3.js:2159
d3_timer_step # d3.js:2139
I truly believe I've done my due diligence looking elsewhere for solutions, but I'm stumped. Any help is greatly appreciated!
You're seeing the error because some of the intermediate d values for the paths that are generated by the transition are invalid. As each of these is only visible for a few milliseconds, you don't actually see the error.
The root cause of the problem is that D3's default transition can't interpolate pie chart segments correctly. To make it work properly, you need to use a custom attribute tween, e.g. as in this example:
path.transition().duration(750).attrTween("d", arcTween);
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
This tells D3 how to generate all the intermediate values; in this case, by interpolating the angle of the segment and using the arc generator to generate the path for the changed angle (as opposed to interpolating the path itself without knowledge on how it was generated in the first place).
I have a simple line graph that checks every 5 seconds for updates & redraws the line/scale if needed. This all works well EXCEPT: the data-point dots.
What am I missing in the redraw to move the dots? The dots are there when the graph is first rendered. But on update, they don't move when the line gets redrawn. So I selected a new data source on update, and the old data-points remained fixed.
Redraw on update
var svgAnimate = d3.select("#animateLine").transition();
svgAnimate.select(".line") // change the line
.duration(750)
.attr("d", valueline(data));
svgAnimate.selectAll(".circle") // change the circle
.duration(750)
.attr("d", valueline(data));
svgAnimate.select(".x.axis") // change the x axis
.duration(750)
.call(xAxis);
svgAnimate.select(".y.axis") // change the y axis
.duration(750)
.call(yAxis);
Initial drawing:
svgAnimate.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("class", "circle")
.attr("r", 5)
.style("fill", function(d) {
if (d.close <= 400) {return "red"}
else { return "black" }
;})
.attr("cx", function(d) { return x(d.date); })
.attr("cy", function(d) { return y(d.close); })
This is what I don't want.
Your problem is that the function valueLine is the function you use to draw the line. Thus, when calling it again with a new data you redraw the line.
For the circles the attribute d has no sense. However, if we consider that the y axis does not change, then you can do something like:
svgAnimate.selectAll(".circle") // change the circle
.data(newData)
.duration(750)
.attr("cx", function(d){return x(d.date)};
If you need to change the y coordinates, then you have to modify the cy attribute of the circle.
My code might not be as rigorous as necessary, please post a jsFiddle if you still have problems.
I had some issues with updating circles in charts too.
Here is a working fiddle and might some people for future searches if they have the same problem:
http://jsfiddle.net/noo8k17n/
The problem is this line:
var svgAnimate = d3.select("#animateLine").transition();
It needs to be removed and then in the update method you can add and remove circles.