(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vega-util'), require('vega-functions'), require('vega-event-selector'), require('vega-scale'), require('vega-dataflow')) :
  typeof define === 'function' && define.amd ? define(['exports', 'vega-util', 'vega-functions', 'vega-event-selector', 'vega-scale', 'vega-dataflow'], factory) :
  (global = global || self, factory(global.vega = {}, global.vega, global.vega, global.vega, global.vega, global.vega));
}(this, (function (exports, vegaUtil, vegaFunctions, vegaEventSelector, vegaScale, vegaDataflow) { 'use strict';

  function parseAutosize(spec) {
    return vegaUtil.isObject(spec) ? spec : {type: spec || 'pad'};
  }

  const number = _ => +_ || 0;

  const paddingObject = _ => ({top: _, bottom: _, left: _, right: _});

  function parsePadding(spec) {
    return !vegaUtil.isObject(spec) ? paddingObject(number(spec))
      : spec.signal ? spec
      : {
          top:    number(spec.top),
          bottom: number(spec.bottom),
          left:   number(spec.left),
          right:  number(spec.right)
        };
  }

  const encoder = _ => vegaUtil.isObject(_) && !vegaUtil.isArray(_)
    ? vegaUtil.extend({}, _)
    : {value: _};

  function addEncode(object, name, value, set) {
    if (value != null) {
      // Always assign signal to update, even if the signal is from the enter block
      if (vegaUtil.isObject(value) && !vegaUtil.isArray(value)) {
        object.update[name] = value;
      } else {
        object[set || 'enter'][name] = {value: value};
      }
      return 1;
    } else {
      return 0;
    }
  }

  function addEncoders(object, enter, update) {
    for (const name in enter) {
      addEncode(object, name, enter[name]);
    }
    for (const name in update) {
      addEncode(object, name, update[name], 'update');
    }
  }

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

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

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

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

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

  var TitleRole = 'title';
  var TitleTextRole = 'title-text';
  var TitleSubtitleRole = 'title-subtitle';

  function applyDefaults(encode, type, role, style, config) {
    const defaults = {}, enter = {};
    let update, key, skip, props;

    // if text mark, apply global lineBreak settings (#2370)
    key = 'lineBreak';
    if (type === 'text' && config[key] != null && !has(key, encode)) {
      applyDefault(defaults, key, config[key]);
    }

    // ignore legend and axis roles
    if (role == 'legend' || String(role).startsWith('axis')) {
      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) applyDefault(defaults, key, props[key]);
    }

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

    encode = vegaUtil.extend({}, encode); // defensive copy
    for (key in defaults) {
      props = defaults[key];
      if (props.signal) {
        (update = update || {})[key] = props;
      } else {
        enter[key] = props;
      }
    }

    encode.enter = vegaUtil.extend(enter, encode.enter);
    if (update) encode.update = vegaUtil.extend(update, encode.update);

    return encode;
  }

  function applyDefault(defaults, key, value) {
    defaults[key] = value && value.signal
      ? {signal: value.signal}
      : {value: value};
  }

  const scaleRef = scale => vegaUtil.isString(scale) ? vegaUtil.stringValue(scale)
    : scale.signal ? `(${scale.signal})`
    : field(scale);

  function entry(enc) {
    if (enc.gradient != null) {
      return gradient(enc);
    }

    let value = enc.signal ? `(${enc.signal})`
      : enc.color ? color(enc.color)
      : enc.field != null ? field(enc.field)
      : enc.value !== undefined ? vegaUtil.stringValue(enc.value)
      : undefined;

    if (enc.scale != null) {
      value = scale(enc, value);
    }

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

    if (enc.exponent != null) {
      value = `pow(${value},${property(enc.exponent)})`;
    }

    if (enc.mult != null) {
      value += `*${property(enc.mult)}`;
    }

    if (enc.offset != null) {
      value += `+${property(enc.offset)}`;
    }

    if (enc.round) {
      value = `round(${value})`;
    }

    return value;
  }

  const _color = (type, x, y, z) =>
    `(${type}(${[x, y, z].map(entry).join(',')})+'')`;

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

  function gradient(enc) {
    // map undefined to null; expression lang does not allow undefined
    const args = [enc.start, enc.stop, enc.count]
      .map(_ => _ == null ? null : vegaUtil.stringValue(_));

    // trim null inputs from the end
    while (args.length && vegaUtil.peek(args) == null) args.pop();

    args.unshift(scaleRef(enc.gradient));
    return `gradient(${args.join(',')})`;
  }

  function property(property) {
    return vegaUtil.isObject(property) ? '(' + entry(property) + ')' : property;
  }

  function field(ref) {
    return resolveField(vegaUtil.isObject(ref) ? ref : {datum: ref});
  }

  function resolveField(ref) {
    let object, level, field;

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

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

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

    if (!ref.signal) {
      field = vegaUtil.isString(field)
        ? vegaUtil.splitAccessPath(field).map(vegaUtil.stringValue).join('][')
        : resolveField(field);
    }

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

  function scale(enc, value) {
    const scale = scaleRef(enc.scale);

    if (enc.range != null) {
      // pull value from scale range
      value = `lerp(_range(${scale}), ${+enc.range})`;
    } else {
      // run value through scale and/or pull scale bandwidth
      if (value !== undefined) value = `_scale(${scale}, ${value})`;

      if (enc.band) {
        value = (value ? value + '+' : '')
          + `_bandwidth(${scale})`
          + (+enc.band === 1 ? '' : '*' + property(enc.band));

        if (enc.extra) {
          // include logic to handle extraneous elements
          value = `(datum.extra ? _scale(${scale}, datum.extra.value) : ${value})`;
        }
      }

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

    return value;
  }

  function rule(enc) {
    let code = '';

    enc.forEach(rule => {
      const value = entry(rule);
      code += rule.test ? `(${rule.test})?${value}:` : value;
    });

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

    return code;
  }

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

    encode = applyDefaults(encode, type, role, style, scope.config);
    for (const key in encode) {
      enc[key] = parseBlock(encode[key], type, params, scope);
    }

    return params;
  }

  function parseBlock(block, marktype, params, scope) {
    const channels = {},
          fields = {};

    for (const name in block) {
      if (block[name] != null) { // skip any null entries
        channels[name] = parse(expr(block[name]), scope, params, fields);
      }
    }

    return {
      $expr:   {marktype, channels},
      $fields: Object.keys(fields),
      $output: Object.keys(block)
    };
  }

  function expr(enc) {
    return vegaUtil.isArray(enc) ? rule(enc) : entry(enc);
  }

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

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

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

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

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

  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$1(type, value, params, parent) {
    return new Entry(type, value, params, parent);
  }

  function operator(value, params) {
    return entry$1('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;
  }

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

  var keyFieldRef = fieldRef('key');

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

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

  // -----

  var Ascending  = 'ascending';

  var Descending = 'descending';

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

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

  // -----

  var Scope = 'scope';

  var View = 'view';

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

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

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

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

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

  var Timer = 'timer';

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

    return method(stream, scope);
  }

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

  function mergeStream(stream, scope) {
    var list = stream.merge.map(s => parseStream(s, scope)),
        entry = streamParameters({merge: list}, stream, scope);
    return scope.addStream(entry).id;
  }

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

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

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

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

  function streamParameters(entry, 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.between = [
        parseStream(param[0], scope),
        parseStream(param[1], scope)
      ];
    }

    param = stream.filter ? [].concat(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.filter = vegaFunctions.parseExpression('(' + param.join(')&&(') + ')', scope).$expr;
    }

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

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

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

    return entry;
  }

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

  // bypass expression parser for internal operator references
  const OP_VALUE_EXPR = {
    code: '_.$value',
    ast: {type: 'Identifier', value: 'value'}
  };

  function parseUpdate(spec, scope, target) {
    var events = spec.events,
        update = spec.update,
        encode = spec.encode,
        sources = [],
        entry = {target: target};

    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(s => s.signal || s.scale ? (sources.push(s), 0) : 1);

    // merge internal operator listeners
    if (sources.length > 1) {
      sources = [mergeSources(sources)];
    }

    // 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
    entry.update = vegaUtil.isString(update) ? vegaFunctions.parseExpression(update, scope)
      : update.expr != null ? vegaFunctions.parseExpression(update.expr, scope)
      : update.value != null ? update.value
      : update.signal != null ? {
          $expr:   OP_VALUE_EXPR,
          $params: {$value: scope.signalRef(update.signal)}
        }
      : vegaUtil.error('Invalid signal update specification.');

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

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

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

  function mergeSources(sources) {
    return {
      signal: '['
        + sources.map(s => s.scale ? 'scale("' + s.scale + '")' : s.signal)
        + ']'
    };
  }

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

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

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

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

  const transform = name => (params, value, parent) =>
    entry$1(name, value, params || undefined, parent);

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

  var FIELD_REF_ID = 0;

  var MULTIDOMAIN_SORT_OPS  = {min: 'min', max: 'max', count: 'sum'};

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

    if (!vegaScale.isValidScaleType(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) {
      params.nice = parseScaleNice(spec.nice);
    }

    if (spec.bins != null) {
      params.bins = parseScaleBins(spec.bins, scope);
    }

    for (key in spec) {
      if (vegaUtil.hasOwnProperty(params, 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(v => 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 vegaScale.isDiscrete(spec.type)
        ? data.valuesRef(scope, domain.field, parseSort(domain.sort, false))
        : vegaScale.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 (vegaScale.isDiscrete(spec.type) ? ordinalMultipleDomain
      : vegaScale.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 sort = parseSort(domain.sort, true),
        counts, p, 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, sort);
    });

    // aggregate the results from each domain field
    p = {groupby: keyFieldRef, pulse: counts};
    if (sort) {
      a = sort.op || 'count';
      v = sort.field ? aggrField(a, sort.field) : 'count';
      p.ops = [MULTIDOMAIN_SORT_OPS[a]];
      p.fields = [scope.fieldRef(v)];
      p.as = [v];
    }
    a = scope.add(Aggregate(p));

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

    // extract values for combined domain
    v = scope.add(Values({
      field: keyFieldRef,
      sort:  scope.sortRef(sort),
      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) {
        if (sort.op && !MULTIDOMAIN_SORT_OPS[sort.op]) {
          vegaUtil.error('Multiple domain scales can not be sorted using ' + sort.op);
        }
      }
    }
    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 BINS -----

  function parseScaleBins(v, scope) {
    return v.signal || vegaUtil.isArray(v)
      ? parseArray(v, scope)
      : scope.objectProperty(v);
  }

  // -- SCALE NICE -----

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

  // -- SCALE INTERPOLATION -----

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

  // -- SCALE RANGE -----

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

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

    return range.map(v => (vegaUtil.isArray(v) ? parseArray : parseLiteral)(v, scope));
  }

  function parseProjection(proj, scope) {
    var config = scope.config.projection || {},
        params = {};

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

    // apply projection defaults from config
    for (name in config) {
      if (params[name] == null) {
        params[name] = parseParameter(config[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(_));
  }

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

  const Vertical = 'vertical';

  const Start = 'start';
  const Middle = 'middle';
  const End = 'end';

  const Index  = 'index';
  const Label  = 'label';
  const Offset = 'offset';
  const Perc   = 'perc';
  const Perc2  = 'perc2';
  const Value  = 'value';

  const GuideLabelStyle = 'guide-label';
  const GuideTitleStyle = 'guide-title';
  const GroupTitleStyle = 'group-title';
  const GroupSubtitleStyle = 'group-subtitle';

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

  const Size = 'size';
  const Shape = 'shape';
  const Fill = 'fill';
  const Stroke = 'stroke';
  const StrokeWidth = 'strokeWidth';
  const StrokeDash = 'strokeDash';
  const Opacity = 'opacity';

  // Encoding channels supported by legends
  // In priority order of 'canonical' scale
  const LegendScales = [
    Size,
    Shape,
    Fill,
    Stroke,
    StrokeWidth,
    StrokeDash,
    Opacity
  ];

  const Skip = {
    name: 1,
    style: 1,
    interactive: 1
  };

  const zero = {value: 0};
  const one = {value: 1};

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

  function guideGroup(mark) {
    mark.type = GroupMark;
    mark.interactive = mark.interactive || false;
    return mark;
  }

  function lookup(spec, config) {
    const _ = (name, dflt) => value(spec[name], value(config[name], dflt));

    _.isVertical = s => Vertical === value(
      spec.direction,
      config.direction || (s ? config.symbolDirection : config.gradientDirection)
    );

    _.gradientLength = () => value(
      spec.gradientLength,
      config.gradientLength || config.gradientWidth
    );

    _.gradientThickness = () => value(
      spec.gradientThickness,
      config.gradientThickness || config.gradientHeight
    );

    _.entryColumns = () => value(
      spec.columns,
      value(config.columns, +_.isVertical(true))
    );

    return _;
  }

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

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

  function anchorExpr(s, e, m) {
    return `item.anchor === '${Start}' ? ${s} : item.anchor === '${End}' ? ${e} : ${m}`;
  }

  const alignExpr = anchorExpr(
    vegaUtil.stringValue(Left),
    vegaUtil.stringValue(Right),
    vegaUtil.stringValue(Center)
  );

  function tickBand(_) {
    let v = _('tickBand'),
        offset = _('tickOffset'),
        band, extra;

    if (!v) {
      // if no tick band entry, fall back on other properties
      band = _('bandPosition');
      extra = _('tickExtra');
    } else if (v.signal) {
      // if signal, augment code to interpret values
      band = {signal: `(${v.signal}) === 'extent' ? 1 : 0.5`};
      extra = {signal: `(${v.signal}) === 'extent'`};
      if (!vegaUtil.isObject(offset)) {
        offset = {signal: `(${v.signal}) === 'extent' ? 0 : ${offset}`};
      }
    } else if (v === 'extent') {
      // if constant, simply set values
      band = 1;
      extra = true;
      offset = 0;
    } else {
      band = 0.5;
      extra = false;
    }

    return {extra, band, offset};
  }

  function extendOffset(value, offset) {
    return !offset ? value
      : !value ? offset
      : !vegaUtil.isObject(value) ? { value, offset }
      : Object.assign({}, value, { offset: extendOffset(value.offset, offset) });
  }

  function guideMark(mark, extras) {
    if (extras) {
      mark.name = extras.name;
      mark.style = extras.style || mark.style;
      mark.interactive = !!extras.interactive;
      mark.encode = extendEncode(mark.encode, extras, Skip);
    } else {
      mark.interactive = false;
    }
    return mark;
  }

  function legendGradient(spec, scale, config, userEncode) {
    var _ = lookup(spec, config),
        vertical = _.isVertical(),
        thickness = _.gradientThickness(),
        length = _.gradientLength(),
        encode, enter, start, stop, width, height;

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

    encode = {
      enter: enter = {
        opacity: zero,
        x: zero,
        y: zero,
        width: encoder(width),
        height: encoder(height)
      },
      update: vegaUtil.extend({}, enter, {
        opacity: one,
        fill: {gradient: scale, start: start, stop: stop}
      }),
      exit: {
        opacity: zero
      }
    };

    addEncoders(encode, {
      stroke:      _('gradientStrokeColor'),
      strokeWidth: _('gradientStrokeWidth')
    }, { // update
      opacity:     _('gradientOpacity')
    });

    return guideMark({
      type: RectMark,
      role: LegendGradientRole,
      encode
    }, userEncode);
  }

  function legendGradientDiscrete(spec, scale, config, userEncode, dataRef) {
    var _ = lookup(spec, config),
        vertical = _.isVertical(),
        thickness = _.gradientThickness(),
        length = _.gradientLength(),
        encode, enter, u, v, uu, vv, adjust = '';

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

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

    encode = {
      enter: enter,
      update: vegaUtil.extend({}, enter, {opacity: one}),
      exit: {opacity: zero}
    };

    addEncoders(encode, {
      stroke:      _('gradientStrokeColor'),
      strokeWidth: _('gradientStrokeWidth')
    }, { // update
      opacity:     _('gradientOpacity')
    });

    return guideMark({
      type: RectMark,
      role: LegendBandRole,
      key:  Value,
      from: dataRef,
      encode
    }, userEncode);
  }

  const alignExpr$1 = `datum.${Perc}<=0?"${Left}":datum.${Perc}>=1?"${Right}":"${Center}"`,
        baselineExpr = `datum.${Perc}<=0?"${Bottom}":datum.${Perc}>=1?"${Top}":"${Middle}"`;

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

    encode = {
      enter: enter = {
        opacity: zero
      },
      update: update = {
        opacity: one,
        text: {field: Label}
      },
      exit: {
        opacity: zero
      }
    };

    addEncoders(encode, {
      fill:        _('labelColor'),
      fillOpacity: _('labelOpacity'),
      font:        _('labelFont'),
      fontSize:    _('labelFontSize'),
      fontStyle:   _('labelFontStyle'),
      fontWeight:  _('labelFontWeight'),
      limit:       value(spec.labelLimit, config.gradientLabelLimit)
    });

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

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

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

    overlap = overlap ? {
      separation: _('labelSeparation'),
      method: overlap,
      order: 'datum.' + Index
    } : undefined;

    // type, role, style, key, dataRef, encode, extras
    return guideMark({
      type:  TextMark,
      role:  LegendLabelRole,
      style: GuideLabelStyle,
      key:   Value,
      from: dataRef,
      encode,
      overlap
    }, userEncode);
  }

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

    yEncode.mult = 0.5;

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

    var baseFill = null,
        baseStroke = null;
    if (!spec.fill) {
      baseFill = config.symbolBaseFillColor;
      baseStroke = config.symbolBaseStrokeColor;
    }

    addEncoders(encode, {
      fill:             _('symbolFillColor', baseFill),
      shape:            _('symbolType'),
      size:             _('symbolSize'),
      stroke:           _('symbolStrokeColor', baseStroke),
      strokeDash:       _('symbolDash'),
      strokeDashOffset: _('symbolDashOffset'),
      strokeWidth:      _('symbolStrokeWidth')
    }, { // update
      opacity:          _('symbolOpacity')
    });

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

    symbols = guideMark({
      type: SymbolMark,
      role: LegendSymbolRole,
      key:  Value,
      from: valueRef,
      clip: height ? true : undefined,
      encode
    }, userEncode.symbols);

    // -- LEGEND LABELS --
    labelOffset = encoder(symbolOffset);
    labelOffset.offset = _('labelOffset');

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

    addEncoders(encode, {
      align:       _('labelAlign'),
      baseline:    _('labelBaseline'),
      fill:        _('labelColor'),
      fillOpacity: _('labelOpacity'),
      font:        _('labelFont'),
      fontSize:    _('labelFontSize'),
      fontStyle:   _('labelFontStyle'),
      fontWeight:  _('labelFontWeight'),
      limit:       _('labelLimit')
    });

    labels = guideMark({
      type:  TextMark,
      role:  LegendLabelRole,
      style: GuideLabelStyle,
      key:   Value,
      from:  valueRef,
      encode
    }, userEncode.labels);

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

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

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

    return guideGroup({
      role:   ScopeRole,
      from:   dataRef,
      encode: extendEncode(encode, entries, Skip),
      marks:  [symbols, labels],
      name,
      interactive,
      sort
    });
  }

  function legendSymbolLayout(spec, config) {
    const _ = lookup(spec, config);

    // layout parameters for legend entries
    return {
      align:   _('gridAlign'),
      columns: _.entryColumns(),
      center:  {
        row: true,
        column: false
      },
      padding: {
        row:    _('rowPadding'),
        column: _('columnPadding')
      }
    };
  }

  // expression logic for align, anchor, angle, and baseline calculation
  const isL = 'item.orient === "left"',
        isR = 'item.orient === "right"',
        isLR = `(${isL} || ${isR})`,
        isVG = `datum.vgrad && ${isLR}`,
        baseline = anchorExpr('"top"', '"bottom"', '"middle"'),
        alignFlip = anchorExpr('"right"', '"left"', '"center"'),
        exprAlign = `datum.vgrad && ${isR} ? (${alignFlip}) : (${isLR} && !(datum.vgrad && ${isL})) ? "left" : ${alignExpr}`,
        exprAnchor = `item._anchor || (${isLR} ? "middle" : "start")`,
        exprAngle = `${isVG} ? (${isL} ? -90 : 90) : 0`,
        exprBaseline = `${isLR} ? (datum.vgrad ? (${isR} ? "bottom" : "top") : ${baseline}) : "top"`;

  function legendTitle(spec, config, userEncode, dataRef) {
    var _ = lookup(spec, config), encode;

    encode = {
      enter: {opacity: zero},
      update: {
        opacity: one,
        x: {field: {group: 'padding'}},
        y: {field: {group: 'padding'}}
      },
      exit: {opacity: zero}
    };

    addEncoders(encode, {
      orient:      _('titleOrient'),
      _anchor:     _('titleAnchor'),
      anchor:      {signal: exprAnchor},
      angle:       {signal: exprAngle},
      align:       {signal: exprAlign},
      baseline:    {signal: exprBaseline},
      text:        spec.title,
      fill:        _('titleColor'),
      fillOpacity: _('titleOpacity'),
      font:        _('titleFont'),
      fontSize:    _('titleFontSize'),
      fontStyle:   _('titleFontStyle'),
      fontWeight:  _('titleFontWeight'),
      limit:       _('titleLimit'),
      lineHeight:  _('titleLineHeight')
    }, { // require update
      align:       _('titleAlign'),
      baseline:    _('titleBaseline')
    });

    return guideMark({
      type:  TextMark,
      role:  LegendTitleRole,
      style: GuideTitleStyle,
      from:  dataRef,
      encode
    }, userEncode);
  }

  function clip(clip, scope) {
    var expr;

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

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

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

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

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

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

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

    var t = entry$1(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 = spec[def.name];

    if (type === 'index') {
      return parseIndexParameter(def, spec, scope);
    } else if (value === 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)
      ? value.map(function(v) { return parameterValue(def, v, scope); })
      : parameterValue(def, value, scope);
  }

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

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

  /**
   * 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 = spec[def.name];

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

  /**
   * Parse a sub-parameter object.
   */
  function parseSubParameter(def, value, 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[k]) { pdef = null; break; }
      }
      if (pdef) break;
    }
    // raise error if matching key not found
    if (!pdef) vegaUtil.error('Unsupported parameter: ' + vegaUtil.stringValue(value));

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

  // -- Utilities -----

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    if (input && input.type === 'load') {
      input = entries[1];
    }

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

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

  var prototype = DataScope.prototype;

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

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

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

    return v.ref;
  };

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // add visual encoders
    op = enc = scope.add(Encode(parseEncode(
      spec.encode, spec.type, role, spec.style, scope,
      {mod: false, 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(_) {
        const tx = parseTransform(_, scope),
              md = tx.metadata;
        if (md.generates || md.changes) {
          vegaUtil.error('Mark transforms should not generate new data.');
        }
        if (!md.nomod) enc.params.mod = true; // update encode mod handling
        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),
        pulse: ref(op)
      }));
    }

    encodeRef = ref(op);

    // add view layout operator if needed
    if (facet || layout) {
      layout = scope.add(ViewLayout({
        layout:   scope.objectProperty(spec.layout),
        legends:  scope.legends,
        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
          : scope.parse(spec); // guide group, we can avoid nested scopes
      scope.popState();

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

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

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

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

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

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

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

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

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

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

    // resolve scales and 'canonical' scale name
    LegendScales.forEach(s => spec[s]
      ? (scales[s] = spec[s], scale = scale || spec[s]) : 0
    );
    if (!scale) vegaUtil.error('Missing valid scale for legend.');

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

    // single-element data source for legend group
    datum = {
      title:  spec.title != null,
      scales: scales,
      type:   type,
      vgrad:  type !== 'symbol' &&  _.isVertical()
    };
    dataRef = ref(scope.add(Collect(null, [datum])));

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

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

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

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

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

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

    // generate legend marks
    children = [
      guideGroup({
        role: LegendEntryRole,
        from: dataRef,
        encode: entryEncode,
        marks: children,
        layout: entryLayout,
        interactive
      })
    ];

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

    // parse legend specification
    return parseMark(
      guideGroup({
        role:        LegendRole,
        from:        dataRef,
        encode:      legendEncode,
        marks:       children,
        aria:        _('aria'),
        description: _('description'),
        zindex:      _('zindex'),
        name,
        interactive,
        style
      }),
      scope
    );
  }

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

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

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

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

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

    addEncoders(encode, {
      orient:       _('orient'),
      offset:       _('offset'),
      padding:      _('padding'),
      titlePadding: _('titlePadding'),
      cornerRadius: _('cornerRadius'),
      fill:         _('fillColor'),
      stroke:       _('strokeColor'),
      strokeWidth:  config.strokeWidth,
      strokeDash:   config.strokeDash,
      x:            _('legendX'),
      y:            _('legendY'),

      // accessibility support
      format:       spec.format,
      formatType:   spec.formatType
    });

    return encode;
  }

  function sizeExpression(spec, scope, marks) {
    var size = deref(getChannel('size', spec, marks)),
        strokeWidth = deref(getChannel('strokeWidth', spec, marks)),
        fontSize = deref(getFontSize(marks[1].encode, scope, GuideLabelStyle));

    return vegaFunctions.parseExpression(
      `max(ceil(sqrt(${size})+${strokeWidth}),${fontSize})`,
      scope
    );
  }

  function getChannel(name, spec, marks) {
    return spec[name]
      ? `scale("${spec[name]}",datum)`
      : getEncoding(name, marks[0].encode);
  }

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

  const angleExpr = `item.orient==="${Left}"?-90:item.orient==="${Right}"?90:0`;

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

    var _ = lookup(spec, scope.config.title),
        encode = spec.encode || {},
        userEncode = encode.group || {},
        name = userEncode.name || undefined,
        interactive = userEncode.interactive,
        style = userEncode.style,
        children = [],
        datum, dataRef;

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

    // include title text
    children.push(buildTitle(spec, _, titleEncode(spec), dataRef));

    // include subtitle text
    if (spec.subtitle) {
      children.push(buildSubTitle(spec, _, encode.subtitle, dataRef));
    }

    // parse title specification
    return parseMark(
      guideGroup({
        role:        TitleRole,
        from:        dataRef,
        encode:      groupEncode(_, userEncode),
        marks:       children,
        aria:        _('aria'),
        description: _('description'),
        zindex:      _('zindex'),
        name,
        interactive,
        style
      }),
      scope
    );
  }

  // provide backwards-compatibility for title custom encode;
  // the top-level encode block has been *deprecated*.
  function titleEncode(spec) {
    const encode = spec.encode;
    return (encode && encode.title) || vegaUtil.extend({
      name: spec.name,
      interactive: spec.interactive,
      style: spec.style
    }, encode);
  }

  function groupEncode(_, userEncode) {
    var encode = {enter: {}, update: {}};

    addEncoders(encode, {
      orient:      _('orient'),
      anchor:      _('anchor'),
      align:       {signal: alignExpr},
      angle:       {signal: angleExpr},
      limit:       _('limit'),
      frame:       _('frame'),
      offset:      _('offset') || 0,
      padding:     _('subtitlePadding')
    });

    return extendEncode(encode, userEncode, Skip);
  }

  function buildTitle(spec, _, userEncode, dataRef) {
    var zero = {value: 0},
        text = spec.text,
        encode = {
          enter: {opacity: zero},
          update: {opacity: {value: 1}},
          exit: {opacity: zero}
        };

    addEncoders(encode, {
      text:       text,
      align:      {signal: 'item.mark.group.align'},
      angle:      {signal: 'item.mark.group.angle'},
      limit:      {signal: 'item.mark.group.limit'},
      baseline:   'top',
      dx:         _('dx'),
      dy:         _('dy'),
      fill:       _('color'),
      font:       _('font'),
      fontSize:   _('fontSize'),
      fontStyle:  _('fontStyle'),
      fontWeight: _('fontWeight'),
      lineHeight: _('lineHeight')
    }, { // update
      align:      _('align'),
      angle:      _('angle'),
      baseline:   _('baseline')
    });

    return guideMark({
      type: TextMark,
      role: TitleTextRole,
      style: GroupTitleStyle,
      from: dataRef,
      encode
    }, userEncode);
  }

  function buildSubTitle(spec, _, userEncode, dataRef) {
    var zero = {value: 0},
        text = spec.subtitle,
        encode = {
          enter: {opacity: zero},
          update: {opacity: {value: 1}},
          exit: {opacity: zero}
        };

    addEncoders(encode, {
      text:       text,
      align:      {signal: 'item.mark.group.align'},
      angle:      {signal: 'item.mark.group.angle'},
      limit:      {signal: 'item.mark.group.limit'},
      baseline:   'top',
      dx:         _('dx'),
      dy:         _('dy'),
      fill:       _('subtitleColor'),
      font:       _('subtitleFont'),
      fontSize:   _('subtitleFontSize'),
      fontStyle:  _('subtitleFontStyle'),
      fontWeight: _('subtitleFontWeight'),
      lineHeight: _('subtitleLineHeight')
    }, { // update
      align:      _('align'),
      angle:      _('angle'),
      baseline:   _('baseline')
    });

    return guideMark({
      type:  TextMark,
      role:  TitleSubtitleRole,
      style: GroupSubtitleStyle,
      from:  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) {
    var output = [],
        source = null,
        modify = false,
        generate = false,
        upstream, i, n, t, m;

    if (data.values) {
      // hard-wired input data set
      if (hasSignal(data.values) || hasSignal(data.format)) {
        // if either values or format has signal, use dynamic loader
        output.push(load(scope, data));
        output.push(source = collect());
      } else {
        // otherwise, ingest upon dataflow init
        output.push(source = collect({
          $ingest: data.values,
          $format: data.format
        }));
      }
    } else if (data.url) {
      // load data from external source
      if (hasSignal(data.url) || hasSignal(data.format)) {
        // if either url or format has signal, use dynamic loader
        output.push(load(scope, data));
        output.push(source = collect());
      } else {
        // otherwise, request load upon dataflow init
        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;
  }

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

  const isX = orient => orient === Bottom || orient === Top;

  // get sign coefficient based on axis orient
  const getSign = (orient, a, b) => isSignal(orient)
    ? ifLeftTopExpr(orient.signal, a, b)
    : orient === Left || orient === Top ? a : b;

  // condition on axis x-direction
  const ifX = (orient, a, b) => isSignal(orient)
    ? ifXEnc(orient.signal, a, b)
    : isX(orient) ? a : b;

  // condition on axis y-direction
  const ifY = (orient, a, b) => isSignal(orient)
    ? ifYEnc(orient.signal, a, b)
    : isX(orient) ? b : a;

  const ifTop = (orient, a, b) => isSignal(orient)
    ? ifTopExpr(orient.signal, a, b)
    : orient === Top ? {value: a} : {value: b};

  const ifRight = (orient, a, b) => isSignal(orient)
    ? ifRightExpr(orient.signal, a, b)
    : orient === Right ? {value: a} : {value: b};

  const ifXEnc = ($orient, a, b) => ifEnc(
    `${$orient} === '${Top}' || ${$orient} === '${Bottom}'`, a, b
  );

  const ifYEnc = ($orient, a, b) => ifEnc(
    `${$orient} !== '${Top}' && ${$orient} !== '${Bottom}'`, a, b
  );

  const ifLeftTopExpr = ($orient, a, b) => ifExpr(
    `${$orient} === '${Left}' || ${$orient} === '${Top}'`, a, b
  );

  const ifTopExpr = ($orient, a, b) => ifExpr(
    `${$orient} === '${Top}'`, a, b
  );

  const ifRightExpr = ($orient, a, b) => ifExpr(
    `${$orient} === '${Right}'`, a, b
  );

  const ifEnc = (test, a, b) => {
    // ensure inputs are encoder objects (or null)
    a = a != null ? encoder(a) : a;
    b = b != null ? encoder(b) : b;

    if (isSimple(a) && isSimple(b)) {
      // if possible generate simple signal expression
      a = a ? (a.signal || vegaUtil.stringValue(a.value)) : null;
      b = b ? (b.signal || vegaUtil.stringValue(b.value)) : null;
      return {signal: `${test} ? (${a}) : (${b})`};
    } else {
      // otherwise generate rule set
      return [vegaUtil.extend({test}, a)].concat(b || []);
    }
  };

  const isSimple = enc => (
    enc == null || Object.keys(enc).length === 1
  );

  const ifExpr = (test, a, b) => ({
    signal: `${test} ? (${toExpr(a)}) : (${toExpr(b)})`
  });

  const ifOrient = ($orient, t, b, l, r) => ({
    signal: (l != null ? `${$orient} === '${Left}' ? (${toExpr(l)}) : ` : '')
          + (b != null ? `${$orient} === '${Bottom}' ? (${toExpr(b)}) : ` : '')
          + (r != null ? `${$orient} === '${Right}' ? (${toExpr(r)}) : ` : '')
          + (t != null ? `${$orient} === '${Top}' ? (${toExpr(t)}) : ` : '')
          + '(null)'
  });

  const toExpr = v => isSignal(v)
    ? v.signal
    : v == null ? null : vegaUtil.stringValue(v);

  const mult = (sign, value) => value === 0 ? 0 : isSignal(sign)
    ? {signal: `(${sign.signal}) * ${value}`}
    : {value: sign * value};

  const patch = (value, base) => {
    const s = value.signal;
    return s && s.endsWith('(null)')
      ? {signal: s.slice(0, -6) + base.signal}
      : value;
  };

  function fallback(prop, config, axisConfig, style) {
    let styleProp;

    if (config && vegaUtil.hasOwnProperty(config, prop)) {
      return config[prop];
    }
    else if (vegaUtil.hasOwnProperty(axisConfig, prop)) {
      return axisConfig[prop];
    }
    else if (prop.startsWith('title')) {
      switch (prop) {
        case 'titleColor':
          styleProp = 'fill';
          break;
        case 'titleFont':
        case 'titleFontSize':
        case 'titleFontWeight':
          styleProp = prop[5].toLowerCase() + prop.slice(6);
      }
      return style[GuideTitleStyle][styleProp];
    }
    else if (prop.startsWith('label')) {
      switch (prop) {
        case 'labelColor':
          styleProp = 'fill';
          break;
        case 'labelFont':
        case 'labelFontSize':
          styleProp = prop[5].toLowerCase() + prop.slice(6);
      }
      return style[GuideLabelStyle][styleProp];
    }

    return null;
  }

  function keys(objects) {
    const map = {};
    for (const obj of objects) {
      if (!obj) continue;
      for (const key in obj) map[key] = 1;
    }
    return Object.keys(map);
  }

  function axisConfig(spec, scope) {
    var config = scope.config,
        style = config.style,
        axis = config.axis,
        band = scope.scaleType(spec.scale) === 'band' && config.axisBand,
        orient = spec.orient,
        xy, or, key;

    if (isSignal(orient)) {
      const xyKeys = keys([
              config.axisX, config.axisY
            ]),
            orientKeys = keys([
              config.axisTop, config.axisBottom,
              config.axisLeft, config.axisRight
            ]);

      xy = {};
      for (key of xyKeys) {
        xy[key] = ifX(
          orient,
          fallback(key, config.axisX, axis, style),
          fallback(key, config.axisY, axis, style)
        );
      }

      or = {};
      for (key of orientKeys) {
        or[key] = ifOrient(
          orient.signal,
          fallback(key, config.axisTop, axis, style),
          fallback(key, config.axisBottom, axis, style),
          fallback(key, config.axisLeft, axis, style),
          fallback(key, config.axisRight, axis, style)
        );
      }
    } else {
      xy = (orient === Top || orient === Bottom) ? config.axisX : config.axisY;
      or = config['axis' + orient[0].toUpperCase() + orient.slice(1)];
    }

    var result = (xy || or || band)
      ? vegaUtil.extend({}, axis, xy, or, band)
      : axis;

    return result;
  }

  function axisDomain(spec, config, userEncode, dataRef) {
    var _ = lookup(spec, config),
        orient = spec.orient,
        encode, enter, update;

    encode = {
      enter: enter = {opacity: zero},
      update: update = {opacity: one},
      exit: {opacity: zero}
    };

    addEncoders(encode, {
      stroke:           _('domainColor'),
      strokeCap:        _('domainCap'),
      strokeDash:       _('domainDash'),
      strokeDashOffset: _('domainDashOffset'),
      strokeWidth:      _('domainWidth'),
      strokeOpacity:    _('domainOpacity')
    });

    const pos0 = position(spec, 0);
    const pos1 = position(spec, 1);

    enter.x = update.x = ifX(orient, pos0, zero);
    enter.x2 = update.x2 = ifX(orient, pos1);

    enter.y = update.y = ifY(orient, pos0, zero);
    enter.y2 = update.y2 = ifY(orient, pos1);

    return guideMark({
      type: RuleMark,
      role: AxisDomainRole,
      from: dataRef,
      encode
    }, userEncode);
  }

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

  function axisGrid(spec, config, userEncode, dataRef, band) {
    var _ = lookup(spec, config),
        orient = spec.orient,
        vscale = spec.gridScale,
        sign = getSign(orient, 1, -1),
        offset = offsetValue(spec.offset, sign),
        encode, enter, exit, update,
        tickPos, gridStart, gridEnd, sz;

    encode = {
      enter: enter = {opacity: zero},
      update: update = {opacity: one},
      exit: exit = {opacity: zero}
    };

    addEncoders(encode, {
      stroke:           _('gridColor'),
      strokeCap:        _('gridCap'),
      strokeDash:       _('gridDash'),
      strokeDashOffset: _('gridDashOffset'),
      strokeOpacity:    _('gridOpacity'),
      strokeWidth:      _('gridWidth')
    });

    tickPos = {
      scale:  spec.scale,
      field:  Value,
      band:   band.band,
      extra:  band.extra,
      offset: band.offset,
      round:  _('tickRound')
    };

    sz = ifX(orient, {signal: 'height'}, {signal: 'width'});

    gridStart = vscale
      ? {scale: vscale, range: 0, mult: sign, offset: offset}
      : {value: 0, offset: offset};

    gridEnd = vscale
      ? {scale: vscale, range: 1, mult: sign, offset: offset}
      : vegaUtil.extend(sz, {mult: sign, offset: offset});

    enter.x = update.x = ifX(orient, tickPos, gridStart);
    enter.y = update.y = ifY(orient, tickPos, gridStart);
    enter.x2 = update.x2 = ifY(orient, gridEnd);
    enter.y2 = update.y2 = ifX(orient, gridEnd);
    exit.x = ifX(orient, tickPos);
    exit.y = ifY(orient, tickPos);

    return guideMark({
      type: RuleMark,
      role: AxisGridRole,
      key:  Value,
      from: dataRef,
      encode
     }, userEncode);
  }

  function offsetValue(offset, sign)  {
    if (sign === 1) ; else if (!vegaUtil.isObject(offset)) {
      offset = isSignal(sign)
        ? {signal: `(${sign.signal}) * (${offset || 0})`}
        : sign * (offset || 0);
    } else {
      let entry = offset = vegaUtil.extend({}, offset);
      while (entry.mult != null) {
        if (!vegaUtil.isObject(entry.mult)) {
          entry.mult = isSignal(sign) // no offset if sign === 1
            ? {signal: `(${entry.mult}) * (${sign.signal})`}
            : entry.mult * sign;
          return offset;
        } else {
          entry = entry.mult = vegaUtil.extend({}, entry.mult);
        }
      }
      entry.mult = sign;
    }

    return offset;
  }

  function axisTicks(spec, config, userEncode, dataRef, size, band) {
    var _ = lookup(spec, config),
        orient = spec.orient,
        sign = getSign(orient, -1, 1),
        encode, enter, exit, update, tickSize, tickPos;

    encode = {
      enter: enter = {opacity: zero},
      update: update = {opacity: one},
      exit: exit = {opacity: zero}
    };

    addEncoders(encode, {
      stroke:           _('tickColor'),
      strokeCap:        _('tickCap'),
      strokeDash:       _('tickDash'),
      strokeDashOffset: _('tickDashOffset'),
      strokeOpacity:    _('tickOpacity'),
      strokeWidth:      _('tickWidth')
    });

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

    tickPos = {
      scale:  spec.scale,
      field:  Value,
      band:   band.band,
      extra:  band.extra,
      offset: band.offset,
      round:  _('tickRound')
    };

    update.y = enter.y = ifX(orient, zero, tickPos);
    update.y2 = enter.y2 = ifX(orient, tickSize);
    exit.x = ifX(orient, tickPos);

    update.x = enter.x = ifY(orient, zero, tickPos);
    update.x2 = enter.x2 = ifY(orient, tickSize);
    exit.y = ifY(orient, tickPos);

    return guideMark({
      type: RuleMark,
      role: AxisTickRole,
      key:  Value,
      from: dataRef,
      encode
    }, userEncode);
  }

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

  function axisLabels(spec, config, userEncode, dataRef, size, band) {
    var _ = lookup(spec, config),
        orient = spec.orient,
        scale = spec.scale,
        sign = getSign(orient, -1, 1),
        flush = deref(_('labelFlush')),
        flushOffset = deref(_('labelFlushOffset')),
        flushOn = flush === 0 || !!flush,
        labelAlign = _('labelAlign'),
        labelBaseline = _('labelBaseline'),
        encode, enter, update, tickSize, tickPos,
        align, baseline, bound, overlap, offsetExpr;

    tickSize = encoder(size);
    tickSize.mult = sign;
    tickSize.offset = encoder(_('labelPadding') || 0);
    tickSize.offset.mult = sign;

    tickPos = {
      scale:  scale,
      field:  Value,
      band:   0.5,
      offset: extendOffset(band.offset, _('labelOffset'))
    };

    align = ifX(orient,
      flushOn
        ? flushExpr(scale, flush, '"left"', '"right"', '"center"')
        : {value: 'center'},
      ifRight(orient, 'left', 'right')
    );

    baseline = ifX(orient,
      ifTop(orient, 'bottom', 'top'),
      flushOn
        ? flushExpr(scale, flush, '"top"', '"bottom"', '"middle"')
        : {value: 'middle'}
    );

    offsetExpr = flushExpr(scale, flush, `-(${flushOffset})`, flushOffset, 0);
    flushOn = flushOn && flushOffset;

    enter = {
      opacity: zero,
      x: ifX(orient, tickPos, tickSize),
      y: ifY(orient, tickPos, tickSize)
    };

    encode = {
      enter: enter,
      update: update = {
        opacity: one,
        text: {field: Label},
        x: enter.x,
        y: enter.y,
        align,
        baseline
      },
      exit: {
        opacity: zero,
        x: enter.x,
        y: enter.y
      }
    };

    addEncoders(encode, {
      dx: !labelAlign && flushOn ? ifX(orient, offsetExpr) : null,
      dy: !labelBaseline && flushOn ? ifY(orient, offsetExpr) : null
    });

    addEncoders(encode, {
      angle:       _('labelAngle'),
      fill:        _('labelColor'),
      fillOpacity: _('labelOpacity'),
      font:        _('labelFont'),
      fontSize:    _('labelFontSize'),
      fontWeight:  _('labelFontWeight'),
      fontStyle:   _('labelFontStyle'),
      limit:       _('labelLimit'),
      lineHeight:  _('labelLineHeight')
    }, {
      align:       labelAlign,
      baseline:    labelBaseline
    });

    bound   = _('labelBound');
    overlap = _('labelOverlap');

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

    if (update.align !== align) {
      update.align = patch(update.align, align);
    }
    if (update.baseline !== baseline) {
      update.baseline = patch(update.baseline, baseline);
    }

    return guideMark({
      type:  TextMark,
      role:  AxisLabelRole,
      style: GuideLabelStyle,
      key:   Value,
      from:  dataRef,
      encode,
      overlap
    }, userEncode);
  }

  function axisTitle(spec, config, userEncode, dataRef) {
    var _ = lookup(spec, config),
        orient = spec.orient,
        sign = getSign(orient, -1, 1),
        encode, enter, update, titlePos;

    encode = {
      enter: enter = {
        opacity: zero,
        anchor: encoder(_('titleAnchor', null)),
        align: {signal: alignExpr}
      },
      update: update = vegaUtil.extend({}, enter, {
        opacity: one,
        text: encoder(spec.title)
      }),
      exit: {
        opacity: zero
      }
    };

    titlePos = {
      signal: `lerp(range("${spec.scale}"), ${anchorExpr(0, 1, 0.5)})`
    };

    update.x = ifX(orient, titlePos);
    update.y = ifY(orient, titlePos);
    enter.angle = ifX(orient, zero, mult(sign, 90));
    enter.baseline = ifX(orient, ifTop(orient, Bottom, Top), {value: Bottom});
    update.angle = enter.angle;
    update.baseline = enter.baseline;

    addEncoders(encode, {
      fill:        _('titleColor'),
      fillOpacity: _('titleOpacity'),
      font:        _('titleFont'),
      fontSize:    _('titleFontSize'),
      fontStyle:   _('titleFontStyle'),
      fontWeight:  _('titleFontWeight'),
      limit:       _('titleLimit'),
      lineHeight:  _('titleLineHeight')
    }, { // require update
      align:       _('titleAlign'),
      angle:       _('titleAngle'),
      baseline:    _('titleBaseline')
    });

    autoLayout(_, orient, encode, userEncode);
    encode.update.align = patch(encode.update.align, enter.align);
    encode.update.angle = patch(encode.update.angle, enter.angle);
    encode.update.baseline = patch(encode.update.baseline, enter.baseline);

    return guideMark({
      type:  TextMark,
      role:  AxisTitleRole,
      style: GuideTitleStyle,
      from:  dataRef,
      encode
    }, userEncode);
  }

  function autoLayout(_, orient, encode, userEncode) {
    const auto = (value, dim) => value != null
      ? (encode.update[dim] = patch(encoder(value), encode.update[dim]), false)
      : !has(dim, userEncode) ? true : false;

    const autoY = auto(_('titleX'), 'x'),
          autoX = auto(_('titleY'), 'y');

    encode.enter.auto = autoX === autoY
      ? encoder(autoX)
      : ifX(orient, encoder(autoX), encoder(autoY));
  }

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

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

    // encoding properties for axis group item
    axisEncode = extendEncode(
      buildAxisEncode(_, spec), axisEncode, Skip
    );

    // data source for axis ticks
    ticksRef = ref(scope.add(AxisTicks({
      scale:   scope.scaleRef(spec.scale),
      extra:   scope.property(band.extra),
      count:   scope.objectProperty(spec.tickCount),
      values:  scope.objectProperty(spec.values),
      minstep: scope.property(spec.tickMinStep),
      formatType: scope.property(spec.formatType),
      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, band));
    }

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

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

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

    // parse axis specification
    return parseMark(
      guideGroup({
        role:        AxisRole,
        from:        dataRef,
        encode:      axisEncode,
        marks:       children,
        aria:        _('aria'),
        description: _('description'),
        zindex:      _('zindex'),
        name,
        interactive,
        style
      }),
      scope
    );
  }

  function buildAxisEncode(_, spec) {
    var encode = {enter: {}, update: {}};

    addEncoders(encode, {
      orient:       _('orient'),
      offset:       _('offset') || 0,
      position:     value(spec.position, 0),
      titlePadding: _('titlePadding'),
      minExtent:    _('minExtent'),
      maxExtent:    _('maxExtent'),
      range:        {signal: `abs(span(range("${spec.scale}")))`},
      translate:    _('translate'),

      // accessibility support
      format:       spec.format,
      formatType:   spec.formatType
    });

    return encode;
  }

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

    // parse signal definitions, if not already preprocessed
    if (!preprocessed) signals.forEach(_ => parseSignal(_, scope));

    // parse cartographic projection definitions
    vegaUtil.array(spec.projections).forEach(_ => parseProjection(_, scope));

    // initialize scale references
    scales.forEach(_ => initScale(_, scope));

    // parse data sources
    vegaUtil.array(spec.data).forEach(_ => parseData$1(_, scope));

    // parse scale definitions
    scales.forEach(_ => parseScale(_, scope));

    // parse signal updates
    (preprocessed || signals).forEach(_ => parseSignalUpdates(_, scope));

    // parse axis definitions
    vegaUtil.array(spec.axes).forEach(_ => parseAxis(_, scope));

    // parse mark definitions
    vegaUtil.array(spec.marks).forEach(_ => parseMark(_, scope));

    // parse legend definitions
    vegaUtil.array(spec.legends).forEach(_ => parseLegend(_, scope));

    // parse title, if defined
    if (spec.title) parseTitle(spec.title, scope);

    // parse collected lambda (anonymous) expressions
    scope.parseLambdas();

    return scope;
  }

  const rootEncode = spec => extendEncode(
    {
      enter: {
        x: {value: 0},
        y: {value: 0}
      },
      update: {
        width: {signal: 'width'},
        height: {signal: 'height'}
      }
    },
    spec
  );

  function parseView(spec, scope) {
    const config = scope.config;

    // add scenegraph root
    const root = ref(scope.root = scope.add(operator()));

    // parse top-level signal definitions
    const signals = collectSignals(spec, config);
    signals.forEach(_ => parseSignal(_, scope));

    // assign description, event, legend, and locale configuration
    scope.description = spec.description || config.description;
    scope.eventConfig = config.events;
    scope.legends = scope.objectProperty(config.legend && config.legend.layout);
    scope.locale = config.locale;

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

    // encode root group item
    const encode = scope.add(Encode(parseEncode(
      rootEncode(spec.encode), GroupMark, FrameRole,
      spec.style, scope, {pulse: ref(input)}
    )));

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

    // parse remainder of specification
    scope.pushState(ref(encode), ref(parent), null);
    parseScope(spec, scope, signals);
    scope.operators.push(parent);

    // bound / render / sieve root item
    let 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 signalObject(name, value) {
    return value && value.signal
      ? { name, update: value.signal }
      : { name, value };
  }

  /**
   * Collect top-level signals, merging values as needed. Signals
   * defined in the config signals arrays are added only if that
   * signal is not explicitly defined in the specification.
   * Built-in signals (autosize, background, padding, width, height)
   * receive special treatment. They are initialized using the
   * top-level spec property, or, if undefined in the spec, using
   * the corresponding top-level config property. If this property
   * is a signal reference object, the signal expression maps to the
   * signal 'update' property. If the spec's top-level signal array
   * contains an entry that matches a built-in signal, that entry
   * will be merged with the built-in specification, potentially
   * overwriting existing 'value' or 'update' properties.
   */
  function collectSignals(spec, config) {
    const _ = name => value(spec[name], config[name]),
          signals = [
            signalObject('background', _('background')),
            signalObject('autosize', parseAutosize(_('autosize'))),
            signalObject('padding', parsePadding(_('padding'))),
            signalObject('width', _('width') || 0),
            signalObject('height', _('height') || 0)
          ],
          pre = signals.reduce((p, s) => (p[s.name] = s, p), {}),
          map = {};

    // add spec signal array
    vegaUtil.array(spec.signals).forEach(s => {
      if (vegaUtil.hasOwnProperty(pre, s.name)) {
        // merge if built-in signal
        s = vegaUtil.extend(pre[s.name], s);
      } else {
        // otherwise add to signal list
        signals.push(s);
      }
      map[s.name] = s;
    });

    // add config signal array
    vegaUtil.array(config.signals).forEach(s => {
      if (!vegaUtil.hasOwnProperty(map, s.name) && !vegaUtil.hasOwnProperty(pre, s.name)) {
        // add to signal list if not already defined
        signals.push(s);
      }
    });

    return signals;
  }

  function Scope$1(config, options) {
    this.config = config || {};
    this.options = options || {};

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

    this.streams = [];
    this.updates = [];
    this.operators = [];
    this.eventConfig = null;
    this.locale = 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.options = scope.options;
    this.legends = scope.legends;

    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.parse = function(spec) {
    return parseScope(spec, this);
  };

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

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

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

  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) { ref.$ref = op.id; });
      op.refs = null;
    }
    return op;
  };

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

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

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

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

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

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

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

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

    return this;
  };

  // ----

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

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

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

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

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

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

  // ----

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

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

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

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

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

    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),
        o = sort.order || Ascending;

    return o.signal
      ? ref(this.add(Compare({
          fields: a,
          orders: this.signalRef(o.signal)
        })))
      : compareRef(a, 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.hasOwnSignal = function(name) {
    return vegaUtil.hasOwnProperty(this.signals, name);
  };

  prototype$1.addSignal = function(name, value) {
    if (this.hasOwnSignal(name)) {
      vegaUtil.error('Duplicate signal name: ' + vegaUtil.stringValue(name));
    }
    var op = value instanceof Entry ? value : this.add(operator(value));
    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 (!vegaUtil.hasOwnProperty(this.lambdas, 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 = vegaFunctions.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) {
    var code = '[',
        i = 0,
        n = array.length,
        value;

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

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

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

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

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

  // ----

  prototype$1.addScaleProj = function(name, transform) {
    if (vegaUtil.hasOwnProperty(this.scales, 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 (vegaUtil.hasOwnProperty(this.data, 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 (vegaUtil.hasOwnProperty(this.data, name)) {
      vegaUtil.error('Duplicate data set name: ' + vegaUtil.stringValue(name));
    }
    return this.addData(name, DataScope.fromEntries(this, entries));
  };

  /**
   * 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() {
    const defaultFont = 'sans-serif',
          defaultSymbolSize = 30,
          defaultStrokeWidth = 2,
          defaultColor = '#4c78a8',
          black = '#000',
          gray = '#888',
          lightGray = '#ddd';

    return {
      // default visualization description
      description: 'Vega visualization',

      // 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
      },
      trail: {
        fill: defaultColor,
        size: defaultStrokeWidth
      },

      // 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'
        },
        // chart subtitle
        'group-subtitle': {
          fill: black,
          font: defaultFont,
          fontSize: 12
        },
        // 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 title
      title: {
        orient: 'top',
        anchor: 'middle',
        offset: 4,
        subtitlePadding: 3
      },

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

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

      // defaults for cartographic projection
      projection: {
        type: 'mercator'
      },

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

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

  function parse$1(spec, config, options) {
    if (!vegaUtil.isObject(spec)) {
      vegaUtil.error('Input Vega specification must be an object.');
    }

    config = vegaUtil.mergeConfig(defaults(), config, spec.config);
    return parseView(spec, new Scope$1(config, options)).toRuntime();
  }

  exports.AxisDomainRole = AxisDomainRole;
  exports.AxisGridRole = AxisGridRole;
  exports.AxisLabelRole = AxisLabelRole;
  exports.AxisRole = AxisRole;
  exports.AxisTickRole = AxisTickRole;
  exports.AxisTitleRole = AxisTitleRole;
  exports.DataScope = DataScope;
  exports.FrameRole = FrameRole;
  exports.LegendEntryRole = LegendEntryRole;
  exports.LegendLabelRole = LegendLabelRole;
  exports.LegendRole = LegendRole;
  exports.LegendSymbolRole = LegendSymbolRole;
  exports.LegendTitleRole = LegendTitleRole;
  exports.MarkRole = MarkRole;
  exports.Scope = Scope$1;
  exports.ScopeRole = ScopeRole;
  exports.config = defaults;
  exports.parse = parse$1;
  exports.signal = parseSignal;
  exports.signalUpdates = parseSignalUpdates;
  exports.stream = parseStream;

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

})));
