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

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

var parsePadding = function(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';
var OUTER_INVALID = ['value', 'update', 'react', 'bind'];

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

var parseSignal = function(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$1(_, specifier) {
  return formatter('format', d3Format.format, specifier)(_);
}

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

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

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

function utcParse$1(_, 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$1(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');
}

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);
}

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

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

/**
 * 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.
 */
var clampRange = function(range$$1, min, max) {
  var lo = range$$1[0],
      hi = range$$1[1],
      span;

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

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

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];
}

var flush = function(range$$1, value, threshold, left, right, center) {
  var l = Math.abs(value - range$$1[0]),
      r = Math.abs(vegaUtil.peek(range$$1) - value);
  return l < r && l <= threshold ? left
    : r <= threshold ? right
    : center;
};

var span = function(array$$1) {
  return (array$$1[array$$1.length-1] - array$$1[0]) || 0;
};

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$1(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$$1, group) {
  var s = getScale(name, (group || this).context);
  return !s ? undefined
    : vegaUtil.isArray(range$$1) ? (s.invertRange || s.invert)(range$$1)
    : (s.invert || s.invertExtent)(range$$1);
}

var scaleGradient = function(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 = stops[stops.length-1],
      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 !== stops[stops.length-1]) 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$1 = geoMethod('area', d3Geo.geoArea);
var geoBounds$1 = geoMethod('bounds', d3Geo.geoBounds);
var geoCentroid$1 = 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$$1, value) {
  var index = this.context.data[name]['index:' + field$$1],
      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$$1 = args[1].value,
      indexName = indexPrefix + field$$1;

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

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;
}

var inrange = function(value, range$$1, left, right) {
  var r0 = range$$1[0], r1 = range$$1[range$$1.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);
};

var encode = function(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;
  };
}

