I have a D3 barchart which has 5 bars. When I update it I can see it transitioning to the correct 3 bars but some of the original bars are left visible - how do I make them exit?
This is what it initially looks like:
This is what it ends up looking like:
The dark blue bars are correct. The current code for updating the "rect" objects is the following:
var plot = d3.select("#barChartPlot")
.datum(currentDatasetBarChart);
/* Note that here we only have to select the elements - no more appending! */
plot.selectAll("rect")
.data(currentDatasetBarChart)
.transition()
.duration(750)
.attr("x", function (d, i) {
return xScale(i);
})
.attr("width", width / currentDatasetBarChart.length - barPadding)
.attr("y", function (d) {
return yScale(+d.measure);
})
.attr("height", function (d) {
return height - yScale(+d.measure);
})
.attr("fill", colorChosen);
You only have 3 new bars, so the number of elements on your data has changed.
You need to use the update pattern.
var rects = plot.selectAll("rect")
.data(currentDatasetBarChart);
rects.enter()
.append("rect")
//Code to style and define new rectangles.
//Update
rects.update()
.transition()
.duration(750)
.attr("x", function (d, i) {
return xScale(i);
})
.attr("width", width / currentDatasetBarChart.length - barPadding)
.attr("y", function (d) {
return yScale(+d.measure);
})
.attr("height", function (d) {
return height - yScale(+d.measure);
})
.attr("fill", colorChosen);
// Remove unused rects
rects.exit().remove();
Related
I am using d3.js to render a grouped bar chart and I am looking to animate transition the bars - (show/hide different series) when clicking on the legend.
from this.
to this
perhaps also changing the scale
http://jsfiddle.net/0ht35rpb/202/
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color)
.on("click", function(d) {
console.log("d", d);
});
Some bar transition code
bars.transition()
.attr("id", function(d){ return 'tag'+d.state.replace(/\s|\(|\)|\'|\,+/g, '');})
.attr("x", function(d) { return x(d.state); })
.attr("width", x.rangeBand())
.attr("y", function(d) {return y(d.value); })
.attr("height", function(d) { return height - y(d.value); });
bars.exit().remove();
Other grouped bar chart references.
https://bl.ocks.org/mbostock/3887051
https://plnkr.co/edit/JUaLXmeCvHh0zUmrKClQ?p=preview
http://jsfiddle.net/ramseyfeng/8790t2vk/
There are a few ways to go through this. You could easily use an enter/update/exit cycle, though this is a little complex when compared to typical use of the cycle because of the nested elements and the need to set keys to ensure smooth transitions between chart states.
In this situation, it may be easier to simply use an array to hold bars that are to be filtered out, hide those bars, update the scales to not use those keys' values, and update the remaining bars.
This requires an onclick event for each legend item. When clicked, in our clicked function we manage the array of filtered out (filtered) items like so, where d is the datum associated with the legend rectangle:
// add the clicked key if not included:
if (filtered.indexOf(d) == -1) {
filtered.push(d);
// if all bars are un-checked, reset:
if(filtered.length == keys.length) filtered = [];
}
// otherwise remove it:
else {
filtered.splice(filtered.indexOf(d), 1);
}
Then we can update the scales (we need the all the keys that are not in the filtered array for the domain of the x1 scale, hence the newKeys variable):
var newKeys = [];
keys.forEach(function(d) {
if (filtered.indexOf(d) == -1 ) {
newKeys.push(d);
}
})
x1.domain(newKeys).rangeRound([0, x0.bandwidth()]);
y.domain([0, d3.max(data, function(d) { return d3.max(keys, function(key) { if (filtered.indexOf(key) == -1) return d[key]; }); })]).nice();
Then we can select our rectangles, filter by whether they should be hidden or shown, and update accordingly:
var bars = svg.selectAll(".bar").selectAll("rect")
.data(function(d) { return keys.map(function(key) { return {key: key, value: d[key]}; }); })
// filter out bars:
bars.filter(function(d) {
return filtered.indexOf(d.key) > -1;
})
.transition()
.attr("x", function(d) {
return (+d3.select(this).attr("x")) + (+d3.select(this).attr("width"))/2;
})
.attr("height",0)
.attr("width",0)
.attr("y", function(d) { return height; })
.duration(500);
// update persistent bars:
bars.filter(function(d) {
return filtered.indexOf(d.key) == -1;
})
.transition()
.attr("x", function(d) { return x1(d.key); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.attr("width", x1.bandwidth())
.attr("fill", function(d) { return z(d.key); })
.duration(500);
This solution could be made a little bit more "d3-ish" with the enter/update/exit cycle, but as our elements are relatively fixed in number, this is not as useful as in many other situations.
Here is the above code in action:
https://bl.ocks.org/andrew-reid/64a6c1892d1893009d2b99b8abee75a7
And as noted in the comments, you also need to update the axis, not just the scale. To do so, I added a class to the y scale to allow easy selection when updating the chart:
svg.select(".y")
.transition()
.call(d3.axisLeft(y).ticks(null, "s"))
.duration(500);
I have a map and a matching legend on my website. As the user selects different values from a select list, the map is updated and in the same function, the legend should be updated with new values. As the map actualization works properly, the values of the legend stay the same even in the console are logged the right values if I log the variables.
This is the function that draws the legend:
color_domain = [wert1, wert2, wert3, wert4, wert5];
ext_color_domain = [0, wert1, wert2, wert3, wert4, wert5];
console.log(ext_color_domain);
legend_labels = ["< "+wert1, ""+wert1, ""+wert2, ""+wert3, ""+wert4, "> "+wert5];
color = d3.scale.threshold()
.domain(color_domain)
.range(["#85db46", "#ffe800", "#ffba00", "#ff7d73", "#ff4e40", "#ff1300"]);
var legend = svg.selectAll("g.legend")
.data(ext_color_domain)
.enter().append("g")
.attr("class", "legend");
var ls_w = 20, ls_h = 20;
legend.append("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.7);
legend.append("text")
.attr("x", 50)
.attr("y", function(d, i){ return height - (i*ls_h) - ls_h - 4;})
.text(function(d, i){ return legend_labels[i]; });
console.log(legend_labels); //gives the right legend_labels but doesn't display them correctly
};
Sadly even the map is updated with new colors they're colored with the old thresholds. This is the way the map is colored:
svg.append("g")
.attr("class", "id")
.selectAll("path")
.data(topojson.feature(map, map.objects.immoscout).features)
.enter().append("path")
.attr("d", path)
.style("fill", function(d) {
return color(rateById[d.id]);
})
This is tough to answer without a complete, working code sample but...
You are not handling the enter, update, exit pattern correctly. You never really update existing elements, you are only re-binding data and entering new ones.
Say you've called your legend function once already, now you have new data and you do:
var legend = svg.selectAll("g.legend")
.data(ext_color_domain)
.enter().append("g")
.attr("class", "legend");
This re-binds the data and computes an enter selection. It says, hey d3, what data elements are new? For those new ones, you then append a g. Further:
legend.append("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.7);
Again, this is operating on those newly entered elements only. The ones that already existed on the page aren't touched at all.
Untested code, but hopefully it points you in the right direction:
// selection of all enter, update, exit
var legend = svg.selectAll("g.legend")
.data(ext_color_domain); //<-- a key function would be awesome here
legend.exit().remove(); //<-- did the data go away? remove the g bound to it
// ok, what data is coming in? create new elements;
var legendEnter = legend.enter().append("g")
.attr("class", "legend");
legendEnter.append("rect");
legendEnter.append("text");
// ok, now handle our updates...
legend.selectAll("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.7);
legend.selectall("text")
...
There's some really great tutorials on this; and it's confusing as hell, but it's the foundation of d3.
An example that helps you get started with updating d3 (d3, v4):
const line = svg.selectAll('line').data(d3Data); // binds things
line.exit().remove(); // removes old data
line.enter()
.append('line') // add new lines for new items on enter
.merge(line) // <--- this will make the updates to the lines
.attr('fill', 'none')
.attr('stroke', 'red');
I'm creating a bar chart as part of a bigger data visualization in d3. I want to be able to change the data in one part of the visualization and all the charts will be updated. A simplified version of the chart is as follows.
var dataset = [1, 3, 5, 3, 3];
...
var svg = d3.select("body #container").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g");
...
I create other charts like a map, circle etc with this svg element. The bar chart is implemented like this.
function bars(dataset) {
var barChart = g.selectAll("rect.bar")
.data(dataset)
.enter();
barChart.append("rect")
.attr("class", "bar")
.attr("x", function(d, i) { return i * 30 + 100; })
.attr("y", function(d) { return (height - 130) - d * 4;})
.attr("width", 25)
.attr("height", function(d) { return d * 4; });
barChart.append("text")
.text(function(d) { return d; })
.attr("x", function(d, i) { return i * 30 + 103; })
.attr("y", function(d) { return (height - 130) - d/10 - 5;})
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "darkgray");
}
Now this renders the bar chart fine but there is a function
...
.on("click", function() {
...
var newdata = [5, 2, 6, 2, 4]; // new values
g.selectAll("rect.bar").remove(); // This removes the bars
g.selectAll("text").remove(); // Problem here: All texts are removed
bars(newdata);
}
I have tried to transition the bar chart with new values with the .remove() function. This works for the bar rectangles because there are no othe bar charts but when I tried to remove the value labels like shown above all the other text elements were also removed. Is there a way to only update the text associated with the bars?
Have you tried applying a class to the text and only selecting those ones for removal?
e.g.
barChart.append("text")
.attr('class','label')
.text(function(d) { return d; })
then
g.selectAll(".label").remove();
Incidentally, if not all of the elements are being deleted between updates, then instead of removing all of the elements, have you considered using enter() and exit() to bind the new data to the existing elements and only remove the elements that are changing?
EDIT Like this:
function bars(dataset) {
var bar = g.selectAll(".bar").data(dataset);
bar.exit().remove();
bar.enter().append("rect").attr("class", "bar");
bar
.attr("x", function(d, i) { return i * 30 + 100; })
.attr("y", function(d) { return (height - 130) - d * 4;})
.attr("width", 25)
.attr("height", function(d) { return d * 4; });
var label = g.selectAll(".label").data(dataset);
label.exit().remove();
label.enter().append("text").attr("class", "label");
label
.text(function(d) { return d; })
.attr("x", function(d, i) { return i * 30 + 103; })
.attr("y", function(d) { return (height - 130) - d/10 - 5;})
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "darkgray");
}
I'm working on this bar chart application.
http://jsfiddle.net/NYEaX/166/
How do I
a) animate the bars so they grow from the bottom
b) morph the axis accordingly to the new data set
animateBars: function(data){
var svg = d3.select(methods.el["selector"] + " .barchart");
var barrects = d3.select(methods.el["selector"] + " .barrects");
var initialHeight = 0;
var bar = barrects.selectAll("rect")
.data(data);
// Enter
bar.enter()
.append("rect")
.attr("class", "bar")
.attr("y", initialHeight);
// Update
bar
.attr("height", initialHeight)
.transition()
.duration(500)
.attr("x", function(d) { return methods.x(d.letter); })
.attr("width", methods.x.rangeBand())
.attr("y", function(d) { return methods.y(d.frequency); })
.attr("height", function(d) { return methods.height - methods.y(d.frequency); })
// Exit
bar.exit()
.transition()
.duration(250)
.attr("y", initialHeight)
.attr("height", initialHeight)
.remove();
},
For the former, set the y attribute to be the max height instead of 0:
.attr("y", methods.height)
For the latter, recompute the x domain and call the axis component again:
methods.x.domain(data.map(function(d) { return d.letter; }));
svg.select("g.x").call(methods.xAxis);
Complete example here.
I am having issues trying to build this jquery based d3.js barchart plugin.
the bars are displaced to the left, not sure why
the bars are not updating to new data.
I've tried to get the bars to animate - but not had any success.
http://jsfiddle.net/NYEaX/161/
Here is the animate bars function
animateBars: function(data){
var svg = d3.select(methods.el["selector"] + " .barchart");
var bars = svg.selectAll(".bar")
.data(data);
bars
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return methods.x(d.letter); })
.attr("width", methods.x.rangeBand())
.attr("y", function(d) { return methods.y(d.frequency); })
.attr("height", function(d) { return methods.height - methods.y(d.frequency); })
.transition()
.duration(300)
bars
.transition()
.duration(300)
bars.exit()
.transition()
.duration(300)
}
I fixed the transition for new bars in your jsfiddle. I hope it reveals the functionality of transitions:
http://jsfiddle.net/NYEaX/893/
How it works: After it sets the initial height and y value, it adds a transition to the end height and y value.
bars
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return methods.x(d.letter); })
.attr("width", methods.x.rangeBand())
.attr("y", function(d) { return methods.y(0); })
.attr("height", function(d) { return methods.height - methods.y(0); })
.transition().duration(1000)
.attr("y", function(d) { return methods.y(d.frequency); })
.attr("height", function(d) { return methods.height - methods.y(d.frequency); });