8

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();
Nick Doulgeridis
  • 583
  • 1
  • 16
  • 31
  • Do you want a filter where you can choose 2016 or 2017? – cirofdo May 03 '17 at 16:37
  • It seems to show two lines. What's the expected behavior? – Gordon May 03 '17 at 16:41
  • 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 May 03 '17 at 16:44
  • 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 May 03 '17 at 16:47
  • 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 May 04 '17 at 10:31
  • Took me a while, but I've provided two approaches below. I think this question might not have received enough attention because of the random data generation. I think it's easier for people to work with some fake data that's close to your actual shape, rather than reading some generation code. That way they can see the shape directly. – Gordon May 17 '17 at 22:46

2 Answers2

1

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/

Gordon
  • 19,811
  • 4
  • 36
  • 74
1

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.

Gordon
  • 19,811
  • 4
  • 36
  • 74