0

So trying to create a stacked bar graph in D3.js. I have got the axes working, but the graph data isn't showing, any ideas where I'm going wrong?

JS:

var svg = d3.select("#recovery__table"),
    margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = +svg.attr("width") - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom,
    aspect = width/height,
    g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var x = d3.scaleBand()
    .rangeRound([0, width])
    .padding(0.1)
    .align(0.1);

var y = d3.scaleLinear()
    .rangeRound([height, 0]);

var z = d3.scaleOrdinal()
    .range(["#717C8B", "#7FDDC3", "#39B3CD"]);

var stack = d3.stack();

data.forEach(function(d) {
    d.year = d['trades.closed_half_year_year'];
    d.loss = d['loss'];
    d.recovered = d['recovered'];
    d.recovery = d['in_recovery'];
    d.total = d.loss + d.recovery + d.recovered;
});

var div = d3.select("body").append("div")
    .attr("class", "tooltip3")
    .style("opacity", "0");

 x.domain(data.map(function(d) { return d.year; }));
 y.domain([0, d3.max(data, function(d) { return d.total; })]).nice();
 z.domain(d3.keys(data[0]).filter(function(key){ return key == 'loss' && key == 'recovered' && key == 'in_recovery' }));

 g.selectAll(".serie")
      .data(data)
      .enter().append("rect")
      .attr("class", "bar")
      .attr("fill", function(d){ return z(d.keys); })
      .attr("x", function(d) { return x(d.year); })
      .attr("width", x.bandwidth())
      .attr("y", function(d) { return y(d.total); })
      .attr("height", function(d) { return y[0] - y[1]; })
      .on("mouseover", function(d) {
        var value = parseInt($(this).attr('data-value'));
        div.transition()
          .duration(200)
          .style("opacity", .5);
        div.html(d.data.year + "<br/>£" + total.formatMoney())
          .style("left", (d3.event.pageX) + "px")
          .style("top", (d3.event.pageY - 28) + "px");
      })
      .on("mouseout", function(d) {
        div.transition()
          .duration(500)
          .style("opacity", 0);
      });
    ;

 g.append("g")
      .attr("class", "axis axis--x")
      .attr("transform", "translate(0," + height + ")")
      .attr('x', 20)
      .call(d3.axisBottom(x));

 g.append("g")
      .attr("class", "axis axis--y")
      .call(d3.axisLeft(y).ticks(5, "s"))
      .append("text")
      .attr("x", 2)
      .attr("y", y(y.ticks(10).pop()))
      .attr("dy", "0.35em")
      .attr("text-anchor", "start")
      .attr("fill", "#000");

 var legend = g.selectAll(".legend")
      .data(data)
      .enter().append("g")
      .attr('width', 100)
      .attr("class", "legend")
      .attr('transform', function(d, i) {
        var horz = 100*i;                       // NEW
        var vert = 0;
        if (horz >= width) {
          horz = 100 * (i - 3);
          vert = 40;
        }

        return 'translate(' + horz + ',' + vert + ')';        // NEW
      })
      .style("font", "10px sans-serif");

 legend.append("rect")
      .attr("x", "33%")
      .attr("width", 18)
      .attr("height", 18)
      .attr("fill", z);

 legend.append("text")
      .attr("x", "43%")
      .attr("y", 9)
      .attr("dy", ".35em")
      .attr("text-anchor", "end")
      .text(function(d) { return d; });

JSON example

[{"trades.closed_half_year_year":"2017","auctioncollectioninfos.total_advanced_amount_delinquent_and_collection_completed_gbp_daily":"£0.00","auctioncollectioninfos.total_crystallized_loss_gbp_daily":"£0.00","auctioncollectioninfos.total_outstanding_amount_delinquent_gbp_daily":"£","auctioncollectioninfos.total_advanced_amount_delinquent_gbp_daily":"£0.00","loss":"£0.00","recovered":"£0.00","in_recovery":"£0"},
{"trades.closed_half_year_year":"2016","auctioncollectioninfos.total_advanced_amount_delinquent_and_collection_completed_gbp_daily":"£123,456.78","auctioncollectioninfos.total_crystallized_loss_gbp_daily":"£0.00","auctioncollectioninfos.total_outstanding_amount_delinquent_gbp_daily":"£1,234,234","auctioncollectioninfos.total_advanced_amount_delinquent_gbp_daily":"£1,321,245.56","loss":"£0.00","recovered":"£457,468.31","in_recovery":"£1,890,567"},
{"trades.closed_half_year_year":"2015","auctioncollectioninfos.total_advanced_amount_delinquent_and_collection_completed_gbp_daily":"£3,345,768.54","auctioncollectioninfos.total_crystallized_loss_gbp_daily":"£555,555.08","auctioncollectioninfos.total_outstanding_amount_delinquent_gbp_daily":"£321,321","auctioncollectioninfos.total_advanced_amount_delinquent_gbp_daily":"£3,321,321.32","loss":"£456,324.33","recovered":"£2,324,234.345","in_recovery":"£333,333"}]

