Repository: vega/vega-lite
Pull request #701 (2015-10-31 15:57:35)

Major scale revision
# Syntax
- Add all of vega's scale properties that should be supported #181 
- add scale property to `row`, `column` and `shape`
- remove `encDef.band`
  - `band.size` => `scale.bandWidth`
  - `band.padding` => `scale.padding`

# Internal
- add `encoding.padding()` helper
- Add separate methods for 'bandWidth', 'nice', 'padding', 'points', 'range', 'reverse', 'round', ‘zero’
- revise default valus for `nice`, `points` and `zero`
- revise `useRawDomain` to support only aggregate functions that produces values ranging in the domain of the source data (`'mean'`, `'average'`, `'stdev'`, `'stdevp'`, `'median'`, `'q1'`, `'q3'`, `'min'`, `'max'`)
- rename
  - `bandSize` => `bandWidth`
  - `scale.sort()` => `scale.domain.sort()`



Patch:
diff --git a/docs/Encoding-Mapping.md b/docs/Encoding-Mapping.md
index 7dfc47969..3fb8bc942 100644
--- a/docs/Encoding-Mapping.md
+++ b/docs/Encoding-Mapping.md
@@ -78,29 +78,32 @@ If none of the specified encoding channel contains aggregation, no additional da
 
 Vega-lite's `scale` object supports the following Vega scale properties:
 