var modify = function(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 BIN = 'bin_';
var INTERSECT = 'intersect';
var UNION = 'union';
var UNIT_INDEX = 'index:unit';

function testPoint(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, value;

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

// 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$$1 = 'unit',
      indexName = indexPrefix + field$$1;

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

  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$$1, 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,
      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$$1 && entry.fields[i] === field$$1)) {
      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;
  }

  if (unitIdx && op === INTERSECT) {
    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)
          : discreteDomain(units[unit], UNION)
      };
    });
  } 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$$1, 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$$1 && interval.field === field$$1)) {
      if (!interval.extent) return undefined;
      index = i;
      discrete = interval.extent.length > 2;
      break;
    }
  }

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

    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 && v.count !== count) continue;
    domain.push(v.value);
  }

  return domain.length ? domain : undefined;
}

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

  for (; i<n; ++i) {
    extent = entries[i].value;
    if (vegaUtil.isDate(extent[0])) extent = extent.map(vegaUtil.toNumber);
    lo = extent[0];
    hi = extent[1];
    if (lo > hi) {
      hi = extent[0];
      lo = extent[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$1,
  utcFormat: utcFormat$1,
  utcParse: utcParse$1,
  timeFormat: timeFormat$1,
  timeParse: timeParse$1,
  monthFormat: monthFormat,
  monthAbbrevFormat: monthAbbrevFormat,
  dayFormat: dayFormat,
  dayAbbrevFormat: dayAbbrevFormat,
  quarter: quarter,
  utcquarter: utcquarter,
  warn: warn,
  info: info,
  debug: debug,
  inScope: inScope,
  clampRange: clampRange,
  pinchDistance: pinchDistance,
  pinchAngle: pinchAngle,
  screen: screen,
  containerSize: containerSize,
  windowSize: windowSize,
  span: span,
  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'];
var eventPrefix = 'event.vega.';
var thisPrefix = 'this.';
var 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$1, scaleVisitor);
expressionFunction('invert', invert, scaleVisitor);
expressionFunction('scale', scale, scaleVisitor);
expressionFunction('gradient', scaleGradient, scaleVisitor);
expressionFunction('geoArea', geoArea$1, scaleVisitor);
expressionFunction('geoBounds', geoBounds$1, scaleVisitor);
expressionFunction('geoCentroid', geoCentroid$1, scaleVisitor);
expressionFunction('geoShape', geoShape, scaleVisitor);
expressionFunction('indata', indata, indataVisitor);
expressionFunction('data', data, dataVisitor);
expressionFunction('vlSingle', vlPoint, dataVisitor);
expressionFunction('vlSingleDomain', vlPointDomain, dataVisitor);
expressionFunction('vlMulti', vlPoint, vlMultiVisitor);
expressionFunction('vlMultiDomain', vlPointDomain, vlMultiVisitor);
expressionFunction('vlInterval', vlInterval, dataVisitor);
expressionFunction('vlIntervalDomain', vlIntervalDomain, dataVisitor);
expressionFunction('treePath', treePath, dataVisitor);
expressionFunction('treeAncestors', treeAncestors, dataVisitor);

// Build expression function registry
function buildFunctions(codegen$$1) {
  var fn = vegaExpression.functions(codegen$$1);
  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 = '$';

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

  // parse the expression to an abstract syntax tree (ast)
  try {
    ast = vegaExpression.parse(expr);
  } catch (err) {
    vegaUtil.error('Expression parse error: ' + vegaUtil.stringValue(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$$1, name) {
  return name ? {$field: field$$1, $name: name} : {$field: field$$1};
}

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$$1) {
  return (op && op.signal ? '$' + op.signal : op || '')
    + (op && field$$1 ? '_' : '')
    + (field$$1 && field$$1.signal ? '$' + field$$1.signal : field$$1 || '');
}

// -----

var Scope = 'scope';

var View = 'view';

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

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

var parseStream = function(stream, scope) {
  return stream.signal ? scope.getSignal(stream.signal).id
    : stream.scale ? scope.getScale(stream.scale).id
    : parseStream$1(stream, scope);
};

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

function parseStream$1(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 mergeStream(stream, scope) {
  var list = stream.merge.map(function(s) {
    return parseStream$1(s, scope);
  });

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

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

function eventStream(stream, scope) {
  var 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$1(param[0], scope),
      parseStream$1(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 = parseExpression('(' + 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;';

var parseUpdate = function(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) ? parseExpression(update, scope, preamble)
    : update.expr != null ? parseExpression(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) {
    source = {source: parseStream(source, scope)};
    scope.addUpdate(vegaUtil.extend(source, entry$$1));
  });
};

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

  if (signal.update) {
    var expr = parseExpression(signal.update, 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 Facet = transform('facet');
var Field = transform('field');
var Key = transform('key');
var LegendEntries = transform('legendentries');
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',
  'linear', 'pow', 'sqrt', 'log', 'sequential',
  'time', 'utc',
  'quantize', 'quantile', 'threshold'
];

var allTypes = vegaUtil.toSet(types);
var ordinalTypes = vegaUtil.toSet(types.slice(1, 6));

function isOrdinal(type) {
  return ordinalTypes.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$$1 = spec.range,
      config = scope.config.range;

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

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

var parseProjection = function(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 Index  = 'index';
var Label  = 'label';
var Offset = 'offset';
var Perc   = 'perc';
var Size   = 'size';
var Total  = 'total';
var Value  = 'value';

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

var LegendScales = [
  'shape',
  'size',
  'fill',
  'stroke',
  'strokeDash',
  'opacity'
];

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

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

var adjustSpatial = function(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;
};

var color = function(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;
};

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

var field$1 = function(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$$1;

  if (ref.signal) {
    object = 'datum';
    field$$1 = expression(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$$1 = ref.parent;
      object += '.datum';
    } else {
      field$$1 = ref.group;
    }
  } else if (ref.datum) {
    object = 'datum';
    field$$1 = ref.datum;
  } else {
    vegaUtil.error('Invalid field reference: ' + vegaUtil.stringValue(ref));
  }

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

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

var scale$1 = function(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(name.signal, scope, params, fields) + ')'
        : field$1(name, scope, params, fields));
  }

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

var gradient = function(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)
    + ')';
};

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

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

  var value = enc.signal ? expression(enc.signal, scope, params, fields)
    : enc.color ? color(enc.color, scope, params, fields)
    : enc.field != null ? field$1(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;
};

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

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

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

  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 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(_) ? _ : {value: _};
}

function addEncode(object, name, value) {
  if (value != null) {
    object[name] = vegaUtil.isObject(value) && !vegaUtil.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] = {value: 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] = {value: props[key]};
      }
    }
  });

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

  return encode;
}

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

var guideMark = function(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)
  };
};

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

var legendGradient = function(spec, scale, config, userEncode) {
  var zero = {value: 0},
      encode = {}, enter, update;

  encode.enter = enter = {
    opacity: zero,
    x: zero,
    y: zero
  };
  addEncode(enter, 'width', config.gradientWidth);
  addEncode(enter, 'height', config.gradientHeight);
  addEncode(enter, 'stroke', config.gradientStrokeColor);
  addEncode(enter, 'strokeWidth', config.gradientStrokeWidth);

  encode.exit = {
    opacity: zero
  };

  encode.update = update = {
    x: zero,
    y: zero,
    fill: {gradient: scale, start: [0,0], stop: [1,0]},
    opacity: {value: 1}
  };
  addEncode(update, 'width', config.gradientWidth);
  addEncode(update, 'height', config.gradientHeight);

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

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

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

  encode.enter = enter = {
    opacity: zero
  };
  addEncode(enter, 'fill', config.labelColor);
  addEncode(enter, 'font', config.labelFont);
  addEncode(enter, 'fontSize', config.labelFontSize);
  addEncode(enter, 'fontWeight', config.labelFontWeight);
  addEncode(enter, 'baseline', config.gradientLabelBaseline);
  addEncode(enter, 'limit', config.gradientLabelLimit);

  encode.exit = {
    opacity: zero
  };

  encode.update = update = {
    opacity: {value: 1},
    text: {field: Label}
  };

  enter.x = update.x = {
    field: Perc,
    mult: config.gradientWidth
  };

  enter.y = update.y = {
    value: config.gradientHeight,
    offset: config.gradientLabelOffset
  };

  enter.align = update.align = {signal: alignExpr};

  return guideMark(TextMark, LegendLabelRole, GuideLabelStyle, Perc, dataRef, encode, userEncode);
};

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

  encode.enter = enter = {
    opacity: zero
  };
  addEncode(enter, 'align', config.labelAlign);
  addEncode(enter, 'baseline', config.labelBaseline);
  addEncode(enter, 'fill', config.labelColor);
  addEncode(enter, 'font', config.labelFont);
  addEncode(enter, 'fontSize', config.labelFontSize);
  addEncode(enter, 'fontWeight', config.labelFontWeight);
  addEncode(enter, 'limit', config.labelLimit);

  encode.exit = {
    opacity: zero
  };

  encode.update = update = {
    opacity: {value: 1},
    text: {field: Label}
  };

  enter.x = update.x = {
    field:  Offset,
    offset: config.labelOffset
  };

  enter.y = update.y = {
    field:  Size,
    mult:   0.5,
    offset: {
      field: Total,
      offset: {
        field: {group: 'entryPadding'},
        mult: {field: Index}
      }
    }
  };

  return guideMark(TextMark, LegendLabelRole, GuideLabelStyle, Value, dataRef, encode, userEncode);
};

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

  encode.enter = enter = {
    opacity: zero
  };
  addEncode(enter, 'shape', config.symbolType);
  addEncode(enter, 'size', config.symbolSize);
  addEncode(enter, 'strokeWidth', config.symbolStrokeWidth);
  if (!spec.fill) {
    addEncode(enter, 'fill', config.symbolFillColor);
    addEncode(enter, 'stroke', config.symbolStrokeColor);
  }

  encode.exit = {
    opacity: zero
  };

  encode.update = update = {
    opacity: {value: 1}
  };

  enter.x = update.x = {
    field: Offset,
    mult:  0.5
  };

  enter.y = update.y = {
    field: Size,
    mult:  0.5,
    offset: {
      field: Total,
      offset: {
        field: {group: 'entryPadding'},
        mult: {field: Index}
      }
    }
  };

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

  return guideMark(SymbolMark, LegendSymbolRole, null, Value, dataRef, encode, userEncode);
};

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

  encode.enter = enter = {
    x: {field: {group: 'padding'}},
    y: {field: {group: 'padding'}},
    opacity: zero
  };
  addEncode(enter, 'align', config.titleAlign);
  addEncode(enter, 'baseline', config.titleBaseline);
  addEncode(enter, 'fill', config.titleColor);
  addEncode(enter, 'font', config.titleFont);
  addEncode(enter, 'fontSize', config.titleFontSize);
  addEncode(enter, 'fontWeight', config.titleFontWeight);
  addEncode(enter, 'limit', config.titleLimit);

  encode.exit = {
    opacity: zero
  };

  encode.update = {
    opacity: {value: 1},
    text: title && title.signal ? {signal: title.signal} : {value: title + ''}
  };

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

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

