Mike Davlantes
Mike Davlantes

Reputation: 1035

Drawing gaps for missing dates in dc.js time series line graph

Okay, so I've seen this ticket and this question and have tried several examples already. Maybe I'm just dense, but I really haven't been able to crack this one.

I have a time series of events that has gaps in it. By default, dc.js connects a straight line over the gap (making it look like things are represented there when they really shouldn't be). For example, in this graph we have data as follows:

    {"time":"2014-06-09T18:45:00.000Z","input":17755156,"output":250613233.333333},
    {"time":"2014-06-09T18:46:00.000Z","input":18780286.6666667,"output":134619822.666667},
    {"time":"2014-06-09T18:47:00.000Z","input":20074614.6666667,"output":203239834.666667},
    {"time":"2014-06-09T18:48:00.000Z","input":22955373.3333333,"output":348996205.333333},
    {"time":"2014-06-09T18:49:00.000Z","input":19119089.3333333,"output":562631022.666667},
    {"time":"2014-06-09T18:50:00.000Z","input":15404272,"output":389916332},
    {"time":"2014-06-09T18:51:00.000Z","input":null,"output":null},
    {"time":"2014-06-09T21:25:20.000Z","input":5266038.66666667,"output":62598396},
    {"time":"2014-06-09T21:26:20.000Z","input":6367678.66666667,"output":84494096},
    {"time":"2014-06-09T21:27:20.000Z","input":5051610.66666667,"output":88812540},
    {"time":"2014-06-09T21:28:20.000Z","input":5761069.33333333,"output":79098036},
    {"time":"2014-06-09T21:29:20.000Z", "input":5110277.33333333,"output":45816729.3333333}

Even though there's only two actual groups of data, there's a line on that graph connecting them. How do I make dc.js line graphs draw 0 where there is no data at all. I've tried using .defined(function(d) { return !isNaN(d.x);}) and .defined(function(d) { return d.y != null; }) and such, but this is just iterating through data which isn't there.

Upvotes: 1

Views: 346

Answers (1)

Gordon
Gordon

Reputation: 20140

It's tricky trying to preserve nulls when using crossfilter, because crossfilter is all about aggregation.

Remember that reduceSum will add any values it finds, starting from zero, and 0 + null === 0.

In your case, it looks like you're not actually aggregating, since your timestamps are unique, so you could do something like this:

var input  = time.group().reduce(
    function(p, d) {
       if(d.input !== null)
           p += d.input;
       else p = null;
       return p;
    },
    function(p, d) {
       if(d.input !== null)
           p -= d.input;
       else p = null;
       return p;
    },
    function(){ return 0; }
);

Yeah, that's a lot more complicated than reduceSum, and it may get even more complicated if more than one datum falls into a bucket. (Not sure what you'd want to do there - is it possible for a data point to be partly defined?)

With the reduction defined this way, null reduces to null and dc.js is able to find the gaps: line chart with gaps

Fork of your fiddle (thanks!): http://jsfiddle.net/gordonwoodhull/omLko77k/3/

Edit: counting nulls

If you're doing a "real" reduction where there is more than one value in a bin, I think you'll need to count the number of non-null values as well as keeping a running sum.

When there are no non-null values, the sum should be null.

Reusing our code a bit better this time:

function null_counter(field) {
    return {
        add: function(p, d) {
            if(d[field] !== null) {
                p.nvalues++;
                p.sum += d[field];
            }
            return p;
        },
        remove: function(p, d) {
            if(d[field] !== null) {
                p.nvalues--;
                p.sum -= d[field];
                if(!p.nvalues)
                    p.sum = null;
            }
            return p;
        },
        init: function() {
            return {nvalues: 0, sum: null};
        }
    }
}

Applied like this (and getting the fields right this time):

var input_reducer = null_counter('input');
var input = time.group().reduce(
    input_reducer.add,
    input_reducer.remove,
    input_reducer.init
);
var output_reducer = null_counter('output');
var output = time.group().reduce(
    output_reducer.add,
    output_reducer.remove,
    output_reducer.init
);

Since we're reducing to an object with two values {nvalues, sum}, we need to make all our accessors a little more complicated:

    .valueAccessor(function(kv) { return kv.value.sum; })
    .defined(function(d){
        return (d.data.value.sum !== null);
    })
chart.stack(output, "Output bits",
            function(kv) { return kv.value.sum; });

Updated fork: http://jsfiddle.net/gordonwoodhull/omLko77k/9/

Upvotes: 1

Related Questions