1
votes

I'm trying to create a stacked bar chart using d3.js but I am having a problem. The problem is when there is a bar with the value of 1 in a 10,000 height Y axis, the bar has 0px height and it doesn't appear on the chart.

Example of data that causes the problem: [{green: 1, yellow: 9400, white: 999999}]

The "green" bar will be so small, it's like it doesn't exist.

Here's the relevant code:

var x = d3.scale.ordinal()
        .rangeRoundBands([0, width], 1),
    y = d3.scale.linear()
        .rangeRound([height + 2, margin.top]);

var d3Elm = d3.select(element[0]);

var dataStackLayout = d3.layout.stack()(data);

x.domain(dataStackLayout[0].map(function(d){
    return d3StackedBarCtrl.labelEllipsis(d.displayName, 10);
}));

y.domain([0,
    d3.max(dataStackLayout[dataStackLayout.length - 1],
        function (d) {
            return d.y0 + d.y;
        })
    ]).nice();

var layer = svg.selectAll('.stack')
    .data(dataStackLayout)
    .enter().append('g')
    .attr('class', 'stack');

layer.selectAll('rect')
    .data(function(d){
        return d;
    })
    .enter().append('rect')
    .attr('x', function(d){
        return x(d.displayName);
    })
    .attr('y', function(d){
        return y(d.y + d.y0);
    })
    .attr('width', 24)
    .attr('height', function(d){
        return y(d.y0) - y(d.y + d.y0);
    })
    .attr('class', 'rect');

How can I set a minimum height for bars and have the other bars reduce their height on the account of extended bars?!

Thanks.

1

1 Answers

0
votes

Before anything else, what you're doing is lying with data. If green is several orders of magnitude smaller than white, let it be, and explain that for your readers somewhere in the chart. But I understand that you just want make the respective rectangle visible.

There are two ways of doing that, changing the matrix of the stacked data or changing the rectangles themselves. As you just want to make the bar barely visible, I'd go with the second option, that doesn't involve changing the data (aka lying!), creating a rule that sets a minimum height for the bar. As this bar will overlap just a little bit the next bar we won't need to change the other bars values (contrary to your request); besides that, it deals with pixels, not with data.

The rule can be something as simple as this:

.attr("height", function(d) { 
    return y(d.y0) - y(d.y0 + d.y) < 2 ? 2 : y(d.y0) - y(d.y0 + d.y); 
})

Which says: "check for the computed height. Is it less than 2 pixels? If yes, set the height for 2 pixels. If not, set the height for the computed one".

Check this demo (this code is not mine, I just found it in this fiddle: http://jsfiddle.net/xavipolo/q5q6331p/). I made "oranges" have a small value in 2007. Still, the respective bar has a minimum height of 4 pixels:

// Setup svg using Bostock's margin convention

var margin = {top: 20, right: 160, bottom: 35, left: 30};

var width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var svg = d3.select("body")
  .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 + ")");


/* Data in strings like it would be if imported from a csv */

var data = [
  { year: "2006", redDelicious: "100", mcintosh: "150", oranges: "90", pears: "60" },
  { year: "2007", redDelicious: "120", mcintosh: "180", oranges: "1", pears: "40" },
  { year: "2008", redDelicious: "050", mcintosh: "200", oranges: "80", pears: "20" },
  { year: "2009", redDelicious: "010", mcintosh: "150", oranges: "50", pears: "40" },
  { year: "2010", redDelicious: "020", mcintosh: "100", oranges: "40", pears: "20" },
  { year: "2011", redDelicious: "030", mcintosh: "120", oranges: "60", pears: "30" },
  { year: "2012", redDelicious: "040", mcintosh: "150", oranges: "80", pears: "10" },
  { year: "2013", redDelicious: "060", mcintosh: "110", oranges: "90", pears: "40" },
  { year: "2014", redDelicious: "100", mcintosh: "130", oranges: "90", pears: "50" },
  { year: "2015", redDelicious: "160", mcintosh: "190", oranges: "60", pears: "90" },
  { year: "2016", redDelicious: "190", mcintosh: "170", oranges: "50", pears: "70" },
];