var clip = function(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);
}

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

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

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

/**
 * Parse a data transform specification.
 */
var parseTransform = function(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) ? parseExpression(value$$1.expr, scope)
         : expr && outerField(value$$1) ? fieldRef(value$$1.field)
         : isExpr(type) ? parseExpression(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'
}

var parseData = function(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 = ref(scope.getData(facet.data).output);
    } 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 = ref(scope.getData(facet.data).output);
        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 = from.$ref ? from
      : ref(scope.getData(from.data).output);
  }

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

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$$1, sort) {
  var ds = this,
      cache = ds.counts || (ds.counts = {}),
      k = fieldKey(field$$1), v, a, p;

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

  if (!v) {
    p = {
      groupby: scope.fieldRef(field$$1, '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$$1) {
  return vegaUtil.isString(field$$1) ? field$$1 : 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$$1, counts, index) {
  var cache = ds[name] || (ds[name] = {}),
      sort = sortKey(counts),
      k = fieldKey(field$$1), 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$$1, counts)}
      : {field: scope.fieldRef(field$$1), pulse: ref(ds.output)};
    if (sort) params.sort = scope.sortRef(counts);
    op = scope.add(entry(optype, undefined, params));
    if (index) ds.index[field$$1] = op;
    v = ref(op);
    if (k != null) cache[k] = v;
  }
  return v;
}

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

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

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

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

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

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

