Nick Doulgeridis
Nick Doulgeridis

Reputation: 613

dc.js Series Chart multi line

I am trying to create 2 charts one bar and one series which the bar will show total earnings per store and series will show multi line earnings per year.

Here is the jsfiddle https://jsfiddle.net/xc4bwgLj/

So when I click on Bar chart Store 1, I want in series chart to see for this store earning for 2017 and 2016 each on a new line. Currently the series chart show the total earnings for each store like bar chart.

Any idea how can I change series chart to show 2016 and 2017 earnings per store?

JsFiddle code:

// generate data
var data = [];
var n = 1000.;

for (var i = 0; i < n; i++) {
console.log(Math.floor(Math.random() * (1 - 0 + 1)) + 0);
  data.push({
    id: (Math.floor(Math.random() * (1 - 0 + 1)) + 0),
    "i": i,
    x: Math.random(),
    "store_name": "Store"+(Math.floor(Math.random() * (1 - 0 + 1)) + 0),
    "2017_earnings": Math.random()*110,
    "2016_earnings": Math.random()*80
  });
}

// do some crossfilter stuff
var cf = crossfilter(data),
  series = cf.dimension(function(d) {
    return [d.store_name, d.i];
  }),
  series_grouped = series
  .group(function(d) {
  console.log(d)
    return [d[0], Math.floor(d[1] / 100.) * 100.];
  })
  .reduceSum(function(d) {
    return d.x;
  }),
  id = cf.dimension(function(d) {
    return d.store_name;
  }),
  id_grouped = id.group().reduceSum(function(d) {
    return d.x;
  });

// generate charts
var chart_width = 960,
  chart_height = 200;
console.log(dc);
dc.seriesChart("#chart_a").height(chart_height).width(.74 * chart_width)
  .chart(function(c) {
    return dc.lineChart(c).renderArea(true)
        .filterHandler(function(dimension, filter) {
        if(filter[0]) {
            dimension.filterFunction(function(d) {
            return d[1] > filter[0][0] && d[1] < filter[0][1];
          });
        } else {
            dimension.filterAll();
        }
        setTimeout(dc.redrawAll,0);
        return filter;
        });
  })
  .x(d3.scale.linear().domain([0, n]))
  .dimension(series)
  .group(series_grouped)
  .seriesAccessor(function(d) {
    return d.key[0];
  })
  .keyAccessor(function(d) {
    return d.key[1];
  })
  .valueAccessor(function(d) {
    return d.value;
  }).legend(dc.legend().x(350).y(350).itemHeight(13).gap(5).horizontal(1).legendWidth(140).itemWidth(70));
dc.barChart("#chart_b").height(chart_height).width(.24 * chart_width)
  .dimension(id)
  .group(id_grouped)
  .x(d3.scale.ordinal())
  .xUnits(dc.units.ordinal)
  .xAxis();

dc.renderAll();

Upvotes: 8

Views: 1213

Answers (2)

Gordon
Gordon

Reputation: 20150

Here's a different approach that keeps the shape of the source the same, but split the group for the use of the series chart. In my other answer I change the source data shape.

Using a fake group

Whenever we need to preprocess data, e.g. to change the shape that crossfilter returns, we can use a fake group

We'll reduce both columns separately using an ordinary reduction of multiple fields:

  series = cf.dimension(function(d) {
    return d.i;
  }),
  series_grouped = series.group(function(k) {
    return Math.floor(k / 100.) * 100.;
  })
  .reduce(
    function(p, d) { // add
      p[2016] += d['2016_earnings'];
      p[2017] += d['2017_earnings'];
      return p;
    },
    function(p, d) { // remove
      p[2016] -= d['2016_earnings'];
      p[2017] -= d['2017_earnings'];
      return p;
    },
    function() {
     return {2016: 0, 2017: 0};
    }),

Then this split group will take the names of two fields, and will split each bin into two, using the field name as the first part of the multikey:

function split_group(group, field1, field2) {
  return {
    all: function() {
      var ret = [];
      group.all().forEach(function(kv) {
        ret.push({
          key: [field1, kv.key],
          value: kv.value[field1]
        });
        ret.push({
          key: [field2, kv.key],
          value: kv.value[field2]
        });
      });
      return ret;
    }
  }
}

Use it like this:

  series_split = split_group(series_grouped, 2016, 2017)
  // ...
  chart
    .group(series_split)

Hard to tell with the random number generation, but I think the result is identical to the other answer. Just a different approach.

Upvotes: 1

Gordon
Gordon

Reputation: 20150

Here's a solution using a different data shape. My other example uses a fake group to change the shape after aggregation.

Using a different data shape.

In this case, I don't think the shape of the data is conducive to what you want to do, so in this answer I'll change the shape. I'll try using a fake group in a second answer.

The series chart takes a single group with multikeys, and a group filters by rows. Since each row contains both 2016 and 2017 earnings, it's not possible to aggregate them separately using crossfilter.

So for this attempt I've split the records by earnings year:

for (var i = 0; i < n; i++) {
  var id = Math.round(Math.random()),
    x = Math.random(),
      store = "Store"+Math.round(Math.random());
  data.push({
    id: id,
    i: i,
    x: x,
    store_name: store,
    earnings_year: 2016,
    earnings: Math.random()*80
  });
  data.push({
    id: id,
    i: i,
    x: x,
    store_name: store,
    earnings_year: 2017,
    earnings: Math.random()*110,
  });
}

I think this preserves all the qualities of your original data, but it splits the records by earnings year. I also simplified the random number generation. ;-)

Now we can easily create multikey dimension which uses earnings_year.

I don't think doing the rounding in the group key function was working, because the dimension and group key functions need to have the same ordering, so I've moved it up:

  series = cf.dimension(function(d) {
    return [d.earnings_year, Math.floor(d.i / 100.) * 100.];
  }),

Now we simply group with the same keys, and reduce by the sum of earnings instead of x (which is what I think was intended).

  series_grouped = series.group()
  .reduceSum(function(d) {
    return d.earnings;
  }),

Fork of your fiddle, with filtering by store https://jsfiddle.net/gordonwoodhull/urxLwh81/

Upvotes: 1

Related Questions