Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

rangeRoundBands outerPadding in bar chart way too big

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:

enter image description here

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.

like image 317
bgbrink Avatar asked Apr 17 '14 07:04

bgbrink


1 Answers

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.

like image 128
Ben Mosher Avatar answered Sep 18 '22 12:09

Ben Mosher