Repository: vega/vega-lite
Pull request #693 (2015-10-23 20:19:34)

Reimplement bin using `bin_start`, `bin_end`
- Reimplement binning to use linear scale instead of ordinal scale based on @jheer’s example in https://github.com/vega/vega/pull/388 using - use the new bin transform’s `bin_end`

- Make `scale.zero` automatically determined rather than always `true`
  - always returns `false` for bin and time
  - Otherwise, return true for `X`, `Y` and `SIZE`

## TODO
- use `bin_mid` instead


Patch:
diff --git a/gallery/examples.js b/gallery/examples.js
index a027d8152..671773c3c 100644
--- a/gallery/examples.js
+++ b/gallery/examples.js
@@ -1,4 +1,5 @@
 'use strict';
+// jshint quotmark: false
 
 var EXAMPLES = [
   {
@@ -252,6 +253,81 @@ var EXAMPLES = [
       },
       'data': {'url': 'data/cars.json'}
     }
+  },{
+    title: 'Histogram',
+    spec: {
+      "marktype": "bar",
+      "encoding": {
+        "x": {"bin": true,"name": "Acceleration","type": "Q"},
+        "y": {
+          "name": "*",
+          "aggregate": "count",
+          "type": "Q",
+          "displayName": "Number of Records"
+        }
+      },
+      "data": {"url": "data/cars.json"}
+    }
+  },{
+    title: 'Horizontal Histogram',
+    spec: {
+      "marktype": "bar",
+      "encoding": {
+        "y": {"bin": true,"name": "Acceleration","type": "Q"},
+        "x": {
+          "name": "*",
+          "aggregate": "count",
+          "type": "Q",
+          "displayName": "Number of Records"
+        }
+      },
+      "data": {"url": "data/cars.json"}
+    }
+  },{
+    title: 'Histogram with point instead',
+    spec: {
+      "marktype": "point",
+      "encoding": {
+        "x": {"bin": true,"name": "Acceleration","type": "Q"},
+        "y": {
+          "name": "*",
+          "aggregate": "count",
+          "type": "Q",
+          "displayName": "Number of Records"
+        }
+      },
+      "data": {"url": "data/cars.json"}
+    }
+  },{
+    title: 'Histogram with line instead',
+    spec: {
+      "marktype": "line",
+      "encoding": {
+        "x": {"bin": true,"name": "Acceleration","type": "Q"},
+        "y": {
+          "name": "*",
+          "aggregate": "count",
+          "type": "Q",
+          "displayName": "Number of Records"
+        }
+      },
+      "data": {"url": "data/cars.json"}
+    }
+  },{
+    title: 'Histogram with area instead',
+    spec: {
+      "marktype": "area",
+      "encoding": {
+        "x": {"bin": true,"name": "Acceleration","type": "Q"},
+        "y": {
+          "name": "*",
+          "aggregate": "count",
+          "type": "Q",
+          "displayName": "Number of Records"
+        }
+      },
+      "data": {"url": "data/cars.json"}
+    }
   }
 ];
 
diff --git a/src/compiler/data.js b/src/compiler/data.js
index 3ddd08166..de6b9a95d 100644
--- a/src/compiler/data.js
+++ b/src/compiler/data.js
@@ -114,9 +114,18 @@ data.raw.transform.bin = function(encoding) {
       transform.push({
         type: 'bin',
         field: encDef.name,
-        output: {start: encoding.fieldRef(encType)},
+        output: {
+          start: encoding.fieldRef(encType, {bin_suffix: '_start'}),
+          end: encoding.fieldRef(encType, {bin_suffix: '_end'})
+        },
         maxbins: encoding.bin(encType).maxbins
       });
+      // temporary fix for adding missing `bin_mid` from the bin transform
+      transform.push({
+        type: 'formula',
+        field: encoding.fieldRef(encType, {bin_suffix: '_mid'}),
+        expr: '(' + encoding.fieldRef(encType, {datum:1, bin_suffix: '_start'}) + '+' + encoding.fieldRef(encType, {datum:1, bin_suffix: '_end'}) + ')/2'
+      });
     }
     return transform;
   }, []);
