I'm drawing a pie chart with d3.js. I want to transition the pie slices when new data is added. (i'm using the reusable chart API). I'm first creating a chart group using enter and then appending the chart arc path to that:
http://jsfiddle.net/EuK6H/4/
var arcGroup = svg.select(".pie").selectAll(".arc " + selector)
.data(pie(data))
.enter().append("g")
.attr("class", "arc " + selector);
if (options.left) {
arcGroup.attr('transform', 'translate(' + options.left + ',' + options.top + ')');
} else {
arcGroup.attr('transform', 'translate(' + options.width / 2 + ',' + options.height / 2 + ')');
}
//append an arc path to each group
arcGroup.append("path")
.attr("d", arc)
//want to add the transition in here somewhere
.attr("class", function (d) { return 'slice-' + d.data.type; })
.style("fill", function (d, i) {
return color(d.data.amount)
});
//...
Problem is when new data comes in I need to be able to transition the path (and also the text nodes shown in the the fiddle) but the enter selection is made on the the parent group. How can I add a transition() so it applies to the path?
You can use .select(), which will propagate the data to the selected element. Then you can apply a transition based on the new data. The code would look something like this:
var sel = svg.selectAll(".pie").data(pie(newData));
// handle enter + exit selections
// update paths
sel.select("path")
.transition()
.attrTween("d", arcTween);
Related
I have created d3 PCP chart and Here is my code:
var m = [60, 10, 10, 60],
w = 1000 - m[1] - m[3],
h = 270 - m[0] - m[2];
var x=d3.scaleOrdinal()
.range([0, w]),
y = {},
dragging = {};
var line = d3.line(),
background,
foreground;
var svg = d3.select("#test").append("svg:svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
//let dimensions
// Extract the list of dimensions and create a scale for each.
x.domain(this.dimensions = d3.keys(data[0]).filter(function(d) {
if (d === "name") return false;
if (d === "Plant" || d === "Chemical" || d === "Pathway" || d === "Gene" || d === "Disease") {
y[d] = d3.scaleOrdinal()
.domain(data.map(function(p) {
return p[d];
}))
.range([h, 0]);
} else {
y[d] = d3.scaleLinear().domain([0, d3.max(data.map(function(p) { return p[d]; }))]).range([h, 0]);
}
return true;
}));
//alert(this.dimensions)
// Add grey background lines for context.
background = svg.append("svg:g")
.attr("class", "background")
.selectAll("path")
.data(data)
.enter().append("svg:path")
.style('fill', 'none')
.attr("d", path.bind(this));
// Add blue foreground lines for focus.
foreground = svg.append("svg:g")
.attr("class", "foreground")
.selectAll("path")
.data(data)
.enter().append("svg:path")
.style('fill', 'none')
.style('stroke', 'steelblue')
.attr("d", path.bind(this));
// Add a group element for each dimension.
let g = svg.selectAll(".dimension")
.data(this.dimensions)
.enter().append("svg:g")
.attr("class", "dimension")
.attr("transform", function(d) {
return "translate(" + x(d) + ")";
})
// Add an axis and title.
g.append("svg:g")
.attr("class", "axis")
.each(function(d) {
d3.select(this).call(d3.axisLeft(y[d]));
})
.append("svg:text")
.attr("text-anchor", "middle")
.attr("y", -50)
.attr("x",-10)
.style("fill", "black")
.text(function(d) {return d });
var firstAxis = d3.selectAll('g.dimension g.axis');
firstAxis
.append("svg:image")
.attr('x',-20)
.attr('y',-60)
.attr('width', 40)
.attr('height', 60)
.attr("xlink:href", function(s) {
return "images/" + s+'.png';
});
function position(d) {
var v = dragging[d];
return v == null ? x(d) : v;
}
function transition(g) {
return g.transition().duration(500);
}
// Returns the path for a given data point.
function path(d) {
return line(this.dimensions.map(function(p) {
return [position(p), y[p](d[p])];
}));
}
I have a simple input from on my webpage which displaying some input drop down and based on selected value we are passing each time our new data set to script.Every time a user changes a value on the form a new dataset is sent to my script. I'm struggling to figure out how to update (refresh) the chart with new dataset I have tried to using exit and remove function but not got much success.
svg.exit().remove();
The exit method:
Returns the exit selection: existing DOM elements in the selection for
which no new datum was found. (The exit selection is empty for
selections not returned by selection.data.) (API documentation).
While you have specified data for the selections g, foreground, and background, you have not specified any data for the selection svg, so the exit selection will be empty. Consequently, .remove() can't remove anything.
To use an exit selection, note that the exit selection doesn't remove elements in a selection. It is a selection of a subset of elements in a selection that no longer have corresponding bound data.
If a selection holds 10 DOM elements, and the selection's data array is set to an array with 9 elements, the exit selection will contain one element: the excess element.
We can remove this subset with .exit().remove(). The exit is which elements we no longer need, and remove() gets rid of them. Exit doesn't remove elements because we may want to still do something with them, such as transition them, or hide them, etc.
As noted in the quote above, to populate an exit selection you must use selection.data():
var selection = svg.selectAll("path")
.data(data);
Now we can access the enter selection, the exit selection, and the update selection.
Unless specifying a key, only one of the enter or exit selections can contain elements: if we need more elements we don't need to exit any, if we have excess elements, we don't need any new ones.
To remove excess elements, we can now use:
var selection = svg.selectAll("path")
.data(data);
selection.exit().remove();
Given you are specifying data for other selections such as foreground, background, and g, these are the selections you should be using .exit() on, as part of a complete enter/update/exit cycle. Keep in mind, in d3v4 you need to merge the update and enter selections in order to perform operations on pre-existing and new elements in the selection.
However, if you simply want to get rid of the svg and start fresh (which is what it looks like you want to do - as you are trying to remove the svg) you can simply remove the svg:
d3.select("#test")
.select("svg")
.remove();
However, this won't allow you transition nicely between charts or utilize the enter/update/exit cycle.
Why note use svg.remove()? Because the selection svg holds a g element:
var svg = d3.select("#test") // returns a selection with #test
.append("svg:svg") // returns a selection with the new svg
.attr(...) // returns the selection with the newly created svg again
.append("svg:g") // returns a selection with the new g
.attr(...); // returns the selection with the newly created g again
svg is consequently a selection of a g, and removing it won't remove the svg element. In fact running the above code block after svg.remove() will add a new svg element to the DOM while not removing the old one.
I have currently have a line graph that looks like this:
on jsfiddle http://jsfiddle.net/vertaire/kttndjgc/1/
I've been trying to manually position the values on the graph so they get get printed next to the legend looking something like this:
Unintentional Injuries: 1980, 388437
I tried to set the positions manually, but it seems when I try and adjust to positioning, that positioning is relative to the that of the circle on the line like this:
How can I set the coordinates so that the values appear next to the legend?
Here is the code snippet for printing the values:
var mouseCircle = causation.append("g") // for each line, add group to hold text and circle
.attr("class","mouseCircle");
mouseCircle.append("circle") // add a circle to follow along path
.attr("r", 7)
.style("stroke", function(d) { console.log(d); return color(d.key); })
.style("fill", function(d) { console.log(d); return color(d.key); })
.style("stroke-width", "1px");
mouseCircle.append("text")
.attr("transform", "translate(10,3)"); // text to hold coordinates
.on('mousemove', function() { // mouse moving over canvas
if(!frozen) {
d3.select(".mouseLine")
.attr("d", function(){
yRange = y.range(); // range of y axis
var xCoor = d3.mouse(this)[0]; // mouse position in x
var xDate = x.invert(xCoor); // date corresponding to mouse x
d3.selectAll('.mouseCircle') // for each circle group
.each(function(d,i){
var rightIdx = bisect(data[1].values, xDate); // find date in data that right off mouse
yVal = data[i].values[rightIdx-1].VALUE;
yCoor = y(yVal);
var interSect = get_line_intersection(xCoor, // get the intersection of our vertical line and the data line
yRange[0],
xCoor,
yRange[1],
x(data[i].values[rightIdx-1].YEAR),
y(data[i].values[rightIdx-1].VALUE),
x(data[i].values[rightIdx].YEAR),
y(data[i].values[rightIdx].VALUE));
d3.select(this) // move the circle to intersection
.attr('transform', 'translate(' + interSect.x + ',' + interSect.y + ')');
d3.select(this.children[1]) // write coordinates out
.text(xDate.getFullYear() + "," + yVal);
yearCurrent = xDate.getFullYear();
console.log(yearCurrent)
return yearCurrent;
});
return "M"+ xCoor +"," + yRange[0] + "L" + xCoor + "," + yRange[1]; // position vertical line
});
}
});
First thing I would do is create the legend dynamically instead of hard coding each item:
var legEnter = chart1.append("g")
.attr("class","legend")
.selectAll('.legendItem')
.data(data)
.enter();
legEnter.append("text")
.attr("class","legendItem")
.attr("x",750)
.attr("y", function(d,i){
return 6 + (20 * i);
})
.text(function(d){
return d.key;
});
legEnter.append("circle")
.attr("cx",740)
.attr("cy", function(d,i){
return 4 + (20 * i);
})
.attr("r", 7)
.attr("fill", function(d,i){
return color(d.key);
});
Even if you leave it as you have it, the key here is to assign each text a class of legendItem. Then in your mouseover, find it and update it's value:
d3.select(d3.selectAll(".legendItem")[0][i]) // find it by index
.text(function(d,i){
return d.key + ": " + xDate.getFullYear() + "," + yVal;
});
Updated fiddle.
I trying to understand how the D3 chord diagram works. My first step is to display the arcs for the diagram with following script. But for some reason, the arcs are not showing up.
See web page HERE
Can some one tell me what I am missing?
<body>
<script>
// Chart dimensions.
var width = 960,
height = 750,
innerRadius = Math.min(width, height) * .41,
outerRadius = innerRadius * 1.1;
//Create SVG element with chart dementions
var svg = d3. select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append ("g")
.attr("transform", "translate (" + width / 2 + "," + height / 2 + ")");
//------------Reformat Data ------------------------------------------
var matrix = []; // <- here is the data
d3.tsv('picData.tsv', function(err, data)
{
//console.log(data);
pictures = d3.keys(data[0]).slice(1);
//console.log(pictures);
data.forEach(function(row)
{
var mrow = [];
pictures.forEach(function(c)
{
mrow.push(Number(row[c]));
});
matrix.push(mrow);
//console.log(mrow);
});
//console.log('1st row: ' + matrix[0]);
//console.log(matrix);
});
//---------------- Define diagram layout ----------------------------
var chord = d3.layout.chord() //<-- produce a chord diagram from a matrix of input data
.matrix(matrix) //<-- data in matrix form
.padding(0.05)
.sortSubgroups(d3.descending);
var fill = d3.scale.category20(); //<-- https://github.com/mbostock/d3/wiki/API-Reference#d3scale-scales
//console.log(fill);
var g = svg.selectAll("g.group")
.data(chord.groups)
.enter().append("svg:g")
.attr("class", "group");
//console.log(g);
// create arcs
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
//console.log(arc);
g.append("path")
.attr("d", arc)
.style("fill", function(d) { console.log(d.index); return fill(d.index);})
.style("stroke", function(d) { return fill(d.index); })
.attr("id", function(d, i) { return"group-" + d.index });;
g.append("svg:text")
.attr("x", 6)
.attr("class", "picture")
.attr("dy", 15)
.append("svg:textPath")
.attr("xlink:href", function(d) { return "#group-" + d.index; })
.text(function(d) { return pictures[d.index]; });
//console.log(g);
</script>
</body>
Your problem stems from the fact that d3.tsv is asynchronous:
Issues an HTTP GET request for the comma-separated values (CSV) file at the specified url... The request is processed asynchronously.
As a result, all of your code under "Define diagram layout" is being executed before any data is loaded. Otherwise, your code works fine (See image below). So just move all your code into your d3.tsv(...) call and you'll be all set.
Your script is running without errors, but no elements are being created from your data join. That's usually a sign that you are passing in a zero-length data array.
In fact, you're not passing in an array at all; you're passing a function object. When d3 looks up the array length of that object, it returns undefined, which gets coerced to the number zero, and so no groups and no chords are created.
Relevant part of your code:
var g = svg.selectAll("g.group")
.data(chord.groups)
.enter().append("svg:g")
.attr("class", "group");
To actually get the array of chord group data objects, you need to call chord.groups(). Without the () at the end, chord.groups is just the name of the function as an object.
Edited to add:
Ooops, I hadn't even noticed that your drawing code wasn't included inside your d3.tsv callback function. Fix that, as described in mdml's answer, then fix the above.
I'm making a donut chart that can be switched between several different data sets. I have been able to get the slices to transition nicely, and am positioning the labels with arc.centroid, but I can't figure out how to apply the arc tweening function to the labels. I think I've almost got it, any hints would be appreciated.
Here's a live example: http://jsbin.com/otAjUSO/1/edit?html,output
Add same transition effect to label group also
DEMO
label_group.data(pie)
.transition().duration(750)
.attr("transform", function(d) {
var c = arc.centroid(d);
return "translate(" + c[0] +"," + c[1] + ")";
})
Simply add a transition to the group:
label_group.data(pie)
.transition().duration(750)
// The above transition is all you need
.attr("transform", function(d) {
var c = arc.centroid(d);
return "translate(" + c[0] +"," + c[1] + ")";
});
I'm working with a data set that's categorically identical from year to year, and I want to make a D3 pie chart with animated transitions from year to year. The data is in a 2-d array, each inner array is a year. Because the number of values isn't changing, I think I can just replace the data set for the transition, and I don't need to do a data join (?).
I have the pie chart working well initially, and I'm updating the data via click event. But my transitions aren't working. Here's the code for the first pie chart (there are variable declarations and other data managing that I've left out to save space, and because that stuff's working):
var outerRadius = w/2;
var innerRadius = 0;
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var svg= d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var arcs = svg.selectAll("g.arc")
.data(pie(datamod[0]))
.enter()
.append("g")
.attr("class", "arc")
.attr("transform", "translate(" + outerRadius + ", " +outerRadius + ")");
arcs.append("path")
.attr("fill", function(d,i){
return colors[i];
})
.attr("d", arc);
And then to update...clickToChange() is called when users click anywhere in the body. it's loading new data from the next spot in the 2-d array and also updates text for the year, and there's some code in here to keep it from restarting if it's already running... But the main problem I think is with the code to update the arcs...
function clickToChange()
{ if(!isRunning)
{
isRunning = true;
myTimer =setInterval(function() {if (yearcounter < 11)
{
yearcounter++;
}
else
{
yearcounter = 0;
stopDisplay();
}
var thisyear = 2000 + yearcounter; //updating happens here...
svg.selectAll("g.arc")
.data(pie(datamod[yearcounter]))
.transition()
.attr("class", "arc")
.attr("transform", "translate(" + outerRadius + ", " +outerRadius + ")");
arcs.attr("fill", function(d,i){
return colors[i];
// console.log(d.value);
// return "rgb(" + colorscale(d.value) + ",50,50)";
})
.attr("d", arc);
document.getElementById('year').innerHTML = thisyear;
}, 2000); //end set interval
}//end if
}
function stopDisplay()
{
clearInterval(myTimer);
isRunning = false;
}
I think the problem is that I'm possibly not binding the data properly to the correct elements, and if I'm using the correct notation to select the arcs?
Okay, I can see multiple issues/drawbacks with your approach.
1) In your code:
arcs.append("path")
.attr("fill", function(d,i){
return colors[i];
})
.attr("d", arc);
arc is a function call that you are making that doesn't actually exist in the code that you have shared with us, or you need to write. You have this arc function call multiple times, so this will need to be addressed.
2) I would check into using the .on("click", function(d,i) { do your transitions here in this function call }); method instead of setting the transition and attributes of each of the items. I have found that it makes the transition calls easier to manage if you start doing anything more fancy with the transitions. You can see an example of what I mean in the Chord Diagram at http://bl.ocks.org/mbostock/4062006
Hopefully this helps you out a bit.