7

I'm building my first line graph in d3:

http://jsfiddle.net/j94RZ/

I want to know how to utilize either the scale or axis allow me to draw a grid (of, presumably rectangles) where I can set a different background colour for each of the section of the grid...so I can alternate colours for each cell of the grid. I want the grid to be drawn and be constrained by the axes of my graph and then also adapt if the spacing of the axes ticks change (i.e. the axes changes like this: http://bl.ocks.org/mbostock/1667367). So if my graph has an x axis with 4 ticks and a y axis of 7 ticks then my graph will have a background grid that's 7 blocks high and 4 blocks wide.

I've been playing with the idea of using a range which starts at zero and ends at the full width of the graph but I don't know what value I can use for the step. Is there any way to sort of query the axis and return how many ticks there are?

var gridRange = d3.range(0, width, step?); 
Mike Rifgin
  • 10,409
  • 21
  • 75
  • 111
  • 1
    [This question](http://stackoverflow.com/questions/15580300/proper-way-to-draw-gridlines) may help. – Lars Kotthoff Oct 01 '13 at 19:10
  • Yeah that gets me closer. I've now got grid lines....I just need to adapt that somehow, so instead of lines I'm drawing rectangles... – Mike Rifgin Oct 01 '13 at 19:56
  • 1
    Perhaps [this questions](http://stackoverflow.com/questions/18825721/data-driven-vertical-horizontal-lines-in-d3/18827158#18827158) might also help. You can control the number of tick using ['linear.ticks'](https://github.com/mbostock/d3/wiki/Quantitative-Scales#wiki-linear_ticks) although only approximately. – user1614080 Oct 01 '13 at 20:44
  • The only way I can figure to do this is to use a for loop...so in the for loop i have to divide the width of the graph by the number of ticks (which I've now had to hard code) and same for the height. I can then work out how wide a rectangle needs to be....it works but it's not pretty....I'm surprised d3 has no way to utilise the scale or ticks somehow so I can achieve this – Mike Rifgin Oct 02 '13 at 09:21
  • 2
    Check out the ordinal.rangeBands function: https://github.com/mbostock/d3/wiki/Ordinal-Scales#wiki-ordinal_rangeBands – Tyson Anderson Oct 02 '13 at 10:01
  • 1
    ok, that looks interesting. So rangebands take a low and high value, and automatically divide that into even chunks based on the length of the domain. My domain length would be the amount of ticks on the x axis I suppose. I can see how I could use rangebands to create vertical bands as wide as the space inbetween each of the ticks on my axis but no idea how to get the rangebands to work with the height or the ticks on the y axis at the same time so I can create a grid where the grid squares (rectangles) match up to the x and y axis – Mike Rifgin Oct 02 '13 at 11:59

2 Answers2

9

A better approach than your current solution would be to use scale.ticks() explicitly to get the tick values. The advantage of that is that it will still work if you change the number of ticks for some reason.

To get an alternating grid pattern instead of a single fill, you can use something like this code.

.attr("fill", function(d, i) {
    return (i % 2) == 1 ? "green" : "blue";
})

Finally, to get the full grid pattern, you can either use an explicit loop as you've suggested, or nested selections. The idea here is to first pass in the y ticks, create a g element for each and then pass the x ticks to each one of these groups. In code, this looks something like this.

svg.selectAll("g.grid")
   .data(y.ticks()).enter().append("g").attr("class", "grid")
   .selectAll("rect")
   .data(x.ticks()).enter().append("rect");

To set the position, you can access the indices within the top and bottom level data arrays like this.

.attr("x", function(d, i) {
    return xScale(i);
})
.attr("y", function(d, i, j) {
    return yScale(j);
})

To set the x position, you need the index of the inner array (passed to the set of g elements), which can be accessed through the second argument of your callback. For the outer array, simply add another argument (j here).

And that's really all there is to it. Complete jsfiddle here. To update this grid dynamically, you would simply pass in the new tick values (gotten from scale.ticks()), match with the existing data, and handle the enter/update/exit selections in the usual manner.

If you want to do without the auxiliary scales (i.e. without .rangeBand()), you can calculate the width/height of the rectangles by taking the extent of the range of a scale and dividing it by the number of ticks minus 1. Altogether, this makes the code a bit uglier (mostly because you need one fewer rectangle than ticks and therefore need to subtract/remove), but a bit more general. A jsfiddle that takes this approach is here.

Lars Kotthoff
  • 107,425
  • 16
  • 204
  • 204
  • Closer, but there's still a lot of hard-coded values hungover from my initial experiments. Removing the ticks definition from where the yAxis is declared breaks the grid for example..and there are also still hardcoded ranges in the ordinal scales domains, which I can't have as the amount of ticks will change....I've tried moving the ordinal scale definitions underneath the scale domain defintions and then using the axis.ticks() to hopefully dynamically output the amount of ticks but that doesn't work http://jsfiddle.net/Ny2FJ/5/ – Mike Rifgin Oct 14 '13 at 15:39
  • This seems to sort of work...but the grid isn't lined up to the axes....http://jsfiddle.net/Ny2FJ/6/ – Mike Rifgin Oct 14 '13 at 15:46
  • I've removed your additional scales and only used the original ones -- http://jsfiddle.net/Ny2FJ/7/ Is this what you're looking for? The code is a bit uglier but should work fine in practice. – Lars Kotthoff Oct 14 '13 at 17:25
  • Ok, I'll just add some more explanation on the latest jsfiddle to the answer. – Lars Kotthoff Oct 15 '13 at 09:04
  • 1
    I think you mean [scale.ticks](https://github.com/mbostock/d3/wiki/Quantitative-Scales#wiki-linear_ticks) rather than [axis.ticks](https://github.com/mbostock/d3/wiki/SVG-Axes#wiki-ticks). scale.ticks is what generates the tick values for the domain, while axis.ticks lets you configure how scale.ticks is invoked by the axis. – mbostock Oct 16 '13 at 15:23
  • @LarsKotthoff Why is x.ticks not working as a function for me? Did a similar thing based on this and my log tells me x.ticks isn't a function. Any help? – Alexey Ayzin Jul 21 '15 at 21:36
  • @AlexeyAyzin No idea without seeing the code. You may want to post a separate question. – Lars Kotthoff Jul 21 '15 at 21:54
  • @LarsKotthoff Question is up here: http://stackoverflow.com/questions/31550875/rectangles-instead-of-ticks-d3 – Alexey Ayzin Jul 21 '15 at 22:42
1

So after a few helpful comments above I've got close to a solution. Using Ordinal rangebands get me close to where I want to go.

I've created the range bands by using the number of ticks on my axis as a basis for the range of the input domain:

  var xScale = d3.scale.ordinal()
            .domain(d3.range(10))
            .rangeRoundBands([0, width],0);


 var yScale = d3.scale.ordinal()
            .domain(d3.range(4))
            .rangeRoundBands([0, height],0);

I've then tried drawing the rectangles out like so:

svg.selectAll("rect")
           .data(p)
           .enter()
           .append("rect")
           .attr("x", function(d, i) {
                return xScale(i);
           })
           .attr("y", function(d,i) {
                0
           })
           .attr("width", xScale.rangeBand())
           .attr("height", yScale.rangeBand())
                       .attr("fill", "green").
                       attr('stroke','red');

This gets me the desired effect but for only one row deep:

http://jsfiddle.net/Ny2FJ/2/

I want,somehow to draw the green blocks for the whole table (and also without having to hard code the amount of ticks in the ordinal scales domain). I tried to then apply the range bands to the y axis like so (knowing that this wouldn't really work though) http://jsfiddle.net/Ny2FJ/3/

svg.selectAll("rect")
       .data(p)
       .enter()
       .append("rect")
       .attr("x", function(d, i) {
            return xScale(i);
       })
       .attr("y", function(d,i) {
            return yScale(i);
       })
       .attr("width", xScale.rangeBand())
       .attr("height", yScale.rangeBand())
                   .attr("fill", "green").
                   attr('stroke','red');

The only way I can think to do this is to introduce a for loop to run the block of code in this fiddle http://jsfiddle.net/Ny2FJ/2/ for each tick of the y axis.

Mike Rifgin
  • 10,409
  • 21
  • 75
  • 111