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

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

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

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

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

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

  const wrap = method => function(value, spec) {
    const locale = this.context.dataflow.locale();
    return locale[method](spec)(value);
  };

  const format = wrap('format');
  const timeFormat = wrap('timeFormat');
  const utcFormat = wrap('utcFormat');
  const timeParse = wrap('timeParse');
  const utcParse = wrap('utcParse');

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

  function time(month, day, specifier) {
    if (!Number.isInteger(month) || !Number.isInteger(day)) return '';
    dateObj.setYear(2000);
    dateObj.setMonth(month);
    dateObj.setDate(day);
    return timeFormat.call(this, dateObj, specifier);
  }

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

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

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

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

  const DataPrefix   = ':';
  const IndexPrefix  = '@';
  const ScalePrefix  = '%';
  const SignalPrefix = '$';

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

    const data = args[0].value,
          dataName = DataPrefix + data;

    if (!vegaUtil.hasOwnProperty(dataName, params)) {
      try {
        params[dataName] = scope.getData(data).tuplesRef();
      } catch (err) {
        // if data set does not exist, there's nothing to track
      }
    }
  }

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

    const data = args[0].value,
          field = args[1].value,
          indexName = IndexPrefix + field;

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

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

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

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

  function internalScaleFunctions(codegen, fnctx, visitors) {
    // add helper method to the 'this' expression function context
    fnctx.__bandwidth = s => s && s.bandwidth ? s.bandwidth() : 0;

    // register AST visitors for internal scale functions
    visitors._bandwidth = scaleVisitor;
    visitors._range = scaleVisitor;
    visitors._scale = scaleVisitor;

    // resolve scale reference directly to the signal hash argument
    const ref = arg => '_[' + (
      arg.type === vegaExpression.Literal
        ? vegaUtil.stringValue(ScalePrefix + arg.value)
        : vegaUtil.stringValue(ScalePrefix) + '+' + codegen(arg)
    ) + ']';

    // define and return internal scale function code generators
    // these internal functions are called by mark encoders
    return {
      _bandwidth: args => `this.__bandwidth(${ref(args[0])})`,
      _range: args => `${ref(args[0])}.range()`,
      _scale: args => `${ref(args[0])}(${codegen(args[1])})`
    };
  }

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

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

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

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

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

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

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

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

  // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
  function channel_luminance_value(channelValue) {
    const val = channelValue / 255;
    if (val <= 0.03928) {
      return val / 12.92;
    }
    return Math.pow((val + 0.055) / 1.055, 2.4);
  }

  function luminance(color) {
    const c = d3Color.rgb(color),
          r = channel_luminance_value(c.r),
          g = channel_luminance_value(c.g),
          b = channel_luminance_value(c.b);
    return 0.2126 * r + 0.7152 * g + 0.0722 * b;
  }

  // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
  function contrast(color1, color2) {
    const lum1 = luminance(color1),
          lum2 = luminance(color2),
          lumL = Math.max(lum1, lum2),
          lumD = Math.min(lum1, lum2);
    return (lumL + 0.05) / (lumD + 0.05);
  }

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

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

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

  function equalObject(a, b) {
    for (let key in a) {
      if (!equal(a[key], b[key])) return false;
    }
    return true;
  }

  function removePredicate(props) {
    return _ => equalObject(props, _);
  }

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

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

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

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

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

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

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

    return 1;
  }

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

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

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

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

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

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

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

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

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

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

    const gradient = vegaScenegraph.Gradient(p0, p1);

    let stops = scale.domain(),
        min = stops[0],
        max = vegaUtil.peek(stops),
        fraction = vegaUtil.identity;

    if (!(max - min)) {
      // expand scale if domain has zero span, fix #1479
      scale = (scale.interpolator
        ? vegaScale.scale('sequential')().interpolator(scale.interpolator())
        : vegaScale.scale('linear')().interpolate(scale.interpolate()).range(scale.range())
      ).domain([min=0, max=1]);
    } else {
      fraction = vegaScale.scaleFraction(scale, min, max);
    }

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

    stops.forEach(_ => gradient.stop(fraction(_), scale(_)));

    return gradient;
  }

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

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

  const datum = d => d.data;

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

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

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

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

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

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

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

  function intersect(b, opt, group) {
    if (!b) return [];

    const [u, v] = b,
          box = new vegaScenegraph.Bounds().set(u[0], u[1], v[0], v[1]),
          scene = group || this.context.dataflow.scenegraph().root;

    return vegaScenegraph.intersect(scene, box, filter(opt));
  }

  function filter(opt) {
    let p = null;

    if (opt) {
      const types = vegaUtil.array(opt.marktype),
            names = vegaUtil.array(opt.markname);
      p = _ => (!types.length || types.some(t => _.marktype === t))
            && (!names.length || names.some(s => _.name === s));
    }

    return p;
  }

  // Expression function context object
  const functionContext = {
    random: function() { return vegaStatistics.random(); }, // override default
    cumulativeNormal: vegaStatistics.cumulativeNormal,
    cumulativeLogNormal: vegaStatistics.cumulativeLogNormal,
    cumulativeUniform: vegaStatistics.cumulativeUniform,
    densityNormal: vegaStatistics.densityNormal,
    densityLogNormal: vegaStatistics.densityLogNormal,
    densityUniform: vegaStatistics.densityUniform,
    quantileNormal: vegaStatistics.quantileNormal,
    quantileLogNormal: vegaStatistics.quantileLogNormal,
    quantileUniform: vegaStatistics.quantileUniform,
    sampleNormal: vegaStatistics.sampleNormal,
    sampleLogNormal: vegaStatistics.sampleLogNormal,
    sampleUniform: vegaStatistics.sampleUniform,
    isArray: vegaUtil.isArray,
    isBoolean: vegaUtil.isBoolean,
    isDate: vegaUtil.isDate,
    isDefined: function(_) { return _ !== undefined; },
    isNumber: vegaUtil.isNumber,
    isObject: vegaUtil.isObject,
    isRegExp: vegaUtil.isRegExp,
    isString: vegaUtil.isString,
    isTuple: vegaDataflow.isTuple,
    isValid: function(_) { return _ != null && _ === _; },
    toBoolean: vegaUtil.toBoolean,
    toDate: vegaUtil.toDate,
    toNumber: vegaUtil.toNumber,
    toString: vegaUtil.toString,
    flush: vegaUtil.flush,
    lerp: vegaUtil.lerp,
    merge,
    pad: vegaUtil.pad,
    peek: vegaUtil.peek,
    span: vegaUtil.span,
    inrange: vegaUtil.inrange,
    truncate: vegaUtil.truncate,
    rgb: d3Color.rgb,
    lab: d3Color.lab,
    hcl: d3Color.hcl,
    hsl: d3Color.hsl,
    luminance,
    contrast,
    sequence: d3Array.range,
    format,
    utcFormat,
    utcParse,
    utcOffset: vegaTime.utcOffset,
    utcSequence: vegaTime.utcSequence,
    timeFormat,
    timeParse,
    timeOffset: vegaTime.timeOffset,
    timeSequence: vegaTime.timeSequence,
    timeUnitSpecifier: vegaTime.timeUnitSpecifier,
    monthFormat,
    monthAbbrevFormat,
    dayFormat,
    dayAbbrevFormat,
    quarter: vegaUtil.quarter,
    utcquarter: vegaUtil.utcquarter,
    week: vegaTime.week,
    utcweek: vegaTime.utcweek,
    dayofyear: vegaTime.dayofyear,
    utcdayofyear: vegaTime.utcdayofyear,
    warn,
    info,
    debug,
    extent: vegaUtil.extent,
    inScope,
    intersect,
    clampRange: vegaUtil.clampRange,
    pinchDistance,
    pinchAngle,
    screen,
    containerSize,
    windowSize,
    bandspace,
    setdata,
    pathShape,
    panLinear: vegaUtil.panLinear,
    panLog: vegaUtil.panLog,
    panPow: vegaUtil.panPow,
    panSymlog: vegaUtil.panSymlog,
    zoomLinear: vegaUtil.zoomLinear,
    zoomLog: vegaUtil.zoomLog,
    zoomPow: vegaUtil.zoomPow,
    zoomSymlog: vegaUtil.zoomSymlog,
    encode,
    modify
  };

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

  // Build expression function registry
  function buildFunctions(codegen) {
    const fn = vegaExpression.functions(codegen);
    eventFunctions.forEach(name => fn[name] = eventPrefix + name);
    for (let name in functionContext) { fn[name] = thisPrefix + name; }
    vegaUtil.extend(fn, internalScaleFunctions(codegen, functionContext, astVisitors));
    return fn;
  }

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

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

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

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

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

  // register Vega-Lite selection functions
  expressionFunction('vlSelectionTest', vegaSelections.selectionTest, vegaSelections.selectionVisitor);
  expressionFunction('vlSelectionResolve', vegaSelections.selectionResolve, vegaSelections.selectionVisitor);

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

  var codeGenerator = vegaExpression.codegen(codegenParams);

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

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

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

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

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

    // return generated expression code and dependencies
    return {
      $expr:   vegaUtil.extend({code: gen.code}, scope.options.ast ? {ast} : null),
      $fields: gen.fields,
      $params: params
    };
  }

  exports.DataPrefix = DataPrefix;
  exports.IndexPrefix = IndexPrefix;
  exports.ScalePrefix = ScalePrefix;
  exports.SignalPrefix = SignalPrefix;
  exports.bandspace = bandspace;
  exports.bandwidth = bandwidth;
  exports.codeGenerator = codeGenerator;
  exports.codegenParams = codegenParams;
  exports.containerSize = containerSize;
  exports.contrast = contrast;
  exports.copy = copy;
  exports.data = data;
  exports.dataVisitor = dataVisitor;
  exports.dayAbbrevFormat = dayAbbrevFormat;
  exports.dayFormat = dayFormat;
  exports.debug = debug;
  exports.domain = domain;
  exports.encode = encode;
  exports.expressionFunction = expressionFunction;
  exports.format = format;
  exports.functionContext = functionContext;
  exports.geoArea = geoArea;
  exports.geoBounds = geoBounds;
  exports.geoCentroid = geoCentroid;
  exports.geoShape = geoShape;
  exports.inScope = inScope;
  exports.indata = indata;
  exports.indataVisitor = indataVisitor;
  exports.info = info;
  exports.invert = invert;
  exports.luminance = luminance;
  exports.merge = merge;
  exports.modify = modify;
  exports.monthAbbrevFormat = monthAbbrevFormat;
  exports.monthFormat = monthFormat;
  exports.parseExpression = parser;
  exports.pathShape = pathShape;
  exports.pinchAngle = pinchAngle;
  exports.pinchDistance = pinchDistance;
  exports.range = range;
  exports.scale = scale;
  exports.scaleGradient = scaleGradient;
  exports.scaleVisitor = scaleVisitor;
  exports.screen = screen;
  exports.setdata = setdata;
  exports.timeFormat = timeFormat;
  exports.timeParse = timeParse;
  exports.treeAncestors = treeAncestors;
  exports.treePath = treePath;
  exports.utcFormat = utcFormat;
  exports.utcParse = utcParse;
  exports.warn = warn;
  exports.windowSize = windowSize;

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

})));