var parse = d3.time.format("%Y").parse;


// Transpose the data into layers
var dataset = d3.layout.stack()(["redDelicious", "mcintosh", "oranges", "pears"].map(function(fruit) {
  return data.map(function(d) {
    return {x: parse(d.year), y: +d[fruit]};
  });
}));


// Set x, y and colors
var x = d3.scale.ordinal()
  .domain(dataset[0].map(function(d) { return d.x; }))
  .rangeRoundBands([10, width-10], 0.02);

var y = d3.scale.linear()
  .domain([0, d3.max(dataset, function(d) {  return d3.max(d, function(d) { return d.y0 + d.y; });  })])
  .range([height, 0]);

var colors = ["b33040", "#d25c4d", "#f2b447", "#d9d574"];


// Define and draw axes
var yAxis = d3.svg.axis()
  .scale(y)
  .orient("left")
  .ticks(5)
  .tickSize(-width, 0, 0)
  .tickFormat( function(d) { return d } );

var xAxis = d3.svg.axis()
  .scale(x)
  .orient("bottom")
  .tickFormat(d3.time.format("%Y"));

svg.append("g")
  .attr("class", "y axis")
  .call(yAxis);

svg.append("g")
  .attr("class", "x axis")
  .attr("transform", "translate(0," + height + ")")
  .call(xAxis);


// Create groups for each series, rects for each segment 
var groups = svg.selectAll("g.cost")
  .data(dataset)
  .enter().append("g")
  .attr("class", "cost")
  .style("fill", function(d, i) { return colors[i]; });

var rect = groups.selectAll("rect")
  .data(function(d) { return d; })
  .enter()
  .append("rect")
  .attr("x", function(d) { return x(d.x); })
  .attr("y", function(d) { return y(d.y0 + d.y); })
  .attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y) < 4 ? 4 : y(d.y0) - y(d.y0 + d.y); })
  .attr("width", x.rangeBand())
  .on("mouseover", function() { tooltip.style("display", null); })
  .on("mouseout", function() { tooltip.style("display", "none"); })
  .on("mousemove", function(d) {
    var xPosition = d3.mouse(this)[0] - 15;
    var yPosition = d3.mouse(this)[1] - 25;
    tooltip.attr("transform", "translate(" + xPosition + "," + yPosition + ")");
    tooltip.select("text").text(d.y);
  });


// Draw legend
var legend = svg.selectAll(".legend")
  .data(colors)
  .enter().append("g")
  .attr("class", "legend")
  .attr("transform", function(d, i) { return "translate(30," + i * 19 + ")"; });
 
legend.append("rect")
  .attr("x", width - 18)
  .attr("width", 18)
  .attr("height", 18)
  .style("fill", function(d, i) {return colors.slice().reverse()[i];});
 
legend.append("text")
  .attr("x", width + 5)
  .attr("y", 9)
  .attr("dy", ".35em")
  .style("text-anchor", "start")
  .text(function(d, i) { 
    switch (i) {
      case 0: return "Anjou pears";
      case 1: return "Naval oranges";
      case 2: return "McIntosh apples";
      case 3: return "Red Delicious apples";
    }
  });


// Prep the tooltip bits, initial display is hidden
var tooltip = svg.append("g")
  .attr("class", "tooltip")
  .style("display", "none");
    
tooltip.append("rect")
  .attr("width", 30)
  .attr("height", 20)
  .attr("fill", "white")
  .style("opacity", 0.5);

tooltip.append("text")
  .attr("x", 15)
  .attr("dy", "1.2em")
  .style("text-anchor", "middle")
  .attr("font-size", "12px")
  .attr("font-weight", "bold");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>