Transition effects in population pyramid - d3.js

I am working on a population pyramid that has an updating function.
http://bricbracs.com/hh/
As you can see the bars expand and contract in a horizontal line when you update it with new data. I want to modify the transition effect so that the bars enter and exit vertically like this:
http://vis.stanford.edu/jheer/d3/pyramid/shift.html
I have been following this tutorial and modifying the code but so far no luck.
https://strongriley.github.io/d3/tutorial/bar-2.html
Here is the code that first draws the bars on loading. (this is the male bar group, the female bar group is the same)
leftBarGroup.selectAll('.bar.left')
.data(data)
.enter()
.append('rect')
.attr('class', 'bar left')
.attr('y', function(d) { return yScale(d.group); })
.attr("width", 0)
.attr("opacity", 0)
.transition()
.duration(500)
.attr('width', function(d) { return xScale(d.male); })
.attr('height', yScale.rangeBand())
.attr("opacity", 1)
And here is the corresponding part of the code in the updating function which changes the bars.
var sel = leftBarGroup.selectAll('.bar.left')
.attr('class', 'bar left')
.data(data)
.data(data, function(d) { return d.male; })
.transition()
.attr('y',0)
.duration(500)
.attr('y', function(d) { return yScale(d.group); })
.attr('height', yScale.rangeBand())
.attr('width', function(d) { return xScale(d.male); })
.attr('height', yScale.rangeBand())
Thanks in advance.

Here's one way to reproduce the effect in your linked example. I offset the bars and then slide them back into place. You then handle the top and bottom bars slightly different.
Note, I only did the slide down on the male side of the pyramid, if you need help going the rest of the way just leave me a comment.
var sel = leftBarGroup.selectAll('.bar.left')
.attr('class', 'bar left')
.data(data)
.data(data, function(d) {
return d.male;
})
// offset y to slide down
.attr('y', function(d){
var self = d3.select(this);
return +self.attr('y') - yScale.rangeBand();
})
.transition()
.duration(500)
// slide it back into place
.attr('y', function(d) {
return yScale(d.group);
})
// and set new width
.attr('width', function(d) {
return xScale(d.male);
});
// for the very top bar
// not only slide it but "fade it in"
leftBarGroup.select(':last-child')
.style('opacity', 0)
.transition()
.duration(500)
.style('opacity', 1)
.attr('y', function(d) {
return yScale(d.group);
});
// append a fake bar on the bottom
// to slide and fade out
leftBarGroup.append('rect')
.attr('y', function(d) {
return yScale('0-4');
})
.attr('height', yScale.rangeBand())
.attr('width', function(){
return leftBarGroup.select(':first-child').attr('width');
})
.style('fill', 'steelblue')
.style('opacity', 0.6)
.transition()
.duration(500)
.attr('y', function(d) {
return yScale('0-4') + yScale.rangeBand();
})
.style('opacity', 0)
.remove();
EDITS
Going up is just a matter of reversing the logic:
var sel = leftBarGroup.selectAll('.bar.left')
.attr('class', 'bar left')
.data(data)
.data(data, function(d) {
return d.male;
})
// offset y to slide up
.attr('y', function(d){
var self = d3.select(this);
return +self.attr('y') + yScale.rangeBand()
})
.transition()
.duration(500)
// slide it back into place
.attr('y', function(d) {
return yScale(d.group);
})
// and set new width
.attr('width', function(d) {
return xScale(d.male);
});
// for the very bottom bar
// not only slide it but "fade it in"
leftBarGroup.select(':first-child')
.style('opacity', 0)
.transition()
.duration(500)
.style('opacity', 1)
.attr('y', function(d) {
return yScale(d.group);
});
// append a fake bar on the top
// to slide and fade out
var w = leftBarGroup.select(':last-child').attr('width');
leftBarGroup.append('rect')
.attr('class','fake')
.attr('y', function(d) {
return yScale('90+');
})
.attr('height', yScale.rangeBand())
.attr('width', w)
.style('fill', 'steelblue')
.style('opacity', 0.6)
.transition()
.duration(500)
.attr('y', function(d) {
return yScale('90+') - yScale.rangeBand();
})
.style('opacity', 0)
.remove();
Updated code sample here.

Related

How do you update an existing svg element with the d3v5 join syntax