+- [Common Scale Properties](https://github.com/vega/vega/wiki/Scales#common-scale-properties)__<sup>1</sup>__:
+`type`__<sup>2</sup>__, `domain`, `range`, and `round`.
 
-- [Vega Common Scale Properties](https://github.com/vega/vega/wiki/Scales#common-scale-properties)__<sup>2</sup>__: `type`__<sup>1,2</sup>__.
+- [Quantitative Scale Properties](https://github.com/vega/vega/wiki/Scales#quantitative-scale-properties):
+`clamp`, `exponent`, `nice`, and `zero`.
 
+- [Time Scale Properties](https://github.com/vega/vega/wiki/Scales#time-scale-properties):
+`clamp` and `nice`.
 
-- [Vega Quantitative Scale Properties](https://github.com/vega/vega/wiki/Scales#quantitative-scale-properties)__<sup>3</sup>__: `nice` and `zero`
-
+- [Ordinal Scale Properties](https://github.com/vega/vega/wiki/Scales#ordinal-scale-properties):
+`bandWidth`, `padding` and `points`.
 
 See [Vega's documentation](https://github.com/vega/vega/wiki/Scales#common-scale-properties) for more information about these properties.
 
-
 Moreover, Vega-lite has the following additional scale properties:
 
 | Property      | Type          | Description    |
 | :------------ |:-------------:| :------------- |
-| useRawDomain  | Boolean       | Use the raw data instead of summary data for scale domain (Only for aggregated field).  Note that this option does not work with sum or count aggregate as they could have a substantially larger scale range. |
+| useRawDomain  | Boolean       | Use the raw data instead of summary data for scale domain (Only for aggregated field).  This property only works with aggregate functions that produce values ranging in the domain of the source data (`'mean'`, `'average'`, `'stdev'`, `'stdevp'`, `'median'`, `'q1'`, `'q3'`, `'min'`, `'max'`).  Otherwise, this property is ignored.  If the scale's `domain` is specified, this property is also ignored. |
 
+__TODO: document default behavior for each properties__
 
 __<sup>1</sup>__ `reverse` is excluded from Vega-lite's `scale` to avoid conflicting with `sort` property.  Please use `sort='descending'` instead.
 
-__<sup>2</sup>__ __In Roadmap__:
-Other applicable Vega scale properties will be added. [#181](../../issues/181)
 
-__<sup>3</sup>__
+__<sup>2</sup>__
 Vega-lite automatically determines scale's `type` based on the field's data type.
 By default, scales of nominal and ordinal fields are ordinal scales.
 Scales of time fields are time scales if time unit conversion is not applied.
@@ -157,8 +160,8 @@ For `color`, `shape`, `size` and `detail`, this determines the layer order
 
 `sort` property can be specified for sorting the field's values in two ways:
 
-1. (Supported by all types of fields) as __String__ with the following values: 
-    - `'ascending'` –  the field is sort by the field's value in ascending order.  This is the default value when `sort` is not specified. 
+1. (Supported by all types of fields) as __String__ with the following values:
+    - `'ascending'` –  the field is sort by the field's value in ascending order.  This is the default value when `sort` is not specified.
     - `'descending'` –  the field is sort by the field's value in descending order.
     - `'unsorted`' – The field is not sorted. (This is equivalent to specifying `sort:false` in [Vega's scales](https://github.com/vega/vega/wiki/Scales).)
 
diff --git a/src/Encoding.js b/src/Encoding.js
index 547ce24d5..3fe614c2a 100644
--- a/src/Encoding.js
+++ b/src/Encoding.js
@@ -128,15 +128,31 @@ module.exports = (function() {
     return this._enc[et].axis || {};
   };
 
-  proto.bandSize = function(encType, useSmallBand) {
+  proto.bandWidth = function(encType, useSmallBand) {
+    if (this.encDef(encType).scale.bandWidth !== undefined) {
+      // explicit value
+      return this.encDef(encType).scale.bandWidth;
+    }
+
+    // If not specified, draw value from config.
+
     useSmallBand = useSmallBand ||
       //isBandInSmallMultiples
       (encType === Y && this.has(ROW) && this.has(Y)) ||
       (encType === X && this.has(COL) && this.has(X));
 
-    // if band.size is explicitly specified, follow the specification, otherwise draw value from config.
-    return this.encDef(encType).band.size ||
-      this.config(useSmallBand ? 'smallBandSize' : 'largeBandSize');
+    return this.config(useSmallBand ? 'smallBandWidth' : 'largeBandWidth');
+  };
+
+  proto.padding = function(encType) {
+    if (this.encDef(encType).scale.padding !== undefined) {
+      // explicit value
+      return this.encDef(encType).scale.padding;
+    }
+    if (encType === ROW || encType === COL) {
+      return this.config('cellPadding');
+    }
+    return this.config('padding');
   };
 
   // returns false if binning is disabled, otherwise an object with binning properties
diff --git a/src/compiler/layout.js b/src/compiler/layout.js
index ce817931a..e783df125 100644
--- a/src/compiler/layout.js
+++ b/src/compiler/layout.js
@@ -41,7 +41,7 @@ function box(encoding, stats) {
   if (hasX) {
     if (encoding.isOrdinalScale(X)) {
       // for ordinal, hasCol or not doesn't matter -- we scale based on cardinality
-      cellWidth = (xCardinality + encoding.encDef(X).band.padding) * encoding.bandSize(X, useSmallBand);
+      cellWidth = (xCardinality + encoding.padding(X)) * encoding.bandWidth(X, useSmallBand);
     } else {
       cellWidth = hasCol || hasRow ? encoding.encDef(COL).width :  encoding.config('singleWidth');
     }
@@ -49,7 +49,7 @@ function box(encoding, stats) {
     if (marktype === TEXT) {
       cellWidth = encoding.config('textCellWidth');
     } else {
-      cellWidth = encoding.bandSize(X);
+      cellWidth = encoding.bandWidth(X);
     }
   }
 
@@ -57,12 +57,12 @@ function box(encoding, stats) {
   if (hasY) {
     if (encoding.isOrdinalScale(Y)) {
       // for ordinal, hasCol or not doesn't matter -- we scale based on cardinality
-      cellHeight = (yCardinality + encoding.encDef(Y).band.padding) * encoding.bandSize(Y, useSmallBand);
+      cellHeight = (yCardinality + encoding.padding(Y)) * encoding.bandWidth(Y, useSmallBand);
     } else {
       cellHeight = hasCol || hasRow ? encoding.encDef(ROW).height :  encoding.config('singleHeight');
     }
   } else {
-    cellHeight = encoding.bandSize(Y);
+    cellHeight = encoding.bandWidth(Y);
   }
 
   // Cell bands use rangeBands(). There are n-1 padding.  Outerpadding = 0 for cells
diff --git a/src/compiler/marks.js b/src/compiler/marks.js
index 47e516153..de2d05139 100644
--- a/src/compiler/marks.js
+++ b/src/compiler/marks.js
@@ -119,7 +119,7 @@ function bar_props(e, layout, style) {
         p.width = {scale: SIZE, field: e.fieldRef(SIZE)};
       } else {
         p.width = {
-          value: e.bandSize(X, layout.x.useSmallBand),
+          value: e.bandWidth(X, layout.x.useSmallBand),
           offset: -1
         };
       }
@@ -149,7 +149,7 @@ function bar_props(e, layout, style) {
       p.height = {scale: SIZE, field: e.fieldRef(SIZE)};
     } else {
       p.height = {
-        value: e.bandSize(Y, layout.y.useSmallBand),
+        value: e.bandWidth(Y, layout.y.useSmallBand),
         offset: -1
       };
     }
@@ -176,14 +176,14 @@ function point_props(e, layout, style) {
   if (e.has(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};
+    p.x = {value: e.bandWidth(X, layout.x.useSmallBand) / 2};
   }
 
   // y
   if (e.has(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};
+    p.y = {value: e.bandWidth(Y, layout.y.useSmallBand) / 2};
   }
 
   // size
@@ -304,7 +304,7 @@ function tick_props(e, layout, style) {
   if (e.has(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;
+      p.x.offset = -e.bandWidth(X, layout.x.useSmallBand) / 3;
     }
   } else if (!e.has(X)) {
     p.x = {value: 0};
@@ -314,7 +314,7 @@ function tick_props(e, layout, style) {
   if (e.has(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;
+      p.y.offset = -e.bandWidth(Y, layout.y.useSmallBand) / 3;
     }
   } else if (!e.has(Y)) {
     p.y = {value: 0};
@@ -323,7 +323,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};
+    p.width = {value: e.bandWidth(X, layout.y.useSmallBand) / 1.5};
   } else {
     p.width = {value: 1};
   }
@@ -331,7 +331,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};
+    p.height = {value: e.bandWidth(Y, layout.y.useSmallBand) / 1.5};
   } else {
     p.height = {value: 1};
   }
@@ -357,14 +357,14 @@ function filled_point_props(shape) {
     if (e.has(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};
+      p.x = {value: e.bandWidth(X, layout.x.useSmallBand) / 2};
     }
 
     // y
     if (e.has(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};
+      p.y = {value: e.bandWidth(Y, layout.y.useSmallBand) / 2};
     }
 
     // size
@@ -402,7 +402,7 @@ function text_props(e, layout, style, stats) {
     if (e.has(TEXT) && e.isType(TEXT, Q)) {
       p.x = {value: layout.cellWidth-5};
     } else {
-      p.x = {value: e.bandSize(X, layout.x.useSmallBand) / 2};
+      p.x = {value: e.bandWidth(X, layout.x.useSmallBand) / 2};
     }
   }
 
@@ -410,7 +410,7 @@ function text_props(e, layout, style, stats) {
   if (e.has(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};
+    p.y = {value: e.bandWidth(Y, layout.y.useSmallBand) / 2};
   }
 
   // size
diff --git a/src/compiler/scale.js b/src/compiler/scale.js
index 0b5e31647..a538801ff 100644
--- a/src/compiler/scale.js
+++ b/src/compiler/scale.js
@@ -19,22 +19,22 @@ scale.defs = function(names, encoding, layout, stats, facet) {
     var scaleDef = {};
 
     scaleDef.name = name;
-    scaleDef.type = scale.type(name, encoding);
-    scaleDef.domain = scale.domain(encoding, name, scaleDef.type, facet);
+    var type = scaleDef.type = scale.type(name, encoding);
+    scaleDef.domain = scale.domain(encoding, name, type, facet);
 
     // 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);
+    var properties = ['range', 'reverse', 'round',
+        'clamp', 'nice', // quantitative / time
+        'exponent', 'zero', // quantitative
+        'bandWidth', 'padding', 'points' // ordinal
+      ];
+
+    properties.forEach(function(property) {
+      var value = scale[property](encoding, name, type, layout, stats);
+      if (value !== undefined) {
+        scaleDef[property] = value;
+      }
+    });
 
     return (a.push(scaleDef), a);
   }, []);
@@ -43,7 +43,8 @@ scale.defs = function(names, encoding, layout, stats, facet) {
 scale.type = function(name, encoding) {
   switch (encoding.type(name)) {
     case N: //fall through
-    case O: return 'ordinal';
+    case O:
+      return 'ordinal';
     case T:
       var timeUnit = encoding.encDef(name).timeUnit;
       return timeUnit ? time.scale.type(timeUnit, name) : 'time';
@@ -58,6 +59,10 @@ scale.type = function(name, encoding) {
 scale.domain = function (encoding, name, type, facet) {
   var encDef = encoding.encDef(name);
 
+  if (encDef.scale.domain) { // explicit value
+    return encDef.scale.domain;
+  }
+
   // special case for temporal scale
   if (encoding.isType(name, T)) {
     var range = time.scale.domain(encDef.timeUnit, name);
@@ -77,7 +82,7 @@ scale.domain = function (encoding, name, type, facet) {
   }
 
   var useRawDomain = scale._useRawDomain(encoding, name);
-  var sort = scale.sort(encoding, name, type);
+  var sort = scale.domain.sort(encoding, name, type);
 
   if (useRawDomain) { // useRawDomain - only Q/T
     return {
@@ -108,7 +113,7 @@ scale.domain = function (encoding, name, type, facet) {
   }
 };
 
-scale.sort = function(encoding, name, type) {
+scale.domain.sort = function(encoding, name, type) {
   var sort = encoding.encDef(name).sort;
   if (sort === 'ascending' || sort === 'descending') {
     return true;
@@ -126,9 +131,11 @@ scale.sort = function(encoding, name, type) {
 
 scale.reverse = function(encoding, name) {
   var sort = encoding.encDef(name).sort;
-  return sort && (sort === 'descending' || (sort.order === 'descending'));
+  return sort && (sort === 'descending' || (sort.order === 'descending')) ? true : undefined;
 };
 
+var sharedDomainAggregate = ['mean', 'average', 'stdev', 'stdevp', 'median', 'q1', 'q3', 'min', 'max'];
+
 /**
  * Determine if useRawDomain should be activated for this scale.
  * @return {Boolean} Returns true if all of the following conditons applies:
@@ -147,17 +154,17 @@ scale._useRawDomain = function (encoding, name) {
   var useRawDomainEnabled = scaleUseRawDomain !== undefined ?
       scaleUseRawDomain : encoding.config('useRawDomain');
 
-  var notCountOrSum = !encDef.aggregate ||
-    (encDef.aggregate !=='count' && encDef.aggregate !== 'sum');
-
   return  useRawDomainEnabled &&
-    notCountOrSum && (
-      // Q always uses quantitative scale except when it's binned and thus uses ordinal scale.
-      (
-        encoding.isType(name, Q) &&
-        !encDef.bin // TODO(#614): this must be changed once bin is reimplemented
-      ) ||
-      // TODO: revise this
+    // only applied to aggregate table
+    encDef.aggregate &&
+    // only activated if used with aggregate functions that produces values ranging in the domain of the source data
+    sharedDomainAggregate.indexOf(encDef.aggregate) >= 0 &&
+    (
+      // Q always uses quantitative scale except when it's binned.
+      // Binned field has similar values in both the source table and the summary table
+      // but the summary table has fewer values, therefore binned fields draw
+      // domain values from the summary table.
+      (encoding.isType(name, Q) && !encDef.bin) ||
       // T uses non-ordinal scale when there's no unit or when the unit is not ordinal.
       (
         encoding.isType(name, T) &&
@@ -166,116 +173,167 @@ 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
+scale.bandWidth = function(encoding, name, type, layout) {
+  // TODO: eliminate layout
+
+  switch (name) {
+    case X: /* fall through */
+    case Y:
+      if (type === 'ordinal') {
+        return encoding.bandWidth(name, layout[name].useSmallBand);
+      }
+      break;
+    case ROW: // support only ordinal
+      return layout.cellHeight;
+    case COL: // support only ordinal
+      return layout.cellWidth;
   }
+  return undefined;
+};
 
-  if (encoding.isType(name, T) && (!timeUnit || timeUnit === 'year')) { // FIXME revise this
-    // Returns false (undefined)  by default for time scale
-    return false;
+scale.clamp = function(encoding, name) {
+  // only return value if explicit value is specified.
+  return encoding.encDef(name).scale.clamp;
+};
+
+scale.exponent = function(encoding, name) {
+  // only return value if explicit value is specified.
+  return encoding.encDef(name).scale.exponent;
+};
+
+scale.nice = function(encoding, name, type) {
+  if (encoding.encDef(name).scale.nice !== undefined) {
+    // explicit value
+    return encoding.encDef(name).scale.nice;
   }
-  if (encDef.bin) {
-    // Returns false (undefined) by default of bin
-    return false;
+
+  switch (name) {
+    case X: /* fall through */
+    case Y:
+      if (type === 'time' || type === 'ordinal') {
+        return undefined;
+      }
+      return true;
+
+    case ROW: /* fall through */
+    case COL:
+      return true;
   }
-  // if not bin / temporal, returns true for X and Y encoding.
-  return name === X || name === Y;
+  return undefined;
+};
+
+scale.padding = function(encoding, name, type) {
+  if (type === 'ordinal') {
+    // Both explicit and non-explicit values are handled by the helper method.
+    return encoding.padding(name);
+  }
+  return undefined;
+};
+
+scale.points = function(encoding, name, type) {
+  if (type === 'ordinal') {
+    if (encoding.encDef(name).scale.points !== undefined) {
+      // explicit value
+      return encoding.encDef(name).scale.points;
+    }
+
+    switch (name) {
+      case X:
+      case Y:
+        return true;
+    }
+  }
+  return undefined;
 };
 
-scale.range = function (scaleDef, encoding, layout, stats) {
-  var encDef = encoding.encDef(scaleDef.name);
-  var timeUnit = encDef.timeUnit;
 
-  switch (scaleDef.name) {
+scale.range = function (encoding, name, type, layout, stats) {
+  var encDef = encoding.encDef(name);
+
+  if (encDef.scale.range) { // explicit value
+    return encDef.scale.range;
+  }
+
+  switch (name) {
     case X:
-      scaleDef.range = layout.cellWidth ? [0, layout.cellWidth] : 'width';
-      if (scaleDef.type === 'ordinal') {
-        scaleDef.bandWidth = encoding.bandSize(X, layout.x.useSmallBand);
-      }
-      scaleDef.round = true;
-      if (scaleDef.type === 'time') {
-        scaleDef.nice = timeUnit || encoding.config('timeScaleNice');
-      }else {
-        scaleDef.nice = true;
-      }
-      break;
+      return layout.cellWidth ? [0, layout.cellWidth] : 'width';
     case Y:
-      if (scaleDef.type === 'ordinal') {
-        scaleDef.range = layout.cellHeight ?
+      if (type === 'ordinal') {
+        return layout.cellHeight ?
           (encDef.bin ? [layout.cellHeight, 0] : [0, layout.cellHeight]) :
           'height';
-        scaleDef.bandWidth = encoding.bandSize(Y, layout.y.useSmallBand);
-      } else {
-        scaleDef.range = layout.cellHeight ? [layout.cellHeight, 0] : 'height';
-      }
-
-      scaleDef.round = true;
-
-      if (scaleDef.type === 'time') {
-        scaleDef.nice = timeUnit || encoding.config('timeScaleNice');
-      }else {
-        scaleDef.nice = true;
       }
-      break;
-    case ROW: // support only ordinal
-      scaleDef.bandWidth = layout.cellHeight;
-      scaleDef.round = true;
-      scaleDef.nice = true;
-      break;
-    case COL: // support only ordinal
-      scaleDef.bandWidth = layout.cellWidth;
-      scaleDef.round = true;
-      scaleDef.nice = true;
-      break;
+      return layout.cellHeight ? [layout.cellHeight, 0] : 'height';
     case SIZE:
       if (encoding.is('bar')) {
         // FIXME this is definitely incorrect
         // but let's fix it later since bar size is a bad encoding anyway
-        scaleDef.range = [3, Math.max(encoding.bandSize(X), encoding.bandSize(Y))];
+        return [3, Math.max(encoding.bandWidth(X), encoding.bandWidth(Y))];
       } else if (encoding.is(TEXT)) {
-        scaleDef.range = [8, 40];
-      } else { //point
-        var bandSize = Math.min(encoding.bandSize(X), encoding.bandSize(Y)) - 1;
-        scaleDef.range = [10, 0.8 * bandSize*bandSize];
+        return [8, 40];
       }
-      scaleDef.round = true;
-      break;
+      // else -- point
+      var bandWidth = Math.min(encoding.bandWidth(X), encoding.bandWidth(Y)) - 1;
+      return [10, 0.8 * bandWidth*bandWidth];
     case SHAPE:
-      scaleDef.range = 'shapes';
-      break;
+      return 'shapes';
     case COLOR:
-      scaleDef.range = scale.color(scaleDef, encoding, stats);
-      break;
-    default:
-      throw new Error('Unknown encoding name: '+ scaleDef.name);
+      return scale.color(encoding, name, type, stats);
+  }
+
+  return undefined;
+};
+
+scale.round = function(encoding, name) {
+  if (encoding.encDef(name).scale.round !== undefined) {
+    return encoding.encDef(name).scale.round;
   }
 
-  // FIXME(kanitw): Jul 29, 2015 - consolidate this with above
-  switch (scaleDef.name) {
+  // FIXME: revise if round is already the default value
+  switch (name) {
+    case X: /* fall through */
+    case Y:
     case ROW:
     case COL:
-      scaleDef.padding = encoding.config('cellPadding');
-      scaleDef.outerPadding = 0;
-      break;
-    case X:
-    case Y:
-      if (scaleDef.type === 'ordinal') { //&& !s.bandWidth
-        scaleDef.points = true;
-        scaleDef.padding = encoding.encDef(scaleDef.name).band.padding;
-      }
+    case SIZE:
+      return true;
   }
+  return undefined;
+};
+
+scale.zero = function(encoding, name) {
+  var encDef = encoding.encDef(name);
+  var timeUnit = encDef.timeUnit;
 
-  return scaleDef;
+  if (encDef.scale.zero !== undefined) {
+    // explicit value
+    return encDef.scale.zero;
+  }
+
+  if (encoding.isType(name, T)) {
+    if (timeUnit === 'year') {
+      // year is using linear scale, but should not include zero
+      return false;
+    }
+    // If there is no timeUnit or the timeUnit uses ordinal scale,
+    // zero property is ignored by vega so we should not generate them any way
+    return undefined;
+  }
+  if (encDef.bin) {
+    // Returns false (undefined) by default of bin
+    return false;
+  }
+
+  return name === X || name === Y ?
+    // if not bin / temporal, returns undefined for X and Y encoding
+    // since zero is true by default in vega for linear scale
+    undefined :
+    false;
 };
 
-scale.color = function(s, encoding, stats) {
+
+scale.color = function(encoding, name, scaleType, stats) {
   var colorScale = encoding.scale(COLOR),
     range = colorScale.range,
     cardinality = encoding.cardinality(COLOR, stats),
@@ -285,7 +343,7 @@ scale.color = function(s, encoding, stats) {
     var ordinalPalette = colorScale.ordinalPalette,
       quantitativeRange = colorScale.quantitativeRange;
 
-    if (s.type === 'ordinal') {
+    if (scaleType === 'ordinal') {
       if (type === N) {
         // use categorical color scale
         if (cardinality <= 10) {
diff --git a/src/schema/schema.js b/src/schema/schema.js
index 44e81a12d..3881d6c5b 100644
--- a/src/schema/schema.js
+++ b/src/schema/schema.js
@@ -28,7 +28,7 @@ schema.aggregate = {
     Q: VALID_AGG_OPS,
     O: ['median','min','max'],
     N: [],
-    T: ['mean', 'median', 'min', 'max'],
+    T: ['mean', 'median', 'min', 'max'], // TODO: revise what should time support
     '': ['count']
   },
   supportedTypes: toMap([Q, N, O, T, ''])
@@ -84,6 +84,105 @@ var bin = {
   supportedTypes: toMap([Q]) // TODO: add O after finishing #81
 };
 
+var scale = {
+  type: 'object',
+  // TODO: refer to Vega's scale schema
+  properties: {
+    /* Common Scale Properties */
+    type: schema.scale_type,
+    domain: {
+      default: undefined,
+      type: ['array', 'object'],
+      description: 'The domain of the scale, representing the set of data values. For quantitative data, this can take the form of a two-element array with minimum and maximum values. For ordinal/categorical data, this may be an array of valid input values. The domain may also be specified by a reference to a data source.'
+    },
+    range: {
+      default: undefined,
+      type: ['array', 'object'],
+      description: 'The range of the scale, representing the set of visual values. For numeric values, the range can take the form of a two-element array with minimum and maximum values. For ordinal or quantized data, the range may by an array of desired output values, which are mapped to elements in the specified domain. For ordinal scales only, the range can be defined using a DataRef: the range values are then drawn dynamically from a backing data set.'
+    },
+    round: {
+      default: undefined, // TODO: revise default
+      type: 'boolean',
+      description: 'If true, rounds numeric output values to integers. This can be helpful for snapping to the pixel grid.'
+    }
+  }
+};
+
+var ordinalScaleMixin = {
+  properties: {
+    bandWidth: {
+      type: 'integer',
+      minimum: 0,
+      default: undefined
+    },
+    /* Ordinal Scale Properties */
+    padding: {
+      type: 'number',
+      default: undefined,
+      description: 'Applies spacing among ordinal elements in the scale range. The actual effect depends on how the scale is configured. If the __points__ parameter is `true`, the padding value is interpreted as a multiple of the spacing between points. A reasonable value is 1.0, such that the first and last point will be offset from the minimum and maximum value by half the distance between points. Otherwise, padding is typically in the range [0, 1] and corresponds to the fraction of space in the range interval to allocate to padding. A value of 0.5 means that the range band width will be equal to the padding width. For more, see the [D3 ordinal scale documentation](https://github.com/mbostock/d3/wiki/Ordinal-Scales).'
+        },
+    points: {
+      type: 'boolean',
+      default: undefined,
+      description: 'If true, distributes the ordinal values over a quantitative range at uniformly spaced points. The spacing of the points can be adjusted using the padding property. If false, the ordinal scale will construct evenly-spaced bands, rather than points.'
+    }
+  }
+};
+
+var typicalScaleMixin = {
+  properties: {
+    /* Quantitative and temporal Scale Properties */
+    clamp: {
+      type: 'boolean',
+      default: true,
+      description: 'If true, values that exceed the data domain are clamped to either the minimum or maximum range value'
+    },
+    nice: {
+      default: undefined,
+      oneOf: [
+        {
+          type: 'boolean',
+          description: 'If true, modifies the scale domain to use a more human-friendly number range (e.g., 7 instead of 6.96).'
+        },{
+      type: 'string',
+      enum: ['second', 'minute', 'hour', 'day', 'week', 'month', 'year'],
+          description: 'If specified, modifies the scale domain to use a more human-friendly value range. For time and utc scale types only, the nice value should be a string indicating the desired time interval; legal values are "second", "minute", "hour", "day", "week", "month", or "year".'
+        }
+      ],
+      // FIXME this part might break polestar
+      supportedTypes: toMap([Q, T]),
+      description: ''
+    },
+
+    /* Quantitative Scale Properties */
+    exponent: {
+      type: 'number',
+      default: undefined,
+      description: 'Sets the exponent of the scale transformation. For pow scale types only, otherwise ignored.'
+    },
+    zero: {
+      type: 'boolean',
+      description: 'If true, ensures that a zero baseline value is included in the scale domain. This option is ignored for non-quantitative scales.',
+      default: undefined,
+      supportedTypes: toMap([Q, T])
+    },
+
+    /* Vega-lite only Properties */
+    useRawDomain: {
+      type: 'boolean',
+      default: undefined,
+      description: 'Uses the raw data range as scale domain instead of ' +
+                   'aggregated data for aggregate axis. ' +
+                   'This option does not work with sum or count aggregate' +
+                   'as they might have a substantially larger scale range.' +
+                   'By default, use value from config.useRawDomain.'
+    }
+  }
+};
+
+var ordinalOnlyScale = merge(clone(scale), ordinalScaleMixin);
+var typicalScale = merge(clone(scale), ordinalScaleMixin, typicalScaleMixin);
+
 var typicalField = merge(clone(schema.field), {
   type: 'object',
   properties: {
@@ -94,37 +193,7 @@ var typicalField = merge(clone(schema.field), {
     aggregate: schema.aggregate,
     timeUnit: schema.timeUnit,
     bin: bin,
-    scale: {
-      type: 'object',
-      properties: {
-        /* Common Scale Properties */
-        type: schema.scale_type,
-
-        /* Quantitative Scale Properties */
-        nice: {
-          type: 'string',
-          enum: ['second', 'minute', 'hour', 'day', 'week', 'month', 'year'],
-          supportedTypes: toMap([T])
-        },
-        zero: {
-          type: 'boolean',
-          description: 'Include zero',
-          default: undefined,
-          supportedTypes: toMap([Q, T])
-        },
-
-        /* Vega-lite only Properties */
-        useRawDomain: {
-          type: 'boolean',
-          default: undefined,
-          description: 'Use the raw data range as scale domain instead of ' +
-                       'aggregated data for aggregate axis. ' +
-                       'This option does not work with sum or count aggregate' +
-                       'as they might have a substantially larger scale range.' +
-                       'By default, use value from config.useRawDomain.'
-        }
-      }
-    }
+    scale: typicalScale
   }
 });
 
@@ -144,7 +213,8 @@ var onlyOrdinalField = merge(clone(schema.field), {
       type: 'string',
       enum: ['count'],
       supportedTypes: toMap([N, O]) // FIXME this looks weird to me
-    }
+    },
+    scale: ordinalOnlyScale
   }
 });
 
@@ -258,27 +328,6 @@ var sortMixin = {
   }
 };
 
-var bandMixin = {
-  type: 'object',
-  properties: {
-    band: {
-      type: 'object',
-      properties: {
-        size: {
-          type: 'integer',
-          minimum: 0,
-          default: undefined
-        },
-        padding: {
-          type: 'integer',
-          minimum: 0,
-          default: 1
-        }
-      }
-    }
-  }
-};
-
 var legendMixin = {
   type: 'object',
   properties: {
@@ -555,7 +604,7 @@ var onlyQuantitativeField = merge(clone(typicalField), {
   }
 });
 
-var x = merge(clone(multiRoleField), axisMixin, bandMixin, requiredNameType, sortMixin);
+var x = merge(clone(multiRoleField), axisMixin, requiredNameType, sortMixin);
 var y = clone(x);
 
 var facet = merge(clone(onlyOrdinalField), requiredNameType, facetMixin, sortMixin);
@@ -684,12 +733,12 @@ var config = {
       minimum: 0
     },
     // band size
-    largeBandSize: {
+    largeBandWidth: {
       type: 'integer',
       default: 21,
       minimum: 0
     },
-    smallBandSize: {
+    smallBandWidth: {
       //small multiples or single plot with high cardinality
       type: 'integer',
       default: 12,
@@ -699,10 +748,16 @@ var config = {
       type: 'integer',
       default: 10
     },
+    padding: {
+      type: 'number',
+      default: 1,
+      description: 'default scale padding for ordinal x/y scales.'
+    },
     // small multiples
     cellPadding: {
       type: 'number',
-      default: 0.1
+      default: 0.1,
+      description: 'default scale padding for row/column scales.'
     },
     cellGridColor: {
       type: 'string',


Test patch:
diff --git a/test/compiler/marks.spec.js b/test/compiler/marks.spec.js
index 3921a926c..be1ef1e6b 100644
--- a/test/compiler/marks.spec.js
+++ b/test/compiler/marks.spec.js
@@ -77,7 +77,7 @@ describe('compile.marks', function() {
           e = Encoding.fromSpec(f),
           def = marks.point.prop(e, mockLayout, {});
       it('should be centered', function() {
-        expect(def.y).to.eql({value: e.bandSize(Y, mockLayout.y.useSmallBand) / 2});
+        expect(def.y).to.eql({value: e.bandWidth(Y, mockLayout.y.useSmallBand) / 2});
       });
       it('should scale on x', function() {
         expect(def.x).to.eql({scale: X, field: "year"});
@@ -89,7 +89,7 @@ describe('compile.marks', function() {
           e = Encoding.fromSpec(f),
           def = marks.point.prop(e, mockLayout, {});
       it('should be centered', function() {
-        expect(def.x).to.eql({value: e.bandSize(X, mockLayout.x.useSmallBand) / 2});
+        expect(def.x).to.eql({value: e.bandWidth(X, mockLayout.x.useSmallBand) / 2});
       });
       it('should scale on y', function() {
         expect(def.y).to.eql({scale: Y, field: "year"});