score:6

Accepted answer

You seem a bit confused about your two x-scales, what each one does and when to use them.

Using the variable names from the linked example, the x0 scale spaces the different clusters of bars across the page.

I wasn't entirely clear what you meant by "I would like [to] have the X axis be by time and the bars be grouped by 'group.'" I'm assuming you want all the "groups" for each month clustered together, with the different months spread out across the page. If you instead want all the dates for each "group" to be clustered together, then the domain for x0 should be your groups, not your dates, and you'll have to adapt the rest of this discussion.

Because dates can be treated as continuous numbers, you can use a linear scale for them; that appears to be what you're doing, by passing in the extent (i.e., max and min) of the data instead of a sorted list of all possible values. However, a linear scale doesn't have an easy method to tell you how much space there is between each datapoint, which we need for positioning and sizing the bars. So to make everything easier, make it an ordinal scale and set its domain to the sorted dates.

The x1 scale spaces the individual bars within a cluster, relative to the start position of that cluster. So the domain is the "groups" you want listed at each month.

It doesn't look like you're ever setting the range for this scale. The range of the x1 scale is the width available for each cluster, which is determined by the band-width from the x0 scale: if you have lots of clusters, each cluster will be narrower, so the individual bars will also have to be narrower.

So your axis settings would be (assuming 20% padding between clusters, and none between individual bars):

// initialization
main_x0 = d3.scale.ordinal().rangeRoundBands([0, main_width], 0.2); 
main_x1 = d3.scale.ordinal();
main_y  = d3.scale.linear().range([main_height, 0] );

// once you have the data
main_x0.domain(data.result.map( function(d) { return d.date; } )
                          .sort(d3.ascending) 
               );

main_x1.domain(data.result.map( function(d) { return d.group; } )
                          .sort(d3.ascending) 
               )
       .rangeRoundBands([0, main_x0.rangeBand() ], 0);

main_y.domain(d3.extent(data.result, function(d) { return d.buildFixTime ; }));

(P.S. I'm not sure why you had all sorts of conversion equations on your y-scale domain. You only use the raw buildFixTime when you plot the data, so that's what you want in your domain. The conversion from milliseconds to hours should be done in your tickFormat function.)

Then, in your graphing methods, you need to to remember that main_x0 is your date-cluster scale, while main_x1 is your group within each date scale. The horizontal position of each bar is determined by first shifting it over to the start of the cluster and then shifting it over again to that group's position within the cluster. The x0 scale, applied to the date, gives you the first shift, while the x1 scale, applied to the group, gives you the second shift.

In other words, if you want a layout like this:

abcde  abcde  abcde  abcde
 Jan.   Feb.   Mar.   Apr.  
       *             
       >>>>|

Then to find the position for category "e" for February, you would need x0(Feb) + x1(e). The first gives you the start of the February cluster (marked *), the second gives you the additional shift to get to category e (marked with >).

Your data is nested by "group". So each of your <g> elements consists of all the bars in a particular category: all the as in one, all the bs in another. While you could use a transform on your <g> elements to shift the start point of each group according to how much shift that group requires in each cluster, I think that would be complicating things. As Lars says, you can do it all when positioning individual bars.

So your code would look like:

var bar = main.selectAll(".bars")
    .data(nested)
  .enter().append("g")
    .attr("class", function(d){return d.key;})
    //add a useful class based on the data
    //d.key is the value used to create the nested array
    //i.e., the group value for all the bars in the <g>

    .style("fill", function(d) { return color(d.key); });
    //Set the style here and it will be inherited by the bars
    //in the group. If you set it on the individual rectangles, 
    //you will need to use d.group (the data property), 
    //not d.key (the property created by the nest method) 

bar.selectAll("rect").append("rect")
    .data(function(d) { return d.values; })
     //the nested sub-array for each group

  .enter().append("rect")
    .attr("transform", function(d) {
          return "translate(" + main_x0(d.date) + ",0)"; 
     })
    .attr("x", function(d) { return main_x1(d.group); })
    //by using both a transform and an x-position, the two scales
    //are kept separate.  But make sure that you pass in the correct
    //data variable for each scale!  

    .attr("width", main_x1.rangeBand())
    //the width is calculated from the x1 scale, which positions
    //the individual bars within a cluster

    .attr("y", function(d) { return main_y(d.buildFixTime); })
    .attr("height", function(d) { 
             return main_height - main_y(d.buildFixTime); 
     });
    //note that these functions are based on a y-scale with an inverted range, 
    //one that goes from [main_height, 0], as I set up above

Again, if you want your bars grouped the other way around, you'll have to switch all your x0 scale functions to use d.group, and all your x1 scale functions to use d.date.


Related Query