var parseFacet = function(spec, scope, group) {
  var facet = spec.from.facet,
      name = facet.name,
      data = ref(scope.getData(facet.data).output),
      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()
  };
};

var parseSubflow = function(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()
  };
};

var parseTrigger = function(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 = parseExpression(update, scope);
  op.update = expr.$expr;
  op.params = expr.$params;
};

var parseMark = function(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$1(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 (overlap) {
    op = {
      method: overlap.method === true ? 'parity' : overlap.method,
      pulse:  boundRef
    };
    if (overlap.order) {
      op.sort = scope.compareRef({field: overlap.order});
    }
    if (overlap.bound) {
      op.boundScale = scope.scaleRef(overlap.bound.scale);
      op.boundOrient = overlap.bound.orient;
      op.boundTolerance = overlap.bound.tolerance;
    }
    boundRef = ref(scope.add(Overlap(op)));
  }

  // 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);
    });
  }
};

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

  // resolve 'canonical' scale name
  var scale = spec.size || spec.shape || spec.fill || spec.stroke
           || spec.strokeDash || spec.opacity;

  if (!scale) {
    vegaUtil.error('Missing valid scale for legend.');
  }

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

  // encoding properties for legend group
  legendEncode = extendEncode({
    enter: legendEnter(config),
    update: {
      offset:       encoder(value(spec.offset, config.offset)),
      padding:      encoder(value(spec.padding, config.padding)),
      titlePadding: encoder(value(spec.titlePadding, config.titlePadding))
    }
  }, legendEncode, Skip);

  // encoding properties for legend entry sub-group
  entryEncode = {
    update: {
      x: {field: {group: 'padding'}},
      y: {field: {group: 'padding'}},
      entryPadding: encoder(value(spec.entryPadding, config.entryPadding))
    }
  };

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

    children = [
      legendGradient(spec, scale, config, encode.gradient),
      legendGradientLabels(spec, config, encode.labels, entryRef)
    ];
  }

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

    children = [
      legendSymbols(spec, config, encode.symbols, entryRef),
      legendLabels(spec, config, encode.labels, entryRef)
    ];

    params.size = sizeExpression(spec, scope, children);
  }

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

  // include legend title if defined
  if (datum.title) {
    title = legendTitle(spec, config, encode.title, dataRef);
    entryEncode.update.y.offset = {
      field: {group: 'titlePadding'},
      offset: get('fontSize', title.encode, scope, GuideTitleStyle)
    };
    children.push(title);
  }

  // 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 sizeExpression(spec, scope, marks) {
  var fontSize = get('fontSize', marks[1].encode, scope, GuideLabelStyle),
      symbolSize = spec.size
        ? 'scale("' + spec.size + '",datum)'
        : deref(get('size', marks[0].encode, scope)),
      expr = 'max(ceil(sqrt(' + symbolSize + ')),' + deref(fontSize) + ')';

  return parseExpression(expr, scope);
}

