Search code examples
dc.jscrossfilter

How to render timeseries/categorical stacked bar chart in dc.js, filtering stack and bar


Most examples I have found are using data that has time and number

var data =      [
  {
    "Time": "19-Jan-2018 11:24:49.000 UTC",
    "Speed": 1.885
  },
  {
    "Time": "19-Jan-2018 11:24:59.000 UTC",
    "Speed": 1.875
  },
  {
    "Time": "19-Jan-2018 11:25:00.000 UTC",
    "Speed": 1.878
  },
  {
    "Time": "19-Jan-2018 11:25:01.000 UTC",
    "Speed": 1.876
  }
]

I am looking to stack type

    var data =      [
  {
    "Time": "19-Jan-2018 11:24:49.000 UTC",
    "type": "CAT"
  },
  {
    "Time": "19-Jan-2018 11:24:59.000 UTC",
    "type": "DOG"
  },
  {
    "Time": "19-Jan-2018 11:25:00.000 UTC",
    "type": "CAT"
  },
  {
    "Time": "19-Jan-2018 11:25:01.000 UTC",
    "Type": "BAT"
  }
]

How can I stack categorical data, while allowing the user to select time/category pairs, as in the following Example?


Solution

  • I adapted the example to time/category data in this fiddle.

    Those dates would only parse in Chrome, so

    const parseDate = d3.utcParse("%d-%b-%Y %H:%M:%S.%L UTC");
    data.forEach(d => {
      d.Time = parseDate(d.Time);
    })
    

    I changed the key functions to use ,

    function multikey(x,y) {
      return x + ',' + y;
    }
    function splitkey(k) {
      return k.split(',');
    }
    

    I also changed fake group stack_second to convert string-dates from the multikeys back into Dates, and to initialize categories to 0 (since every stack has to be present for every X).

    function stack_second(group, categories) {
      return {
        all: function() {
          var all = group.all(),
              m = {};
          // build matrix from multikey/value pairs
          all.forEach(function(kv) {
            var ks = splitkey(kv.key);
            m[ks[0]] = m[ks[0]] || Object.fromEntries(categories.map(c=>[c,0]));
            m[ks[0]][ks[1]] = kv.value;
          });
          // then produce multivalue key/value pairs
          return Object.keys(m).map(function(k) {
            return {key: new Date(k), value: m[k]};
          });
        }
      };
    }
    

    Get the array of categories from the source data:

    const categories = Array.from(new Set(data.map(d => d.Type)).values());
    

    When dealing with date/time, you have to choose a d3 time interval appropriate for your data. Here minutes looked right. Using UTC d3-time methods everywhere because your source data is UTC.

    const interval = d3.utcMinute;
    

    Calculate xscale domain and apply:

    let extent = d3.extent(data, d=>d.Time);
    extent[1] = interval.offset(extent[1], 1)
    chart
      .x(d3.scaleTime().domain(extent))
      .xUnits(interval.range)
    

    Right number of ticks, also formatted in UTC:

    chart.xAxis().ticks(d3.utcMinute).tickFormat(d3.utcFormat('%H:%M'))
    

    Match colors between stacks and wedges with

    .colors(d3.scaleOrdinal().domain(categories).range(d3.schemeCategory10))
    

    Crossfilter initialization, using the interval and categories:

    const interval = d3.utcMinute; // choose appropriate to your data
    var cf = crossfilter(data),
        timeTypeDim = cf.dimension(function(d) { return multikey(interval(d.Time), d.Type); }),
        timeTypeGroup = timeTypeDim.group(), // reduceCount by default
        stackedGroup = stack_second(timeTypeGroup, categories);
    

    And here's the chart code for completeness, although we've already discussed the relevant parts:

    function sel_stack(i) {
      return function(d) {
        return d.value[i];
      };
    }
    chart
      .width(600)
      .height(400)
      .colors(d3.scaleOrdinal().domain(categories).range(d3.schemeCategory10))
    
      .controlsUseVisibility(true)
      .x(d3.scaleTime().domain(extent))
      .xUnits(interval.range)
      .margins({left: 80, top: 20, right: 10, bottom: 20})
      .brushOn(false)
      .clipPadding(10)
      .title(function(d) {
      return d.key + '[' + this.layer + ']: ' + d.value[this.layer];
    })
      .legend(dc.legend().x(540).y(50))
      .dimension(timeTypeDim)
      .group(stackedGroup, categories[0], sel_stack(categories[0]))
      .renderLabel(true);
    

    time/category stacked bars