Search code examples
d3.jsdc.jscrossfilter

dcjs dynamic zooming to fit range of values


I have a rowchart in DCjs that plots the top N values of a given parameter. However, for the unfiltered data these differ from each other by a very small number.

Accounts plotted with no filters, account ID and full range

I've had to label each row with it's unique identifier, as my random generator produced two identical names, meaning that if I use name as the dimension, I end up with one of ten thousand data points having a value greater than 100%.

However, the main problem here is that the difference between each row is tiny, and can be around 0.0001.

However if I zoom in on that part of the x-axis using

var max = dim.top[1][0].value;
var min = dim.top(10)[9].value;    

chart
  .dimension(dim)
  .group(group)
  .x(d3.scaleLinear()
     .range([-100, chart.width()])
     .domain([min-(max-min)*0.1,max])
  )
  .cap(10)
  .othersGrouper(null)
  .colors(['#ff0000']);

enter image description here

Firstly I loose the ID label on the left. Secondly as I also have to use .elasticX(false) for the zooming to work, it means that when I add filters, the range of the x-axis doesn't change with the values e.g.

enter image description here

Is there a way to get dynamic zooming such that the range of the x-axis depends on the range of values presented?


Solution

  • elasticX is a really simple feature which does pretty much what your code does, although it locks the min or max to zero depending if the data is positive or negative:

            var extent = d3.extent(_rowData, _chart.cappedValueAccessor);
            if (extent[0] > 0) {
                extent[0] = 0;
            }
            if (extent[1] < 0) {
                extent[1] = 0;
            }
    

    (calculateAxisScale source)

    This code gets called (indirectly) before each render and redraw.

    Here's some general purpose advice for when elasticX or elasticY doesn't do exactly what you want. I've never seen it fail! (Which is saying something in such a quirky codebase as dc.js.)

    First, disable elasticX. Then create a function which calculates the X domain and sets it:

    function custom_elastic(chart) {
      var max = chart.dimension().top[1][0].value;
      var min = chart.dimension().top(10)[9].value;
      chart.x().domain([min,max]);
    }
    

    I've parameterized it on the chart for generality.

    Now we can have this function called on the preRender and preRedraw events. These events will pass the chart when they fire:

    chart.on('preRender', custom_elastic)
         .on('preRedraw', custom_elastic);
    

    And that should do it!

    BTW, you probably don't want to set the range of the scale - this is set automatically by the chart, and it's a little more complicated than you have it since it takes margins into account.

    Debugging the min and max

    Looking at your fiddle I realized that I hadn't given a second look to how you are calculating the min and max.

    I also hadn't noticed that you had the range start at -100.

    Good first step logging it; it reports

    min: 0.81, max: 0.82
    

    which is incorrect. The top ten are from 0.96 to 1.

    The issue is that the dimension's key is the id, so the rows returned by .top() are in reverse alphabetical order (the "largest" strings).

    Again you're on the right track with

    console.log(Group.top(Infinity))
    

    Yes! The group will give you the top 10 by value.

    var top10 = thischart.group().top(10);
    var max = top10[0].value;
    var min = top10[9].value;
    

    Nice!

    semi-working bar elastic

    fiddle

    But wait, doesn't it look like the bars are stretching off the left side of the chart?

    Hacking the row chart with a renderlet to draw bars starting at the left edge

    Okay now it's clear that the row chart doesn't support this. Here is a hack to resize the bars to the left edge:

    chart.on('renderlet', function(chart) {
        var transform = chart.select('g.row rect').attr('transform');
        var tx = +transform.split('(')[1].split(',')[0];
        chart.selectAll('g.row rect')
            .attr('transform', null)
            .attr('width', function(d) {
                return +d3.select(this).attr('width') + tx;
            })
        chart.selectAll('g.row text.row')
            .attr('transform', null);    
    })
    

    All the row rects are going to be offset by a large negative number, which we grab first in tx. Then we remove the transform from both the rects and the text, and add tx to the width of the row rects.

    good except for the last bar

    fiddle

    Great! But where's the last bar? Well, we took the top ten values for the min and max, so the tenth bar is the minimum value.

    You'll have to figure out what works for you, but I found that looking at the top 20 values left the top 10 at good sizes:

    var N = 20;
    var topN = thischart.group().top(N);
    var max = topN[0].value;
    var min = topN[N-1].value;
    

    good bounds

    final fiddle

    This hack does not play well with the built-in transitions, so I turned them off for this chart:

    chart.transitionDuration(0)
    

    It would be a lot more work to hack that part, and better to fix it in the chart itself.