I am using V3 of the popular d3 library and basically want to have three transitions, followed by each other: The first transition should apply to the exit selection, the second to the update selection and the third to the enter selection. They should be chained in such a manner that when one of the selections is empty, its respective transition is skipped. I.e. when there is no exit selection, the update selection should start immediately. So far, I have come up with this code (using the delay function).
// DATA JOIN
var items = d3.select('#data').selectAll('.item');
items = items.data(data, function(d){
return d.twitter_screenname;
});
// EXIT
items.exit().transition().duration(TRANSITION_DURATION).style('opacity', 0).remove();
// UPDATE
// Divs bewegen
items.transition().duration(TRANSITION_DURATION).delay(TRANSITION_DURATION * 1)
.style('left', function(d, i) {
return positions[i].left + "px";
}).style('top', function(d, i) {
return positions[i].top + "px";
});
// ENTER
// Divs hinzufügen
var div = items.enter().append('div')
.attr('class', 'item')
.style('left', function(d, i) {
return positions[i].left + "px";
}).style('top', function(d, i) {
return positions[i].top + "px";
});
div.style('opacity', 0)
.transition().duration(TRANSITION_DURATION).delay(TRANSITION_DURATION * 2)
.style('opacity', 1);
First of all it doesn't allow to "skip" transitions and secondly I think there is a better way than delay. I've looked at http://bl.ocks.org/mbostock/3903818 but I did not really understand what is happening.
Also, somehow just writing items.exit().transition().duration(TRANSITION_DURATION).remove() does not work with the items, probably because they are not SVG elements but divs.
Sure. Here are two ways.
First, you could use an explicit delay, which you then compute using selection.empty to skip empty transitions. (This is only a minor modification of what you have already.)
var div = d3.select("body").selectAll("div")
.data(["enter", "update"], function(d) { return d || this.textContent; });
// 2. update
div.transition()
.duration(duration)
.delay(!div.exit().empty() * duration)
.style("background", "orange");
// 3. enter
div.enter().append("div")
.text(function(d) { return d; })
.style("opacity", 0)
.transition()
.duration(duration)
.delay((!div.exit().empty() + !div.enter().empty()) * duration)
.style("background", "green")
.style("opacity", 1);
// 1. exit
div.exit()
.style("background", "red")
.transition()
.duration(duration)
.style("opacity", 0)
.remove();
http://bl.ocks.org/mbostock/5779682
One tricky thing here is that you have to create the transition on the updating elements before you create the transition on the entering elements; that’s because enter.append merges entering elements into the update selection, and you want to keep them separate; see the Update-only Transition example for details.
Alternatively, you could use transition.transition to chain transitions, and transition.each to apply these chained transitions to existing selections. Within the context of transition.each, selection.transition inherits the existing transition rather than creating a new one.
var div = d3.select("body").selectAll("div")
.data(["enter", "update"], function(d) { return d || this.textContent; });
// 1. exit
var exitTransition = d3.transition().duration(750).each(function() {
div.exit()
.style("background", "red")
.transition()
.style("opacity", 0)
.remove();
});
// 2. update
var updateTransition = exitTransition.transition().each(function() {
div.transition()
.style("background", "orange");
});
// 3. enter
var enterTransition = updateTransition.transition().each(function() {
div.enter().append("div")
.text(function(d) { return d; })
.style("opacity", 0)
.transition()
.style("background", "green")
.style("opacity", 1);
});
http://bl.ocks.org/mbostock/5779690
I suppose the latter is a bit more idiomatic, although using transition.each to apply transitions to selections (rather than derive transitions with default parameters) isn’t a widely-known feature.
Related
I have a grouped bar chart similar to https://bl.ocks.org/mbostock/3887051
I used a mouseover function to fade the bars the mouse is currently not over
function mouseover(bar)
{
d3.selectAll(".bar")
.filter(function(d){ return (d != bar);})
.transition(t)
.style("opacity", 0.5);
}
While this works nicely to highlight a single bar, I now need to highlight the entire group / fade everything but this group.
So far I haven't been able to figure out though how to get from the datum element d passed via .on("mouseover", function(d) ... back to the entire group this element belongs to.
Is there a simple way to achieve this in D3v4?
In D3 4.0 the callback function for the .on() method is passed 3 arguments: the current datum (d), the current index (i), and the current group (nodes).
Within the mouseover callback, you can selectAll("rect"), and filter out items which are in the current group (node). With this selection, you then set opacity to 0.5. On mouseout, you just need to set all opacity back to 1.0. The pertinent code is:
...
.on('mouseover', function(d, i, node) {
d3.selectAll("rect")
.filter(function (x) { return !isInArray(this, node)})
.attr('opacity', 0.5);
}
)
.on('mouseout', function() {
d3.selectAll("rect").attr('opacity', 1.0);
});
with a small helper function to check if a value is present in an array (array of DOM elements in our case):
function isInArray(value, array) {
return array.indexOf(value) > -1;
}
The full code in context (given your linked example):
g.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d) { return "translate(" + x0(d.State) + ",0)"; })
.selectAll("rect")
.data(function(d) { return keys.map(function(key) { return {key: key, value: d[key]}; }); })
.enter().append("rect")
.attr("x", function(d) { return x1(d.key); })
.attr("y", function(d) { return y(d.value); })
.attr("width", x1.bandwidth())
.attr("height", function(d) { return height - y(d.value); })
.attr("fill", function(d) { return z(d.key); })
.on('mouseover', function(d, i, node) {
d3.selectAll("rect")
.filter(function (x) { return !isInArray(this, node)})
.attr('opacity', 0.5);
}
)
.on('mouseout', function() {
d3.selectAll("rect").attr('opacity', 1.0);
});
One solution could be:
Make a function which selects all group and gives it a transition of opacity 0.
The DOM on which mouse is over give opacity 1.
function hoverIn(){
d3.selectAll(".group-me").transition()
.style("opacity", 0.01);//all groups given opacity 0
d3.select(this).transition()
.style("opacity", 1);//give opacity 1 to group on which it hovers.
}
Make a function which selects all group and gives it a transition of opacity 1, when the mouse is out.
function hoverOut(){
d3.selectAll(".group-me").transition()
.style("opacity", 1);
}
On the group add a class and add the mouse out and in function like
g.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.classed("group-me", true)//add a class for selection.
.on("mouseover", hoverIn)
.on("mouseout", hoverOut)
working code here
I am trying to make a stacked bar graph through d3js and have it update when new data is passed through an update function. I call this update function to initially call the graph and it works fine. However, when I change the data and call it again, it erases all the "rect" elements from the graph (When I console log the data, it appears to be passing through). How can I make the graph be redrawn appropriately? I have tried experimenting with the .remove() statement at the beginning, but without it the data doesn't pass through when the bars are redrawn.
function update(my_data) {
svg.selectAll(".year").remove();
var year = svg.selectAll(".year")
.data(my_data)
.enter().append("g")
.attr("class", "year")
.attr("transform", function(d) { return "translate(" + x0(d.Year) + ",0)"; });
var bar = year.selectAll(".bar")
.data( function(d){ return d.locations; });
bar
.enter().append("rect")
.attr("class", "bar")
.attr("width", x0.rangeBand())
.attr("y", function(d) { return y(d.y1); })
.attr("height", function(d) { return y(d.y0) - y(d.y1); })
.style("fill", function(d) { return color(d.name); });
}
update(data);
It's hard to tell exactly what you're doing cause your question doesn't include the data or the DOM. It would help if you included a link to a work-in-progress jsFiddle or something.
If I had to guess what's going wrong, it looks like you're doing a nested join where each year gets bound to a g element and then each location gets bound to a rect inside each g element.
The issue is likely you are only specifying the enter behavior, but not the update behavior or the exit behavior. As a result, when you try to redraw, nothing updates and nothing exits - but new data elements will get added.
It would seem that is why you have to add the selectAll().remove() to get anything to redraw. By removing everything, all the data elements will trigger the enter condition and get added again.
Take a look at these tutorials to better understand how the enter/update/exit pattern works and how nested joins work.
General Update Pattern: https://bl.ocks.org/mbostock/3808218
Nested Selections: https://bost.ocks.org/mike/nest/
Also, here is a jsFiddle I wrote some time ago to demonstrate how to use nested selections and the general update pattern together:
https://jsfiddle.net/reblace/bWp8L/
var series = svg.selectAll("g.row").data(data, function(d) { return d.key; });
/*
* This section handles the "enter" for each row
*/
// Adding a g element to wrap the svg elements of each row
var seriesEnter = series.enter().append("g");
seriesEnter
.attr("class", "row")
.attr("transform", function(d, i){
return "translate(" + margin.left + "," + (margin.top + (span*i)) + ")";
})
.attr("opacity", 0).transition().duration(200).attr("opacity", 1);
// Adding a text label for each series
seriesEnter.append("text")
.style("text-anchor", "end")
.attr("x", -6)
.attr("y", boxMargin + (boxDim/2))
.attr("dy", ".32em")
.text(function(d){ return d.key; });
// nested selection for the rects associated with each row
var seriesEnterRect = seriesEnter.selectAll("rect").data(function(d){ return d.values; });
// rect enter. don't need to worry about updates/exit when a row is added
seriesEnterRect.enter().append("rect")
.attr("fill", function(d){ return colorScale(d)})
.attr("x", function(d, i){ return i*span + boxMargin; })
.attr("y", boxMargin)
.attr("height", boxDim)
.attr("width", boxDim);
/*
* This section handles updates to each row
*/
var seriesUpdateRect = series.selectAll("rect").data(function(d){ return d.values});
// rect update (Will handle updates after enter)
// rect enter
seriesUpdateRect.enter().append("rect")
.attr("x", function(d, i){ return i*span + boxMargin; })
.attr("y", boxMargin)
.attr("height", boxDim)
.attr("width", boxDim);
// rect enter + update
seriesUpdateRect
.attr("fill", function(d){ return colorScale(d)});
// Exit
seriesUpdateRect.exit();
/*
* This section handles row exit
*/
series.exit()
.attr("opacity", 1)
.transition().duration(200).attr("opacity", 0)
.remove();
I am working on developing a force directed graph in D3. Right now when I mouseover nodes it changes the opacity of the connected links from 0 to 1 and then when I mouseout it returns the opacity back to 0.
That works fine but what I am having trouble with is making a click on the node maintain the opacity of the links at 1 even after the mouseout event. Then I want to be able to click on other nodes to make their links opacity 1 as well. Then also be able to click on the some of the previously clicked nodes to be able to return the opacity of their associated links to 0.
In short, I want to be able to toggle the opacity of the associated links of a node with out it being affect by mouseout events. A sample of my current code is below. I am thinking I might have to set a new id to toggle on and off when I click on a node?
var nodeClick = function(d) {
svg.selectAll(".link")
.filter(function(p) {
return _(d.facets).contains(p.target.name)
})
.transition()
.style('stroke-opacity', 0.9);
};
var overText1 = function(d) {
svg.selectAll(".link")
.filter(function(p) {
return _(d.facets).contains(p.target.name)
})
.transition()
.style('stroke-opacity', 0.9);
};
var overText0 = function(d) {
svg.selectAll(".link")
.transition()
.duration(500)
.style('stroke-opacity', 0);
};
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", function (d) {
return d.group === 1 ? "nodeBig" : "node";
})
.attr("r", function(d) {return d.radius })
.style("fill", function (d) {
return color(d.group);
})
.on("mouseover", overText1)
.on('click', nodeClick)
.on('mouseout', overText0)
.call(force.drag);
I actually finally figured this out on my own. I created a lock field that accepts either a "true" or "false". Then I put an if statement in the mouseoff function that only enables mouseoff functionality on elements that don't have "true" in their lock field.
I want to have a function that handles every transition part, and to be able to chain it using .call:
it
.attr //state 1
.transition()
.duration(1000)
.attr //state 2
to
function move(it){
return it.transition().duration(1000)
}
it
.attr //state 1
.transition()
.call move
.attr //state 2
I want to aggregate all transition within one function, and making it conditional. Example below:
var w = 960;
var h = 500;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var animation = true;
function runAnimation(it){ // this doesn't work, it will just skip the animation
if(animation){
return it.transition()
.duration(1000);
}
else{
return it;
}
}
svg.append("circle")
.attr("cx", w / 2)
.attr("cy", h / 2)
.attr("r", 2)
.call(runAnimation)
.attr("cx", 50)
I am aware that I can do:
function runAnimation2(it, nextStep){
if(animation){
return it.transition()
.duration(1000)
.call(nextStep)
}
else{
return it;
}
}
svg.append("circle")
.attr("cx", w / 2)
.attr("cy", h / 2 + 10)
.attr("r", 2)
.attr("fill", "red")
.call(runAnimation2, function(it){ it.attr("cx", 50)})
Yet I feel it breaks the "flow".
Quoting from the documentation:
The call operator always returns the current selection, regardless of the return value of the specified function.
This is why your first code fragment will not work. You have to use something like the second fragment.
In this code, http://enjalot.com/inlet/4124664/
of which the main part is:
function render(data) {
var nodz = svg.selectAll(".node")
.data(data);
nodz.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return "id"+d[0]+d[1]; })
.attr("x",0)
.attr("y", 0)
.attr("transform", function(d) {
return "translate(" + x(d[0]) + "," + y(d[1]) + ")";
})
.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("stroke", "black")
.text(function(d) {return d[2]; });
// update
nodz.selectAll("text")
.text(function(d) {
return d[2]; });
// another go
d3.selectAll("text")
.text(function(d) {
return d[2]; });
}
// data in form [x pos, y pos, value to display]
var nodes = [[0,0,0],[1,1,0],[1, -1,0],[2,0,0], [2,2,0]];
render(nodes);
nodes2 = [[0,0,1],[1,1,1],[1, -1,1],[2,0,1], [2,2,1], [2, -2,1]];
render(nodes2);
I call the code to draw some nodes twice.
I expect it to draw five nodes with a value of zero in the first pass,
Then I add another item to the list and update all the values to 1 so expect to see all the values change to 1 and a new node appear. Instead, I'm only seeing the last one being set to 1. I've tried adding a unique id to bind the node to the data but this isn't working. Also tried reselecting to see if the data is now bound. In all the tutorials I've been through, just calling the selectAll().data() part updates the data, what am I missing?
The second optional argument to .data() is a function that tells d3 how to match elements. That's where you need to compare your IDs, see the documentation. That said, it should work without IDs in your case as it matches by index by default.
The problem with updating the text is that after calling .selectAll() you need to call .data() again to let d3 know what you want to match to that selection (i.e. that the new data should be bound to the old data).