function legendEnter(config) {
  var enter = {},
      count = addEncode(enter, 'fill', config.fillColor)
            + addEncode(enter, 'stroke', config.strokeColor)
            + addEncode(enter, 'strokeWidth', config.strokeWidth)
            + addEncode(enter, 'strokeDash', config.strokeDash)
            + addEncode(enter, 'cornerRadius', config.cornerRadius);
  return count ? enter : undefined;
}

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

function get(name, encode, scope, style) {
  var v = encode && (
    (encode.update && encode.update[name]) ||
    (encode.enter && encode.enter[name])
  );
  return v && v.signal ? v
    : v ? +v.value
    : ((v = scope.config.style[style]) && +v[name]);
}

var parseTitle = function(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: spec.orient != null ? spec.orient : config.orient
  };
  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 title = spec.text,
      orient = spec.orient || config.orient,
      anchor = spec.anchor || config.anchor,
      sign = (orient === Left || orient === Top) ? -1 : 1,
      horizontal = (orient === Top || orient === Bottom),
      extent = {group: (horizontal ? 'width' : 'height')},
      encode = {}, enter, update, pos, opp, mult, align;

  encode.enter = enter = {
    opacity: {value: 0}
  };
  addEncode(enter, 'fill', config.color);
  addEncode(enter, 'font', config.font);
  addEncode(enter, 'fontSize', config.fontSize);
  addEncode(enter, 'fontWeight', config.fontWeight);

  encode.exit = {
    opacity: {value: 0}
  };

  encode.update = update = {
    opacity: {value: 1},
    text: vegaUtil.isObject(title) ? title : {value: title + ''},
    offset: encoder((spec.offset != null ? spec.offset : config.offset) || 0)
  };

  if (anchor === 'start') {
    mult = 0;
    align = 'left';
  } else {
    if (anchor === 'end') {
      mult = 1;
      align = 'right';
    } else {
      mult = 0.5;
      align = 'center';
    }
  }

  pos = {field: extent, mult: mult};

  opp = sign < 0 ? {value: 0}
    : horizontal ? {field: {group: 'height'}}
    : {field: {group: 'width'}};

  if (horizontal) {
    update.x = pos;
    update.y = opp;
    update.angle = {value: 0};
    update.baseline = {value: orient === Top ? 'bottom' : 'top'};
  } else {
    update.x = opp;
    update.y = pos;
    update.angle = {value: sign * 90};
    update.baseline = {value: 'bottom'};
  }
  update.align = {value: align};
  update.limit = {field: extent};

  addEncode(update, 'angle', config.angle);
  addEncode(update, 'baseline', config.baseline);
  addEncode(update, 'limit', config.limit);

  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
    output.push(source = collect({$request: data.url, $format: data.format}));
  } 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;
}