I'm trying to make a heatmap where the x and y cell values can remain constant however the "fill" attribute can change based on the data. I tried to do something like this:
var tiles = svg.selectAll('rect')
.data(data.values, function(d) {return d.mt_aa+':'+d.position_aa;})
// set up the enter/update/exit block
tiles.join(
function(enter) {
return enter
.append("rect")
.attr("x", function(d) {return x(d.mt_aa) }) // set x coordinate
.attr("y", function(d) {return y(d.position_aa) }) // set y coordinate
.attr("width", x.bandwidth() ) // set tile width
.attr("height", y.bandwidth() ) // set tile height
.style("fill", function(d) {return myColor(d.score) }) // set tile fill based on viridis pallete
.style("opacity", 0); // set opacity as 0 so elements are added but not visible
},
function(update) {
return update
.transition()
.duration(1000)
.attr("x", function(d) {return x(d.mt_aa) })
.attr("y", function(d) {return y(d.position_aa) })
.attr("width", x.bandwidth() )
.attr("height", y.bandwidth() )
.style("fill", function(d) {return myColor(d.score) })
.style("opacity", 0);
},
function(exit){
return exit
.transition()
.duration(1000)
.style('opacity', 0)
.on('end', function() {
d3.select(this).remove();
});
}
)
.transition()
.duration(1000)
.style("opacity", 1);
This mostly works, transitions appropriately, etc., but only if one or more of the heatmap cells are new. If I pass through data where nothing changes but the fill (d.score) nothing transitions/updates. I suspect this is occurring because nothing is being added or removed and so as far as d3 is concerned nothing needs to be updated and the update block never executes. However I'm unsure how to go about solving this using the join syntax in d3 v5.
Updated with the working answer suggested by #anbnyc
// create tiles with 'rect' elements
var tiles = svg.selectAll('rect') // from our svg select the rectangle elements, important for the enter/update/exit block below
.data(data.values, function(d) {return d.mt_aa+':'+d.position_aa;}) // bind the data to the rect selection
const t = svg.transition()
.duration(1000);
// set up the enter/update/exit block
tiles.join(
enter => enter
.append("rect")
.attr("x", function(d) {return x(d.mt_aa) })
.attr("y", function(d) {return y(d.position_aa) })
.attr("width", x.bandwidth() )
.attr("height", y.bandwidth() )
.style("fill", function(d) {return myColor(d.score) })
.style("opacity", 0)
.call(enter => enter.transition(t)
.style("opacity", 1)),
update => update
.attr("x", function(d) {return x(d.mt_aa) })
.attr("y", function(d) {return y(d.position_aa) })
.attr("width", x.bandwidth() )
.attr("height", y.bandwidth() )
.style("opacity", 0)
.call(update => update.transition(t)
.style("fill", function(d) {return myColor(d.score) })
.style("opacity", 1)),
exit => exit
.style('opacity', 0)
.call( exit => exit.transition(t)
.remove())
)
Your update function needs to return a selection, but it's currently returning a transition. Use .call to apply a transition to a selection inside join. See examples at https://observablehq.com/#d3/selection-join

Transition height stacked bar chart

I have a codepen here - https://codepen.io/anon/pen/GybENz
I've created a simple stacked bar chart with a legend to filter the chart.
I'd like to animated the height of the bar from the bottom axis up.
Currently its animating from the left and down
let layersBar = layersBarArea.selectAll('.layer').data(stackedSeries)
.enter()
.append('g')
.attr('class', 'layer')
.style('fill', (d, i) => {
return colors[i];
});
layersBar.selectAll('rect')
.data((d) => {
return d
})
.enter()
.append('rect')
.attr('height', 100)
.transition()
.duration(400)
.attr('height', (d, i) => {
return y(d[0]) - y(d[1]);
})
.attr('y', 0)
.attr('y', (d) => {
return y(d[1]);
})
.attr('x', (d, i) => {
return x(d.data.date)
})
.attr('width', x.bandwidth());
}
Set the x position, the width, the y position (as the baseline) and the height (as zero) before the transition:
.attr('height', 0)
.attr("y", h - margin.bottom - margin.top)
.attr('x', (d, i) => {
return x(d.data.date)
})
.attr('width', x.bandwidth())
Here is the updated CodePen: https://codepen.io/anon/pen/ypdoMK?editors=0010
PS: It would be a good idea transitioning each rectangle individually. For instance, if the user clicked usedInf, you should transition only those rectangles... however, since you did this...
layersBarArea.selectAll('g.layer').remove();
... at the beginning of your drawChart function, which is a wrong approach, such suggestion will need a big refactor, out of the scope of this question/answer.

