(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vega-statistics'), require('d3-color'), require('d3-array'), require('vega-scale'), require('d3-geo'), require('vega-scenegraph'), require('vega-expression'), require('vega-event-selector'), require('vega-dataflow'), require('vega-util'), require('d3-format'), require('d3-time-format')) :
  typeof define === 'function' && define.amd ? define(['exports', 'vega-statistics', 'd3-color', 'd3-array', 'vega-scale', 'd3-geo', 'vega-scenegraph', 'vega-expression', 'vega-event-selector', 'vega-dataflow', 'vega-util', 'd3-format', 'd3-time-format'], factory) :
  (factory((global.vega = {}),global.vega,global.d3,global.d3,global.vega,global.d3,global.vega,global.vega,global.vega,global.vega,global.vega,global.d3,global.d3));
}(this, (function (exports,vegaStatistics,d3Color,d3Array,vegaScale,d3Geo,vegaScenegraph,vegaExpression,vegaEventSelector,vegaDataflow,vegaUtil,d3Format,d3TimeFormat) { 'use strict';

  function parseAutosize(spec, config) {
    spec = spec || config.autosize;
    if (vegaUtil.isObject(spec)) {
      return spec;
    } else {
      spec = spec || 'pad';
      return {type: spec};
    }
  }

  function parsePadding(spec, config) {
    spec = spec || config.padding;
    return vegaUtil.isObject(spec)
      ? {
          top:    number(spec.top),
          bottom: number(spec.bottom),
          left:   number(spec.left),
          right:  number(spec.right)
        }
      : paddingObject(number(spec));
  }

  function number(_) {
    return +_ || 0;
  }

  function paddingObject(_) {
    return {top: _, bottom: _, left: _, right: _};
  }

  var OUTER = 'outer',
      OUTER_INVALID = ['value', 'update', 'init', 'react', 'bind'];

  function outerError(prefix, name) {
    vegaUtil.error(prefix + ' for "outer" push: ' + vegaUtil.stringValue(name));
  }

  function parseSignal(signal, scope) {
    var name = signal.name;

    if (signal.push === OUTER) {
      // signal must already be defined, raise error if not
      if (!scope.signals[name]) outerError('No prior signal definition', name);
      // signal push must not use properties reserved for standard definition
      OUTER_INVALID.forEach(function(prop) {
        if (signal[prop] !== undefined) outerError('Invalid property ', prop);
      });
    } else {
      // define a new signal in the current scope
      var op = scope.addSignal(name, signal.value);
      if (signal.react === false) op.react = false;
      if (signal.bind) scope.addBinding(name, signal.bind);
    }
  }

  var formatCache = {};

  function formatter(type, method, specifier) {
    var k = type + ':' + specifier,
        e = formatCache[k];
    if (!e || e[0] !== method) {
      formatCache[k] = (e = [method, method(specifier)]);
    }
    return e[1];
  }

  function format(_, specifier) {
    return formatter('format', d3Format.format, specifier)(_);
  }

  function timeFormat(_, specifier) {
    return formatter('timeFormat', d3TimeFormat.timeFormat, specifier)(_);
  }

  function utcFormat(_, specifier) {
    return formatter('utcFormat', d3TimeFormat.utcFormat, specifier)(_);
  }

  function timeParse(_, specifier) {
    return formatter('timeParse', d3TimeFormat.timeParse, specifier)(_);
  }

  function utcParse(_, specifier) {
    return formatter('utcParse', d3TimeFormat.utcParse, specifier)(_);
  }

  var dateObj = new Date(2000, 0, 1);

  function time(month, day, specifier) {
    dateObj.setMonth(month);
    dateObj.setDate(day);
    return timeFormat(dateObj, specifier);
  }

  function monthFormat(month) {
    return time(month, 1, '%B');
  }

  function monthAbbrevFormat(month) {
    return time(month, 1, '%b');
  }

  function dayFormat(day) {
    return time(0, 2 + day, '%A');
  }

  function dayAbbrevFormat(day) {
    return time(0, 2 + day, '%a');
  }

  /**
   * Return an array with minimum and maximum values, in the
   * form [min, max]. Ignores null, undefined, and NaN values.
   */
  function extent(array) {
    var i = 0, n, v, min, max;

    if (array && (n = array.length)) {
      // find first valid value
      for (v = array[i]; v == null || v !== v; v = array[++i]);
      min = max = v;

      // visit all other values
      for (; i<n; ++i) {
        v = array[i];
        // skip null/undefined; NaN will fail all comparisons
        if (v != null) {
          if (v < min) min = v;
          if (v > max) max = v;
        }
      }
    }

    return [min, max];
  }

  /**
   * Predicate that returns true if the value lies within the span
   * of the given range. The left and right flags control the use
   * of inclusive (true) or exclusive (false) comparisons.
   */
  function inrange(value, range, left, right) {
    var r0 = range[0], r1 = range[range.length-1], t;
    if (r0 > r1) {
      t = r0;
      r0 = r1;
      r1 = t;
    }
    left = left === undefined || left;
    right = right === undefined || right;

    return (left ? r0 <= value : r0 < value) &&
      (right ? value <= r1 : value < r1);
  }

  /**
   * Span-preserving range clamp. If the span of the input range is less
   * than (max - min) and an endpoint exceeds either the min or max value,
   * the range is translated such that the span is preserved and one
   * endpoint touches the boundary of the min/max range.
   * If the span exceeds (max - min), the range [min, max] is returned.
   */
  function clampRange(range, min, max) {
    var lo = range[0],
        hi = range[1],
        span;

    if (hi < lo) {
      span = hi;
      hi = lo;
      lo = span;
    }
    span = hi - lo;

    return span >= (max - min)
      ? [min, max]
      : [
          (lo = Math.min(Math.max(lo, min), max - span)),
          lo + span
        ];
  }

  /**
   * Return the numerical span of an array: the difference between
   * the last and first values.
   */
  function span(array) {
    return (array[array.length-1] - array[0]) || 0;
  }

  function quarter(date) {
    return 1 + ~~(new Date(date).getMonth() / 3);
  }

  function utcquarter(date) {
    return 1 + ~~(new Date(date).getUTCMonth() / 3);
  }

  function log(df, method, args) {
    try {
      df[method].apply(df, ['EXPRESSION'].concat([].slice.call(args)));
    } catch (err) {
      df.warn(err);
    }
    return args[args.length-1];
  }

  function warn() {
    return log(this.context.dataflow, 'warn', arguments);
  }

  function info() {
    return log(this.context.dataflow, 'info', arguments);
  }

  function debug() {
    return log(this.context.dataflow, 'debug', arguments);
  }

  function inScope(item) {
    var group = this.context.group,
        value = false;

    if (group) while (item) {
      if (item === group) { value = true; break; }
      item = item.mark.group;
    }
    return value;
  }

  function pinchDistance(event) {
    var t = event.touches,
        dx = t[0].clientX - t[1].clientX,
        dy = t[0].clientY - t[1].clientY;
    return Math.sqrt(dx * dx + dy * dy);
  }

  function pinchAngle(event) {
    var t = event.touches;
    return Math.atan2(
      t[0].clientY - t[1].clientY,
      t[0].clientX - t[1].clientX
    );
  }

  var _window = (typeof window !== 'undefined' && window) || null;

  function screen() {
    return _window ? _window.screen : {};
  }

  function windowSize() {
    return _window
      ? [_window.innerWidth, _window.innerHeight]
      : [undefined, undefined];
  }

  function containerSize() {
    var view = this.context.dataflow,
        el = view.container && view.container();
    return el
      ? [el.clientWidth, el.clientHeight]
      : [undefined, undefined];
  }

  function flush(range, value, threshold, left, right, center) {
    if (!threshold && threshold !== 0) return center;

    var a = range[0],
        b = vegaUtil.peek(range),
        t = +threshold,
        l, r;

    // swap endpoints if range is reversed
    if (b < a) {
      l = a; a = b; b = l;
    }

    // compare value to endpoints
    l = Math.abs(value - a);
    r = Math.abs(b - value);

    // adjust if value within threshold distance of endpoint
    return l < r && l <= t ? left : r <= t ? right : center;
  }

  function merge() {
    var args = [].slice.call(arguments);
    args.unshift({});
    return vegaUtil.extend.apply(null, args);
  }

  var Literal = 'Literal';
  var Identifier = 'Identifier';

  var indexPrefix  = '@';
  var scalePrefix  = '%';
  var dataPrefix   = ':';

  function getScale(name, ctx) {
    var s;
    return vegaUtil.isFunction(name) ? name
      : vegaUtil.isString(name) ? (s = ctx.scales[name]) && s.value
      : undefined;
  }

  function addScaleDependency(scope, params, name) {
    var scaleName = scalePrefix + name;
    if (!params.hasOwnProperty(scaleName)) {
      try {
        params[scaleName] = scope.scaleRef(name);
      } catch (err) {
        // TODO: error handling? warning?
      }
    }
  }

  function scaleVisitor(name, args, scope, params) {
    if (args[0].type === Literal) {
      // add scale dependency
      addScaleDependency(scope, params, args[0].value);
    }
    else if (args[0].type === Identifier) {
      // indirect scale lookup; add all scales as parameters
      for (name in scope.scales) {
        addScaleDependency(scope, params, name);
      }
    }
  }

  function range(name, group) {
    var s = getScale(name, (group || this).context);
    return s && s.range ? s.range() : [];
  }

  function domain(name, group) {
    var s = getScale(name, (group || this).context);
    return s ? s.domain() : [];
  }

  function bandwidth(name, group) {
    var s = getScale(name, (group || this).context);
    return s && s.bandwidth ? s.bandwidth() : 0;
  }

  function bandspace(count, paddingInner, paddingOuter) {
    return vegaScale.bandSpace(count || 0, paddingInner || 0, paddingOuter || 0);
  }

  function copy(name, group) {
    var s = getScale(name, (group || this).context);
    return s ? s.copy() : undefined;
  }

  function scale(name, value, group) {
    var s = getScale(name, (group || this).context);
    return s ? s(value) : undefined;
  }

  function invert(name, range, group) {
    var s = getScale(name, (group || this).context);
    return !s ? undefined
      : vegaUtil.isArray(range) ? (s.invertRange || s.invert)(range)
      : (s.invert || s.invertExtent)(range);
  }

  function scaleGradient(scale$$1, p0, p1, count, group) {
    scale$$1 = getScale(scale$$1, (group || this).context);

    var gradient = vegaScenegraph.Gradient(p0, p1),
        stops = scale$$1.domain(),
        min = stops[0],
        max = vegaUtil.peek(stops),
        fraction = vegaScale.scaleFraction(scale$$1, min, max);

    if (scale$$1.ticks) {
      stops = scale$$1.ticks(+count || 15);
      if (min !== stops[0]) stops.unshift(min);
      if (max !== vegaUtil.peek(stops)) stops.push(max);
    }

    for (var i=0, n=stops.length; i<n; ++i) {
      gradient.stop(fraction(stops[i]), scale$$1(stops[i]));
    }

    return gradient;
  }

  function geoMethod(methodName, globalMethod) {
    return function(projection, geojson, group) {
      if (projection) {
        // projection defined, use it
        var p = getScale(projection, (group || this).context);
        return p && p.path[methodName](geojson);
      } else {
        // projection undefined, use global method
        return globalMethod(geojson);
      }
    };
  }

  var geoArea = geoMethod('area', d3Geo.geoArea);
  var geoBounds = geoMethod('bounds', d3Geo.geoBounds);
  var geoCentroid = geoMethod('centroid', d3Geo.geoCentroid);

  function geoShape(projection, geojson, group) {
    var p = getScale(projection, (group || this).context);
    return function(context) {
      return p ? p.path.context(context)(geojson) : '';
    }
  }

  function pathShape(path) {
    var p = null;
    return function(context) {
      return context
        ? vegaScenegraph.pathRender(context, (p = p || vegaScenegraph.pathParse(path)))
        : path;
    };
  }

  function data(name) {
    var data = this.context.data[name];
    return data ? data.values.value : [];
  }

  function dataVisitor(name, args, scope, params) {
    if (args[0].type !== Literal) {
      vegaUtil.error('First argument to data functions must be a string literal.');
    }

    var data = args[0].value,
        dataName = dataPrefix + data;

    if (!params.hasOwnProperty(dataName)) {
      params[dataName] = scope.getData(data).tuplesRef();
    }
  }

  function indata(name, field, value) {
    var index = this.context.data[name]['index:' + field],
        entry = index ? index.value.get(value) : undefined;
    return entry ? entry.count : entry;
  }

  function indataVisitor(name, args, scope, params) {
    if (args[0].type !== Literal) vegaUtil.error('First argument to indata must be a string literal.');
    if (args[1].type !== Literal) vegaUtil.error('Second argument to indata must be a string literal.');

    var data = args[0].value,
        field = args[1].value,
        indexName = indexPrefix + field;

    if (!params.hasOwnProperty(indexName)) {
      params[indexName] = scope.getData(data).indataRef(scope, field);
    }
  }

  function setdata(name, tuples) {
    var df = this.context.dataflow,
        data = this.context.data[name],
        input = data.input;

    df.pulse(input, df.changeset().remove(vegaUtil.truthy).insert(tuples));
    return 1;
  }

  var EMPTY = {};

  function datum(d) { return d.data; }

  function treeNodes(name, context) {
    var tree = data.call(context, name);
    return tree.root && tree.root.lookup || EMPTY;
  }

  function treePath(name, source, target) {
    var nodes = treeNodes(name, this),
        s = nodes[source],
        t = nodes[target];
    return s && t ? s.path(t).map(datum) : undefined;
  }

  function treeAncestors(name, node) {
    var n = treeNodes(name, this)[node];
    return n ? n.ancestors().map(datum) : undefined;
  }

  function encode(item, name, retval) {
    if (item) {
      var df = this.context.dataflow,
          target = item.mark.source;
      df.pulse(target, df.changeset().encode(item, name));
    }
    return retval !== undefined ? retval : item;
  }

  function equal(a, b) {
    return a === b || a !== a && b !== b ? true
      : vegaUtil.isArray(a) && vegaUtil.isArray(b) && a.length === b.length ? equalArray(a, b)
      : false;
  }

  function equalArray(a, b) {
    for (var i=0, n=a.length; i<n; ++i) {
      if (!equal(a[i], b[i])) return false;
    }
    return true;
  }

  function removePredicate(props) {
    return function(_) {
      for (var key in props) {
        if (!equal(_[key], props[key])) return false;
      }
      return true;
    };
  }

  function modify(name, insert, remove, toggle, modify, values) {
    var df = this.context.dataflow,
        data = this.context.data[name],
        input = data.input,
        changes = data.changes,
        stamp = df.stamp(),
        predicate, key;

    if (df._trigger === false || !(input.value.length || insert || toggle)) {
      // nothing to do!
      return 0;
    }

    if (!changes || changes.stamp < stamp) {
      data.changes = (changes = df.changeset());
      changes.stamp = stamp;
      df.runAfter(function() {
        data.modified = true;
        df.pulse(input, changes).run();
      }, true, 1);
    }

    if (remove) {
      predicate = remove === true ? vegaUtil.truthy
        : (vegaUtil.isArray(remove) || vegaDataflow.isTuple(remove)) ? remove
        : removePredicate(remove);
      changes.remove(predicate);
    }

    if (insert) {
      changes.insert(insert);
    }

    if (toggle) {
      predicate = removePredicate(toggle);
      if (input.value.some(predicate)) {
        changes.remove(predicate);
      } else {
        changes.insert(toggle);
      }
    }

    if (modify) {
      for (key in values) {
        changes.modify(modify, key, values[key]);
      }
    }

    return 1;
  }

  var TYPE_ENUM = 'E',
      TYPE_RANGE_INC = 'R',
      TYPE_RANGE_EXC = 'R-E',
      TYPE_RANGE_LE = 'R-LE',
      TYPE_RANGE_RE = 'R-RE',
      INTERSECT = 'intersect',
      UNION = 'union',
      UNIT_INDEX = 'index:unit';

  // TODO: revisit date coercion?
  function testPoint(datum, entry) {
    var fields = entry.fields,
        values = entry.values,
        n = fields.length,
        i = 0, dval, f;

    for (; i<n; ++i) {
      f = fields[i];
      f.getter = vegaUtil.field.getter || vegaUtil.field(f.field);
      dval = f.getter(datum);

      if (vegaUtil.isDate(dval)) dval = vegaUtil.toNumber(dval);
      if (vegaUtil.isDate(values[i])) values[i] = vegaUtil.toNumber(values[i]);
      if (vegaUtil.isDate(values[i][0])) values[i] = values[i].map(vegaUtil.toNumber);

      if (f.type === TYPE_ENUM) {
        // Enumerated fields can either specify individual values (single/multi selections)
        // or an array of values (interval selections).
        if(vegaUtil.isArray(values[i]) ? values[i].indexOf(dval) < 0 : dval !== values[i]) {
          return false;
        }
      } else {
        if (f.type === TYPE_RANGE_INC) {
          if (!inrange(dval, values[i])) return false;
        } else if (f.type === TYPE_RANGE_RE) {
          // Discrete selection of bins test within the range [bin_start, bin_end).
          if (!inrange(dval, values[i], true, false)) return false;
        } else if (f.type === TYPE_RANGE_EXC) { // 'R-E'/'R-LE' included for completeness.
          if (!inrange(dval, values[i], false, false)) return false;
        } else if (f.type === TYPE_RANGE_LE) {
          if (!inrange(dval, values[i], false, true)) return false;
        }
      }
    }

    return true;
  }

  /**
   * Tests if a tuple is contained within an interactive selection.
   * @param {string} name - The name of the data set representing the selection.
   *                 Tuples in the dataset are of the form
   *                 {unit: string, fields: array<fielddef>, values: array<*>}.
   *                 Fielddef is of the form
   *                 {field: string, channel: string, type: 'E' | 'R'} where
   *                 'type' identifies whether tuples in the dataset enumerate
   *                 values for the field, or specify a continuous range.
   * @param {object} datum - The tuple to test for inclusion.
   * @param {string} op - The set operation for combining selections.
   *   One of 'intersect' or 'union' (default).
   * @return {boolean} - True if the datum is in the selection, false otherwise.
   */
  function vlSelectionTest(name, datum, op) {
    var data$$1 = this.context.data[name],
        entries = data$$1 ? data$$1.values.value : [],
        unitIdx = data$$1 ? data$$1[UNIT_INDEX] && data$$1[UNIT_INDEX].value : undefined,
        intersect = op === INTERSECT,
        n = entries.length,
        i = 0,
        entry, miss, count, unit, b;

    for (; i<n; ++i) {
      entry = entries[i];

      if (unitIdx && intersect) {
        // multi selections union within the same unit and intersect across units.
        miss = miss || {};
        count = miss[unit=entry.unit] || 0;

        // if we've already matched this unit, skip.
        if (count === -1) continue;

        b = testPoint(datum, entry);
        miss[unit] = b ? -1 : ++count;

        // if we match and there are no other units return true
        // if we've missed against all tuples in this unit return false
        if (b && unitIdx.size === 1) return true;
        if (!b && count === unitIdx.get(unit).count) return false;
      } else {
        b = testPoint(datum, entry);

        // if we find a miss and we do require intersection return false
        // if we find a match and we don't require intersection return true
        if (intersect ^ b) return b;
      }
    }

    // if intersecting and we made it here, then we saw no misses
    // if not intersecting, then we saw no matches
    // if no active selections, return false
    return n && intersect;
  }

  function vlSelectionVisitor(name, args, scope, params) {
    if (args[0].type !== Literal) vegaUtil.error('First argument to indata must be a string literal.');

    var data$$1 = args[0].value,
        op = args.length >= 2 && args[args.length-1].value,
        field = 'unit',
        indexName = indexPrefix + field;

    if (op === INTERSECT && !params.hasOwnProperty(indexName)) {
      params[indexName] = scope.getData(data$$1).indataRef(scope, field);
    }

    dataVisitor(name, args, scope, params);
  }

  /**
   * Resolves selection for use as a scale domain or reads via the API.
   * @param {string} name - The name of the dataset representing the selection
   * @param {string} [op='union'] - The set operation for combining selections.
   *                 One of 'intersect' or 'union' (default).
   * @returns {object} An object of selected fields and values.
   */
  function vlSelectionResolve(name, op) {
    var data$$1 = this.context.data[name],
      entries = data$$1 ? data$$1.values.value : [],
      resolved = {}, types = {},
      entry, fields, values, unit, field, res, resUnit, type, union,
      n = entries.length, i = 0, j, m;

    // First union all entries within the same unit.
    for (; i < n; ++i) {
      entry = entries[i];
      unit = entry.unit;
      fields = entry.fields;
      values = entry.values;

      for (j = 0, m = fields.length; j < m; ++j) {
        field = fields[j];
        res = resolved[field.field] || (resolved[field.field] = {});
        resUnit = res[unit] || (res[unit] = []);
        types[field.field] = type = field.type.charAt(0);
        union = ops[type + '_union'];
        res[unit] = union(resUnit, vegaUtil.array(values[j]));
      }
    }

    // Then resolve fields across units as per the op.
    op = op || UNION;
    Object.keys(resolved).forEach(function (field) {
      resolved[field] = Object.keys(resolved[field])
        .map(function (unit) { return resolved[field][unit]; })
        .reduce(function (acc, curr) {
          return acc === undefined ? curr :
            ops[types[field] + '_' + op](acc, curr);
        });
    });

    return resolved;
  }

  var ops = {
    'E_union': function (base, value) {
      if (!base.length) return value;

      var i = 0, n = value.length;
      for (; i<n; ++i) if (base.indexOf(value[i]) < 0) base.push(value[i]);
      return base;
    },

    'E_intersect': function (base, value) {
      return !base.length ? value :
        base.filter(function (v) { return value.indexOf(v) >= 0; });
    },

    'R_union': function (base, value) {
      var lo = vegaUtil.toNumber(value[0]), hi = vegaUtil.toNumber(value[1]);
      if (lo > hi) {
        lo = value[1];
        hi = value[0];
      }

      if (!base.length) return [lo, hi];
      if (base[0] > lo) base[0] = lo;
      if (base[1] < hi) base[1] = hi;
      return base;
    },

    'R_intersect': function (base, value) {
      var lo = vegaUtil.toNumber(value[0]), hi = vegaUtil.toNumber(value[1]);
      if (lo > hi) {
        lo = value[1];
        hi = value[0];
      }

      if (!base.length) return [lo, hi];
      if (hi < base[0] || base[1] < lo) {
        return [];
      } else {
        if (base[0] < lo) base[0] = lo;
        if (base[1] > hi) base[1] = hi;
      }
      return base;
    }
  };

  var BIN = 'bin_',
      INTERSECT$1 = 'intersect',
      UNION$1 = 'union',
      UNIT_INDEX$1 = 'index:unit';

  function testPoint$1(datum, entry) {
    var fields = entry.fields,
        values = entry.values,
        getter = entry.getter || (entry.getter = []),
        n = fields.length,
        i = 0, dval;

    for (; i<n; ++i) {
      getter[i] = getter[i] || vegaUtil.field(fields[i]);
      dval = getter[i](datum);
      if (vegaUtil.isDate(dval)) dval = vegaUtil.toNumber(dval);
      if (vegaUtil.isDate(values[i])) values[i] = vegaUtil.toNumber(values[i]);
      if (entry[BIN + fields[i]]) {
        if (vegaUtil.isDate(values[i][0])) values[i] = values[i].map(vegaUtil.toNumber);
        if (!inrange(dval, values[i], true, false)) return false;
      } else if (dval !== values[i]) {
        return false;
      }
    }

    return true;
  }

  // TODO: revisit date coercion?
  // have selections populate with consistent types upon write?

  function testInterval(datum, entry) {
    var ivals = entry.intervals,
        n = ivals.length,
        i = 0,
        getter, extent$$1, value;

    for (; i<n; ++i) {
      extent$$1 = ivals[i].extent;
      getter = ivals[i].getter || (ivals[i].getter = vegaUtil.field(ivals[i].field));
      value = getter(datum);
      if (!extent$$1 || extent$$1[0] === extent$$1[1]) return false;
      if (vegaUtil.isDate(value)) value = vegaUtil.toNumber(value);
      if (vegaUtil.isDate(extent$$1[0])) extent$$1 = ivals[i].extent = extent$$1.map(vegaUtil.toNumber);
      if (vegaUtil.isNumber(extent$$1[0]) && !inrange(value, extent$$1)) return false;
      else if (vegaUtil.isString(extent$$1[0]) && extent$$1.indexOf(value) < 0) return false;
    }

    return true;
  }

  /**
   * Tests if a tuple is contained within an interactive selection.
   * @param {string} name - The name of the data set representing the selection.
   * @param {object} datum - The tuple to test for inclusion.
   * @param {string} op - The set operation for combining selections.
   *   One of 'intersect' or 'union' (default).
   * @param {function(object,object):boolean} test - A boolean-valued test
   *   predicate for determining selection status within a single unit chart.
   * @return {boolean} - True if the datum is in the selection, false otherwise.
   */
  function vlSelection(name, datum, op, test) {
    var data$$1 = this.context.data[name],
        entries = data$$1 ? data$$1.values.value : [],
        unitIdx = data$$1 ? data$$1[UNIT_INDEX$1] && data$$1[UNIT_INDEX$1].value : undefined,
        intersect = op === INTERSECT$1,
        n = entries.length,
        i = 0,
        entry, miss, count, unit, b;

    for (; i<n; ++i) {
      entry = entries[i];

      if (unitIdx && intersect) {
        // multi selections union within the same unit and intersect across units.
        miss = miss || {};
        count = miss[unit=entry.unit] || 0;

        // if we've already matched this unit, skip.
        if (count === -1) continue;

        b = test(datum, entry);
        miss[unit] = b ? -1 : ++count;

        // if we match and there are no other units return true
        // if we've missed against all tuples in this unit return false
        if (b && unitIdx.size === 1) return true;
        if (!b && count === unitIdx.get(unit).count) return false;
      } else {
        b = test(datum, entry);

        // if we find a miss and we do require intersection return false
        // if we find a match and we don't require intersection return true
        if (intersect ^ b) return b;
      }
    }

    // if intersecting and we made it here, then we saw no misses
    // if not intersecting, then we saw no matches
    // if no active selections, return false
    return n && intersect;
  }

  // Assumes point selection tuples are of the form:
  // {unit: string, encodings: array<string>, fields: array<string>, values: array<*>, bins: object}
  function vlPoint(name, datum, op) {
    return vlSelection.call(this, name, datum, op, testPoint$1);
  }

  // Assumes interval selection typles are of the form:
  // {unit: string, intervals: array<{encoding: string, field:string, extent:array<number>}>}
  function vlInterval(name, datum, op) {
    return vlSelection.call(this, name, datum, op, testInterval);
  }

  function vlMultiVisitor(name, args, scope, params) {
    if (args[0].type !== Literal) vegaUtil.error('First argument to indata must be a string literal.');

    var data$$1 = args[0].value,
        // vlMulti, vlMultiDomain have different # of params, but op is always last.
        op = args.length >= 2 && args[args.length-1].value,
        field = 'unit',
        indexName = indexPrefix + field;

    if (op === INTERSECT$1 && !params.hasOwnProperty(indexName)) {
      params[indexName] = scope.getData(data$$1).indataRef(scope, field);
    }

    dataVisitor(name, args, scope, params);
  }

  /**
   * Materializes a point selection as a scale domain.
   * @param {string} name - The name of the dataset representing the selection.
   * @param {string} [encoding] - A particular encoding channel to materialize.
   * @param {string} [field] - A particular field to materialize.
   * @param {string} [op='intersect'] - The set operation for combining selections.
   * One of 'intersect' (default) or 'union'.
   * @returns {array} An array of values to serve as a scale domain.
   */
  function vlPointDomain(name, encoding, field, op) {
    var data$$1 = this.context.data[name],
        entries = data$$1 ? data$$1.values.value : [],
        unitIdx = data$$1 ? data$$1[UNIT_INDEX$1] && data$$1[UNIT_INDEX$1].value : undefined,
        entry = entries[0],
        i = 0, n, index, values, continuous, units;

    if (!entry) return undefined;

    for (n = encoding ? entry.encodings.length : entry.fields.length; i<n; ++i) {
      if ((encoding && entry.encodings[i] === encoding) ||
          (field && entry.fields[i] === field)) {
        index = i;
        continuous = entry[BIN + entry.fields[i]];
        break;
      }
    }

    // multi selections union within the same unit and intersect across units.
    // if we've got only one unit, enforce union for more efficient materialization.
    if (unitIdx && unitIdx.size === 1) {
      op = UNION$1;
    }

    if (unitIdx && op === INTERSECT$1) {
      units = entries.reduce(function(acc, entry) {
        var u = acc[entry.unit] || (acc[entry.unit] = []);
        u.push({unit: entry.unit, value: entry.values[index]});
        return acc;
      }, {});

      values = Object.keys(units).map(function(unit) {
        return {
          unit: unit,
          value: continuous
            ? continuousDomain(units[unit], UNION$1)
            : discreteDomain(units[unit], UNION$1)
        };
      });
    } else {
      values = entries.map(function(entry) {
        return {unit: entry.unit, value: entry.values[index]};
      });
    }

    return continuous ? continuousDomain(values, op) : discreteDomain(values, op);
  }

  /**
   * Materializes an interval selection as a scale domain.
   * @param {string} name - The name of the dataset representing the selection.
   * @param {string} [encoding] - A particular encoding channel to materialize.
   * @param {string} [field] - A particular field to materialize.
   * @param {string} [op='union'] - The set operation for combining selections.
   * One of 'intersect' or 'union' (default).
   * @returns {array} An array of values to serve as a scale domain.
   */
  function vlIntervalDomain(name, encoding, field, op) {
    var data$$1 = this.context.data[name],
        entries = data$$1 ? data$$1.values.value : [],
        entry = entries[0],
        i = 0, n, interval, index, values, discrete;

    if (!entry) return undefined;

    for (n = entry.intervals.length; i<n; ++i) {
      interval = entry.intervals[i];
      if ((encoding && interval.encoding === encoding) ||
          (field && interval.field === field)) {
        if (!interval.extent) return undefined;
        index = i;
        discrete = interval.extent.length > 2;
        break;
      }
    }

    values = entries.reduce(function(acc, entry) {
      var extent$$1 = entry.intervals[index].extent,
          value = discrete
             ? extent$$1.map(function (d) { return {unit: entry.unit, value: d}; })
             : {unit: entry.unit, value: extent$$1};

      if (discrete) {
        acc.push.apply(acc, value);
      } else {
        acc.push(value);
      }
      return acc;
    }, []);


    return discrete ? discreteDomain(values, op) : continuousDomain(values, op);
  }

  function discreteDomain(entries, op) {
    var units = {}, count = 0,
        values = {}, domain = [],
        i = 0, n = entries.length,
        entry, unit, v, key;

    for (; i<n; ++i) {
      entry = entries[i];
      unit  = entry.unit;
      key   = entry.value;

      if (!units[unit]) units[unit] = ++count;
      if (!(v = values[key])) {
        values[key] = v = {value: key, units: {}, count: 0};
      }
      if (!v.units[unit]) v.units[unit] = ++v.count;
    }

    for (key in values) {
      v = values[key];
      if (op === INTERSECT$1 && v.count !== count) continue;
      domain.push(v.value);
    }

    return domain.length ? domain : undefined;
  }

  function continuousDomain(entries, op) {
    var merge = op === INTERSECT$1 ? intersectInterval : unionInterval,
        i = 0, n = entries.length,
        extent$$1, domain, lo, hi;

    for (; i<n; ++i) {
      extent$$1 = entries[i].value;
      if (vegaUtil.isDate(extent$$1[0])) extent$$1 = extent$$1.map(vegaUtil.toNumber);
      lo = extent$$1[0];
      hi = extent$$1[1];
      if (lo > hi) {
        hi = extent$$1[0];
        lo = extent$$1[1];
      }
      domain = domain ? merge(domain, lo, hi) : [lo, hi];
    }

    return domain && domain.length && (+domain[0] !== +domain[1])
      ? domain
      : undefined;
  }

  function unionInterval(domain, lo, hi) {
    if (domain[0] > lo) domain[0] = lo;
    if (domain[1] < hi) domain[1] = hi;
    return domain;
  }

  function intersectInterval(domain, lo, hi) {
    if (hi < domain[0] || domain[1] < lo) {
      return [];
    } else {
      if (domain[0] < lo) domain[0] = lo;
      if (domain[1] > hi) domain[1] = hi;
    }
    return domain;
  }

  // Expression function context object
  var functionContext = {
    random: function() { return vegaStatistics.random(); }, // override default
    isArray: vegaUtil.isArray,
    isBoolean: vegaUtil.isBoolean,
    isDate: vegaUtil.isDate,
    isNumber: vegaUtil.isNumber,
    isObject: vegaUtil.isObject,
    isRegExp: vegaUtil.isRegExp,
    isString: vegaUtil.isString,
    isTuple: vegaDataflow.isTuple,
    toBoolean: vegaUtil.toBoolean,
    toDate: vegaUtil.toDate,
    toNumber: vegaUtil.toNumber,
    toString: vegaUtil.toString,
    pad: vegaUtil.pad,
    peek: vegaUtil.peek,
    truncate: vegaUtil.truncate,
    rgb: d3Color.rgb,
    lab: d3Color.lab,
    hcl: d3Color.hcl,
    hsl: d3Color.hsl,
    sequence: d3Array.range,
    format: format,
    utcFormat: utcFormat,
    utcParse: utcParse,
    timeFormat: timeFormat,
    timeParse: timeParse,
    monthFormat: monthFormat,
    monthAbbrevFormat: monthAbbrevFormat,
    dayFormat: dayFormat,
    dayAbbrevFormat: dayAbbrevFormat,
    quarter: quarter,
    utcquarter: utcquarter,
    warn: warn,
    info: info,
    debug: debug,
    extent: extent,
    inScope: inScope,
    clampRange: clampRange,
    pinchDistance: pinchDistance,
    pinchAngle: pinchAngle,
    screen: screen,
    containerSize: containerSize,
    windowSize: windowSize,
    span: span,
    merge: merge,
    flush: flush,
    bandspace: bandspace,
    inrange: inrange,
    setdata: setdata,
    pathShape: pathShape,
    panLinear: vegaUtil.panLinear,
    panLog: vegaUtil.panLog,
    panPow: vegaUtil.panPow,
    zoomLinear: vegaUtil.zoomLinear,
    zoomLog: vegaUtil.zoomLog,
    zoomPow: vegaUtil.zoomPow,
    encode: encode,
    modify: modify
  };

  var eventFunctions = ['view', 'item', 'group', 'xy', 'x', 'y'], // event functions
      eventPrefix = 'event.vega.', // event function prefix
      thisPrefix = 'this.', // function context prefix
      astVisitors = {}; // AST visitors for dependency analysis

  function expressionFunction(name, fn, visitor) {
    if (arguments.length === 1) {
      return functionContext[name];
    }

    // register with the functionContext
    functionContext[name] = fn;

    // if there is an astVisitor register that, too
    if (visitor) astVisitors[name] = visitor;

    // if the code generator has already been initialized,
    // we need to also register the function with it
    if (codeGenerator) codeGenerator.functions[name] = thisPrefix + name;
    return this;
  }

  // register expression functions with ast visitors
  expressionFunction('bandwidth', bandwidth, scaleVisitor);
  expressionFunction('copy', copy, scaleVisitor);
  expressionFunction('domain', domain, scaleVisitor);
  expressionFunction('range', range, scaleVisitor);
  expressionFunction('invert', invert, scaleVisitor);
  expressionFunction('scale', scale, scaleVisitor);
  expressionFunction('gradient', scaleGradient, scaleVisitor);
  expressionFunction('geoArea', geoArea, scaleVisitor);
  expressionFunction('geoBounds', geoBounds, scaleVisitor);
  expressionFunction('geoCentroid', geoCentroid, scaleVisitor);
  expressionFunction('geoShape', geoShape, scaleVisitor);
  expressionFunction('indata', indata, indataVisitor);
  expressionFunction('data', data, dataVisitor);
  expressionFunction('treePath', treePath, dataVisitor);
  expressionFunction('treeAncestors', treeAncestors, dataVisitor);

  // Vega-Lite selection functions.
  expressionFunction('vlSelectionTest', vlSelectionTest, vlSelectionVisitor);
  expressionFunction('vlSelectionResolve', vlSelectionResolve, vlSelectionVisitor);

  // Deprecated selection functions kept around to avoid a major version bump.
  expressionFunction('vlSingle', vlPoint, dataVisitor);
  expressionFunction('vlSingleDomain', vlPointDomain, dataVisitor);
  expressionFunction('vlMulti', vlPoint, vlMultiVisitor);
  expressionFunction('vlMultiDomain', vlPointDomain, vlMultiVisitor);
  expressionFunction('vlInterval', vlInterval, dataVisitor);
  expressionFunction('vlIntervalDomain', vlIntervalDomain, dataVisitor);

  // Build expression function registry
  function buildFunctions(codegen) {
    var fn = vegaExpression.functions(codegen);
    eventFunctions.forEach(function(name) { fn[name] = eventPrefix + name; });
    for (var name in functionContext) { fn[name] = thisPrefix + name; }
    return fn;
  }

  // Export code generator and parameters
  var codegenParams = {
    blacklist:  ['_'],
    whitelist:  ['datum', 'event', 'item'],
    fieldvar:   'datum',
    globalvar:  function(id) { return '_[' + vegaUtil.stringValue('$' + id) + ']'; },
    functions:  buildFunctions,
    constants:  vegaExpression.constants,
    visitors:   astVisitors
  };

  var codeGenerator = vegaExpression.codegen(codegenParams);

  var signalPrefix = '$';

  function expression(expr, scope, preamble) {
    var params = {}, ast, gen;

    // parse the expression to an abstract syntax tree (ast)
    try {
      expr = vegaUtil.isString(expr) ? expr : (vegaUtil.stringValue(expr) + '');
      ast = vegaExpression.parse(expr);
    } catch (err) {
      vegaUtil.error('Expression parse error: ' + expr);
    }

    // analyze ast function calls for dependencies
    ast.visit(function visitor(node) {
      if (node.type !== 'CallExpression') return;
      var name = node.callee.name,
          visit = codegenParams.visitors[name];
      if (visit) visit(name, node.arguments, scope, params);
    });

    // perform code generation
    gen = codeGenerator(ast);

    // collect signal dependencies
    gen.globals.forEach(function(name) {
      var signalName = signalPrefix + name;
      if (!params.hasOwnProperty(signalName) && scope.getSignal(name)) {
        params[signalName] = scope.signalRef(name);
      }
    });

    // return generated expression code and dependencies
    return {
      $expr:   preamble ? preamble + 'return(' + gen.code + ');' : gen.code,
      $fields: gen.fields,
      $params: params
    };
  }

  function Entry(type, value, params, parent) {
    this.id = -1;
    this.type = type;
    this.value = value;
    this.params = params;
    if (parent) this.parent = parent;
  }

  function entry(type, value, params, parent) {
    return new Entry(type, value, params, parent);
  }

  function operator(value, params) {
    return entry('operator', value, params);
  }

  // -----

  function ref(op) {
    var ref = {$ref: op.id};
    // if operator not yet registered, cache ref to resolve later
    if (op.id < 0) (op.refs = op.refs || []).push(ref);
    return ref;
  }

  var tupleidRef = {
    $tupleid: 1,
    toString: function() { return ':_tupleid_:'; }
  };

  function fieldRef(field, name) {
    return name ? {$field: field, $name: name} : {$field: field};
  }

  var keyFieldRef = fieldRef('key');

  function compareRef(fields, orders) {
    return {$compare: fields, $order: orders};
  }

  function keyRef(fields, flat) {
    var ref = {$key: fields};
    if (flat) ref.$flat = true;
    return ref;
  }

  // -----

  var Ascending  = 'ascending';

  var Descending = 'descending';

  function sortKey(sort) {
    return !vegaUtil.isObject(sort) ? ''
      : (sort.order === Descending ? '-' : '+')
        + aggrField(sort.op, sort.field);
  }

  function aggrField(op, field) {
    return (op && op.signal ? '$' + op.signal : op || '')
      + (op && field ? '_' : '')
      + (field && field.signal ? '$' + field.signal : field || '');
  }

  // -----

  var Scope = 'scope';

  var View = 'view';

  function isSignal(_) {
    return _ && _.signal;
  }

  function hasSignal(_) {
    if (isSignal(_)) return true;
    if (vegaUtil.isObject(_)) for (var key in _) {
      if (hasSignal(_[key])) return true;
    }
    return false;
  }

  function value(specValue, defaultValue) {
    return specValue != null ? specValue : defaultValue;
  }

  function deref(v) {
    return v && v.signal || v;
  }

  var Timer = 'timer';

  function parseStream(stream, scope) {
    var method = stream.merge ? mergeStream
      : stream.stream ? nestedStream
      : stream.type ? eventStream
      : vegaUtil.error('Invalid stream specification: ' + vegaUtil.stringValue(stream));

    return method(stream, scope);
  }

  function eventSource(source) {
     return source === Scope ? View : (source || View);
  }

  function mergeStream(stream, scope) {
    var list = stream.merge.map(function(s) {
      return parseStream(s, scope);
    });

    var entry$$1 = streamParameters({merge: list}, stream, scope);
    return scope.addStream(entry$$1).id;
  }

  function nestedStream(stream, scope) {
    var id = parseStream(stream.stream, scope),
        entry$$1 = streamParameters({stream: id}, stream, scope);
    return scope.addStream(entry$$1).id;
  }

  function eventStream(stream, scope) {
    var id, entry$$1;

    if (stream.type === Timer) {
      id = scope.event(Timer, stream.throttle);
      stream = {between: stream.between, filter: stream.filter};
    } else {
      id = scope.event(eventSource(stream.source), stream.type);
    }

    entry$$1 = streamParameters({stream: id}, stream, scope);
    return Object.keys(entry$$1).length === 1
      ? id
      : scope.addStream(entry$$1).id;
  }

  function streamParameters(entry$$1, stream, scope) {
    var param = stream.between;

    if (param) {
      if (param.length !== 2) {
        vegaUtil.error('Stream "between" parameter must have 2 entries: ' + vegaUtil.stringValue(stream));
      }
      entry$$1.between = [
        parseStream(param[0], scope),
        parseStream(param[1], scope)
      ];
    }

    param = stream.filter ? vegaUtil.array(stream.filter) : [];
    if (stream.marktype || stream.markname || stream.markrole) {
      // add filter for mark type, name and/or role
      param.push(filterMark(stream.marktype, stream.markname, stream.markrole));
    }
    if (stream.source === Scope) {
      // add filter to limit events from sub-scope only
      param.push('inScope(event.item)');
    }
    if (param.length) {
      entry$$1.filter = expression('(' + param.join(')&&(') + ')').$expr;
    }

    if ((param = stream.throttle) != null) {
      entry$$1.throttle = +param;
    }

    if ((param = stream.debounce) != null) {
      entry$$1.debounce = +param;
    }

    if (stream.consume) {
      entry$$1.consume = true;
    }

    return entry$$1;
  }

  function filterMark(type, name, role) {
    var item = 'event.item';
    return item
      + (type && type !== '*' ? '&&' + item + '.mark.marktype===\'' + type + '\'' : '')
      + (role ? '&&' + item + '.mark.role===\'' + role + '\'' : '')
      + (name ? '&&' + item + '.mark.name===\'' + name + '\'' : '');
  }

  var preamble = 'var datum=event.item&&event.item.datum;';

  function parseUpdate(spec, scope, target) {
    var events = spec.events,
        update = spec.update,
        encode = spec.encode,
        sources = [],
        value$$1 = '', entry$$1;

    if (!events) {
      vegaUtil.error('Signal update missing events specification.');
    }

    // interpret as an event selector string
    if (vegaUtil.isString(events)) {
      events = vegaEventSelector.selector(events, scope.isSubscope() ? Scope : View);
    }

    // separate event streams from signal updates
    events = vegaUtil.array(events).filter(function(stream) {
      if (stream.signal || stream.scale) {
        sources.push(stream);
        return 0;
      } else {
        return 1;
      }
    });

    // merge event streams, include as source
    if (events.length) {
      sources.push(events.length > 1 ? {merge: events} : events[0]);
    }

    if (encode != null) {
      if (update) vegaUtil.error('Signal encode and update are mutually exclusive.');
      update = 'encode(item(),' + vegaUtil.stringValue(encode) + ')';
    }

    // resolve update value
    value$$1 = vegaUtil.isString(update) ? expression(update, scope, preamble)
      : update.expr != null ? expression(update.expr, scope, preamble)
      : update.value != null ? update.value
      : update.signal != null ? {
          $expr:   '_.value',
          $params: {value: scope.signalRef(update.signal)}
        }
      : vegaUtil.error('Invalid signal update specification.');

    entry$$1 = {
      target: target,
      update: value$$1
    };

    if (spec.force) {
      entry$$1.options = {force: true};
    }

    sources.forEach(function(source) {
      scope.addUpdate(vegaUtil.extend(streamSource(source, scope), entry$$1));
    });
  }

  function streamSource(stream, scope) {
    return {
      source: stream.signal ? scope.signalRef(stream.signal)
            : stream.scale ? scope.scaleRef(stream.scale)
            : parseStream(stream, scope)
    };
  }

  function parseSignalUpdates(signal, scope) {
    var op = scope.getSignal(signal.name),
        expr = signal.update;

    if (signal.init) {
      if (expr) {
        vegaUtil.error('Signals can not include both init and update expressions.');
      } else {
        expr = signal.init;
        op.initonly = true;
      }
    }

    if (expr) {
      expr = expression(expr, scope);
      op.update = expr.$expr;
      op.params = expr.$params;
    }

    if (signal.on) {
      signal.on.forEach(function(_) {
        parseUpdate(_, scope, op.id);
      });
    }
  }

  function transform(name) {
    return function(params, value$$1, parent) {
      return entry(name, value$$1, params || undefined, parent);
    };
  }

  var Aggregate = transform('aggregate');
  var AxisTicks = transform('axisticks');
  var Bound = transform('bound');
  var Collect = transform('collect');
  var Compare = transform('compare');
  var DataJoin = transform('datajoin');
  var Encode = transform('encode');
  var Expression = transform('expression');
  var Facet = transform('facet');
  var Field = transform('field');
  var Key = transform('key');
  var LegendEntries = transform('legendentries');
  var Load = transform('load');
  var Mark = transform('mark');
  var MultiExtent = transform('multiextent');
  var MultiValues = transform('multivalues');
  var Overlap = transform('overlap');
  var Params = transform('params');
  var PreFacet = transform('prefacet');
  var Projection = transform('projection');
  var Proxy = transform('proxy');
  var Relay = transform('relay');
  var Render = transform('render');
  var Scale = transform('scale');
  var Sieve = transform('sieve');
  var SortItems = transform('sortitems');
  var ViewLayout = transform('viewlayout');
  var Values = transform('values');

  var FIELD_REF_ID = 0;

  var types = [
    'identity',
    'ordinal', 'band', 'point',
    'bin-linear', 'bin-ordinal',
    'quantize', 'quantile', 'threshold',
    'linear', 'pow', 'sqrt', 'log', 'sequential',
    'time', 'utc'
  ];

  var allTypes = vegaUtil.toSet(types),
      discreteTypes = vegaUtil.toSet(types.slice(4, 9)),
      continuousTypes = vegaUtil.toSet(types.slice(9)),
      ordinalTypes = vegaUtil.toSet(types.slice(1, 6));

  function isOrdinal(type) {
    return ordinalTypes.hasOwnProperty(type);
  }

  function isDiscretizing(type) {
    return discreteTypes.hasOwnProperty(type);
  }

  function isContinuous(type) {
    return continuousTypes.hasOwnProperty(type);
  }

  function isQuantile(type) {
    return type === 'quantile';
  }

  function initScale(spec, scope) {
    var type = spec.type || 'linear';

    if (!allTypes.hasOwnProperty(type)) {
      vegaUtil.error('Unrecognized scale type: ' + vegaUtil.stringValue(type));
    }

    scope.addScale(spec.name, {
      type:   type,
      domain: undefined
    });
  }

  function parseScale(spec, scope) {
    var params = scope.getScale(spec.name).params,
        key;

    params.domain = parseScaleDomain(spec.domain, spec, scope);

    if (spec.range != null) {
      params.range = parseScaleRange(spec, scope, params);
    }

    if (spec.interpolate != null) {
      parseScaleInterpolate(spec.interpolate, params);
    }

    if (spec.nice != null) {
      parseScaleNice(spec.nice, params);
    }

    for (key in spec) {
      if (params.hasOwnProperty(key) || key === 'name') continue;
      params[key] = parseLiteral(spec[key], scope);
    }
  }

  function parseLiteral(v, scope) {
    return !vegaUtil.isObject(v) ? v
      : v.signal ? scope.signalRef(v.signal)
      : vegaUtil.error('Unsupported object: ' + vegaUtil.stringValue(v));
  }

  function parseArray(v, scope) {
    return v.signal
      ? scope.signalRef(v.signal)
      : v.map(function(v) { return parseLiteral(v, scope); });
  }

  function dataLookupError(name) {
    vegaUtil.error('Can not find data set: ' + vegaUtil.stringValue(name));
  }

  // -- SCALE DOMAIN ----

  function parseScaleDomain(domain, spec, scope) {
    if (!domain) {
      if (spec.domainMin != null || spec.domainMax != null) {
        vegaUtil.error('No scale domain defined for domainMin/domainMax to override.');
      }
      return; // default domain
    }

    return domain.signal ? scope.signalRef(domain.signal)
      : (vegaUtil.isArray(domain) ? explicitDomain
      : domain.fields ? multipleDomain
      : singularDomain)(domain, spec, scope);
  }

  function explicitDomain(domain, spec, scope) {
    return domain.map(function(v) {
      return parseLiteral(v, scope);
    });
  }

  function singularDomain(domain, spec, scope) {
    var data = scope.getData(domain.data);
    if (!data) dataLookupError(domain.data);

    return isOrdinal(spec.type)
        ? data.valuesRef(scope, domain.field, parseSort(domain.sort, false))
        : isQuantile(spec.type) ? data.domainRef(scope, domain.field)
        : data.extentRef(scope, domain.field);
  }

  function multipleDomain(domain, spec, scope) {
    var data = domain.data,
        fields = domain.fields.reduce(function(dom, d) {
          d = vegaUtil.isString(d) ? {data: data, field: d}
            : (vegaUtil.isArray(d) || d.signal) ? fieldRef$1(d, scope)
            : d;
          dom.push(d);
          return dom;
        }, []);

    return (isOrdinal(spec.type) ? ordinalMultipleDomain
      : isQuantile(spec.type) ? quantileMultipleDomain
      : numericMultipleDomain)(domain, scope, fields);
  }

  function fieldRef$1(data, scope) {
    var name = '_:vega:_' + (FIELD_REF_ID++),
        coll = Collect({});

    if (vegaUtil.isArray(data)) {
      coll.value = {$ingest: data};
    } else if (data.signal) {
      var code = 'setdata(' + vegaUtil.stringValue(name) + ',' + data.signal + ')';
      coll.params.input = scope.signalRef(code);
    }
    scope.addDataPipeline(name, [coll, Sieve({})]);
    return {data: name, field: 'data'};
  }

  function ordinalMultipleDomain(domain, scope, fields) {
    var counts, a, c, v;

    // get value counts for each domain field
    counts = fields.map(function(f) {
      var data = scope.getData(f.data);
      if (!data) dataLookupError(f.data);
      return data.countsRef(scope, f.field);
    });

    // sum counts from all fields
    a = scope.add(Aggregate({
      groupby: keyFieldRef,
      ops:['sum'], fields: [scope.fieldRef('count')], as:['count'],
      pulse: counts
    }));

    // collect aggregate output
    c = scope.add(Collect({pulse: ref(a)}));

    // extract values for combined domain
    v = scope.add(Values({
      field: keyFieldRef,
      sort:  scope.sortRef(parseSort(domain.sort, true)),
      pulse: ref(c)
    }));

    return ref(v);
  }

  function parseSort(sort, multidomain) {
    if (sort) {
      if (!sort.field && !sort.op) {
        if (vegaUtil.isObject(sort)) sort.field = 'key';
        else sort = {field: 'key'};
      } else if (!sort.field && sort.op !== 'count') {
        vegaUtil.error('No field provided for sort aggregate op: ' + sort.op);
      } else if (multidomain && sort.field) {
        vegaUtil.error('Multiple domain scales can not sort by field.');
      } else if (multidomain && sort.op && sort.op !== 'count') {
        vegaUtil.error('Multiple domain scales support op count only.');
      }
    }
    return sort;
  }

  function quantileMultipleDomain(domain, scope, fields) {
    // get value arrays for each domain field
    var values = fields.map(function(f) {
      var data = scope.getData(f.data);
      if (!data) dataLookupError(f.data);
      return data.domainRef(scope, f.field);
    });

    // combine value arrays
    return ref(scope.add(MultiValues({values: values})));
  }

  function numericMultipleDomain(domain, scope, fields) {
    // get extents for each domain field
    var extents = fields.map(function(f) {
      var data = scope.getData(f.data);
      if (!data) dataLookupError(f.data);
      return data.extentRef(scope, f.field);
    });

    // combine extents
    return ref(scope.add(MultiExtent({extents: extents})));
  }

  // -- SCALE NICE -----

  function parseScaleNice(nice, params) {
    params.nice = vegaUtil.isObject(nice)
      ? {
          interval: parseLiteral(nice.interval),
          step: parseLiteral(nice.step)
        }
      : parseLiteral(nice);
  }

  // -- SCALE INTERPOLATION -----

  function parseScaleInterpolate(interpolate, params) {
    params.interpolate = parseLiteral(interpolate.type || interpolate);
    if (interpolate.gamma != null) {
      params.interpolateGamma = parseLiteral(interpolate.gamma);
    }
  }

  // -- SCALE RANGE -----

  function parseScaleRange(spec, scope, params) {
    var range = spec.range,
        config = scope.config.range;

    if (range.signal) {
      return scope.signalRef(range.signal);
    } else if (vegaUtil.isString(range)) {
      if (config && config.hasOwnProperty(range)) {
        spec = vegaUtil.extend({}, spec, {range: config[range]});
        return parseScaleRange(spec, scope, params);
      } else if (range === 'width') {
        range = [0, {signal: 'width'}];
      } else if (range === 'height') {
        range = isOrdinal(spec.type)
          ? [0, {signal: 'height'}]
          : [{signal: 'height'}, 0];
      } else {
        vegaUtil.error('Unrecognized scale range value: ' + vegaUtil.stringValue(range));
      }
    } else if (range.scheme) {
      params.scheme = parseLiteral(range.scheme, scope);
      if (range.extent) params.schemeExtent = parseArray(range.extent, scope);
      if (range.count) params.schemeCount = parseLiteral(range.count, scope);
      return;
    } else if (range.step) {
      params.rangeStep = parseLiteral(range.step, scope);
      return;
    } else if (isOrdinal(spec.type) && !vegaUtil.isArray(range)) {
      return parseScaleDomain(range, spec, scope);
    } else if (!vegaUtil.isArray(range)) {
      vegaUtil.error('Unsupported range type: ' + vegaUtil.stringValue(range));
    }

    return range.map(function(v) {
      return parseLiteral(v, scope);
    });
  }

  function parseProjection(proj, scope) {
    var params = {};

    for (var name in proj) {
      if (name === 'name') continue;
      params[name] = parseParameter(proj[name], name, scope);
    }

    scope.addProjection(proj.name, params);
  }

  function parseParameter(_, name, scope) {
    return vegaUtil.isArray(_) ? _.map(function(_) { return parseParameter(_, name, scope); })
      : !vegaUtil.isObject(_) ? _
      : _.signal ? scope.signalRef(_.signal)
      : name === 'fit' ? _
      : vegaUtil.error('Unsupported parameter object: ' + vegaUtil.stringValue(_));
  }

  var Top = 'top';
  var Left = 'left';
  var Right = 'right';
  var Bottom = 'bottom';
  var Center = 'center';

  var Vertical = 'vertical';

  var Start = 'start';
  var End = 'end';

  var Index  = 'index';
  var Label  = 'label';
  var Offset = 'offset';
  var Perc   = 'perc';
  var Perc2  = 'perc2';
  var Size   = 'size';
  var Value  = 'value';

  var GuideLabelStyle = 'guide-label';
  var GuideTitleStyle = 'guide-title';
  var GroupTitleStyle = 'group-title';

  var Symbols = 'symbol';
  var Gradient = 'gradient';
  var Discrete = 'discrete';

  // Encoding channels supported by legends
  // In priority order of 'canonical' scale
  var LegendScales = [
    'size',
    'shape',
    'fill',
    'stroke',
    'strokeDash',
    'opacity'
  ];

  var Skip = {
    name: 1,
    interactive: 1
  };

  var Skip$1 = vegaUtil.toSet(['rule']),
      Swap = vegaUtil.toSet(['group', 'image', 'rect']);

  function adjustSpatial(encode, marktype) {
    var code = '';

    if (Skip$1[marktype]) return code;

    if (encode.x2) {
      if (encode.x) {
        if (Swap[marktype]) {
          code += 'if(o.x>o.x2)$=o.x,o.x=o.x2,o.x2=$;';
        }
        code += 'o.width=o.x2-o.x;';
      } else {
        code += 'o.x=o.x2-(o.width||0);';
      }
    }

    if (encode.xc) {
      code += 'o.x=o.xc-(o.width||0)/2;';
    }

    if (encode.y2) {
      if (encode.y) {
        if (Swap[marktype]) {
          code += 'if(o.y>o.y2)$=o.y,o.y=o.y2,o.y2=$;';
        }
        code += 'o.height=o.y2-o.y;';
      } else {
        code += 'o.y=o.y2-(o.height||0);';
      }
    }

    if (encode.yc) {
      code += 'o.y=o.yc-(o.height||0)/2;';
    }

    return code;
  }

  function color(enc, scope, params, fields) {
    function color(type, x, y, z) {
      var a = entry$1(null, x, scope, params, fields),
          b = entry$1(null, y, scope, params, fields),
          c = entry$1(null, z, scope, params, fields);
      return 'this.' + type + '(' + [a, b, c].join(',') + ').toString()';
    }

    return (enc.c) ? color('hcl', enc.h, enc.c, enc.l)
      : (enc.h || enc.s) ? color('hsl', enc.h, enc.s, enc.l)
      : (enc.l || enc.a) ? color('lab', enc.l, enc.a, enc.b)
      : (enc.r || enc.g || enc.b) ? color('rgb', enc.r, enc.g, enc.b)
      : null;
  }

  function expression$1(code, scope, params, fields) {
    var expr = expression(code, scope);
    expr.$fields.forEach(function(name) { fields[name] = 1; });
    vegaUtil.extend(params, expr.$params);
    return expr.$expr;
  }

  function field(ref, scope, params, fields) {
    return resolve(vegaUtil.isObject(ref) ? ref : {datum: ref}, scope, params, fields);
  }

  function resolve(ref, scope, params, fields) {
    var object, level, field;

    if (ref.signal) {
      object = 'datum';
      field = expression$1(ref.signal, scope, params, fields);
    } else if (ref.group || ref.parent) {
      level = Math.max(1, ref.level || 1);
      object = 'item';

      while (level-- > 0) {
        object += '.mark.group';
      }

      if (ref.parent) {
        field = ref.parent;
        object += '.datum';
      } else {
        field = ref.group;
      }
    } else if (ref.datum) {
      object = 'datum';
      field = ref.datum;
    } else {
      vegaUtil.error('Invalid field reference: ' + vegaUtil.stringValue(ref));
    }

    if (!ref.signal) {
      if (vegaUtil.isString(field)) {
        fields[field] = 1; // TODO review field tracking?
        field = vegaUtil.splitAccessPath(field).map(vegaUtil.stringValue).join('][');
      } else {
        field = resolve(field, scope, params, fields);
      }
    }

    return object + '[' + field + ']';
  }

  function scale$1(enc, value, scope, params, fields) {
    var scale = getScale$1(enc.scale, scope, params, fields),
        interp, func, flag;

    if (enc.range != null) {
      // pull value from scale range
      interp = +enc.range;
      func = scale + '.range()';
      value = (interp === 0) ? (func + '[0]')
        : '($=' + func + ',' + ((interp === 1) ? '$[$.length-1]'
        : '$[0]+' + interp + '*($[$.length-1]-$[0])') + ')';
    } else {
      // run value through scale and/or pull scale bandwidth
      if (value !== undefined) value = scale + '(' + value + ')';

      if (enc.band && (flag = hasBandwidth(enc.scale, scope))) {
        func = scale + '.bandwidth';
        interp = +enc.band;
        interp = func + '()' + (interp===1 ? '' : '*' + interp);

        // if we don't know the scale type, check for bandwidth
        if (flag < 0) interp = '(' + func + '?' + interp + ':0)';

        value = (value ? value + '+' : '') + interp;

        if (enc.extra) {
          // include logic to handle extraneous elements
          value = '(datum.extra?' + scale + '(datum.extra.value):' + value + ')';
        }
      }

      if (value == null) value = '0';
    }

    return value;
  }

  function hasBandwidth(name, scope) {
    if (!vegaUtil.isString(name)) return -1;
    var type = scope.scaleType(name);
    return type === 'band' || type === 'point' ? 1 : 0;
  }

  function getScale$1(name, scope, params, fields) {
    var scaleName;

    if (vegaUtil.isString(name)) {
      // direct scale lookup; add scale as parameter
      scaleName = scalePrefix + name;
      if (!params.hasOwnProperty(scaleName)) {
        params[scaleName] = scope.scaleRef(name);
      }
      scaleName = vegaUtil.stringValue(scaleName);
    } else {
      // indirect scale lookup; add all scales as parameters
      for (scaleName in scope.scales) {
        params[scalePrefix + scaleName] = scope.scaleRef(scaleName);
      }
      scaleName = vegaUtil.stringValue(scalePrefix) + '+'
        + (name.signal
          ? '(' + expression$1(name.signal, scope, params, fields) + ')'
          : field(name, scope, params, fields));
    }

    return '_[' + scaleName + ']';
  }

  function gradient(enc, scope, params, fields) {
    return 'this.gradient('
      + getScale$1(enc.gradient, scope, params, fields)
      + ',' + vegaUtil.stringValue(enc.start)
      + ',' + vegaUtil.stringValue(enc.stop)
      + ',' + vegaUtil.stringValue(enc.count)
      + ')';
  }

  function property(property, scope, params, fields) {
    return vegaUtil.isObject(property)
        ? '(' + entry$1(null, property, scope, params, fields) + ')'
        : property;
  }

  function entry$1(channel, enc, scope, params, fields) {
    if (enc.gradient != null) {
      return gradient(enc, scope, params, fields);
    }

    var value = enc.signal ? expression$1(enc.signal, scope, params, fields)
      : enc.color ? color(enc.color, scope, params, fields)
      : enc.field != null ? field(enc.field, scope, params, fields)
      : enc.value !== undefined ? vegaUtil.stringValue(enc.value)
      : undefined;

    if (enc.scale != null) {
      value = scale$1(enc, value, scope, params, fields);
    }

    if (value === undefined) {
      value = null;
    }

    if (enc.exponent != null) {
      value = 'Math.pow(' + value + ','
        + property(enc.exponent, scope, params, fields) + ')';
    }

    if (enc.mult != null) {
      value += '*' + property(enc.mult, scope, params, fields);
    }

    if (enc.offset != null) {
      value += '+' + property(enc.offset, scope, params, fields);
    }

    if (enc.round) {
      value = 'Math.round(' + value + ')';
    }

    return value;
  }

  function set(obj, key, value) {
    return obj + '[' + vegaUtil.stringValue(key) + ']=' + value + ';';
  }

  function rule(channel, rules, scope, params, fields) {
    var code = '';

    rules.forEach(function(rule) {
      var value = entry$1(channel, rule, scope, params, fields);
      code += rule.test
        ? expression$1(rule.test, scope, params, fields) + '?' + value + ':'
        : value;
    });

    // if no else clause, terminate with null (vega/vega#1366)
    if (vegaUtil.peek(code) === ':') {
      code += 'null';
    }

    return set('o', channel, code);
  }

  function parseEncode(encode, marktype, params, scope) {
    var fields = {},
        code = 'var o=item,datum=o.datum,$;',
        channel, enc, value;

    for (channel in encode) {
      enc = encode[channel];
      if (vegaUtil.isArray(enc)) { // rule
        code += rule(channel, enc, scope, params, fields);
      } else {
        value = entry$1(channel, enc, scope, params, fields);
        code += set('o', channel, value);
      }
    }

    code += adjustSpatial(encode, marktype);
    code += 'return 1;';

    return {
      $expr:   code,
      $fields: Object.keys(fields),
      $output: Object.keys(encode)
    };
  }

  var MarkRole = 'mark';
  var FrameRole = 'frame';
  var ScopeRole = 'scope';

  var AxisRole = 'axis';
  var AxisDomainRole = 'axis-domain';
  var AxisGridRole = 'axis-grid';
  var AxisLabelRole = 'axis-label';
  var AxisTickRole = 'axis-tick';
  var AxisTitleRole = 'axis-title';

  var LegendRole = 'legend';
  var LegendBandRole = 'legend-band';
  var LegendEntryRole = 'legend-entry';
  var LegendGradientRole = 'legend-gradient';
  var LegendLabelRole = 'legend-label';
  var LegendSymbolRole = 'legend-symbol';
  var LegendTitleRole = 'legend-title';

  var TitleRole = 'title';

  function encoder(_) {
    return vegaUtil.isObject(_) ? vegaUtil.extend({}, _) : {value: _};
  }

  function addEncode(object, name, value, set) {
    if (value != null) {
      if (vegaUtil.isObject(value) && !vegaUtil.isArray(value)) {
        object.update[name] = value;
      } else {
        object[set || 'enter'][name] = {value: value};
      }
      // object[name] = isObject(value) && !isArray(value) ? value : {value: value};
      return 1;
    } else {
      return 0;
    }
  }

  function extendEncode(encode, extra, skip) {
    for (var name in extra) {
      if (skip && skip.hasOwnProperty(name)) continue;
      encode[name] = vegaUtil.extend(encode[name] || {}, extra[name]);
    }
    return encode;
  }

  function encoders(encode, type, role, style, scope, params) {
    var enc, key;
    params = params || {};
    params.encoders = {$encode: (enc = {})};

    encode = applyDefaults(encode, type, role, style, scope.config);

    for (key in encode) {
      enc[key] = parseEncode(encode[key], type, params, scope);
    }

    return params;
  }

  function applyDefaults(encode, type, role, style, config) {
    var enter = {}, key, skip, props;

    // ignore legend and axis
    if (role == 'legend' || String(role).indexOf('axis') === 0) {
      role = null;
    }

    // resolve mark config
    props = role === FrameRole ? config.group
      : (role === MarkRole) ? vegaUtil.extend({}, config.mark, config[type])
      : null;

    for (key in props) {
      // do not apply defaults if relevant fields are defined
      skip = has(key, encode)
        || (key === 'fill' || key === 'stroke')
        && (has('fill', encode) || has('stroke', encode));

      if (!skip) enter[key] = defaultEncode(props[key]);
    }

    // resolve styles, apply with increasing precedence
    vegaUtil.array(style).forEach(function(name) {
      var props = config.style && config.style[name];
      for (var key in props) {
        if (!has(key, encode)) {
          enter[key] = defaultEncode(props[key]);
        }
      }
    });

    encode = vegaUtil.extend({}, encode); // defensive copy
    encode.enter = vegaUtil.extend(enter, encode.enter);

    return encode;
  }

  function defaultEncode(value) {
    return value && value.signal
      ? {signal: value.signal}
      : {value: value};
  }

  function has(key, encode) {
    return encode && (
      (encode.enter && encode.enter[key]) ||
      (encode.update && encode.update[key])
    );
  }

  function guideMark(type, role, style, key, dataRef, encode, extras) {
    return {
      type:  type,
      name:  extras ? extras.name : undefined,
      role:  role,
      style: (extras && extras.style) || style,
      key:   key,
      from:  dataRef,
      interactive: !!(extras && extras.interactive),
      encode: extendEncode(encode, extras, Skip)
    };
  }

  function lookup(name, spec, config) {
    return value(spec[name], config[name]);
  }

  function isVertical(spec, configVal) {
    return value(spec.direction, configVal) === Vertical;
  }

  function gradientLength(spec, config) {
    return value(
      spec.gradientLength,
      config.gradientLength || config.gradientWidth
    );
  }

  function gradientThickness(spec, config) {
    return value(
      spec.gradientThickness,
      config.gradientThickness || config.gradientHeight
    );
  }

  function entryColumns(spec, config) {
    return value(
      spec.columns,
      value(config.columns, +isVertical(spec, config.symbolDirection))
    );
  }

  function getEncoding(name, encode) {
    var v = encode && (
      (encode.update && encode.update[name]) ||
      (encode.enter && encode.enter[name])
    );
    return v && v.signal ? v : v ? v.value : null;
  }

  function getStyle(name, scope, style) {
    var s = scope.config.style[style];
    return s && s[name];
  }

  var GroupMark = 'group';
  var RectMark = 'rect';
  var RuleMark = 'rule';
  var SymbolMark = 'symbol';
  var TextMark = 'text';

  function legendGradient(spec, scale, config, userEncode) {
    var zero = {value: 0},
        vertical = isVertical(spec, config.gradientDirection),
        thickness = gradientThickness(spec, config),
        length = gradientLength(spec, config),
        encode, enter, start, stop, width, height;

    if (vertical) {
      start = [0, 1];
      stop = [0, 0];
      width = thickness;
      height = length;
    } else {
      start = [0, 0];
      stop = [1, 0];
      width = length;
      height = thickness;
    }

    encode = {
      enter: enter = {
        opacity: zero,
        x: zero,
        y: zero,
        width: encoder(width),
        height: encoder(height)
      },
      update: vegaUtil.extend({}, enter, {
        opacity: {value: 1},
        fill: {gradient: scale, start: start, stop: stop}
      }),
      exit: {
        opacity: zero
      }
    };
    addEncode(encode, 'stroke',      lookup('gradientStrokeColor', spec, config));
    addEncode(encode, 'strokeWidth', lookup('gradientStrokeWidth', spec, config));
    addEncode(encode, 'opacity',     lookup('gradientOpacity', spec, config), 'update');

    return guideMark(RectMark, LegendGradientRole, null, undefined, undefined, encode, userEncode);
  }

  function legendGradientDiscrete(spec, scale, config, userEncode, dataRef) {
    var zero = {value: 0},
        vertical = isVertical(spec, config.gradientDirection),
        thickness = gradientThickness(spec, config),
        length = gradientLength(spec, config),
        encode, enter, u, v, uu, vv, adjust = '';

    vertical
      ? (u = 'y', uu = 'y2', v = 'x', vv = 'width', adjust = '1-')
      : (u = 'x', uu = 'x2', v = 'y', vv = 'height');

    enter = {
      opacity: zero,
      fill: {scale: scale, field: Value}
    };
    enter[u]  = {signal: adjust + 'datum.' + Perc, mult: length};
    enter[v]  = zero;
    enter[uu] = {signal: adjust + 'datum.' + Perc2, mult: length};
    enter[vv] = encoder(thickness);

    encode = {
      enter: enter,
      update: vegaUtil.extend({}, enter, {opacity: {value: 1}}),
      exit: {opacity: zero}
    };
    addEncode(encode, 'stroke',      lookup('gradientStrokeColor', spec, config));
    addEncode(encode, 'strokeWidth', lookup('gradientStrokeWidth', spec, config));
    addEncode(encode, 'opacity',     lookup('gradientOpacity', spec, config), 'update');

    return guideMark(RectMark, LegendBandRole, null, Value, dataRef, encode, userEncode);
  }

  var alignExpr = 'datum.' + Perc + '<=0?"left"'
    + ':datum.' + Perc + '>=1?"right":"center"';

  var baselineExpr = 'datum.' + Perc + '<=0?"bottom"'
    + ':datum.' + Perc + '>=1?"top":"middle"';

  function legendGradientLabels(spec, config, userEncode, dataRef) {
    var zero = {value: 0},
        vertical = isVertical(spec, config.gradientDirection),
        thickness = encoder(gradientThickness(spec, config)),
        length = gradientLength(spec, config),
        overlap = lookup('labelOverlap', spec, config),
        encode, enter, update, u, v, adjust = '';

    encode = {
      enter: enter = {
        opacity: zero
      },
      update: update = {
        opacity: {value: 1},
        text: {field: Label}
      },
      exit: {
        opacity: zero
      }
    };
    addEncode(encode, 'fill',        lookup('labelColor', spec, config));
    addEncode(encode, 'font',        lookup('labelFont', spec, config));
    addEncode(encode, 'fontSize',    lookup('labelFontSize', spec, config));
    addEncode(encode, 'fontWeight',  lookup('labelFontWeight', spec, config));
    addEncode(encode, 'fillOpacity', lookup('labelOpacity', spec, config));
    addEncode(encode, 'limit',       value(spec.labelLimit, config.gradientLabelLimit));

    if (vertical) {
      enter.align = {value: 'left'};
      enter.baseline = update.baseline = {signal: baselineExpr};
      u = 'y'; v = 'x'; adjust = '1-';
    } else {
      enter.align = update.align = {signal: alignExpr};
      enter.baseline = {value: 'top'};
      u = 'x'; v = 'y';
    }

    enter[u] = update[u] = {signal: adjust + 'datum.' + Perc, mult: length};

    enter[v] = update[v] = thickness;
    thickness.offset = value(spec.labelOffset, config.gradientLabelOffset) || 0;

    spec = guideMark(TextMark, LegendLabelRole, GuideLabelStyle, Value, dataRef, encode, userEncode);
    if (overlap) spec.overlap = {method: overlap, order:  'datum.' + Index};
    return spec;
  }

  function guideGroup(role, style, name, dataRef, interactive, encode, marks, layout) {
    return {
      type: GroupMark,
      name: name,
      role: role,
      style: style,
      from: dataRef,
      interactive: interactive || false,
      encode: encode,
      marks: marks,
      layout: layout
    };
  }

  var zero = {value: 0};

  // userEncode is top-level, includes entries, symbols, labels
  function legendSymbolGroups(spec, config, userEncode, dataRef, columns) {
    var entries = userEncode.entries,
        interactive = !!(entries && entries.interactive),
        name = entries ? entries.name : undefined,
        height = lookup('clipHeight', spec, config),
        symbolOffset = lookup('symbolOffset', spec, config),
        valueRef = {data: 'value'},
        encode = {},
        xSignal = columns + '?' + 'datum.' + Offset + ':' + 'datum.' + Size,
        yEncode = height ? encoder(height) : {field: Size},
        index = 'datum.' + Index,
        ncols = 'max(1,' + columns + ')',
        enter, update, labelOffset, symbols, labels, nrows, sort;

    yEncode.mult = 0.5;

    // -- LEGEND SYMBOLS --
    encode = {
      enter:  enter = {
        opacity: zero,
        x: {signal: xSignal, mult: 0.5, offset: symbolOffset},
        y: yEncode
      },
      update: update = {
        opacity: {value: 1},
        x: enter.x,
        y: enter.y
      },
      exit: {
        opacity: zero
      }
    };

    if (!spec.fill) {
      addEncode(encode, 'fill',   config.symbolBaseFillColor);
      addEncode(encode, 'stroke', config.symbolBaseStrokeColor);
    }
    addEncode(encode, 'shape',       lookup('symbolType', spec, config));
    addEncode(encode, 'size',        lookup('symbolSize', spec, config));
    addEncode(encode, 'strokeWidth', lookup('symbolStrokeWidth', spec, config));
    addEncode(encode, 'fill',        lookup('symbolFillColor', spec, config));
    addEncode(encode, 'stroke',      lookup('symbolStrokeColor', spec, config));
    addEncode(encode, 'opacity',     lookup('symbolOpacity', spec, config), 'update');

    LegendScales.forEach(function(scale) {
      if (spec[scale]) {
        update[scale] = enter[scale] = {scale: spec[scale], field: Value};
      }
    });

    symbols = guideMark(
      SymbolMark, LegendSymbolRole, null,
      Value, valueRef, encode, userEncode.symbols
    );
    if (height) symbols.clip = true;

    // -- LEGEND LABELS --
    labelOffset = encoder(symbolOffset);
    labelOffset.offset = lookup('labelOffset', spec, config);

    encode = {
      enter:  enter = {
        opacity: zero,
        x: {signal: xSignal, offset: labelOffset},
        y: yEncode
      },
      update: update = {
        opacity: {value: 1},
        text: {field: Label},
        x: enter.x,
        y: enter.y
      },
      exit: {
        opacity: zero
      }
    };

    addEncode(encode, 'align',       lookup('labelAlign', spec, config));
    addEncode(encode, 'baseline',    lookup('labelBaseline', spec, config));
    addEncode(encode, 'fill',        lookup('labelColor', spec, config));
    addEncode(encode, 'font',        lookup('labelFont', spec, config));
    addEncode(encode, 'fontSize',    lookup('labelFontSize', spec, config));
    addEncode(encode, 'fontWeight',  lookup('labelFontWeight', spec, config));
    addEncode(encode, 'limit',       lookup('labelLimit', spec, config));
    addEncode(encode, 'fillOpacity', lookup('labelOpacity', spec, config));

    labels = guideMark(
      TextMark, LegendLabelRole, GuideLabelStyle,
      Value, valueRef, encode, userEncode.labels
    );

    // -- LEGEND ENTRY GROUPS --
    encode = {
      enter: {
        noBound: {value: true}, // ignore width/height in bounds calc
        width: zero,
        height: height ? encoder(height) : zero,
        opacity: zero
      },
      exit: {opacity: zero},
      update: update = {
        opacity: {value: 1},
        row: {signal: null},
        column: {signal: null}
      }
    };

    // annotate and sort groups to ensure correct ordering
    if (isVertical(spec, config.symbolDirection)) {
      nrows = 'ceil(item.mark.items.length/' + ncols + ')';
      update.row.signal = index + '%' + nrows;
      update.column.signal = 'floor(' + index + '/' + nrows + ')';
      sort = {field: ['row', index]};
    } else {
      update.row.signal = 'floor(' + index + '/' + ncols + ')';
      update.column.signal = index + '%' + ncols;
      sort = {field: index};
    }
    // handle zero column case (implies infinite columns)
    update.column.signal = columns + '?' + update.column.signal + ':' + index;

    // facet legend entries into sub-groups
    dataRef = {facet: {data: dataRef, name: 'value', groupby: Index}};

    spec = guideGroup(
      ScopeRole, null, name, dataRef, interactive,
      extendEncode(encode, entries, Skip), [symbols, labels]
    );
    spec.sort = sort;
    return spec;
  }

  function legendSymbolLayout(spec, config) {
    // layout parameters for legend entries
    return {
      align:   lookup('gridAlign', spec, config),
      center:  {row: true, column: false},
      columns: entryColumns(spec, config),
      padding: {
        row:    lookup('rowPadding', spec, config),
        column: lookup('columnPadding', spec, config)
      }
    };
  }

  function legendTitle(spec, config, userEncode, dataRef) {
    var zero = {value: 0},
        encode, enter;

    encode = {
      enter: enter = {
        opacity: zero,
        x: {field: {group: 'padding'}},
        y: {field: {group: 'padding'}}
      },
      update: {
        opacity: {value: 1},
        text: encoder(spec.title),
        x: enter.x,
        y: enter.y
      },
      exit: {
        opacity: zero
      }
    };
    addEncode(encode, 'align',       lookup('titleAlign', spec, config));
    addEncode(encode, 'baseline',    lookup('titleBaseline', spec, config));
    addEncode(encode, 'fill',        lookup('titleColor', spec, config));
    addEncode(encode, 'font',        lookup('titleFont', spec, config));
    addEncode(encode, 'fontSize',    lookup('titleFontSize', spec, config));
    addEncode(encode, 'fontWeight',  lookup('titleFontWeight', spec, config));
    addEncode(encode, 'limit',       lookup('titleLimit', spec, config));
    addEncode(encode, 'fillOpacity', lookup('titleOpacity', spec, config));

    return guideMark(TextMark, LegendTitleRole, GuideTitleStyle, null, dataRef, encode, userEncode);
  }

  function clip(clip, scope) {
    var expr;

    if (vegaUtil.isObject(clip)) {
      if (clip.signal) {
        expr = clip.signal;
      } else if (clip.path) {
        expr = 'pathShape(' + param(clip.path) + ')';
      } else if (clip.sphere) {
        expr = 'geoShape(' + param(clip.sphere) + ', {type: "Sphere"})';
      }
    }

    return expr
      ? scope.signalRef(expr)
      : !!clip;
  }

  function param(value) {
    return vegaUtil.isObject(value) && value.signal
      ? value.signal
      : vegaUtil.stringValue(value);
  }

  function role(spec) {
    var role = spec.role || '';
    return (!role.indexOf('axis') || !role.indexOf('legend'))
      ? role
      : spec.type === GroupMark ? ScopeRole : (role || MarkRole);
  }

  function definition(spec) {
    return {
      marktype:    spec.type,
      name:        spec.name || undefined,
      role:        spec.role || role(spec),
      zindex:      +spec.zindex || undefined
    };
  }

  function interactive(spec, scope) {
    return spec && spec.signal ? scope.signalRef(spec.signal)
      : spec === false ? false
      : true;
  }

  /**
   * Parse a data transform specification.
   */
  function parseTransform(spec, scope) {
    var def = vegaDataflow.definition(spec.type);
    if (!def) vegaUtil.error('Unrecognized transform type: ' + vegaUtil.stringValue(spec.type));

    var t = entry(def.type.toLowerCase(), null, parseParameters(def, spec, scope));
    if (spec.signal) scope.addSignal(spec.signal, scope.proxy(t));
    t.metadata = def.metadata || {};

    return t;
  }

  /**
   * Parse all parameters of a data transform.
   */
  function parseParameters(def, spec, scope) {
    var params = {}, pdef, i, n;
    for (i=0, n=def.params.length; i<n; ++i) {
      pdef = def.params[i];
      params[pdef.name] = parseParameter$1(pdef, spec, scope);
    }
    return params;
  }

  /**
   * Parse a data transform parameter.
   */
  function parseParameter$1(def, spec, scope) {
    var type = def.type,
        value$$1 = spec[def.name];

    if (type === 'index') {
      return parseIndexParameter(def, spec, scope);
    } else if (value$$1 === undefined) {
      if (def.required) {
        vegaUtil.error('Missing required ' + vegaUtil.stringValue(spec.type)
            + ' parameter: ' + vegaUtil.stringValue(def.name));
      }
      return;
    } else if (type === 'param') {
      return parseSubParameters(def, spec, scope);
    } else if (type === 'projection') {
      return scope.projectionRef(spec[def.name]);
    }

    return def.array && !isSignal(value$$1)
      ? value$$1.map(function(v) { return parameterValue(def, v, scope); })
      : parameterValue(def, value$$1, scope);
  }

  /**
   * Parse a single parameter value.
   */
  function parameterValue(def, value$$1, scope) {
    var type = def.type;

    if (isSignal(value$$1)) {
      return isExpr(type) ? vegaUtil.error('Expression references can not be signals.')
           : isField(type) ? scope.fieldRef(value$$1)
           : isCompare(type) ? scope.compareRef(value$$1)
           : scope.signalRef(value$$1.signal);
    } else {
      var expr = def.expr || isField(type);
      return expr && outerExpr(value$$1) ? scope.exprRef(value$$1.expr, value$$1.as)
           : expr && outerField(value$$1) ? fieldRef(value$$1.field, value$$1.as)
           : isExpr(type) ? expression(value$$1, scope)
           : isData(type) ? ref(scope.getData(value$$1).values)
           : isField(type) ? fieldRef(value$$1)
           : isCompare(type) ? scope.compareRef(value$$1)
           : value$$1;
    }
  }

  /**
   * Parse parameter for accessing an index of another data set.
   */
  function parseIndexParameter(def, spec, scope) {
    if (!vegaUtil.isString(spec.from)) {
      vegaUtil.error('Lookup "from" parameter must be a string literal.');
    }
    return scope.getData(spec.from).lookupRef(scope, spec.key);
  }

  /**
   * Parse a parameter that contains one or more sub-parameter objects.
   */
  function parseSubParameters(def, spec, scope) {
    var value$$1 = spec[def.name];

    if (def.array) {
      if (!vegaUtil.isArray(value$$1)) { // signals not allowed!
        vegaUtil.error('Expected an array of sub-parameters. Instead: ' + vegaUtil.stringValue(value$$1));
      }
      return value$$1.map(function(v) {
        return parseSubParameter(def, v, scope);
      });
    } else {
      return parseSubParameter(def, value$$1, scope);
    }
  }

  /**
   * Parse a sub-parameter object.
   */
  function parseSubParameter(def, value$$1, scope) {
    var params, pdef, k, i, n;

    // loop over defs to find matching key
    for (i=0, n=def.params.length; i<n; ++i) {
      pdef = def.params[i];
      for (k in pdef.key) {
        if (pdef.key[k] !== value$$1[k]) { pdef = null; break; }
      }
      if (pdef) break;
    }
    // raise error if matching key not found
    if (!pdef) vegaUtil.error('Unsupported parameter: ' + vegaUtil.stringValue(value$$1));

    // parse params, create Params transform, return ref
    params = vegaUtil.extend(parseParameters(pdef, value$$1, scope), pdef.key);
    return ref(scope.add(Params(params)));
  }

  // -- Utilities -----

  function outerExpr(_) {
    return _ && _.expr;
  }

  function outerField(_) {
    return _ && _.field;
  }

  function isData(_) {
    return _ === 'data';
  }

  function isExpr(_) {
    return _ === 'expr';
  }

  function isField(_) {
    return _ === 'field';
  }

  function isCompare(_) {
    return _ === 'compare'
  }

  function parseData(from, group, scope) {
    var facet, key, op, dataRef, parent;

    // if no source data, generate singleton datum
    if (!from) {
      dataRef = ref(scope.add(Collect(null, [{}])));
    }

    // if faceted, process facet specification
    else if (facet = from.facet) {
      if (!group) vegaUtil.error('Only group marks can be faceted.');

      // use pre-faceted source data, if available
      if (facet.field != null) {
        dataRef = parent = getDataRef(facet, scope);
      } else {
        // generate facet aggregates if no direct data specification
        if (!from.data) {
          op = parseTransform(vegaUtil.extend({
            type:    'aggregate',
            groupby: vegaUtil.array(facet.groupby)
          }, facet.aggregate), scope);
          op.params.key = scope.keyRef(facet.groupby);
          op.params.pulse = getDataRef(facet, scope);
          dataRef = parent = ref(scope.add(op));
        } else {
          parent = ref(scope.getData(from.data).aggregate);
        }

        key = scope.keyRef(facet.groupby, true);
      }
    }

    // if not yet defined, get source data reference
    if (!dataRef) {
      dataRef = getDataRef(from, scope);
    }

    return {
      key: key,
      pulse: dataRef,
      parent: parent
    };
  }

  function getDataRef(from, scope) {
    return from.$ref ? from
      : from.data && from.data.$ref ? from.data
      : ref(scope.getData(from.data).output);
  }

  function DataScope(scope, input, output, values, aggr) {
    this.scope = scope;   // parent scope object
    this.input = input;   // first operator in pipeline (tuple input)
    this.output = output; // last operator in pipeline (tuple output)
    this.values = values; // operator for accessing tuples (but not tuple flow)

    // last aggregate in transform pipeline
    this.aggregate = aggr;

    // lookup table of field indices
    this.index = {};
  }

  DataScope.fromEntries = function(scope, entries) {
    var n = entries.length,
        i = 1,
        input  = entries[0],
        values = entries[n-1],
        output = entries[n-2],
        aggr = null;

    // add operator entries to this scope, wire up pulse chain
    scope.add(entries[0]);
    for (; i<n; ++i) {
      entries[i].params.pulse = ref(entries[i-1]);
      scope.add(entries[i]);
      if (entries[i].type === 'aggregate') aggr = entries[i];
    }

    return new DataScope(scope, input, output, values, aggr);
  };

  var prototype = DataScope.prototype;

  prototype.countsRef = function(scope, field, sort) {
    var ds = this,
        cache = ds.counts || (ds.counts = {}),
        k = fieldKey(field), v, a, p;

    if (k != null) {
      scope = ds.scope;
      v = cache[k];
    }

    if (!v) {
      p = {
        groupby: scope.fieldRef(field, 'key'),
        pulse: ref(ds.output)
      };
      if (sort && sort.field) addSortField(scope, p, sort);
      a = scope.add(Aggregate(p));
      v = scope.add(Collect({pulse: ref(a)}));
      v = {agg: a, ref: ref(v)};
      if (k != null) cache[k] = v;
    } else if (sort && sort.field) {
      addSortField(scope, v.agg.params, sort);
    }

    return v.ref;
  };

  function fieldKey(field) {
    return vegaUtil.isString(field) ? field : null;
  }

  function addSortField(scope, p, sort) {
    var as = aggrField(sort.op, sort.field), s;

    if (p.ops) {
      for (var i=0, n=p.as.length; i<n; ++i) {
        if (p.as[i] === as) return;
      }
    } else {
      p.ops = ['count'];
      p.fields = [null];
      p.as = ['count'];
    }
    if (sort.op) {
      p.ops.push((s=sort.op.signal) ? scope.signalRef(s) : sort.op);
      p.fields.push(scope.fieldRef(sort.field));
      p.as.push(as);
    }
  }

  function cache(scope, ds, name, optype, field, counts, index) {
    var cache = ds[name] || (ds[name] = {}),
        sort = sortKey(counts),
        k = fieldKey(field), v, op;

    if (k != null) {
      scope = ds.scope;
      k = k + (sort ? '|' + sort : '');
      v = cache[k];
    }

    if (!v) {
      var params = counts
        ? {field: keyFieldRef, pulse: ds.countsRef(scope, field, counts)}
        : {field: scope.fieldRef(field), pulse: ref(ds.output)};
      if (sort) params.sort = scope.sortRef(counts);
      op = scope.add(entry(optype, undefined, params));
      if (index) ds.index[field] = op;
      v = ref(op);
      if (k != null) cache[k] = v;
    }
    return v;
  }

  prototype.tuplesRef = function() {
    return ref(this.values);
  };

  prototype.extentRef = function(scope, field) {
    return cache(scope, this, 'extent', 'extent', field, false);
  };

  prototype.domainRef = function(scope, field) {
    return cache(scope, this, 'domain', 'values', field, false);
  };

  prototype.valuesRef = function(scope, field, sort) {
    return cache(scope, this, 'vals', 'values', field, sort || true);
  };

  prototype.lookupRef = function(scope, field) {
    return cache(scope, this, 'lookup', 'tupleindex', field, false);
  };

  prototype.indataRef = function(scope, field) {
    return cache(scope, this, 'indata', 'tupleindex', field, true, true);
  };

  function parseFacet(spec, scope, group) {
    var facet = spec.from.facet,
        name = facet.name,
        data = getDataRef(facet, scope),
        subscope, source, values, op;

    if (!facet.name) {
      vegaUtil.error('Facet must have a name: ' + vegaUtil.stringValue(facet));
    }
    if (!facet.data) {
      vegaUtil.error('Facet must reference a data set: ' + vegaUtil.stringValue(facet));
    }

    if (facet.field) {
      op = scope.add(PreFacet({
        field: scope.fieldRef(facet.field),
        pulse: data
      }));
    } else if (facet.groupby) {
      op = scope.add(Facet({
        key:   scope.keyRef(facet.groupby),
        group: ref(scope.proxy(group.parent)),
        pulse: data
      }));
    } else {
      vegaUtil.error('Facet must specify groupby or field: ' + vegaUtil.stringValue(facet));
    }

    // initialize facet subscope
    subscope = scope.fork();
    source = subscope.add(Collect());
    values = subscope.add(Sieve({pulse: ref(source)}));
    subscope.addData(name, new DataScope(subscope, source, source, values));
    subscope.addSignal('parent', null);

    // parse faceted subflow
    op.params.subflow = {
      $subflow: parseSpec(spec, subscope).toRuntime()
    };
  }

  function parseSubflow(spec, scope, input) {
    var op = scope.add(PreFacet({pulse: input.pulse})),
        subscope = scope.fork();

    subscope.add(Sieve());
    subscope.addSignal('parent', null);

    // parse group mark subflow
    op.params.subflow = {
      $subflow: parseSpec(spec, subscope).toRuntime()
    };
  }

  function parseTrigger(spec, scope, name) {
    var remove = spec.remove,
        insert = spec.insert,
        toggle = spec.toggle,
        modify = spec.modify,
        values = spec.values,
        op = scope.add(operator()),
        update, expr;

    update = 'if(' + spec.trigger + ',modify("'
      + name + '",'
      + [insert, remove, toggle, modify, values]
          .map(function(_) { return _ == null ? 'null' : _; })
          .join(',')
      + '),0)';

    expr = expression(update, scope);
    op.update = expr.$expr;
    op.params = expr.$params;
  }

  function parseMark(spec, scope) {
    var role$$1 = role(spec),
        group = spec.type === GroupMark,
        facet = spec.from && spec.from.facet,
        layout = spec.layout || role$$1 === ScopeRole || role$$1 === FrameRole,
        nested = role$$1 === MarkRole || layout || facet,
        overlap = spec.overlap,
        ops, op, input, store, bound, render, sieve, name,
        joinRef, markRef, encodeRef, layoutRef, boundRef;

    // resolve input data
    input = parseData(spec.from, group, scope);

    // data join to map tuples to visual items
    op = scope.add(DataJoin({
      key:   input.key || (spec.key ? fieldRef(spec.key) : undefined),
      pulse: input.pulse,
      clean: !group
    }));
    joinRef = ref(op);

    // collect visual items
    op = store = scope.add(Collect({pulse: joinRef}));

    // connect visual items to scenegraph
    op = scope.add(Mark({
      markdef:     definition(spec),
      interactive: interactive(spec.interactive, scope),
      clip:        clip(spec.clip, scope),
      context:     {$context: true},
      groups:      scope.lookup(),
      parent:      scope.signals.parent ? scope.signalRef('parent') : null,
      index:       scope.markpath(),
      pulse:       ref(op)
    }));
    markRef = ref(op);

    // add visual encoders
    op = scope.add(Encode(
      encoders(spec.encode, spec.type, role$$1, spec.style, scope, {pulse: markRef})
    ));

    // monitor parent marks to propagate changes
    op.params.parent = scope.encode();

    // add post-encoding transforms, if defined
    if (spec.transform) {
      spec.transform.forEach(function(_) {
        var tx = parseTransform(_, scope);
        if (tx.metadata.generates || tx.metadata.changes) {
          vegaUtil.error('Mark transforms should not generate new data.');
        }
        tx.params.pulse = ref(op);
        scope.add(op = tx);
      });
    }

    // if item sort specified, perform post-encoding
    if (spec.sort) {
      op = scope.add(SortItems({
        sort:  scope.compareRef(spec.sort, true), // stable sort
        pulse: ref(op)
      }));
    }

    encodeRef = ref(op);

    // add view layout operator if needed
    if (facet || layout) {
      layout = scope.add(ViewLayout({
        layout:       scope.objectProperty(spec.layout),
        legendMargin: scope.config.legendMargin,
        mark:         markRef,
        pulse:        encodeRef
      }));
      layoutRef = ref(layout);
    }

    // compute bounding boxes
    bound = scope.add(Bound({mark: markRef, pulse: layoutRef || encodeRef}));
    boundRef = ref(bound);

    // if group mark, recurse to parse nested content
    if (group) {
      // juggle layout & bounds to ensure they run *after* any faceting transforms
      if (nested) { ops = scope.operators; ops.pop(); if (layout) ops.pop(); }

      scope.pushState(encodeRef, layoutRef || boundRef, joinRef);
      facet ? parseFacet(spec, scope, input)          // explicit facet
          : nested ? parseSubflow(spec, scope, input) // standard mark group
          : parseSpec(spec, scope); // guide group, we can avoid nested scopes
      scope.popState();

      if (nested) { if (layout) ops.push(layout); ops.push(bound); }
    }

    // if requested, add overlap removal transform
    if (overlap) {
      boundRef = parseOverlap(overlap, boundRef, scope);
    }

    // render / sieve items
    render = scope.add(Render({pulse: boundRef}));
    sieve = scope.add(Sieve({pulse: ref(render)}, undefined, scope.parent()));

    // if mark is named, make accessible as reactive geometry
    // add trigger updates if defined
    if (spec.name != null) {
      name = spec.name;
      scope.addData(name, new DataScope(scope, store, render, sieve));
      if (spec.on) spec.on.forEach(function(on) {
        if (on.insert || on.remove || on.toggle) {
          vegaUtil.error('Marks only support modify triggers.');
        }
        parseTrigger(on, scope, name);
      });
    }
  }

  function parseOverlap(overlap, source, scope) {
    var method = overlap.method,
        bound = overlap.bound, tol;

    var params = {
      method: isSignal(method) ? scope.signalRef(method.signal) : method,
      pulse:  source
    };

    if (overlap.order) {
      params.sort = scope.compareRef({field: overlap.order});
    }

    if (bound) {
      tol = bound.tolerance;
      params.boundTolerance = isSignal(tol) ? scope.signalRef(tol.signal) : +tol;
      params.boundScale = scope.scaleRef(bound.scale);
      params.boundOrient = bound.orient;
    }

    return ref(scope.add(Overlap(params)));
  }

  function parseLegend(spec, scope) {
    var config = scope.config.legend,
        encode = spec.encode || {},
        legendEncode = encode.legend || {},
        name = legendEncode.name || undefined,
        interactive = legendEncode.interactive,
        style = legendEncode.style,
        entryEncode, entryLayout, params, children,
        type, datum, dataRef, entryRef, group;

    // resolve 'canonical' scale name
    var scale = LegendScales.reduce(function(a, b) { return a || spec[b]; }, 0);
    if (!scale) vegaUtil.error('Missing valid scale for legend.');

    // resolve legend type (symbol, gradient, or discrete gradient)
    type = legendType(spec, scope.scaleType(scale));

    // single-element data source for legend group
    datum = {
      orient: lookup('orient', spec, config),
      title:  spec.title != null,
      type:   type
    };
    dataRef = ref(scope.add(Collect(null, [datum])));

    // encoding properties for legend group
    legendEncode = extendEncode(
      buildLegendEncode(spec, config),legendEncode, Skip
    );

    // encoding properties for legend entry sub-group
    entryEncode = {enter: {x: {value: 0}, y: {value: 0}}};

    // data source for legend values
    entryRef = ref(scope.add(LegendEntries(params = {
      type:   type,
      scale:  scope.scaleRef(scale),
      count:  scope.objectProperty(spec.tickCount),
      values: scope.objectProperty(spec.values),
      formatSpecifier: scope.property(spec.format)
    })));

    // continuous gradient legend
    if (type === Gradient) {
      children = [
        legendGradient(spec, scale, config, encode.gradient),
        legendGradientLabels(spec, config, encode.labels, entryRef)
      ];
      // adjust default tick count based on the gradient length
      params.count = params.count || scope.signalRef(
        'max(2,2*floor((' + deref(gradientLength(spec, config)) + ')/100))'
      );
    }

    // discrete gradient legend
    else if (type === Discrete) {
      children = [
        legendGradientDiscrete(spec, scale, config, encode.gradient, entryRef),
        legendGradientLabels(spec, config, encode.labels, entryRef)
      ];
    }

    // symbol legend
    else {
      // determine legend symbol group layout
      entryLayout = legendSymbolLayout(spec, config);
      children = [
        legendSymbolGroups(spec, config, encode, entryRef, deref(entryLayout.columns))
      ];
      // pass symbol size information to legend entry generator
      params.size = sizeExpression(spec, scope, children[0].marks);
    }

    // generate legend marks
    children = [
      guideGroup(LegendEntryRole, null, null, dataRef, interactive,
                 entryEncode, children, entryLayout)
    ];

    // include legend title if defined
    if (datum.title) {
      children.push(legendTitle(spec, config, encode.title, dataRef));
    }

    // build legend specification
    group = guideGroup(LegendRole, style, name, dataRef, interactive, legendEncode, children);
    if (spec.zindex) group.zindex = spec.zindex;

    // parse legend specification
    return parseMark(group, scope);
  }

  function legendType(spec, scaleType) {
    var type = spec.type || Symbols;

    if (!spec.type && scaleCount(spec) === 1 && (spec.fill || spec.stroke)) {
      type = isContinuous(scaleType) ? Gradient
        : isDiscretizing(scaleType) ? Discrete
        : Symbols;
    }

    return type !== Gradient ? type
      : isDiscretizing(scaleType) ? Discrete
      : Gradient;
  }

  function scaleCount(spec) {
    return LegendScales.reduce(function(count, type) {
      return count + (spec[type] ? 1 : 0);
    }, 0);
  }

  function buildLegendEncode(spec, config) {
    var encode = {enter: {}, update: {}};

    addEncode(encode, 'offset',       lookup('offset', spec, config));
    addEncode(encode, 'padding',      lookup('padding', spec, config));
    addEncode(encode, 'titlePadding', lookup('titlePadding', spec, config));
    addEncode(encode, 'fill',         lookup('fillColor', spec, config));
    addEncode(encode, 'stroke',       lookup('strokeColor', spec, config));
    addEncode(encode, 'strokeWidth',  lookup('strokeWidth', spec, config));
    addEncode(encode, 'cornerRadius', lookup('cornerRadius', spec, config));
    addEncode(encode, 'strokeDash',   config.strokeDash);

    return encode;
  }

  function sizeExpression(spec, scope, marks) {
    var fontSize, size, strokeWidth, expr;

    strokeWidth = getEncoding('strokeWidth', marks[0].encode);

    size = spec.size ? 'scale("' + spec.size + '",datum)'
      : getEncoding('size', marks[0].encode, scope);

    fontSize = getFontSize(marks[1].encode, scope, GuideLabelStyle);

    expr = 'max('
      + 'ceil(sqrt(' + deref(size) + ')+' + deref(strokeWidth) + '),'
      + deref(fontSize)
      + ')';

    return expression(expr, scope);
  }

  function getFontSize(encode, scope, style) {
    return getEncoding('fontSize', encode) || getStyle('fontSize', scope, style);
  }

  function anchorExpr(startValue, endValue, centerValue) {
    return 'item.anchor==="' + Start + '"?' + startValue
      + ':item.anchor==="' + End + '"?' + endValue
      + ':' + centerValue;
  }

  // title text alignment
  var alignExpr$1 = anchorExpr(
    vegaUtil.stringValue(Left), vegaUtil.stringValue(Right), vegaUtil.stringValue(Center)
  );

  // multiplication factor for anchor positioning
  var multExpr = anchorExpr(
    '+(item.orient==="' + Right + '")',
    '+(item.orient!=="' + Left + '")',
    '0.5'
  );

  function parseTitle(spec, scope) {
    spec = vegaUtil.isString(spec) ? {text: spec} : spec;

    var config = scope.config.title,
        encode = vegaUtil.extend({}, spec.encode),
        datum, dataRef, title;

    // single-element data source for group title
    datum = {
      orient: lookup('orient', spec, config)
    };
    dataRef = ref(scope.add(Collect(null, [datum])));

    // build title specification
    encode.name = spec.name;
    encode.interactive = spec.interactive;
    title = buildTitle(spec, config, encode, dataRef);
    if (spec.zindex) title.zindex = spec.zindex;

    // parse title specification
    return parseMark(title, scope);
  }

  function buildTitle(spec, config, userEncode, dataRef) {
    var zero = {value: 0},
        title = spec.text,
        orient = lookup('orient', spec, config),
        anchor = lookup('anchor', spec, config),
        sign = (orient === Left || orient === Top) ? -1 : 1,
        horizontal = (orient === Top || orient === Bottom),
        extent = {group: (horizontal ? 'width' : 'height')},
        encode, enter, update, pos, opp;

    // title positioning along orientation axis
    pos = {field: extent, mult: {signal: multExpr}};

    // title baseline position
    opp = sign < 0 ? zero
      : horizontal ? {field: {group: 'height'}}
      : {field: {group: 'width'}};

    encode = {
      enter: enter = {
        opacity: zero
      },
      update: update = {
        opacity: {value: 1},
        text:   encoder(title),
        anchor: encoder(anchor),
        orient: encoder(orient),
        extent: {field: extent},
        align:  {signal: alignExpr$1}
      },
      exit: {
        opacity: zero
      }
    };

    if (horizontal) {
      update.x = pos;
      update.y = opp;
      enter.angle = zero;
      enter.baseline = {value: orient === Top ? Bottom : Top};
    } else {
      update.x = opp;
      update.y = pos;
      enter.angle = {value: sign * 90};
      enter.baseline = {value: Bottom};
    }

    addEncode(encode, 'align',      lookup('align', spec, config), 'update');
    addEncode(encode, 'angle',      lookup('angle', spec, config));
    addEncode(encode, 'baseline',   lookup('baseline', spec, config));
    addEncode(encode, 'fill',       lookup('color', spec, config));
    addEncode(encode, 'font',       lookup('font', spec, config));
    addEncode(encode, 'fontSize',   lookup('fontSize', spec, config));
    addEncode(encode, 'fontWeight', lookup('fontWeight', spec, config));
    addEncode(encode, 'frame',      lookup('frame', spec, config));
    addEncode(encode, 'limit',      lookup('limit', spec, config));
    addEncode(encode, 'offset',     lookup('offset', spec, config) || 0);

    return guideMark(TextMark, TitleRole, spec.style || GroupTitleStyle,
                     null, dataRef, encode, userEncode);
  }

  function parseData$1(data, scope) {
    var transforms = [];

    if (data.transform) {
      data.transform.forEach(function(tx) {
        transforms.push(parseTransform(tx, scope));
      });
    }

    if (data.on) {
      data.on.forEach(function(on) {
        parseTrigger(on, scope, data.name);
      });
    }

    scope.addDataPipeline(data.name, analyze(data, scope, transforms));
  }

  /**
   * Analyze a data pipeline, add needed operators.
   */
  function analyze(data, scope, ops) {
    // POSSIBLE TODOs:
    // - error checking for treesource on tree operators (BUT what if tree is upstream?)
    // - this is local analysis, perhaps some tasks better for global analysis...

    var output = [],
        source = null,
        modify = false,
        generate = false,
        upstream, i, n, t, m;

    if (data.values) {
      // hard-wired input data set
      output.push(source = collect({
        $ingest: data.values,
        $format: data.format
      }));
    } else if (data.url) {
      // load data from external source
      // if either url or format has signal, use dynamic loader
      // otherwise, request load upon dataflow init
      source = (hasSignal(data.url) || hasSignal(data.format))
        ? {$load: ref(scope.add(load(scope, data, source)))}
        : {$request: data.url, $format: data.format};
      output.push(source = collect(source));
    } else if (data.source) {
      // derives from one or more other data sets
      source = upstream = vegaUtil.array(data.source).map(function(d) {
        return ref(scope.getData(d).output);
      });
      output.push(null); // populate later
    }

    // scan data transforms, add collectors as needed
    for (i=0, n=ops.length; i<n; ++i) {
      t = ops[i];
      m = t.metadata;

      if (!source && !m.source) {
        output.push(source = collect());
      }
      output.push(t);

      if (m.generates) generate = true;
      if (m.modifies && !generate) modify = true;

      if (m.source) source = t;
      else if (m.changes) source = null;
    }

    if (upstream) {
      n = upstream.length - 1;
      output[0] = Relay({
        derive: modify,
        pulse: n ? upstream : upstream[0]
      });
      if (modify || n) {
        // collect derived and multi-pulse tuples
        output.splice(1, 0, collect());
      }
    }

    if (!source) output.push(collect());
    output.push(Sieve({}));
    return output;
  }

  function collect(values) {
    var s = Collect({}, values);
    s.metadata = {source: true};
    return s;
  }

  function load(scope, data) {
    return Load({
      url:    scope.property(data.url),
      format: scope.objectProperty(data.format)
    });
  }

  function axisConfig(spec, scope) {
    var config = scope.config,
        orient = spec.orient,
        xy = (orient === Top || orient === Bottom) ? config.axisX : config.axisY,
        or = config['axis' + orient[0].toUpperCase() + orient.slice(1)],
        band = scope.scaleType(spec.scale) === 'band' && config.axisBand;

    return (xy || or || band)
      ? vegaUtil.extend({}, config.axis, xy, or, band)
      : config.axis;
  }

  function axisDomain(spec, config, userEncode, dataRef) {
    var orient = spec.orient,
        zero = {value: 0},
        encode, enter, update, u, u2, v;

    encode = {
      enter: enter = {
        opacity: zero
      },
      update: update = {
        opacity: {value: 1}
      },
      exit: {
        opacity: zero
      }
    };
    addEncode(encode, 'stroke',        lookup('domainColor', spec, config));
    addEncode(encode, 'strokeWidth',   lookup('domainWidth', spec, config));
    addEncode(encode, 'strokeOpacity', lookup('domainOpacity', spec, config));

    if (orient === Top || orient === Bottom) {
      u = 'x';
      v = 'y';
    } else {
      u = 'y';
      v = 'x';
    }
    u2 = u + '2';

    enter[v] = zero;
    update[u] = enter[u] = position(spec, 0);
    update[u2] = enter[u2] = position(spec, 1);

    return guideMark(RuleMark, AxisDomainRole, null, null, dataRef, encode, userEncode);
  }

  function position(spec, pos) {
    return {scale: spec.scale, range: pos};
  }

  function axisGrid(spec, config, userEncode, dataRef) {
    var orient = spec.orient,
        vscale = spec.gridScale,
        sign = (orient === Left || orient === Top) ? 1 : -1,
        offset = sign * spec.offset || 0,
        zero = {value: 0},
        encode, enter, exit, update, tickPos, u, v, v2, s;

    encode = {
      enter: enter = {
        opacity: zero
      },
      update: update = {
        opacity: {value: 1}
      },
      exit: exit = {
        opacity: zero
      }
    };
    addEncode(encode, 'stroke',        lookup('gridColor', spec, config));
    addEncode(encode, 'strokeDash',    lookup('gridDash', spec, config));
    addEncode(encode, 'strokeOpacity', lookup('gridOpacity', spec, config));
    addEncode(encode, 'strokeWidth',   lookup('gridWidth', spec, config));

    tickPos = {
      scale:  spec.scale,
      field:  Value,
      band:   lookup('bandPosition', spec, config),
      round:  lookup('tickRound', spec, config),
      extra:  lookup('tickExtra', spec, config),
      offset: lookup('tickOffset', spec, config)
    };

    if (orient === Top || orient === Bottom) {
      u = 'x';
      v = 'y';
      s = 'height';
    } else {
      u = 'y';
      v = 'x';
      s = 'width';
    }
    v2 = v + '2';

    update[u] = enter[u] = exit[u] = tickPos;

    if (vscale) {
      enter[v] = {scale: vscale, range: 0, mult: sign, offset: offset};
      update[v2] = enter[v2] = {scale: vscale, range: 1, mult: sign, offset: offset};
    } else {
      enter[v] = {value: offset};
      update[v2] = enter[v2] = {signal: s, mult: sign, offset: offset};
    }

    return guideMark(RuleMark, AxisGridRole, null, Value, dataRef, encode, userEncode);
  }

  function axisTicks(spec, config, userEncode, dataRef, size) {
    var orient = spec.orient,
        sign = (orient === Left || orient === Top) ? -1 : 1,
        zero = {value: 0},
        encode, enter, exit, update, tickSize, tickPos;

    encode = {
      enter: enter = {
        opacity: zero
      },
      update: update = {
        opacity: {value: 1}
      },
      exit: exit = {
        opacity: zero
      }
    };
    addEncode(encode, 'stroke',        lookup('tickColor', spec, config));
    addEncode(encode, 'strokeOpacity', lookup('tickOpacity', spec, config));
    addEncode(encode, 'strokeWidth',   lookup('tickWidth', spec, config));

    tickSize = encoder(size);
    tickSize.mult = sign;

    tickPos = {
      scale:  spec.scale,
      field:  Value,
      band:   lookup('bandPosition', spec, config),
      round:  lookup('tickRound', spec, config),
      extra:  lookup('tickExtra', spec, config),
      offset: lookup('tickOffset', spec, config)
    };

    if (orient === Top || orient === Bottom) {
      update.y = enter.y = zero;
      update.y2 = enter.y2 = tickSize;
      update.x = enter.x = exit.x = tickPos;
    } else {
      update.x = enter.x = zero;
      update.x2 = enter.x2 = tickSize;
      update.y = enter.y = exit.y = tickPos;
    }

    return guideMark(RuleMark, AxisTickRole, null, Value, dataRef, encode, userEncode);
  }

  function flushExpr(scale, threshold, a, b, c) {
    return {
      signal: 'flush(range("' + scale + '"), '
        + 'scale("' + scale + '", datum.value), '
        + threshold + ',' + a + ',' + b + ',' + c + ')'
    };
  }

  function axisLabels(spec, config, userEncode, dataRef, size) {
    var orient = spec.orient,
        sign = (orient === Left || orient === Top) ? -1 : 1,
        isXAxis = (orient === Top || orient === Bottom),
        scale = spec.scale,
        flush = deref(lookup('labelFlush', spec, config)),
        flushOffset = deref(lookup('labelFlushOffset', spec, config)),
        flushOn = flush === 0 || !!flush,
        labelAlign = lookup('labelAlign', spec, config),
        labelBaseline = lookup('labelBaseline', spec, config),
        zero = {value: 0},
        encode, enter, tickSize, tickPos, align, baseline, offset, bound, overlap;

    tickSize = encoder(size);
    tickSize.mult = sign;
    tickSize.offset = encoder(lookup('labelPadding', spec, config) || 0);
    tickSize.offset.mult = sign;

    tickPos = {
      scale:  scale,
      field:  Value,
      band:   0.5,
      offset: lookup('tickOffset', spec, config)
    };

    if (isXAxis) {
      align = labelAlign || (flushOn
        ? flushExpr(scale, flush, '"left"', '"right"', '"center"')
        : 'center');
      baseline = labelBaseline || (orient === Top ? 'bottom' : 'top');
      offset = !labelAlign;
    } else {

      align = labelAlign || (orient === Right ? 'left' : 'right');
      baseline = labelBaseline || (flushOn
        ? flushExpr(scale, flush, '"top"', '"bottom"', '"middle"')
        : 'middle');
      offset = !labelBaseline;
    }

    offset = offset && flushOn && flushOffset
      ? flushExpr(scale, flush, '-' + flushOffset, flushOffset, 0)
      : null;

    encode = {
      enter: enter = {
        opacity: zero,
        x: isXAxis ? tickPos : tickSize,
        y: isXAxis ? tickSize : tickPos
      },
      update: {
        opacity: {value: 1},
        text: {field: Label},
        x: enter.x,
        y: enter.y
      },
      exit: {
        opacity: zero,
        x: enter.x,
        y: enter.y
      }
    };

    addEncode(encode, isXAxis ? 'dx' : 'dy', offset);
    addEncode(encode, 'align',       align);
    addEncode(encode, 'baseline',    baseline);
    addEncode(encode, 'angle',       lookup('labelAngle', spec, config));
    addEncode(encode, 'fill',        lookup('labelColor', spec, config));
    addEncode(encode, 'font',        lookup('labelFont', spec, config));
    addEncode(encode, 'fontSize',    lookup('labelFontSize', spec, config));
    addEncode(encode, 'fontWeight',  lookup('labelFontWeight', spec, config));
    addEncode(encode, 'limit',       lookup('labelLimit', spec, config));
    addEncode(encode, 'fillOpacity', lookup('labelOpacity', spec, config));
    bound   = lookup('labelBound', spec, config);
    overlap = lookup('labelOverlap', spec, config);

    spec = guideMark(TextMark, AxisLabelRole, GuideLabelStyle, Value, dataRef, encode, userEncode);

    // if overlap method or bound defined, request label overlap removal
    if (overlap || bound) {
      spec.overlap = {
        method: overlap,
        order:  'datum.index',
        bound:  bound ? {scale: scale, orient: orient, tolerance: bound} : null
      };
    }

    return spec;
  }

  function axisTitle(spec, config, userEncode, dataRef) {
    var orient = spec.orient,
        sign = (orient === Left || orient === Top) ? -1 : 1,
        horizontal = (orient === Top || orient === Bottom),
        zero = {value: 0},
        encode, enter, update, titlePos;

    encode = {
      enter: enter = {
        opacity: zero
      },
      update: update = {
        opacity: {value: 1},
        text: encoder(spec.title)
      },
      exit: {
        opacity: zero
      }
    };

    titlePos = {
      scale: spec.scale,
      range: 0.5
    };

    if (horizontal) {
      update.x = titlePos;
      enter.angle = {value: 0};
      enter.baseline = {value: orient === Top ? 'bottom' : 'top'};
    } else {
      update.y = titlePos;
      enter.angle = {value: sign * 90};
      enter.baseline = {value: 'bottom'};
    }

    addEncode(encode, 'align',       lookup('titleAlign', spec, config));
    addEncode(encode, 'angle',       lookup('titleAngle', spec, config));
    addEncode(encode, 'baseline',    lookup('titleBaseline', spec, config));
    addEncode(encode, 'fill',        lookup('titleColor', spec, config));
    addEncode(encode, 'font',        lookup('titleFont', spec, config));
    addEncode(encode, 'fontSize',    lookup('titleFontSize', spec, config));
    addEncode(encode, 'fontWeight',  lookup('titleFontWeight', spec, config));
    addEncode(encode, 'limit',       lookup('titleLimit', spec, config));
    addEncode(encode, 'fillOpacity', lookup('titleOpacity', spec, config));

    !addEncode(encode, 'x', lookup('titleX', spec, config), 'update')
      && horizontal && !has('x', userEncode)
      && (encode.enter.auto = {value: true});

    !addEncode(encode, 'y', lookup('titleY', spec, config), 'update')
      && !horizontal && !has('y', userEncode)
      && (encode.enter.auto = {value: true});

    return guideMark(TextMark, AxisTitleRole, GuideTitleStyle, null, dataRef, encode, userEncode);
  }

  function parseAxis(spec, scope) {
    var config = axisConfig(spec, scope),
        encode = spec.encode || {},
        axisEncode = encode.axis || {},
        name = axisEncode.name || undefined,
        interactive = axisEncode.interactive,
        style = axisEncode.style,
        datum, dataRef, ticksRef, size, group, children;

    // single-element data source for axis group
    datum = {
      orient: spec.orient,
      ticks:  !!lookup('ticks',  spec, config),
      labels: !!lookup('labels', spec, config),
      grid:   !!lookup('grid',   spec, config),
      domain: !!lookup('domain', spec, config),
      title:  !!value(spec.title, false)
    };
    dataRef = ref(scope.add(Collect({}, [datum])));

    // encoding properties for axis group item
    axisEncode = extendEncode({
      update: {
        range:        {signal: 'abs(span(range("' + spec.scale + '")))'},
        offset:       encoder(value(spec.offset, 0)),
        position:     encoder(value(spec.position, 0)),
        titlePadding: encoder(lookup('titlePadding', spec, config)),
        minExtent:    encoder(lookup('minExtent', spec, config)),
        maxExtent:    encoder(lookup('maxExtent', spec, config))
      }
    }, encode.axis, Skip);

    // data source for axis ticks
    ticksRef = ref(scope.add(AxisTicks({
      scale:  scope.scaleRef(spec.scale),
      extra:  scope.property(lookup('tickExtra', spec, config)),
      count:  scope.objectProperty(spec.tickCount),
      values: scope.objectProperty(spec.values),
      formatSpecifier: scope.property(spec.format)
    })));

    // generate axis marks
    children = [];

    // include axis gridlines if requested
    if (datum.grid) {
      children.push(axisGrid(spec, config, encode.grid, ticksRef));
    }

    // include axis ticks if requested
    if (datum.ticks) {
      size = lookup('tickSize', spec, config);
      children.push(axisTicks(spec, config, encode.ticks, ticksRef, size));
    }

    // include axis labels if requested
    if (datum.labels) {
      size = datum.ticks ? size : 0;
      children.push(axisLabels(spec, config, encode.labels, ticksRef, size));
    }

    // include axis domain path if requested
    if (datum.domain) {
      children.push(axisDomain(spec, config, encode.domain, dataRef));
    }

    // include axis title if defined
    if (datum.title) {
      children.push(axisTitle(spec, config, encode.title, dataRef));
    }

    // build axis specification
    group = guideGroup(AxisRole, style, name, dataRef, interactive, axisEncode, children);
    if (spec.zindex) group.zindex = spec.zindex;

    // parse axis specification
    return parseMark(group, scope);
  }

  function parseSpec(spec, scope, preprocessed) {
    var signals = vegaUtil.array(spec.signals),
        scales = vegaUtil.array(spec.scales);

    if (!preprocessed) signals.forEach(function(_) {
      parseSignal(_, scope);
    });

    vegaUtil.array(spec.projections).forEach(function(_) {
      parseProjection(_, scope);
    });

    scales.forEach(function(_) {
      initScale(_, scope);
    });

    vegaUtil.array(spec.data).forEach(function(_) {
      parseData$1(_, scope);
    });

    scales.forEach(function(_) {
      parseScale(_, scope);
    });

    signals.forEach(function(_) {
      parseSignalUpdates(_, scope);
    });

    vegaUtil.array(spec.axes).forEach(function(_) {
      parseAxis(_, scope);
    });

    vegaUtil.array(spec.marks).forEach(function(_) {
      parseMark(_, scope);
    });

    vegaUtil.array(spec.legends).forEach(function(_) {
      parseLegend(_, scope);
    });

    if (spec.title) {
      parseTitle(spec.title, scope);
    }

    scope.parseLambdas();
    return scope;
  }

  var defined = vegaUtil.toSet(['width', 'height', 'padding', 'autosize']);

  function parseView(spec, scope) {
    var config = scope.config,
        op, input, encode, parent, root;

    scope.background = spec.background || config.background;
    scope.eventConfig = config.events;
    root = ref(scope.root = scope.add(operator()));
    scope.addSignal('width', spec.width || 0);
    scope.addSignal('height', spec.height || 0);
    scope.addSignal('padding', parsePadding(spec.padding, config));
    scope.addSignal('autosize', parseAutosize(spec.autosize, config));

    vegaUtil.array(spec.signals).forEach(function(_) {
      if (!defined[_.name]) parseSignal(_, scope);
    });

    // Store root group item
    input = scope.add(Collect());

    // Encode root group item
    encode = extendEncode({
      enter: { x: {value: 0}, y: {value: 0} },
      update: { width: {signal: 'width'}, height: {signal: 'height'} }
    }, spec.encode);

    encode = scope.add(Encode(
      encoders(encode, GroupMark, FrameRole, spec.style, scope, {pulse: ref(input)}))
    );

    // Perform view layout
    parent = scope.add(ViewLayout({
      layout:       scope.objectProperty(spec.layout),
      legendMargin: config.legendMargin,
      autosize:     scope.signalRef('autosize'),
      mark:         root,
      pulse:        ref(encode)
    }));
    scope.operators.pop();

    // Parse remainder of specification
    scope.pushState(ref(encode), ref(parent), null);
    parseSpec(spec, scope, true);
    scope.operators.push(parent);

    // Bound / render / sieve root item
    op = scope.add(Bound({mark: root, pulse: ref(parent)}));
    op = scope.add(Render({pulse: ref(op)}));
    op = scope.add(Sieve({pulse: ref(op)}));

    // Track metadata for root item
    scope.addData('root', new DataScope(scope, input, input, op));

    return scope;
  }

  function Scope$1(config) {
    this.config = config;

    this.bindings = [];
    this.field = {};
    this.signals = {};
    this.lambdas = {};
    this.scales = {};
    this.events = {};
    this.data = {};

    this.streams = [];
    this.updates = [];
    this.operators = [];
    this.background = null;
    this.eventConfig = null;

    this._id = 0;
    this._subid = 0;
    this._nextsub = [0];

    this._parent = [];
    this._encode = [];
    this._lookup = [];
    this._markpath = [];
  }

  function Subscope(scope) {
    this.config = scope.config;

    this.field = Object.create(scope.field);
    this.signals = Object.create(scope.signals);
    this.lambdas = Object.create(scope.lambdas);
    this.scales = Object.create(scope.scales);
    this.events = Object.create(scope.events);
    this.data = Object.create(scope.data);

    this.streams = [];
    this.updates = [];
    this.operators = [];

    this._id = 0;
    this._subid = ++scope._nextsub[0];
    this._nextsub = scope._nextsub;

    this._parent = scope._parent.slice();
    this._encode = scope._encode.slice();
    this._lookup = scope._lookup.slice();
    this._markpath = scope._markpath;
  }

  var prototype$1 = Scope$1.prototype = Subscope.prototype;

  // ----

  prototype$1.fork = function() {
    return new Subscope(this);
  };

  prototype$1.isSubscope = function() {
    return this._subid > 0;
  };

  prototype$1.toRuntime = function() {
    this.finish();
    return {
      background:  this.background,
      operators:   this.operators,
      streams:     this.streams,
      updates:     this.updates,
      bindings:    this.bindings,
      eventConfig: this.eventConfig
    };
  };

  prototype$1.id = function() {
    return (this._subid ? this._subid + ':' : 0) + this._id++;
  };

  prototype$1.add = function(op) {
    this.operators.push(op);
    op.id = this.id();
    // if pre-registration references exist, resolve them now
    if (op.refs) {
      op.refs.forEach(function(ref$$1) { ref$$1.$ref = op.id; });
      op.refs = null;
    }
    return op;
  };

  prototype$1.proxy = function(op) {
    var vref = op instanceof Entry ? ref(op) : op;
    return this.add(Proxy({value: vref}));
  };

  prototype$1.addStream = function(stream) {
    this.streams.push(stream);
    stream.id = this.id();
    return stream;
  };

  prototype$1.addUpdate = function(update) {
    this.updates.push(update);
    return update;
  };

  // Apply metadata
  prototype$1.finish = function() {
    var name, ds;

    // annotate root
    if (this.root) this.root.root = true;

    // annotate signals
    for (name in this.signals) {
      this.signals[name].signal = name;
    }

    // annotate scales
    for (name in this.scales) {
      this.scales[name].scale = name;
    }

    // annotate data sets
    function annotate(op, name, type) {
      var data, list;
      if (op) {
        data = op.data || (op.data = {});
        list = data[name] || (data[name] = []);
        list.push(type);
      }
    }
    for (name in this.data) {
      ds = this.data[name];
      annotate(ds.input,  name, 'input');
      annotate(ds.output, name, 'output');
      annotate(ds.values, name, 'values');
      for (var field in ds.index) {
        annotate(ds.index[field], name, 'index:' + field);
      }
    }

    return this;
  };

  // ----

  prototype$1.pushState = function(encode, parent, lookup) {
    this._encode.push(ref(this.add(Sieve({pulse: encode}))));
    this._parent.push(parent);
    this._lookup.push(lookup ? ref(this.proxy(lookup)) : null);
    this._markpath.push(-1);
  };

  prototype$1.popState = function() {
    this._encode.pop();
    this._parent.pop();
    this._lookup.pop();
    this._markpath.pop();
  };

  prototype$1.parent = function() {
    return vegaUtil.peek(this._parent);
  };

  prototype$1.encode = function() {
    return vegaUtil.peek(this._encode);
  };

  prototype$1.lookup = function() {
    return vegaUtil.peek(this._lookup);
  };

  prototype$1.markpath = function() {
    var p = this._markpath;
    return ++p[p.length-1];
  };

  // ----

  prototype$1.fieldRef = function(field, name) {
    if (vegaUtil.isString(field)) return fieldRef(field, name);
    if (!field.signal) {
      vegaUtil.error('Unsupported field reference: ' + vegaUtil.stringValue(field));
    }

    var s = field.signal,
        f = this.field[s],
        params;

    if (!f) {
      params = {name: this.signalRef(s)};
      if (name) params.as = name;
      this.field[s] = f = ref(this.add(Field(params)));
    }
    return f;
  };

  prototype$1.compareRef = function(cmp, stable) {
    function check(_) {
      if (isSignal(_)) {
        signal = true;
        return scope.signalRef(_.signal);
      } else {
        return _;
      }
    }

    var scope = this,
        signal = false,
        fields = vegaUtil.array(cmp.field).map(check),
        orders = vegaUtil.array(cmp.order).map(check);

    if (stable) {
      fields.push(tupleidRef);
    }

    return signal
      ? ref(this.add(Compare({fields: fields, orders: orders})))
      : compareRef(fields, orders);
  };

  prototype$1.keyRef = function(fields, flat) {
    function check(_) {
      if (isSignal(_)) {
        signal = true;
        return ref(sig[_.signal]);
      } else {
        return _;
      }
    }

    var sig = this.signals,
        signal = false;
    fields = vegaUtil.array(fields).map(check);

    return signal
      ? ref(this.add(Key({fields: fields, flat: flat})))
      : keyRef(fields, flat);
  };

  prototype$1.sortRef = function(sort) {
    if (!sort) return sort;

    // including id ensures stable sorting
    var a = [aggrField(sort.op, sort.field), tupleidRef],
        o = sort.order || Ascending;

    return o.signal
      ? ref(this.add(Compare({
          fields: a,
          orders: [o = this.signalRef(o.signal), o]
        })))
      : compareRef(a, [o, o]);
  };

  // ----

  prototype$1.event = function(source, type) {
    var key = source + ':' + type;
    if (!this.events[key]) {
      var id = this.id();
      this.streams.push({
        id: id,
        source: source,
        type: type
      });
      this.events[key] = id;
    }
    return this.events[key];
  };

  // ----

  prototype$1.addSignal = function(name, value$$1) {
    if (this.signals.hasOwnProperty(name)) {
      vegaUtil.error('Duplicate signal name: ' + vegaUtil.stringValue(name));
    }
    var op = value$$1 instanceof Entry ? value$$1 : this.add(operator(value$$1));
    return this.signals[name] = op;
  };

  prototype$1.getSignal = function(name) {
    if (!this.signals[name]) {
      vegaUtil.error('Unrecognized signal name: ' + vegaUtil.stringValue(name));
    }
    return this.signals[name];
  };

  prototype$1.signalRef = function(s) {
    if (this.signals[s]) {
      return ref(this.signals[s]);
    } else if (!this.lambdas.hasOwnProperty(s)) {
      this.lambdas[s] = this.add(operator(null));
    }
    return ref(this.lambdas[s]);
  };

  prototype$1.parseLambdas = function() {
    var code = Object.keys(this.lambdas);
    for (var i=0, n=code.length; i<n; ++i) {
      var s = code[i],
          e = expression(s, this),
          op = this.lambdas[s];
      op.params = e.$params;
      op.update = e.$expr;
    }
  };

  prototype$1.property = function(spec) {
    return spec && spec.signal ? this.signalRef(spec.signal) : spec;
  };

  prototype$1.objectProperty = function(spec) {
    return (!spec || !vegaUtil.isObject(spec)) ? spec
      : this.signalRef(spec.signal || propertyLambda(spec));
  };

  function propertyLambda(spec) {
    return (vegaUtil.isArray(spec) ? arrayLambda : objectLambda)(spec);
  }

  function arrayLambda(array) {
    var code = '[',
        i = 0,
        n = array.length,
        value$$1;

    for (; i<n; ++i) {
      value$$1 = array[i];
      code += (i > 0 ? ',' : '')
        + (vegaUtil.isObject(value$$1)
          ? (value$$1.signal || propertyLambda(value$$1))
          : vegaUtil.stringValue(value$$1));
    }
    return code + ']';
  }

  function objectLambda(obj) {
    var code = '{',
        i = 0,
        key, value$$1;

    for (key in obj) {
      value$$1 = obj[key];
      code += (++i > 1 ? ',' : '')
        + vegaUtil.stringValue(key) + ':'
        + (vegaUtil.isObject(value$$1)
          ? (value$$1.signal || propertyLambda(value$$1))
          : vegaUtil.stringValue(value$$1));
    }
    return code + '}';
  }

  prototype$1.exprRef = function(code, name) {
    var params = {expr: expression(code, this)};
    if (name) params.expr.$name = name;
    return ref(this.add(Expression(params)));
  };

  prototype$1.addBinding = function(name, bind) {
    if (!this.bindings) {
      vegaUtil.error('Nested signals do not support binding: ' + vegaUtil.stringValue(name));
    }
    this.bindings.push(vegaUtil.extend({signal: name}, bind));
  };

  // ----

  prototype$1.addScaleProj = function(name, transform) {
    if (this.scales.hasOwnProperty(name)) {
      vegaUtil.error('Duplicate scale or projection name: ' + vegaUtil.stringValue(name));
    }
    this.scales[name] = this.add(transform);
  };

  prototype$1.addScale = function(name, params) {
    this.addScaleProj(name, Scale(params));
  };

  prototype$1.addProjection = function(name, params) {
    this.addScaleProj(name, Projection(params));
  };

  prototype$1.getScale = function(name) {
    if (!this.scales[name]) {
      vegaUtil.error('Unrecognized scale name: ' + vegaUtil.stringValue(name));
    }
    return this.scales[name];
  };

  prototype$1.projectionRef =
  prototype$1.scaleRef = function(name) {
    return ref(this.getScale(name));
  };

  prototype$1.projectionType =
  prototype$1.scaleType = function(name) {
    return this.getScale(name).params.type;
  };

  // ----

  prototype$1.addData = function(name, dataScope) {
    if (this.data.hasOwnProperty(name)) {
      vegaUtil.error('Duplicate data set name: ' + vegaUtil.stringValue(name));
    }
    return (this.data[name] = dataScope);
  };

  prototype$1.getData = function(name) {
    if (!this.data[name]) {
      vegaUtil.error('Undefined data set name: ' + vegaUtil.stringValue(name));
    }
    return this.data[name];
  };

  prototype$1.addDataPipeline = function(name, entries) {
    if (this.data.hasOwnProperty(name)) {
      vegaUtil.error('Duplicate data set name: ' + vegaUtil.stringValue(name));
    }
    return this.addData(name, DataScope.fromEntries(this, entries));
  };

  function defaults(configs) {
    var output = defaults$1();
    (configs || []).forEach(function(config) {
      var key, value, style;
      if (config) {
        for (key in config) {
          if (key === 'style') {
            style = output.style || (output.style = {});
            for (key in config.style) {
              style[key] = vegaUtil.extend(style[key] || {}, config.style[key]);
            }
          } else {
            value = config[key];
            output[key] = vegaUtil.isObject(value) && !vegaUtil.isArray(value)
              ? vegaUtil.extend(vegaUtil.isObject(output[key]) ? output[key] : {}, value)
              : value;
          }
        }
      }
    });
    return output;
  }

  var defaultFont = 'sans-serif',
      defaultSymbolSize = 30,
      defaultStrokeWidth = 2,
      defaultColor = '#4c78a8',
      black = "#000",
      gray = '#888',
      lightGray = '#ddd';

  /**
   * Standard configuration defaults for Vega specification parsing.
   * Users can provide their own (sub-)set of these default values
   * by passing in a config object to the top-level parse method.
   */
  function defaults$1() {
    return {
      // default padding around visualization
      padding: 0,

      // default for automatic sizing; options: "none", "pad", "fit"
      // or provide an object (e.g., {"type": "pad", "resize": true})
      autosize: 'pad',

      // default view background color
      // covers the entire view component
      background: null,

      // default event handling configuration
      // preventDefault for view-sourced event types except 'wheel'
      events: {
        defaults: {allow: ['wheel']}
      },

      // defaults for top-level group marks
      // accepts mark properties (fill, stroke, etc)
      // covers the data rectangle within group width/height
      group: null,

      // defaults for basic mark types
      // each subset accepts mark properties (fill, stroke, etc)
      mark: null,
      arc: { fill: defaultColor },
      area: { fill: defaultColor },
      image: null,
      line: {
        stroke: defaultColor,
        strokeWidth: defaultStrokeWidth
      },
      path: { stroke: defaultColor },
      rect: { fill: defaultColor },
      rule: { stroke: black },
      shape: { stroke: defaultColor },
      symbol: {
        fill: defaultColor,
        size: 64
      },
      text: {
        fill: black,
        font: defaultFont,
        fontSize: 11
      },

      // style definitions
      style: {
        // axis & legend labels
        "guide-label": {
          fill: black,
          font: defaultFont,
          fontSize: 10
        },
        // axis & legend titles
        "guide-title": {
          fill: black,
          font: defaultFont,
          fontSize: 11,
          fontWeight: 'bold'
        },
        // headers, including chart title
        "group-title": {
          fill: black,
          font: defaultFont,
          fontSize: 13,
          fontWeight: 'bold'
        },
        // defaults for styled point marks in Vega-Lite
        point: {
          size: defaultSymbolSize,
          strokeWidth: defaultStrokeWidth,
          shape: 'circle'
        },
        circle: {
          size: defaultSymbolSize,
          strokeWidth: defaultStrokeWidth
        },
        square: {
          size: defaultSymbolSize,
          strokeWidth: defaultStrokeWidth,
          shape: 'square'
        },
        // defaults for styled group marks in Vega-Lite
        cell: {
          fill: 'transparent',
          stroke: lightGray
        }
      },

      // defaults for axes
      axis: {
        minExtent: 0,
        maxExtent: 200,
        bandPosition: 0.5,
        domain: true,
        domainWidth: 1,
        domainColor: gray,
        grid: false,
        gridWidth: 1,
        gridColor: lightGray,
        labels: true,
        labelAngle: 0,
        labelLimit: 180,
        labelPadding: 2,
        ticks: true,
        tickColor: gray,
        tickOffset: 0,
        tickRound: true,
        tickSize: 5,
        tickWidth: 1,
        titleAlign: 'center',
        titlePadding: 4
      },

      // correction for centering bias
      axisBand: {
        tickOffset: -1
      },

      // defaults for legends
      legend: {
        orient: 'right',
        offset: 18,
        padding: 0,
        gridAlign: 'each',
        columnPadding: 10,
        rowPadding: 2,
        symbolDirection: 'vertical',
        gradientDirection: 'vertical',
        gradientLength: 200,
        gradientThickness: 16,
        gradientStrokeColor: lightGray,
        gradientStrokeWidth: 0,
        gradientLabelOffset: 2,
        labelAlign: 'left',
        labelBaseline: 'middle',
        labelLimit: 160,
        labelOffset: 4,
        labelOverlap: true,
        symbolType: 'circle',
        symbolSize: 100,
        symbolOffset: 0,
        symbolStrokeWidth: 1.5,
        symbolBaseFillColor: 'transparent',
        symbolBaseStrokeColor: gray,
        titleAlign: 'left',
        titleBaseline: 'top',
        titleLimit: 180,
        titlePadding: 5
      },

      // defaults for group title
      title: {
        orient: 'top',
        anchor: 'middle',
        offset: 4
      },

      // defaults for scale ranges
      range: {
        category: {
          scheme: 'tableau10'
        },
        ordinal: {
          scheme: 'blues',
          extent: [0.2, 1]
        },
        heatmap: {
          scheme: 'viridis'
        },
        ramp: {
          scheme: 'blues',
          extent: [0.2, 1]
        },
        diverging: {
          scheme: 'blueorange'
        },
        symbol: [
          'circle',
          'square',
          'triangle-up',
          'cross',
          'diamond',
          'triangle-right',
          'triangle-down',
          'triangle-left'
        ]
      }
    };
  }

  function parse(spec, config) {
    if (!vegaUtil.isObject(spec)) vegaUtil.error('Input Vega specification must be an object.');
    return parseView(spec, new Scope$1(defaults([config, spec.config])))
      .toRuntime();
  }

  exports.formatLocale = d3Format.formatDefaultLocale;
  exports.timeFormatLocale = d3TimeFormat.timeFormatDefaultLocale;
  exports.parse = parse;
  exports.config = defaults;
  exports.signal = parseSignal;
  exports.signalUpdates = parseSignalUpdates;
  exports.stream = parseStream;
  exports.codeGenerator = codeGenerator;
  exports.functionContext = functionContext;
  exports.expressionFunction = expressionFunction;
  exports.MarkRole = MarkRole;
  exports.FrameRole = FrameRole;
  exports.ScopeRole = ScopeRole;
  exports.AxisRole = AxisRole;
  exports.AxisDomainRole = AxisDomainRole;
  exports.AxisGridRole = AxisGridRole;
  exports.AxisLabelRole = AxisLabelRole;
  exports.AxisTickRole = AxisTickRole;
  exports.AxisTitleRole = AxisTitleRole;
  exports.LegendRole = LegendRole;
  exports.LegendEntryRole = LegendEntryRole;
  exports.LegendLabelRole = LegendLabelRole;
  exports.LegendSymbolRole = LegendSymbolRole;
  exports.LegendTitleRole = LegendTitleRole;
  exports.Scope = Scope$1;
  exports.DataScope = DataScope;

  Object.defineProperty(exports, '__esModule', { value: true });

})));