@@ -190,7 +199,15 @@ data.aggregate = function(encoding) {
         meas[encDef.name][encDef.aggregate] = true;
       }
     } else {
-      dims[encDef.name] = encoding.fieldRef(encType);
+      if (encDef.bin) {
+        // TODO(#694) only add dimension for the required ones.
+        dims[encoding.fieldRef(encType, {bin_suffix: '_start'})] = encoding.fieldRef(encType, {bin_suffix: '_start'});
+        dims[encoding.fieldRef(encType, {bin_suffix: '_mid'})] = encoding.fieldRef(encType, {bin_suffix: '_mid'});
+        dims[encoding.fieldRef(encType, {bin_suffix: '_end'})] = encoding.fieldRef(encType, {bin_suffix: '_end'});
+      } else {
+        dims[encDef.name] = encoding.fieldRef(encType);
+      }
+
     }
   });
 
diff --git a/src/compiler/marks.js b/src/compiler/marks.js
index 8ec081013..47e516153 100644
--- a/src/compiler/marks.js
+++ b/src/compiler/marks.js
@@ -96,7 +96,10 @@ function bar_props(e, layout, style) {
   var p = {};
 
   // x's and width
-  if (e.isMeasure(X)) {
+  if (e.encDef(X).bin) {
+    p.x = {scale: X, field: e.fieldRef(X, {bin_suffix: '_start'}), offset: 1};
+    p.x2 = {scale: X, field: e.fieldRef(X, {bin_suffix: '_end'})};
+  } else if (e.isMeasure(X)) {
     p.x = {scale: X, field: e.fieldRef(X)};
     if (!e.has(Y) || e.isDimension(Y)) {
       p.x2 = {value: 0};
@@ -126,7 +129,10 @@ function bar_props(e, layout, style) {
   }
 
   // y's & height
-  if (e.isMeasure(Y)) {
+  if (e.encDef(Y).bin) {
+    p.y = {scale: Y, field: e.fieldRef(Y, {bin_suffix: '_start'})};
+    p.y2 = {scale: Y, field: e.fieldRef(Y, {bin_suffix: '_end'}), offset: 1};
+  } else if (e.isMeasure(Y)) {
     p.y = {scale: Y, field: e.fieldRef(Y)};
     p.y2 = {field: {group: 'height'}};
   } else {
@@ -168,14 +174,14 @@ function point_props(e, layout, style) {
 
   // x
   if (e.has(X)) {
-    p.x = {scale: X, field: e.fieldRef(X)};
+    p.x = {scale: X, field: e.fieldRef(X, {bin_suffix: '_mid'})};
   } else if (!e.has(X)) {
     p.x = {value: e.bandSize(X, layout.x.useSmallBand) / 2};
   }
 
   // y
   if (e.has(Y)) {
-    p.y = {scale: Y, field: e.fieldRef(Y)};
+    p.y = {scale: Y, field: e.fieldRef(Y, {bin_suffix: '_mid'})};
   } else if (!e.has(Y)) {
     p.y = {value: e.bandSize(Y, layout.y.useSmallBand) / 2};
   }
@@ -223,14 +229,14 @@ function line_props(e,layout, style) {
 
   // x
   if (e.has(X)) {
-    p.x = {scale: X, field: e.fieldRef(X)};
+    p.x = {scale: X, field: e.fieldRef(X, {bin_suffix: '_mid'})};
   } else if (!e.has(X)) {
     p.x = {value: 0};
   }
 
   // y
   if (e.has(Y)) {
-    p.y = {scale: Y, field: e.fieldRef(Y)};
+    p.y = {scale: Y, field: e.fieldRef(Y, {bin_suffix: '_mid'})};
   } else if (!e.has(Y)) {
     p.y = {field: {group: 'height'}};
   }
@@ -250,6 +256,7 @@ function line_props(e,layout, style) {
   return p;
 }
 
+// TODO(#694): optimize area's usage with bin
 function area_props(e, layout, style) {
   // jshint unused:false
   var p = {};
@@ -262,7 +269,7 @@ function area_props(e, layout, style) {
       p.orient = {value: 'horizontal'};
     }
   } else if (e.has(X)) {
-    p.x = {scale: X, field: e.fieldRef(X)};
+    p.x = {scale: X, field: e.fieldRef(X, {bin_suffix: '_mid'})};
   } else {
     p.x = {value: 0};
   }
@@ -272,7 +279,7 @@ function area_props(e, layout, style) {
     p.y = {scale: Y, field: e.fieldRef(Y)};
     p.y2 = {scale: Y, value: 0};
   } else if (e.has(Y)) {
-    p.y = {scale: Y, field: e.fieldRef(Y)};
+    p.y = {scale: Y, field: e.fieldRef(Y, {bin_suffix: '_mid'})};
   } else {
     p.y = {field: {group: 'height'}};
   }
@@ -295,7 +302,7 @@ function tick_props(e, layout, style) {
 
   // x
   if (e.has(X)) {
-    p.x = {scale: X, field: e.fieldRef(X)};
+    p.x = {scale: X, field: e.fieldRef(X, {bin_suffix: '_mid'})};
     if (e.isDimension(X)) {
       p.x.offset = -e.bandSize(X, layout.x.useSmallBand) / 3;
     }
@@ -305,7 +312,7 @@ function tick_props(e, layout, style) {
 
   // y
   if (e.has(Y)) {
-    p.y = {scale: Y, field: e.fieldRef(Y)};
+    p.y = {scale: Y, field: e.fieldRef(Y, {bin_suffix: '_mid'})};
     if (e.isDimension(Y)) {
       p.y.offset = -e.bandSize(Y, layout.y.useSmallBand) / 3;
     }
@@ -315,6 +322,7 @@ function tick_props(e, layout, style) {
 
   // width
   if (!e.has(X) || e.isDimension(X)) {
+    // TODO(#694): optimize tick's width for bin
     p.width = {value: e.bandSize(X, layout.y.useSmallBand) / 1.5};
   } else {
     p.width = {value: 1};
@@ -322,6 +330,7 @@ function tick_props(e, layout, style) {
 
   // height
   if (!e.has(Y) || e.isDimension(Y)) {
+    // TODO(#694): optimize tick's height for bin
     p.height = {value: e.bandSize(Y, layout.y.useSmallBand) / 1.5};
   } else {
     p.height = {value: 1};
@@ -346,14 +355,14 @@ function filled_point_props(shape) {
 
     // x
     if (e.has(X)) {
-      p.x = {scale: X, field: e.fieldRef(X)};
+      p.x = {scale: X, field: e.fieldRef(X, {bin_suffix: '_mid'})};
     } else if (!e.has(X)) {
       p.x = {value: e.bandSize(X, layout.x.useSmallBand) / 2};
     }
 
     // y
     if (e.has(Y)) {
-      p.y = {scale: Y, field: e.fieldRef(Y)};
+      p.y = {scale: Y, field: e.fieldRef(Y, {bin_suffix: '_mid'})};
     } else if (!e.has(Y)) {
       p.y = {value: e.bandSize(Y, layout.y.useSmallBand) / 2};
     }
@@ -388,7 +397,7 @@ function text_props(e, layout, style, stats) {
 
   // x
   if (e.has(X)) {
-    p.x = {scale: X, field: e.fieldRef(X)};
+    p.x = {scale: X, field: e.fieldRef(X, {bin_suffix: '_mid'})};
   } else if (!e.has(X)) {
     if (e.has(TEXT) && e.isType(TEXT, Q)) {
       p.x = {value: layout.cellWidth-5};
@@ -399,7 +408,7 @@ function text_props(e, layout, style, stats) {
 
   // y
   if (e.has(Y)) {
-    p.y = {scale: Y, field: e.fieldRef(Y)};
+    p.y = {scale: Y, field: e.fieldRef(Y, {bin_suffix: '_mid'})};
   } else if (!e.has(Y)) {
     p.y = {value: e.bandSize(Y, layout.y.useSmallBand) / 2};
   }
diff --git a/src/compiler/scale.js b/src/compiler/scale.js
index 89a2953a8..cd60042d5 100644
--- a/src/compiler/scale.js
+++ b/src/compiler/scale.js
@@ -3,8 +3,7 @@ require('../globals');
 var util = require('../util'),
   time = require('./time'),
   colorbrewer = require('colorbrewer'),
-  interpolate = require('d3-color').interpolateHsl,
-  schema = require('../schema/schema');
+  interpolate = require('d3-color').interpolateHsl;
 
 var scale = module.exports = {};
 
@@ -21,14 +20,20 @@ scale.defs = function(names, encoding, layout, stats, facet) {
 
     scaleDef.name = name;
     scaleDef.type = scale.type(name, encoding);
-    scaleDef.domain = scale.domain(encoding, name, scaleDef.type, stats, facet);
+    scaleDef.domain = scale.domain(encoding, name, scaleDef.type, facet);
 
-    // add `reverse` if applicable
+    // Add optional properties
     var reverse = scale.reverse(encoding, name);
     if (reverse) {
       scaleDef.reverse = reverse;
     }
 
+    var zero = scale.zero(encoding, name);
+    if (zero !== undefined) {
+      scaleDef.zero = zero;
+    }
+
+    // TODO split scale.range into methods for each properties
     scaleDef = scale.range(scaleDef, encoding, layout, stats);
 
     return (a.push(scaleDef), a);
@@ -44,14 +49,13 @@ scale.type = function(name, encoding) {
       return timeUnit ? time.scale.type(timeUnit, name) : 'time';
     case Q:
       if (encoding.bin(name)) {
-        // TODO: revise this
-        return name === COLOR ? 'linear' : 'ordinal';
+        return 'linear';
       }
       return encoding.scale(name).type;
   }
 };
 
-scale.domain = function (encoding, name, type, stats, facet) {
+scale.domain = function (encoding, name, type, facet) {
   var encDef = encoding.encDef(name);
 
   // special case for temporal scale
@@ -60,18 +64,6 @@ scale.domain = function (encoding, name, type, stats, facet) {
     if (range) return range;
   }
 
-  // For binned, produce fixed stepped domain.
-  // TODO(#614): this must be changed in vg2
-  if (encDef.bin) {
-
-    var fieldStat = stats[encDef.name],
-      bins = util.getbins(fieldStat, encDef.bin.maxbins || schema.MAXBINS_DEFAULT),
-      numbins = (bins.stop - bins.start) / bins.step;
-    return util.range(numbins).map(function(i) {
-      return bins.start + bins.step * i;
-    });
-  }
-
   // For stack, use STACKED data.
   var stack = encoding.stack();
   if (stack && name === stack.value) {
@@ -87,12 +79,20 @@ scale.domain = function (encoding, name, type, stats, facet) {
   var useRawDomain = scale._useRawDomain(encoding, name);
   var sort = scale.sort(encoding, name, type);
 
-  if (useRawDomain) {
+  if (useRawDomain) { // useRawDomain - only Q/T
     return {
       data: RAW,
       field: encoding.fieldRef(name, {noAggregate:true})
     };
-  } else if (sort) { // have sort
+  } else if (encDef.bin) { // bin -- need to merge both bin_start and bin_end
+    return {
+      data: encoding.dataTable(),
+      field: [
+        encoding.fieldRef(name, {bin_suffix:'_start'}),
+        encoding.fieldRef(name, {bin_suffix:'_end'})
+      ]
+    };
+  } else if (sort) { // have sort -- only for ordinal
     return {
       // If sort by aggregation of a specified sort field, we need to use RAW table,
       // so we can aggregate values for the scale independently from the main aggregation.
@@ -166,23 +166,38 @@ scale._useRawDomain = function (encoding, name) {
     );
 };
 
+// FIXME revise if we should produce undefined for shorter spec (and just use vega's default value.)
+// However, let's ignore it for now as it is unclear what is Vega's default value.
+scale.zero = function(encoding, name) {
+  var spec = encoding.scale(name);
+  var encDef = encoding.encDef(name);
+  var timeUnit = encDef.timeUnit;
+
+  if (spec.zero) {
+    return spec.zero; // return explicit value if defined
+  }
+
+  if (encoding.isType(name, T) && (!timeUnit || timeUnit === 'year')) { // FIXME revise this
+    // Returns false (undefined)  by default for time scale
+    return false;
+  }
+  if (encDef.bin) {
+    // Returns false (undefined) by default of bin
+    return false;
+  }
+  // if not bin / temporal, returns true by default
+  return name === X || name === Y || name === SIZE;
+};
 
 scale.range = function (scaleDef, encoding, layout, stats) {
-  var spec = encoding.scale(scaleDef.name),
-    encDef = encoding.encDef(scaleDef.name),
-    timeUnit = encDef.timeUnit;
+  var encDef = encoding.encDef(scaleDef.name);
+  var timeUnit = encDef.timeUnit;
 
   switch (scaleDef.name) {
     case X:
       scaleDef.range = layout.cellWidth ? [0, layout.cellWidth] : 'width';
       if (scaleDef.type === 'ordinal') {
         scaleDef.bandWidth = encoding.bandSize(X, layout.x.useSmallBand);
-      } else {
-        if (encoding.isType(scaleDef.name,T) && timeUnit === 'year') {
-          scaleDef.zero = false;
-        } else {
-          scaleDef.zero = spec.zero === undefined ? true : spec.zero;
-        }
       }
       scaleDef.round = true;
       if (scaleDef.type === 'time') {
@@ -199,11 +214,6 @@ scale.range = function (scaleDef, encoding, layout, stats) {
         scaleDef.bandWidth = encoding.bandSize(Y, layout.y.useSmallBand);
       } else {
         scaleDef.range = layout.cellHeight ? [layout.cellHeight, 0] : 'height';
-        if (encoding.isType(scaleDef.name,T) && timeUnit === 'year') {
-          scaleDef.zero = false;
-        } else {
-          scaleDef.zero = spec.zero === undefined ? true : spec.zero;
-        }
       }
 
       scaleDef.round = true;
@@ -236,14 +246,12 @@ scale.range = function (scaleDef, encoding, layout, stats) {
         scaleDef.range = [10, 0.8 * bandSize*bandSize];
       }
       scaleDef.round = true;
-      scaleDef.zero = false;
       break;
     case SHAPE:
       scaleDef.range = 'shapes';
       break;
     case COLOR:
       scaleDef.range = scale.color(scaleDef, encoding, stats);
-      if (scaleDef.type !== 'ordinal') scaleDef.zero = false;
       break;
     default:
       throw new Error('Unknown encoding name: '+ scaleDef.name);
diff --git a/src/compiler/time.js b/src/compiler/time.js
index 903cb011d..a917f42a8 100644
--- a/src/compiler/time.js
+++ b/src/compiler/time.js
@@ -135,6 +135,7 @@ time.scale.type = function(timeUnit, name) {
     return 'linear'; // time has order, so use interpolated ordinal color scale.
   }
 
+  // FIXME revise this -- should 'year' be linear too?
   return time.isOrdinalFn(timeUnit) || name === COL || name === ROW ? 'ordinal' : 'linear';
 };
 
diff --git a/src/encdef.js b/src/encdef.js
index b60b25971..298f38eed 100644
--- a/src/encdef.js
+++ b/src/encdef.js
@@ -20,6 +20,7 @@ var vlfield = module.exports = {};
  *   opt.datum - include 'datum.'
  *   opt.fn - replace fn with custom function prefix
  *   opt.prefn - prepend fn with custom function prefix
+ *   opt.bin_suffix - append suffix to the field ref for bin (default='_start')
 
  * @return {[type]}       [description]
  */
@@ -34,7 +35,8 @@ vlfield.fieldRef = function(field, opt) {
   } else if (opt.fn) {
     return f + opt.fn + '_' + name;
   } else if (!opt.nofn && field.bin) {
-    return f + 'bin_' + name;
+    var bin_suffix = opt.bin_suffix || '_start';
+    return f + 'bin_' + name + bin_suffix;
   } else if (!opt.nofn && !opt.noAggregate && field.aggregate) {
     return f + field.aggregate + '_' + name;
   } else if (!opt.nofn && field.timeUnit) {
@@ -110,7 +112,7 @@ var isTypes = vlfield.isTypes = function (fieldDef, types) {
  * However, YEAR(T), YEARMONTH(T) use time scale, not ordinal but are dimensions too.
  */
 vlfield.isOrdinalScale = function(field) {
-  return  isTypes(field, [N, O]) || field.bin ||
+  return  isTypes(field, [N, O]) ||
     ( isType(field, T) && field.timeUnit && time.isOrdinalFn(field.timeUnit) );
 };
 
diff --git a/src/schema/schema.js b/src/schema/schema.js
index 578eea232..44e81a12d 100644
--- a/src/schema/schema.js
+++ b/src/schema/schema.js
@@ -109,7 +109,7 @@ var typicalField = merge(clone(schema.field), {
         zero: {
           type: 'boolean',
           description: 'Include zero',
-          default: true,
+          default: undefined,
           supportedTypes: toMap([Q, T])
         },
 


Test patch:
diff --git a/test/compiler/data.spec.js b/test/compiler/data.spec.js
index 74cef1ebe..168f3bad5 100644
--- a/test/compiler/data.spec.js
+++ b/test/compiler/data.spec.js
@@ -120,7 +120,7 @@ describe('data.raw', function() {
         expect(transform[0]).to.eql({
           type: 'bin',
           field: 'Acceleration',
-          output: {start: 'bin_Acceleration'},
+          output: {start: 'bin_Acceleration_start', end: 'bin_Acceleration_end'},
           maxbins: 15
         });
       });
@@ -186,7 +186,8 @@ describe('data.raw', function() {
       expect(transform[0].type).to.eql('filter');
       expect(transform[1].type).to.eql('formula');
       expect(transform[2].type).to.eql('bin');
-      expect(transform[3].type).to.eql('filter');
+      expect(transform[3].type).to.eql('formula'); // formula for bin_mid
+      expect(transform[4].type).to.eql('filter');
     });
 
   });
diff --git a/test/compiler/marks.spec.js b/test/compiler/marks.spec.js
index 36b8e840f..3921a926c 100644
--- a/test/compiler/marks.spec.js
+++ b/test/compiler/marks.spec.js
@@ -132,7 +132,7 @@ describe('compile.marks', function() {
             e = Encoding.fromSpec(f),
             def = marks.point.prop(e, mockLayout, {});
         it('should have scale for shape', function () {
-          expect(def.shape).to.eql({scale: SHAPE, field: "bin_yield"});
+          expect(def.shape).to.eql({scale: SHAPE, field: "bin_yield_start"});
         });
       });
     });
diff --git a/test/compiler/scale.spec.js b/test/compiler/scale.spec.js
index 87cc6d149..75ff8cb32 100644
--- a/test/compiler/scale.spec.js
+++ b/test/compiler/scale.spec.js
@@ -64,7 +64,10 @@ describe('vl.compile.scale', function() {
             }
           }), 'y', 'ordinal', {origin: {min: -5, max:48}}, {});
 
-          expect(domain).to.eql([-5, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45]);
+          expect(domain).to.eql({
+            data: RAW,
+            field: ['bin_origin_start', 'bin_origin_end']
+          });
         });
 
       it('should return the raw domain if useRawDomain is true for non-bin, non-sum Q',
diff --git a/test/compiler/stack.spec.js b/test/compiler/stack.spec.js
index ce85e19cb..a3e6db2d9 100644
--- a/test/compiler/stack.spec.js
+++ b/test/compiler/stack.spec.js
@@ -38,15 +38,15 @@ describe('vl.compile.stack()', function () {
       var tableAggrTransform = tableData[0].transform.filter(function(t) {
         return t.type === 'aggregate';
       })[0];
-      expect(tableAggrTransform.groupby.length).to.equal(2);
-      expect(tableAggrTransform.groupby.indexOf('bin_Cost__Total_$')).to.gt(-1);
+      expect(tableAggrTransform.groupby.length).to.equal(4);
+      expect(tableAggrTransform.groupby.indexOf('bin_Cost__Total_$_start')).to.gt(-1);
 
       var stackedData = vgSpec.data.filter(function(data) {
         return data.name === 'stacked';
       });
       expect(stackedData.length).to.equal(1);
       var stackedAggrTransform = stackedData[0].transform[0];
-      expect(stackedAggrTransform.groupby[0]).to.equal('bin_Cost__Total_$');
+      expect(stackedAggrTransform.groupby[0]).to.equal('bin_Cost__Total_$_start');
     });
   });
 
@@ -63,8 +63,8 @@ describe('vl.compile.stack()', function () {
       var tableAggrTransform = tableData[0].transform.filter(function(t) {
         return t.type === 'aggregate';
       })[0];
-      expect(tableAggrTransform.groupby.length).to.equal(2);
-      expect(tableAggrTransform.groupby.indexOf('bin_Cost__Total_$')).to.gt(-1);
+      expect(tableAggrTransform.groupby.length).to.equal(4);
+      expect(tableAggrTransform.groupby.indexOf('bin_Cost__Total_$_start')).to.gt(-1);
 
       var stackedData = vgSpec.data.filter(function(data) {
         return data.name === 'stacked';
@@ -72,7 +72,7 @@ describe('vl.compile.stack()', function () {
 
       expect(stackedData.length).to.equal(1);
       var stackedAggrTransform = stackedData[0].transform[0];
-      expect(stackedAggrTransform.groupby[0]).to.equal('bin_Cost__Total_$');
+      expect(stackedAggrTransform.groupby[0]).to.equal('bin_Cost__Total_$_start');
     });
   });