D3 Grouped Bar Chart - Selecting entire group? - d3.js

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

Related

D3.js .each() with function that passes data

I would like to put my tooltip into a function such that I can re-use it for multiple elements. When I call the tooltip function for the label element the tooltip displays only the first x value to all labels instead of looping over the X value array. How do I properly access the data in the function?
const tooltip = d3.select('body').append('div')
.attr('id', 'rect-tooltip');
function mouseover(data-x){
d3.select('g')
area.selectAll("text")
.on('mouseover', (d) => {
rect-tooltip.transition()
.duration(100)
.style('opacity', .9)
rect-tooltip.html(`${data-x}`) //Pass in X-values
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
})
.on('mouseout', (d) => {
rect-tooltip.transition()
.duration(400)
.style('opacity', 0);
})
}
const label = d3.select('g')
area.selectAll("text")
.data(data)
.join('text')
.attr("class", "label")
.text( (d)=> {return d.name;})
.attr("x", (d)=> {return d.x;})
.attr("y", (d)=> {return d.y;})
.each(function(d) {
mouseover(d.x);}); // Only first data point is added to each label?
Without an example of the this, I may be mis-reading your issue.
Problem
The key problem stems from iterating through the text elements twice:
area.each(function (d) {
// do something with each element/datum in the selection
})
d3.selectAll("text")
.on("mouseover", function(d) {
// apply an event listner and corrsesponding function to each text element.
})
The problem is you nest the second in the first. For every element in area you select all the text elements: if you have 2 elements you're selecting all the text twice. You only need to select each text element once.
In the pattern you have, for each element in area we pass that element's datum to the nested function which takes a property of that datum and with d3.selectAll("text").on("mouseover" ... applies that single datum to all text mouseover events. Since you do this for every element in area, we end up overwriting the event listeners multiple times.
No where do you reference the current datum in the chain following d3.selectAll("text"), so we only have a value from the current datum in the current iteration of .each().
Solution
You shouldn't need to use .each() here to apply an event listener, .on() should be sufficient.
We have our mouseover and mouseout functions:
function mouseover(d) {
tooltip
.style("opacity", 0.9)
.text(d.x)
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
}
function mouseout() {
tooltip
.style("opacity",0);
}
Then we can call it with:
selection.on("mouseover",mouseover)
.on("mouseout",mouseout);
And we can resuse this on multiple selections or elements. The datum specific to each element will be used to define the tooltip text.
var tooltip = d3.select(".tooltip");
var svg = d3.select("svg");
var data = [{x: 10},{x:50},{x:90},{x:130},{x:170},{x:210},{x:250},{x:290},{x:330}]
var g = svg.selectAll(null)
.data(data)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+[d.x,0]+")"; })
var rect = g.append("rect")
.attr("width", 35)
.attr("height", 100)
.attr("fill","steelblue")
.on("mousemove",mouseover)
.on("mouseout",mouseout)
var text = g.append("text")
.attr("y", 120)
.attr("x", 18)
.style("text-anchor","middle")
.text(function(d) { return d.x; })
.on("mouseover",mouseover)
.on("mouseout",mouseout)
function mouseover(d) {
tooltip
.style("opacity", 0.9)
.text(d.x)
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
}
function mouseout() {
tooltip
.style("opacity",0);
}
.tooltip {
position: absolute;
padding: 5px;
background: yellow;
}
rect, text {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div class="tooltip"></div>
<svg width="400" height="300"></svg>
Based on Andrew his comment I changed my code to:
const tooltip = d3.select('body').append('div')
.attr('id', 'tooltip');
function mouseover(d){
tooltip.transition()
.duration(100)
.style('opacity', .9)
tooltip.html(d)
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
}
function mouseout(){
tooltip.transition()
.duration(400)
.style('opacity', 0);
}
const label = d3.select('g')
area.selectAll("text")
.data(data)
.join('text')
.attr("class", "label")
.text( (d)=> {return d.name;})
.attr("x", (d)=> {return d.x;})
.attr("y", (d)=> {return d.y;})
.on("mouseover", function(d) { mouseover(d.x); })
.on("mouseout", mouseout);

d3js update redraw stacked bar graph

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();

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')

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?

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.

Resources