8
votes

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();
2
Do you want a filter where you can choose 2016 or 2017? - cirofdo
It seems to show two lines. What's the expected behavior? - Gordon
It should show two lines but one line is the 2017 earnings and second 2016 earnings per store. Currently the 2 lines are the sum per store for both years (same with bar chart) - Nick Doulgeridis
I'm not getting it... Also, what is this for Math.floor(Math.random() * (1 - 0 + 1)) + 0 ? You're multiplying by 1 and adding a 0.. - cirofdo
This is to get an integer from 0 to 1 so it is just a random way to get either 0 or 1. - Nick Doulgeridis

2 Answers

1
votes

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/

1
votes

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.