score:1

The problem, I belive, is that you are binding the categories array to the bars selection, like this:

bars.selectAll("rect").data(categories)

As far as I can see (whithout a running demo) categories is an array with only four values (one for each category).

You have to go one step 'deeper' in your nested data structure.

To draw a set of bars for each category you would need to iterate over categories and bind the values array that contains the actual values to the selection.

Something like:

  categories.each(function (category) {
    var klass = category.name;
    bars.selectAll("rect ." + klass)
        .data(category.values)
        .enter()
        .append("rect")
        .attr("class", klass)
        .attr("width", barWidth)
        .attr("x", function (d, i) { /* omitted */})
        .attr("y", function (d) { return yScale(d.rate); })
        .attr("height", function (d) { return h - yScale(d.rate); });
  });

---- Edit

Instead of the above code, think about drawing the bars just like you do with the lines. Like this:

var bars = svg.selectAll(".barGroup")
    .data(categories)
    .enter()
    .append("g")
    .attr("class", function (d) { return lineClass(d.name) + "Bar barGroup"; })
    .attr("transform", function (d, i) {
        var x = i > 1 ? xScale.rangeBand() / 2 : 0;
        return "translate(" + x + ",0)";
    })
    .selectAll('rect')
    .data(function (d) { return d.values; })
    .enter()
    .append("rect")
    .attr("class", "bar")
    .attr("width", barWidth)
    .attr("x", function (d, i) { return xScale(d.date); })
    .attr("y", function (d, i) { return yScale(d.rate); })
    .attr("height", function (d) { return h - yScale(d.rate); });

score:5

Link to jsfiddle: https://jsfiddle.net/sladav/rLh4qwyf/1/

I think the root of the issue is that you want to use two variables that do not explicitly exist in your original data set: (1) Category and (2) Rate.

Your data is formatted in a wide format in that each category gets its own variable and the value for rate exists at the crossroads of month and one of the given categories. I think the way you're nesting ultimately is or at least should address this, but it is unclear to me if or where something gets lost in translation. Conceptually, I think it makes more sense to start with an organization that matches what you are trying to accomplish. I reformatted the original data and approached it again - on a conceptual level the nesting seems straightforward and simple...

NEW COLUMNS:

  • Month: Time Variable; mapped to X axis
  • Category: Categorical values [Actual, Forecast, Budget]; used to group/color
  • Rate: Numerical value; mapped to Y axis

Reorganized CSV (dropped NULLs):

Month,Category,Rate
Jul-14,Actual,200000
Aug-14,Actual,198426.57
Sep-14,Actual,290681.62
Oct-14,Actual,362974.9
Nov-14,Actual,397662.09
Dec-14,Actual,512434.27
Jan-15,Actual,511470.25
Jan-15,Forecast,511470.25
Feb-15,Forecast,536472.5467
Mar-15,Forecast,612579.9047
Apr-15,Forecast,680936.5086
May-15,Forecast,755526.7173
Jun-15,Forecast,811512.772
Jul-14,Budget,74073.86651
Aug-14,Budget,155530.2499
Sep-14,Budget,220881.4631
Oct-14,Budget,314506.6437
Nov-14,Budget,382407.67
Dec-14,Budget,442192.1932
Jan-15,Budget,495847.6137
Feb-15,Budget,520849.9105
Mar-15,Budget,596957.2684
Apr-15,Budget,465313.8723
May-15,Budget,739904.081
Jun-15,Budget,895890.1357

With your newly formatted data, you start by using d3.nest to GROUP your data explicitly with the CATEGORY variable. Now your data exists in two tiers. The first tier has three groups (one for each category). The second tier contains the RATE data for each line/set of bars. You have to nest your data selections as well - the first layer is used to draw the lines, the second layer for the bars.

Nesting your data:

var nestedData = d3.nest()
      .key(function(d) { return d.Category;})
      .entries(data)

Create svg groups for your grouped, 1st-tier data:

d3.select(".plot-space").selectAll(".g-category")
    .data(nestedData)
    .enter().append("g")
    .attr("class", "g-category")

Use this data to add your lines/paths:

d3.selectAll(".g-category").append("path")
    .attr("class", "line")
    .attr("d", function(d){ return lineFunction(d.values);})
    .style("stroke", function(d) {return color(d.key);})

Finally, "step into" 2nd-tier to add bars/rect:

d3.selectAll(".g-category").selectAll(".bars")
     .data(function(d) {return d.values;})
     .enter().append("rect")
        .attr("class", "bar")
        .attr("x", function(d) {return x(d.Month);})
        .attr("y", function(d) {return y(d.Rate);})
        .attr("width", 20)
        .attr("height", function(d) {return height - y(d.Rate)})
        .attr("fill", function(d) {return color(d.Category)})

This is a straightforward approach (to me at least), in that you take it one category at a time, using the grouped data to draw a line, then individual data points to draw the bars.

LAZY EDIT:

To get category bars side by side

Create ordinal scale mapping category to [1,nCategories]. Use this to dynamically offset bars with something like

translate( newScale(category)*barWidth )

To show either bars or lines (not both)

Create a function that selects bars/lines and transitions/toggles their visibility/opacity. Run when your drop-down input changes and with the drop-down input as input to the function.


Related Query

More Query from same tag