Essentially, need the loss, recovery and recovered to stack on the graph, but there is no data loading onto the graph as previously mentioned.

Any ideas?

1 Answer 1

1

There is a little issue, the data you are using is a JSON thus the object will receive the values as strings, you have to parse them correctly into numbers. An easy way to parse a string to a number is the following:

d.loss = +d['loss'];

But even if we did that we would still have problems with your data. Why? Because some of the numbers in your dataset are formatted:

"loss":"£456,324.33" 

so if you are trying to do something like this:

d.total = d.loss + d.in_recovery + d.recovered;

You will get an invalid value because we may be issuing an operation like the following:

d.total = "£456,324.33" + 0 + "£4,324.33"  // "£456,324.330£4,324.33"

This will screw the scales in our chart.

y.domain([0, d3.max(data, function(d) {
   return d.total;
})]).nice(); // spooky domain here :S

Lets take care of the formatting of your values (assuming values are always formatted the way presented in the JSON you provided):

data.forEach(function(d) {
  d.year = +d['trades.closed_half_year_year'];
  d.loss = typeof d.loss === 'number' ? d.loss : +d['loss'].replace(/£|,/g, '')
  d.recovered = typeof d.recovered === 'number' ? d.recovered : +d['recovered'].replace(/£|,/g, '');
  d.in_recovery = typeof d.in_recovery === 'number' ? d.in_recovery : +d['in_recovery'].replace(/£|,/g, '');
  d.total = d.loss + d.in_recovery + d.recovered;
});

Now that we have a correct dataset we should be ready to start using d3 and the stack layout:

var keys = ['loss', 'recovered', 'in_recovery']; // Declare the keys we will want in our stack
z.domain(keys); // Set them as our z domain so we can retrieve our fill color
var stackLayout = d3.stack().keys(keys)(data); // Create our stack layout

Which will create the following structure:

[
   [
      [
         0,
         0
      ],
      [
         0,
         0
      ],
      [
         0,
         456324.33
      ]
      // key: loss
   ],
   [
      [
         0,
         0
      ],
      [
         0,
         457468.31
      ],
      [
         456324.33,
         2780558.6750000003
      ]
      // key: recovered
   ],
   [
      [
         0,
         0
      ],
      [
         457468.31,
         2348035.31
      ],
      [
         2780558.6750000003,
         3113891.6750000003
      ]
      // key: in_recovery
   ]
]

With the structure above we now can create our bars by key-block, as you can see each array has three values and a key. We will need to create a group element for each array element:

g.selectAll(".serie")
  .data(stackLayout) // Set stack layout as data
  .enter()
  .append("g") // Creating group for each key
  .attr("fill", function(d) { return z(d.key); }) // Fill inner elements with the color provided by our z Scale
  .selectAll("rect") 
  .data(function(d) { // Use the inner array to create our rects
    return d;
  })
  .enter().append("rect")
  .attr("x", function(d) { // Position by our x Scale
    return x(d.data.year);
  })
  .attr("y", function(d) { // Position by our y Scale
    return y(d[1]);
  })
  .attr("height", function(d) { // Find the height value by using the values provided in the inner arrays
    return y(d[0]) - y(d[1]);
  })
  .attr("width", x.bandwidth()); 

We also have to change a little the labels:

var legend = g.selectAll(".legend")
  .data(keys.reverse()) // Use our keys
  .enter().append("g")
  .attr("class", "legend")
  .attr('transform', function(d, i) {
    var horz = width - margin.right - (100 * i); // NEW
    var vert = 0;
    return 'translate(' + horz + ',' + vert + ')'; // NEW
  })
  .style("font", "10px sans-serif");

legend.append("text")
  .attr("x", "-5")
  .attr("y", 9)
  .attr("dy", ".35em")
  .attr("text-anchor", "end")
  .text(function(d) {
    return d;
  });

Working plnkr: https://plnkr.co/edit/eTKsOz8jlaqm1Mf3Esej?p=preview

Not the answer you're looking for? Browse other questions tagged or ask your own question.