I am new to D3.js and have a problem with my vertical bar chart. For some reason, the distance between the axis and the bars is way too big when I use rangeRoundBands for scaling. In the API, it is explained like this:
So the problem seems to be the outerPadding. But setting the outerPadding to zero does not help. However, when I use rangeBands instead, the problem disappears and the bars are positioned correctly, right below the axis. But then I will get these nasty antialiasing effects, so this is not really an option. Here is my code:
var margin = {top: 40, right: 40, bottom: 20, left: 20},
width = 900 - margin.left - margin.right,
height = x - margin.top - margin.bottom;
var x = d3.scale.linear().range([0, width]);
var y = d3.scale.ordinal().rangeRoundBands([0, height], .15, 0);
var xAxis = d3.svg.axis()
.scale(x)
.orient("top");
var xAxis2 = d3.svg.axis()
.scale(x)
.orient("bottom");
var xAxis3 = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickSize(-height, 0, 0)
.tickFormat("");
var svg = d3.select("#plotContainer").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain(d3.extent(data, function(d) {
return d.size;
})).nice();
y.domain(data.map(function(d) {
return d.name;
}));
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis2);
svg.append("g")
.attr("class", "grid")
.attr("transform", "translate(0," + height + ")")
.call(xAxis3);
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", function(d) {
return d.size < 0 ? "bar negative" : "bar positive";
})
.attr("x", function(d) {
return x(Math.min(0, d.size));
})
.attr("y", function(d) {
return y(d.name);
})
.attr("width", function(d) {
return Math.abs(x(d.size) - x(0));
})
.attr("height", y.rangeBand())
.append("title")
.text(function(d) {
return "This value is " + d.name;
});
;
svg.selectAll(".bar.positive")
.style("fill", "steelblue")
.on("mouseover", function(d) {
d3.select(this).style("fill", "yellow");
})
.on("mouseout", function(d) {
d3.select(this).style("fill", "steelblue");
});
svg.selectAll(".bar.negative")
.style("fill", "brown")
.on("mouseover", function(d) {
d3.select(this).style("fill", "yellow");
})
.on("mouseout", function(d) {
d3.select(this).style("fill", "brown");
});
svg.selectAll(".axis")
.style("fill", "none")
.style("shape-rendering", "crispEdges")
.style("stroke", "#000")
.style("font", "10px sans-serif");
svg.selectAll(".grid")
.style("fill", "none")
.style("stroke", "lightgrey")
.style("opacity", "0.7");
svg.selectAll(".grid.path")
.style("stroke-width", "0");
EDIT: Please take a look at this fiddle: http://jsfiddle.net/GUYZk/9/
My problem is reproducible there. You cannot alter the outerPadding with rangeRoundBands, whereas rangeBands behaves normal.
TL;DR: this is a consequence of the math. To work around, use rangeBands
to lay out the bars, and use shape-rendering: crispEdges
in CSS to align them to pixel boundaries.
Full explanation:
Because the rangeRoundBands
function must distribute the bars evenly throughout the provided pixel range AND it must also provide an integer rangeBand
, it uses Math.floor
to chop off the fractional bit of each successive bar.
The reason this extra outer padding compounds with longer datasets is because all those fractional pixels have to end up somewhere. The author of this function chose to evenly split them between the beginning and the end of the range.
Because the fraction pixel of each rounded bar is on the interval (0, 1)
, the extra pixels glommed onto each end will span about 1/4 of the data bar count. With 10 bars, 2-3 extra pixels would never be noticed, but if you have 100 or more, the extra 25+ pixels become much more noticeable.
One possible solution that appears to work in Chrome for svg:rect
: use rangeBands
to lay out, but then apply shape-rendering: crispEdges
as a CSS style to your bar paths/rects.
This then leaves the onus on the SVG renderer to nudge each bar to a pixel boundary, but they are more evenly spaced overall, with occasional variance in the spacing to account for the error over the whole chart.
Personally, I use shape-rendering: optimizeSpeed
and let the rendering agent make whatever tradeoffs it must to quickly render the (potentially fractional) bar positions.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With