How to get the current element height? - d3.js

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.

Related

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.

D3 Grouped Bar Chart - Selecting entire group?

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

Appending another element when a sibling element's transition ends

I have a bar chart, which I am using transitions to animate the heights of rect elements like so:
//Create a layer for each category of data that exists, as per dataPointLegend values
//e.g. DOM will render <g class="successful"><g>
layers = svg.selectAll('g.layer')
.data(stacked, function(d) {
return d.dataPointLegend;
})
.enter()
.append('g')
.attr('class', function(d) {
return d.dataPointLegend;
})
//transform below is used to shift the entire layer up by one pixel to allow
//x-axis to appear clearly, otherwise bars inside layer appear over the top.
.attr('transform', 'translate(0,-1)');
//Create a layer for each datapoint object
//DOM will render <g class="successful"><g></g><g>
barLayers = layers.selectAll('g.layer')
.data(function(d) {
return d.dataPointValues;
})
.enter()
.append('g');
//Create rect elements inside each of our data point layers
//DOM will render <g class="successful"><g><rect></rect></g></g>
barLayers
.append('rect')
.attr('x', function(d) {
return x(d.pointKey);
})
.attr('width', x.rangeBand())
.attr('y', height - margin.bottom - margin.top)
.attr('height', 0)
.transition()
.delay(function(d, i) {
return i * transitionDelayMs;
})
.duration(transitionDurationMs)
.attr('y', function(d) {
return y(d.y0 + d.pointValue);
})
.attr('height', function(d) {
return height - margin.bottom - margin.top - y(d.pointValue)
});
I then have a further selection used for appending text elements
//Render any point labels if present
//DOM will render <g><g><rect></rect><text></text></g></g>
if (width > miniChartWidth) {
barLayers
.append('text')
.text(function(d) {
return d.pointLabel
})
.attr('x', function(d) {
return x(d.pointKey) + x.rangeBand() / 2;
})
.attr('y', function(d) {
var textHeight = d3.select(this).node().getBoundingClientRect().height;
//Position the text so it appears below the top edge of the corresponding data bar
return y(d.y0 + d.pointValue) + textHeight;
})
.attr('class', 'data-value')
.attr('fill-opacity', 0)
.transition()
.delay(function(d, i) {
return i * transitionDelayMs + transitionDurationMs;
})
.duration(transitionDurationMs)
.attr('fill-opacity', 1);
}
This fades in the text elements nicely after all the rects have finished growing in height. What I wondered, was whether its possible to append a text element to the corresponding layer as each bar finishes its transition?
I have seen the answer on this SO - Show text only after transition is complete d3.js
Which looks to be along the lines of what I am after, I tried adding an .each('end',...) in my rect rendering cycle like so
.each('end', function(d){
barLayers
.append('text')
.text(function() {
return d.pointLabel
})
.attr('x', function() {
return x(d.pointKey) + x.rangeBand() / 2;
})
.attr('y', function() {
var textHeight = d3.select(this).node().getBoundingClientRect().height;
//Position the text so it appears below the top edge of the corresponding data bar
return y(d.y0 + d.pointValue) + textHeight;
})
.attr('class', 'data-value')
.attr('fill-opacity', 0)
.transition()
.delay(function(d, i) {
return i * transitionDelayMs + transitionDurationMs;
})
.duration(transitionDurationMs)
.attr('fill-opacity', 1);
});
But I end up with lots of text elements for each of my g that holds a single rect for each of my datapoints.
I feel like I'm close, but need some assistance from you wise people :)
Thanks
whateverTheSelectionIs
.each('end', function(d){
barLayers
.append('text')
.each runs separately for every element in your selection, and inside the each you're adding text elements to every barLayer (barLayers). So you're going to get a (barLayers.size() * selection.size()) number of text elements added overall. You need to add only one text element in the each to the right bar / g.
The below is a fudge that might work. It's tricky because the text you want to add is a sibling of the rects in the selection that calls the .each function..., d3.select(this.parentNode) should move you up to the parent of the rect, which would be the right barLayer.
whateverTheSelectionIs
.each('end', function(d,i){
d3.select(this.parentNode)
.append('text')

Transition effects in population pyramid

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.

d3.js multi bar does not refresh

My d3 multi bar does not refresh after data changes. If there is more bars it add it in the end of old but does not remove old ones. If there is less bars it does not add it at all. Axis changes all the time for that from new data
var bars = svg.select(".chart-group")
.selectAll(".state")
.data(data);
bars.enter().append("g")
.attr("class", "state")
.attr("transform", function (d) {
return "translate(" + x0Scale(d[KEY]) + ",0)";
});
bars.exit().remove();
bars.selectAll("rect")
.data(function (d) {
return d.ages;
})
.enter().append("rect")
.attr("width", x1Scale.rangeBand())
.attr("x", function (d) {
return x1Scale(d[SEGMENT]);
})
.attr("y", function (d) {
return yScale(d[DATA]);
})
.attr("height", function (d) {
return chartH - yScale(d[DATA]);
})
.style("fill", function (d) {
return color(d[SEGMENT]);
});
Edit:
Here is fiddle with my problem https://jsfiddle.net/wxw5zdws/
I achieve my goal when whole container is removed:
if (container) {
container.remove();
}
I think it is bad practice, there is an issue in my drawing bars method. It should use old elements and remove/add usseless/needed elements.
What is wrong with this bars?

Resources