Do I need the merge function for this chart?

I have a sample that I converted from version 3 that works as expected until I add the new merge method to it then the data in the chart is wrong.
here is the original sample in version 3:
http://bl.ocks.org/ramdog/805185358382521b77a1
here is the converted sample without the merge,
https://jsfiddle.net/jwaldner/r0ew1ju2/
they look fine, but if I un comment the merge the chart is not the same. Am I using this wrong?
This is the code:
// ENTER
bars
.enter()
.append("rect")
.style("fill", "green")
.attr("width", x.bandwidth())
.attr("x", 0)
.attr("x", function(d) { return x(d.date); })
.attr("y", y(0))
.attr("height", 0)
.style("opacity", 0)
//.merge(bars)
.transition()
.duration(500)
.attr("y", function(d) { return y(d.total); })
.attr("height", function(d) { return height - y(d.total); })
.style("opacity", 1);

How to get the current element height?

So that I can transition the bars in a bar chart smoothly I need to set the height before I call transition().
When the chart first renders the bars animate up from the bottom of the chart as required:
chart.svg.selectAll('.bar')
.attr('y', chart.options.height)
.attr('x', function (d) {
return chart.xScale(d.title);
})
.attr('width', chart.xScale.rangeBand())
.attr('height', function () {
return 0;
})
.transition()
.attr('y', function (d) {
return chart.yScale(d.score);
})
.attr('height', function (d) {
return chart.options.height - chart.yScale(d.score);
});
However, when I change the data I don't want to set the height back to 0. Instead I need to set the height to the current height of the rectangle. How can I access this from the attr function?
.attr('height', function () {
return 0; // how do I get the current height
})
When I log this I have access to the DOM element but not sure where to go from there. I tried d3.select(this).attr('height') but it always returns null.
As #LarsKotthoff is hinting at in his comment, just break apart your initial draw from your update:
// intial draw of bars
node
.enter()
.append("rect")
.attr("class", "myBars")
.style("fill", "steelblue")
.attr('y', config.height)
.attr('x', function(d, i) {
return xScale(i);
})
.attr('width', xScale.rangeBand())
.attr('height', function() {
return 0;
});
Then fire the update to transition the bars from their current position:
function update() {
node = svg
.selectAll(".myBars")
.data(data);
node
.transition()
.attr('y', function(d) {
return yScale(d);
})
.attr("height", function(d) {
return config.height - yScale(d);
});
}
Here's the most minimal example I could code up.

d3.js Unable to call drag on label

Here is a plunker modified from mbostock
I want to make the text labels drag-able and attach a line to the circle when dragged.
.call(drag) works on the dots but not the labels
label = container.append("g")
.attr("class", "label")
.selectAll(".label")
.data(dots)
.enter().append("text")
.text(function(d) {return d.x + d.y; })
.attr("x", function(d) {return d.x; })
.attr("y", function(d) {return d.y; })
.attr("text-anchor", "middle")
.call(drag)
Here's a JSFiddle I made to demonstrate draggable text labels in D3.js
https://jsfiddle.net/h1n6fuwr/
Essentially you want to define the following variables/functions:
const drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended)
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
}
function dragged(d) {
const elem = d3.select(this)
elem.attr('x', +elem.attr('x') + d3.event.dx)
elem.attr('y', +elem.attr('y') + d3.event.dy)
}
function dragended(d) {}
And then call .call(drag) on your text labels.
const labels = ['Drag Me1', 'Drag Me2', 'Drag Me3']
d3.select('svg')
.selectAll('text')
.data(labels)
.enter()
.append('text')
.text(d => d)
.attr('fill', 'green')
.attr('x', (d, i) => 10 + i*30)
.attr('y', (d, i) => 15 + i*30)
.call(drag)
Append a rect behind the text, then .call(drag) on your rect. To get a suitable rect, you can use text.getBBox().

Resources