Repository: vega/vega-lite
Pull request #604 (2015-10-05 13:54:52)

Aggregate, Facet, Subfacet, and Bin for Vega 2
- Migrate aggregate and bin transform to Vega 2.
- Add Aggregate Bar Chart to the gallery



Patch:
diff --git a/gallery/gallery.js b/gallery/gallery.js
index d03b17eac..4abdb07ba 100644
--- a/gallery/gallery.js
+++ b/gallery/gallery.js
@@ -53,6 +53,16 @@ var EXAMPLES = [
       },
       data: {'url': 'data/cars.json'}
     }
+  },{
+    title: 'Aggregate Bar Chart',
+    spec: {
+      'marktype': 'bar',
+      'encoding': {
+        'x': {'name': 'Cylinders','type': 'O'},
+        'y': {'name': 'Acceleration','type': 'Q','aggregate': 'mean'}
+      },
+      'data': {'url': 'data/cars.json'}
+    }
   },{
     title: 'Barleys',
     spec: {
diff --git a/src/compiler/compiler.js b/src/compiler/compiler.js
index 59cf583e7..627a80ef1 100644
--- a/src/compiler/compiler.js
+++ b/src/compiler/compiler.js
@@ -54,7 +54,8 @@ compiler.compileEncoding = function (encoding, stats) {
       width: layout.width,
       height: layout.height,
       padding: 'auto',
-      data: compiler.data(encoding),
+      // FIXME(#514): eliminate stats
+      data: compiler.data(encoding, stats),
       // global scales contains only time unit scales
       scales: compiler.time.scales(encoding),
       marks: [{
diff --git a/src/compiler/data.js b/src/compiler/data.js
index ed82d1e2b..928b4f195 100644
--- a/src/compiler/data.js
+++ b/src/compiler/data.js
@@ -17,8 +17,9 @@ var vlfield = require('../field'),
  *                 If the encoding contains aggregate value, this will also create
  *                 aggregate table as well.
  */
-function data(encoding) {
-  var def = [data.raw(encoding)];
+// FIXME(#514): eliminate stats
+function data(encoding, stats) {
+  var def = [data.raw(encoding, stats)];
 
   var aggregate = data.aggregate(encoding);
   if (aggregate) def.push(data.aggregate(encoding));
@@ -31,7 +32,8 @@ function data(encoding) {
   return def;
 }
 
-data.raw = function(encoding) {
+// FIXME(#514): eliminate stats
+data.raw = function(encoding, stats) {
   var raw = {name: RAW};
 
   // Data source (url or inline)
@@ -49,7 +51,7 @@ data.raw = function(encoding) {
     raw.format.parse = parse;
   }
 
-  raw.transform = data.raw.transform(encoding);
+  raw.transform = data.raw.transform(encoding, stats);
   return raw;
 };
 
@@ -74,12 +76,13 @@ data.raw.formatParse = function(encoding) {
  * Generate Vega transforms for the raw data table.  This can include
  * transforms for time unit, binning and filtering.
  */
-data.raw.transform = function(encoding) {
+// FIXME(#514): eliminate stats
+data.raw.transform = function(encoding, stats) {
   // null filter comes first so transforms are not performed on null values
   // time and bin should come before filter so we can filter by time and bin
   return data.raw.transform.nullFilter(encoding).concat(
     data.raw.transform.time(encoding),
-    data.raw.transform.bin(encoding),
+    data.raw.transform.bin(encoding, stats),
     data.raw.transform.filter(encoding)
   );
 };
@@ -99,14 +102,17 @@ data.raw.transform.time = function(encoding) {
   }, []);
 };
 
-data.raw.transform.bin = function(encoding) {
+// FIXME(#514): eliminate stats
+data.raw.transform.bin = function(encoding, stats) {
   return encoding.reduce(function(transform, field, encType) {
     if (encoding.bin(encType)) {
       transform.push({
         type: 'bin',
         field: field.name,
-        output: encoding.fieldRef(encType),
-        maxbins: encoding.bin(encType).maxbins
+        output: {bin: encoding.fieldRef(encType)},
+        maxbins: encoding.bin(encType).maxbins,
+        min: stats[field.name].min,
+        max: stats[field.name].max
       });
     }
     return transform;
@@ -149,34 +155,46 @@ data.raw.transform.filter = function(encoding) {
 };
 
 data.aggregate = function(encoding) {
-  var dims = {}, meas = {};
+  /* dict set for dimensions */
+  var dims = {};
+
+  /* dictionary mapping field name => dict set of aggregation functions */
+  var meas = {};
+
+  var hasAggregate = false;
 
   encoding.forEach(function(field, encType) {
     if (field.aggregate) {
+      hasAggregate = true;
       if (field.aggregate === 'count') {
-        meas.count = {op: 'count', field: '*'};
-      }else {
-        meas[field.aggregate + '|' + field.name] = {
-          op: field.aggregate,
-          field: field.name
-        };
+        meas['*'] = meas['*'] || {};
+        meas['*'].count = true;
+      } else {
+        meas[field.name] = meas[field.name] || {};
+        meas[field.name][field.aggregate] = true;
       }
     } else {
       dims[field.name] = encoding.fieldRef(encType);
     }
   });
 
-  dims = util.vals(dims);
-  meas = util.vals(meas);
+  var groupby = util.vals(dims);
+
+  // short-format summarize object for Vega's aggregate transform
+  // https://github.com/vega/vega/wiki/Data-Transforms#-aggregate
+  var summarize = util.reduce(meas, function(summarize, fnDictSet, fieldName) {
+    summarize[fieldName] = util.keys(fnDictSet);
+    return summarize;
+  }, {});
 
-  if (meas.length > 0) {
+  if (hasAggregate) {
     return {
       name: AGGREGATE,
       source: RAW,
       transform: [{
         type: 'aggregate',
-        groupby: dims,
-        fields: meas
+        groupby: groupby,
+        summarize: summarize
       }]
     };
   }
diff --git a/src/compiler/facet.js b/src/compiler/facet.js
index 25ca0bc04..2a2d7d6bd 100644
--- a/src/compiler/facet.js
+++ b/src/compiler/facet.js
@@ -54,7 +54,7 @@ function faceting(group, encoding, layout, spec, singleScaleNames, stack, stats)
     if (!encoding.isDimension(ROW)) {
       util.error('Row encoding should be ordinal.');
     }
-    enter.y = {scale: ROW, field: 'keys.' + facetKeys.length};
+    enter.y = {scale: ROW, field: encoding.fieldName(ROW)};
     enter.height = {'value': layout.cellHeight}; // HACK
 
     facetKeys.push(encoding.fieldRef(ROW));
@@ -62,7 +62,7 @@ function faceting(group, encoding, layout, spec, singleScaleNames, stack, stats)
     if (hasCol) {
       from = util.duplicate(group.from);
       from.transform = from.transform || [];
-      from.transform.unshift({type: 'facet', keys: [encoding.fieldRef(COL)]});
+      from.transform.unshift({type: 'facet', groupby: [encoding.fieldRef(COL)]});
     }
 
     axesGrp = groupdef('x-axes', {
@@ -86,7 +86,7 @@ function faceting(group, encoding, layout, spec, singleScaleNames, stack, stats)
     if (!encoding.isDimension(COL)) {
       util.error('Col encoding should be ordinal.');
     }
-    enter.x = {scale: COL, field: 'keys.' + facetKeys.length};
+    enter.x = {scale: COL, field: encoding.fieldName(COL)};
     enter.width = {'value': layout.cellWidth}; // HACK
 
     facetKeys.push(encoding.fieldRef(COL));
@@ -94,7 +94,7 @@ function faceting(group, encoding, layout, spec, singleScaleNames, stack, stats)
     if (hasRow) {
       from = util.duplicate(group.from);
       from.transform = from.transform || [];
-      from.transform.unshift({type: 'facet', keys: [encoding.fieldRef(ROW)]});
+      from.transform.unshift({type: 'facet', groupby: [encoding.fieldRef(ROW)]});
     }
 
     axesGrp = groupdef('y-axes', {
@@ -130,7 +130,7 @@ function faceting(group, encoding, layout, spec, singleScaleNames, stack, stats)
 
   // add facet transform
   var trans = (group.from.transform || (group.from.transform = []));
-  trans.unshift({type: 'facet', keys: facetKeys});
+  trans.unshift({type: 'facet', groupby: facetKeys});
 
   return spec;
 }
diff --git a/src/compiler/stack.js b/src/compiler/stack.js
index 9df167736..5a9115594 100644
--- a/src/compiler/stack.js
+++ b/src/compiler/stack.js
@@ -36,7 +36,7 @@ function stacking(data, encoding, mdef) {
     transform: [{
       type: 'aggregate',
       groupby: [encoding.fieldRef(dim)].concat(facets), // dim and other facets
-      fields: [{op: 'sum', field: encoding.fieldRef(val)}] // TODO check if field with aggregate is correct?
+      summarize: [{ops: ['sum'], field: encoding.fieldRef(val)}] // TODO check if field with aggregate is correct?
     }]
   };
 
@@ -44,8 +44,8 @@ function stacking(data, encoding, mdef) {
     stacked.transform.push({ //calculate max for each facet
       type: 'aggregate',
       groupby: facets,
-      fields: [{
-        op: 'max',
+      summarize: [{
+        ops: ['max'],
         field: encoding.fieldName(val, {fn: 'sum'})
       }]
     });
diff --git a/src/compiler/subfacet.js b/src/compiler/subfacet.js
index 80fd6eaa3..e0cc6c5ee 100644
--- a/src/compiler/subfacet.js
+++ b/src/compiler/subfacet.js
@@ -24,7 +24,7 @@ function subfaceting(group, mdef, details, stack, encoding) {
 
   //TODO test LOD -- we should support stack / line without color (LOD) field
   var trans = (g.from.transform || (g.from.transform = []));
-  trans.unshift({type: 'facet', keys: details});
+  trans.unshift({type: 'facet', groupby: details});
 
   if (stack && encoding.has(COLOR)) {
     trans.unshift({type: 'sort', by: encoding.fieldRef(COLOR)});


Test patch:
diff --git a/test/compiler/data.spec.js b/test/compiler/data.spec.js
index b518a32a0..96bc87539 100644
--- a/test/compiler/data.spec.js
+++ b/test/compiler/data.spec.js
@@ -113,14 +113,24 @@ describe('data.raw', function() {
       }
     });
 
+    var stats = {
+      Acceleration: {
+        min: 0,
+        max: 100
+      }
+    };
+
     describe('bin', function() {
       it('should add bin transform', function() {
-        var transform = data.raw.transform.bin(encoding);
+        var transform = data.raw.transform.bin(encoding, stats);
+
         expect(transform[0]).to.eql({
           type: 'bin',
           field: 'Acceleration',
-          output: 'bin_Acceleration',
-          maxbins: 15
+          output: {bin: 'bin_Acceleration'},
+          maxbins: 15,
+          min: 0,
+          max: 100
         });
       });
     });
@@ -181,7 +191,7 @@ describe('data.raw', function() {
     });
 
     it('should have null filter, timeUnit, bin then filter', function () {
-      var transform = data.raw.transform(encoding);
+      var transform = data.raw.transform(encoding, stats);
       expect(transform[0].type).to.eql('filter');
       expect(transform[1].type).to.eql('formula');
       expect(transform[2].type).to.eql('bin');
@@ -216,13 +226,10 @@ describe('data.aggregated', function () {
       "transform": [{
         "type": "aggregate",
         "groupby": ["origin"],
-        "fields": [{
-          "op": "sum",
-          "field": "Acceleration"
-        },{
-          "op": "count",
-          "field": "*"
-        }]
+        "summarize": {
+          '*': ['count'],
+          'Acceleration': ['sum']
+        }
       }]
     });
   });