var axisConfig = function(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;
};

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

  encode.enter = enter = {
    opacity: zero
  };
  addEncode(enter, 'stroke', config.domainColor);
  addEncode(enter, 'strokeWidth', config.domainWidth);

  encode.exit = {
    opacity: zero
  };

  encode.update = update = {
    opacity: {value: 1}
  };

  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};
}

var axisGrid = function(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
  };
  addEncode(enter, 'stroke', config.gridColor);
  addEncode(enter, 'strokeWidth', config.gridWidth);
  addEncode(enter, 'strokeDash', config.gridDash);

  encode.exit = exit = {
    opacity: zero
  };

  encode.update = update = {};
  addEncode(update, 'opacity', config.gridOpacity);

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

  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);
};

var axisTicks = function(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
  };
  addEncode(enter, 'stroke', config.tickColor);
  addEncode(enter, 'strokeWidth', config.tickWidth);

  encode.exit = exit = {
    opacity: zero
  };

  encode.update = update = {
    opacity: {value: 1}
  };

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

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

  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 + ')'
  };
}

var axisLabels = function(spec, config, userEncode, dataRef, size) {
  var orient = spec.orient,
      sign = (orient === Left || orient === Top) ? -1 : 1,
      scale = spec.scale,
      pad$$1 = value(spec.labelPadding, config.labelPadding),
      bound = value(spec.labelBound, config.labelBound),
      flush = value(spec.labelFlush, config.labelFlush),
      flushOn = flush != null && flush !== false && (flush = +flush) === flush,
      flushOffset = +value(spec.labelFlushOffset, config.labelFlushOffset),
      overlap = value(spec.labelOverlap, config.labelOverlap),
      zero = {value: 0},
      encode = {}, enter, exit, update, tickSize, tickPos;

  encode.enter = enter = {
    opacity: zero
  };
  addEncode(enter, 'angle', config.labelAngle);
  addEncode(enter, 'fill', config.labelColor);
  addEncode(enter, 'font', config.labelFont);
  addEncode(enter, 'fontSize', config.labelFontSize);
  addEncode(enter, 'fontWeight', config.labelFontWeight);
  addEncode(enter, 'limit', config.labelLimit);

  encode.exit = exit = {
    opacity: zero
  };

  encode.update = update = {
    opacity: {value: 1},
    text: {field: Label}
  };

  tickSize = encoder(size);
  tickSize.mult = sign;
  tickSize.offset = encoder(pad$$1);
  tickSize.offset.mult = sign;

  tickPos = {
    scale:  scale,
    field:  Value,
    band:   0.5,
    offset: config.tickOffset
  };

  if (orient === Top || orient === Bottom) {
    update.y = enter.y = tickSize;
    update.x = enter.x = exit.x = tickPos;
    addEncode(update, 'align', flushOn
      ? flushExpr(scale, flush, '"left"', '"right"', '"center"')
      : 'center');
    if (flushOn && flushOffset) {
      addEncode(update, 'dx', flushExpr(scale, flush, -flushOffset, flushOffset, 0));
    }

    addEncode(update, 'baseline', orient === Top ? 'bottom' : 'top');
  } else {
    update.x = enter.x = tickSize;
    update.y = enter.y = exit.y = tickPos;
    addEncode(update, 'align', orient === Right ? 'left' : 'right');
    addEncode(update, 'baseline', flushOn
      ? flushExpr(scale, flush, '"bottom"', '"top"', '"middle"')
      : 'middle');
    if (flushOn && flushOffset) {
      addEncode(update, 'dy', flushExpr(scale, flush, flushOffset, -flushOffset, 0));
    }
  }

  spec = guideMark(TextMark, AxisLabelRole, GuideLabelStyle, Value, dataRef, encode, userEncode);
  if (overlap || bound) {
    spec.overlap = {
      method: overlap,
      order:  'datum.index',
      bound:  bound ? {scale: scale, orient: orient, tolerance: +bound} : null
    };
  }
  return spec;
};

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

  encode.enter = enter = {
    opacity: {value: 0}
  };
  addEncode(enter, 'align', config.titleAlign);
  addEncode(enter, 'fill', config.titleColor);
  addEncode(enter, 'font', config.titleFont);
  addEncode(enter, 'fontSize', config.titleFontSize);
  addEncode(enter, 'fontWeight', config.titleFontWeight);
  addEncode(enter, 'limit', config.titleLimit);

  encode.exit = {
    opacity: {value: 0}
  };

  encode.update = update = {
    opacity: {value: 1},
    text: title && title.signal ? {signal: title.signal} : {value: title + ''}
  };

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

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

  addEncode(update, 'angle', config.titleAngle);
  addEncode(update, 'baseline', config.titleBaseline);

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

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

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

var parseAxis = function(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:  !!value(spec.ticks, config.ticks),
    labels: !!value(spec.labels, config.labels),
    grid:   !!value(spec.grid, config.grid),
    domain: !!value(spec.domain, config.domain),
    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(value(spec.titlePadding, config.titlePadding)),
      minExtent:    encoder(value(spec.minExtent, config.minExtent)),
      maxExtent:    encoder(value(spec.maxExtent, config.maxExtent))
    }
  }, encode.axis, Skip);

  // data source for axis ticks
  ticksRef = ref(scope.add(AxisTicks({
    scale:  scope.scaleRef(spec.scale),
    extra:  config.tickExtra,
    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 = value(spec.tickSize, config.tickSize);
    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);
};

var parseSpec = function(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$$1 in ds.index) {
      annotate(ds.index[field$$1], name, 'index:' + field$$1);
    }
  }

  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$$1, name) {
  if (vegaUtil.isString(field$$1)) return fieldRef(field$$1, name);
  if (!field$$1.signal) {
    vegaUtil.error('Unsupported field reference: ' + vegaUtil.stringValue(field$$1));
  }

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

  if (!f) { // TODO: replace with update signalRef?
    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 ref(sig[_.signal]);
    } else {
      return _;
    }
  }

  var sig = this.signals,
      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 = parseExpression(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$$1) {
  var code = '[',
      i = 0,
      n = array$$1.length,
      value$$1;

  for (; i<n; ++i) {
    value$$1 = array$$1[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.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));
};

var defaults = function(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';
var defaultSymbolSize = 30;
var defaultStrokeWidth = 2;
var defaultColor = '#4c78a8';
var black = "#000";
var gray = '#888';
var 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,
      gridOpacity: 1,
      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,
      entryPadding: 5,
      titlePadding: 5,
      gradientWidth: 100,
      gradientHeight: 20,
      gradientStrokeColor: lightGray,
      gradientStrokeWidth: 0,
      gradientLabelBaseline: 'top',
      gradientLabelOffset: 2,
      labelAlign: 'left',
      labelBaseline: 'middle',
      labelOffset: 8,
      labelLimit: 160,
      symbolType: 'circle',
      symbolSize: 100,
      symbolFillColor: 'transparent',
      symbolStrokeColor: gray,
      symbolStrokeWidth: 1.5,
      titleAlign: 'left',
      titleBaseline: 'top',
      titleLimit: 180
    },

    // 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'
      ]
    }
  };
}

var parse$1 = function(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.parse = parse$1;
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;
exports.formatLocale = d3Format.formatDefaultLocale;
exports.timeFormatLocale = d3TimeFormat.timeFormatDefaultLocale;

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

})));
