(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vega-dataflow'), require('vega-scenegraph'), require('vega-util')) :
  typeof define === 'function' && define.amd ? define(['exports', 'vega-dataflow', 'vega-scenegraph', 'vega-util'], factory) :
  (factory((global.vega = global.vega || {}, global.vega.transforms = {}),global.vega,global.vega,global.vega));
}(this, (function (exports,vegaDataflow,vegaScenegraph,vegaUtil) { 'use strict';

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

  var TopLeft = 'top-left';
  var TopRight = 'top-right';
  var BottomLeft = 'bottom-left';
  var BottomRight = 'bottom-right';

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

  var Group = 'group';

  var AxisRole = 'axis';
  var TitleRole = 'title';
  var FrameRole = 'frame';
  var ScopeRole = 'scope';
  var LegendRole = 'legend';

  var RowHeader = 'row-header';
  var RowFooter = 'row-footer';
  var RowTitle  = 'row-title';
  var ColHeader = 'column-header';
  var ColFooter = 'column-footer';
  var ColTitle  = 'column-title';

  var Padding = 'padding';

  var Symbols = 'symbol';

  var Fit  = 'fit';
  var FitX = 'fit-x';
  var FitY = 'fit-y';
  var Pad  = 'pad';
  var None = 'none';

  var All = 'all';
  var Each = 'each';
  var Flush = 'flush';

  var Column = 'column';
  var Row = 'row';

  /**
   * Calculate bounding boxes for scenegraph items.
   * @constructor
   * @param {object} params - The parameters for this operator.
   * @param {object} params.mark - The scenegraph mark instance to bound.
   */
  function Bound(params) {
    vegaDataflow.Transform.call(this, null, params);
  }

  var prototype = vegaUtil.inherits(Bound, vegaDataflow.Transform);

  prototype.transform = function(_, pulse) {
    var view = pulse.dataflow,
        mark = _.mark,
        type = mark.marktype,
        entry = vegaScenegraph.Marks[type],
        bound = entry.bound,
        markBounds = mark.bounds, rebound;

    if (entry.nested) {
      // multi-item marks have a single bounds instance
      if (mark.items.length) view.dirty(mark.items[0]);
      markBounds = boundItem(mark, bound);
      mark.items.forEach(function(item) {
        item.bounds.clear().union(markBounds);
      });
    }

    else if (type === Group || _.modified()) {
      // operator parameters modified -> re-bound all items
      // updates group bounds in response to modified group content
      pulse.visit(pulse.MOD, function(item) { view.dirty(item); });
      markBounds.clear();
      mark.items.forEach(function(item) {
        markBounds.union(boundItem(item, bound));
      });

      // force reflow for legends to propagate any layout changes
      // suppress other types to prevent overall layout jumpiness
      if (mark.role === LegendRole) pulse.reflow();
    }

    else {
      // incrementally update bounds, re-bound mark as needed
      rebound = pulse.changed(pulse.REM);

      pulse.visit(pulse.ADD, function(item) {
        markBounds.union(boundItem(item, bound));
      });

      pulse.visit(pulse.MOD, function(item) {
        rebound = rebound || markBounds.alignsWith(item.bounds);
        view.dirty(item);
        markBounds.union(boundItem(item, bound));
      });

      if (rebound) {
        markBounds.clear();
        mark.items.forEach(function(item) { markBounds.union(item.bounds); });
      }
    }

    // ensure mark bounds do not exceed any clipping region
    vegaScenegraph.boundClip(mark);

    return pulse.modifies('bounds');
  };

  function boundItem(item, bound, opt) {
    return bound(item.bounds.clear(), item, opt);
  }

  var COUNTER_NAME = ':vega_identifier:';

  /**
   * Adds a unique identifier to all added tuples.
   * This transform creates a new signal that serves as an id counter.
   * As a result, the id counter is shared across all instances of this
   * transform, generating unique ids across multiple data streams. In
   * addition, this signal value can be included in a snapshot of the
   * dataflow state, enabling correct resumption of id allocation.
   * @constructor
   * @param {object} params - The parameters for this operator.
   * @param {string} params.as - The field name for the generated identifier.
   */
  function Identifier(params) {
    vegaDataflow.Transform.call(this, 0, params);
  }

  Identifier.Definition = {
    "type": "Identifier",
    "metadata": {"modifies": true},
    "params": [
      { "name": "as", "type": "string", "required": true }
    ]
  };

  var prototype$1 = vegaUtil.inherits(Identifier, vegaDataflow.Transform);

  prototype$1.transform = function(_, pulse) {
    var counter = getCounter(pulse.dataflow),
        id = counter.value,
        as = _.as;

    pulse.visit(pulse.ADD, function(t) {
      if (!t[as]) t[as] = ++id;
    });

    counter.set(this.value = id);
    return pulse;
  };

  function getCounter(view) {
    var counter = view._signals[COUNTER_NAME];
    if (!counter) {
      view._signals[COUNTER_NAME] = (counter = view.add(0));
    }
    return counter;
  }

  /**
   * Bind scenegraph items to a scenegraph mark instance.
   * @constructor
   * @param {object} params - The parameters for this operator.
   * @param {object} params.markdef - The mark definition for creating the mark.
   *   This is an object of legal scenegraph mark properties which *must* include
   *   the 'marktype' property.
   */
  function Mark(params) {
    vegaDataflow.Transform.call(this, null, params);
  }

  var prototype$2 = vegaUtil.inherits(Mark, vegaDataflow.Transform);

  prototype$2.transform = function(_, pulse) {
    var mark = this.value;

    // acquire mark on first invocation, bind context and group
    if (!mark) {
      mark = pulse.dataflow.scenegraph().mark(_.markdef, lookup(_), _.index);
      mark.group.context = _.context;
      if (!_.context.group) _.context.group = mark.group;
      mark.source = this;
      mark.clip = _.clip;
      mark.interactive = _.interactive;
      this.value = mark;
    }

    // initialize entering items
    var Init = mark.marktype === Group ? vegaScenegraph.GroupItem : vegaScenegraph.Item;
    pulse.visit(pulse.ADD, function(item) { Init.call(item, mark); });

    // update clipping and/or interactive status
    if (_.modified('clip') || _.modified('interactive')) {
      mark.clip = _.clip;
      mark.interactive = !!_.interactive;
      mark.zdirty = true; // force scenegraph re-eval
      pulse.reflow();
    }

    // bind items array to scenegraph mark
    mark.items = pulse.source;
    return pulse;
  };

  function lookup(_) {
    var g = _.groups, p = _.parent;
    return g && g.size === 1 ? g.get(Object.keys(g.object)[0])
      : g && p ? g.lookup(p)
      : null;
  }

  /**
   * Analyze items for overlap, changing opacity to hide items with
   * overlapping bounding boxes. This transform will preserve at least
   * two items (e.g., first and last) even if overlap persists.
   * @param {object} params - The parameters for this operator.
   * @param {function(*,*): number} [params.sort] - A comparator
   *   function for sorting items.
   * @param {object} [params.method] - The overlap removal method to apply.
   *   One of 'parity' (default, hide every other item until there is no
   *   more overlap) or 'greedy' (sequentially scan and hide and items that
   *   overlap with the last visible item).
   * @param {object} [params.boundScale] - A scale whose range should be used
   *   to bound the items. Items exceeding the bounds of the scale range
   *   will be treated as overlapping. If null or undefined, no bounds check
   *   will be applied.
   * @param {object} [params.boundOrient] - The orientation of the scale
   *   (top, bottom, left, or right) used to bound items. This parameter is
   *   ignored if boundScale is null or undefined.
   * @param {object} [params.boundTolerance] - The tolerance in pixels for
   *   bound inclusion testing (default 1). This specifies by how many pixels
   *   an item's bounds may exceed the scale range bounds and not be culled.
   * @constructor
   */
  function Overlap(params) {
    vegaDataflow.Transform.call(this, null, params);
  }

  var prototype$3 = vegaUtil.inherits(Overlap, vegaDataflow.Transform);

  var methods = {
    parity: function(items) {
      return items.filter(function(item, i) {
        return i % 2 ? (item.opacity = 0) : 1;
      });
    },
    greedy: function(items) {
      var a;
      return items.filter(function(b, i) {
        if (!i || !intersect(a.bounds, b.bounds)) {
          a = b;
          return 1;
        } else {
          return b.opacity = 0;
        }
      });
    }
  };

  // compute bounding box intersection
  // allow 1 pixel of overlap tolerance
  function intersect(a, b) {
    return !(
      a.x2 - 1 < b.x1 ||
      a.x1 + 1 > b.x2 ||
      a.y2 - 1 < b.y1 ||
      a.y1 + 1 > b.y2
    );
  }

  function hasOverlap(items) {
    for (var i=1, n=items.length, a=items[0].bounds, b; i<n; a=b, ++i) {
      if (intersect(a, b = items[i].bounds)) return true;
    }
  }

  function hasBounds(item) {
    var b = item.bounds;
    return b.width() > 1 && b.height() > 1;
  }

  function boundTest(scale, orient, tolerance) {
    var range = scale.range(),
        b = new vegaScenegraph.Bounds();

    if (orient === Top || orient === Bottom) {
      b.set(range[0], -Infinity, range[1], +Infinity);
    } else {
      b.set(-Infinity, range[0], +Infinity, range[1]);
    }
    b.expand(tolerance || 1);

    return function(item) {
      return b.encloses(item.bounds);
    };
  }

  // reset all items to be fully opaque
  function reset(source) {
    source.forEach(function(item) { item.opacity = 1; });
    return source;
  }

  // add all tuples to mod, fork pulse if parameters were modified
  // fork prevents cross-stream tuple pollution (e.g., pulse from scale)
  function reflow(pulse, _) {
    return pulse.reflow(_.modified()).modifies('opacity');
  }

  prototype$3.transform = function(_, pulse) {
    var reduce = methods[_.method] || methods.parity,
        source = pulse.materialize(pulse.SOURCE).source,
        items, test;

    if (!source) return;

    if (!_.method) {
      // early exit if method is falsy
      if (_.modified('method')) {
        reset(source);
        pulse = reflow(pulse, _);
      }
      return pulse;
    }

    if (_.sort) {
      source = source.slice().sort(_.sort);
    }

    if (_.method === 'greedy') {
      source = source.filter(hasBounds);
    }

    items = reset(source);
    pulse = reflow(pulse, _);

    if (items.length >= 3 && hasOverlap(items)) {
      do {
        items = reduce(items);
      } while (items.length >= 3 && hasOverlap(items));

      if (items.length < 3 && !vegaUtil.peek(source).opacity) {
        if (items.length > 1) vegaUtil.peek(items).opacity = 0;
        vegaUtil.peek(source).opacity = 1;
      }
    }

    if (_.boundScale && _.boundTolerance >= 0) {
      test = boundTest(_.boundScale, _.boundOrient, +_.boundTolerance);
      source.forEach(function(item) {
        if (!test(item)) item.opacity = 0;
      });
    }

    return pulse;
  };

  /**
   * Queue modified scenegraph items for rendering.
   * @constructor
   */
  function Render(params) {
    vegaDataflow.Transform.call(this, null, params);
  }

  var prototype$4 = vegaUtil.inherits(Render, vegaDataflow.Transform);

  prototype$4.transform = function(_, pulse) {
    var view = pulse.dataflow;

    pulse.visit(pulse.ALL, function(item) { view.dirty(item); });

    // set z-index dirty flag as needed
    if (pulse.fields && pulse.fields['zindex']) {
      var item = pulse.source && pulse.source[0];
      if (item) item.mark.zdirty = true;
    }
  };

  function extractGroups(group) {
    var groups = group.items,
        n = groups.length,
        i = 0, mark, items;

    var views = {
      marks:      [],
      rowheaders: [],
      rowfooters: [],
      colheaders: [],
      colfooters: [],
      rowtitle: null,
      coltitle: null
    };

    // layout axes, gather legends, collect bounds
    for (; i<n; ++i) {
      mark = groups[i];
      items = mark.items;
      if (mark.marktype === Group) {
        switch (mark.role) {
          case AxisRole:
          case LegendRole:
            break;
          case RowHeader: addAll(items, views.rowheaders); break;
          case RowFooter: addAll(items, views.rowfooters); break;
          case ColHeader: addAll(items, views.colheaders); break;
          case ColFooter: addAll(items, views.colfooters); break;
          case RowTitle:  views.rowtitle = items[0]; break;
          case ColTitle:  views.coltitle = items[0]; break;
          default:        addAll(items, views.marks);
        }
      }
    }

    return views;
  }

  function addAll(items, array) {
    for (var i=0, n=items.length; i<n; ++i) {
      array.push(items[i]);
    }
  }

  function bboxFlush(item) {
    return {x1: 0, y1: 0, x2: item.width || 0, y2: item.height || 0};
  }

  function bboxFull(item) {
    var b = item.bounds.clone();
    return b.empty()
      ? b.set(0, 0, 0, 0)
      : b.translate(-(item.x||0), -(item.y||0));
  }

  function boundFlush(item, field) {
    return field === 'x1' ? (item.x || 0)
      : field === 'y1' ? (item.y || 0)
      : field === 'x2' ? (item.x || 0) + (item.width || 0)
      : field === 'y2' ? (item.y || 0) + (item.height || 0)
      : undefined;
  }

  function boundFull(item, field) {
    return item.bounds[field];
  }

  function get(opt, key, d) {
    var v = vegaUtil.isObject(opt) ? opt[key] : opt;
    return v != null ? v : (d !== undefined ? d : 0);
  }

  function offsetValue(v) {
    return v < 0 ? Math.ceil(-v) : 0;
  }

  function gridLayout(view, group, opt) {
    var views = extractGroups(group, opt),
        groups = views.marks,
        flush = opt.bounds === Flush,
        bbox = flush ? bboxFlush : bboxFull,
        bounds = new vegaScenegraph.Bounds(0, 0, 0, 0),
        alignCol = get(opt.align, Column),
        alignRow = get(opt.align, Row),
        padCol = get(opt.padding, Column),
        padRow = get(opt.padding, Row),
        off = opt.offset,
        ncols = group.columns || opt.columns || groups.length,
        nrows = ncols < 0 ? 1 : Math.ceil(groups.length / ncols),
        cells = nrows * ncols,
        xOffset = [], xExtent = [], xMax = 0, xInit = 0,
        yOffset = [], yExtent = [], yMax = 0, yInit = 0,
        n = groups.length,
        m, i, c, r, b, g, px, py, x, y, band, offset;

    for (i=0; i<ncols; ++i) {
      xExtent[i] = 0;
    }
    for (i=0; i<nrows; ++i) {
      yExtent[i] = 0;
    }

    // determine offsets for each group
    for (i=0; i<n; ++i) {
      b = bbox(groups[i]);
      c = i % ncols;
      r = ~~(i / ncols);
      px = Math.ceil(bbox(groups[i]).x2);
      py = Math.ceil(bbox(groups[i]).y2);
      xMax = Math.max(xMax, px);
      yMax = Math.max(yMax, py);
      xExtent[c] = Math.max(xExtent[c], px);
      yExtent[r] = Math.max(yExtent[r], py);
      xOffset.push(padCol + offsetValue(b.x1));
      yOffset.push(padRow + offsetValue(b.y1));
      view.dirty(groups[i]);
    }

    // set initial alignment offsets
    for (i=0; i<n; ++i) {
      if (i % ncols === 0) xOffset[i] = xInit;
      if (i < ncols) yOffset[i] = yInit;
    }

    // enforce column alignment constraints
    if (alignCol === Each) {
      for (c=1; c<ncols; ++c) {
        for (offset=0, i=c; i<n; i += ncols) {
          if (offset < xOffset[i]) offset = xOffset[i];
        }
        for (i=c; i<n; i += ncols) {
          xOffset[i] = offset + xExtent[c-1];
        }
      }
    } else if (alignCol === All) {
      for (offset=0, i=0; i<n; ++i) {
        if (i % ncols && offset < xOffset[i]) offset = xOffset[i];
      }
      for (i=0; i<n; ++i) {
        if (i % ncols) xOffset[i] = offset + xMax;
      }
    } else {
      for (alignCol=false, c=1; c<ncols; ++c) {
        for (i=c; i<n; i += ncols) {
          xOffset[i] += xExtent[c-1];
        }
      }
    }

    // enforce row alignment constraints
    if (alignRow === Each) {
      for (r=1; r<nrows; ++r) {
        for (offset=0, i=r*ncols, m=i+ncols; i<m; ++i) {
          if (offset < yOffset[i]) offset = yOffset[i];
        }
        for (i=r*ncols; i<m; ++i) {
          yOffset[i] = offset + yExtent[r-1];
        }
      }
    } else if (alignRow === All) {
      for (offset=0, i=ncols; i<n; ++i) {
        if (offset < yOffset[i]) offset = yOffset[i];
      }
      for (i=ncols; i<n; ++i) {
        yOffset[i] = offset + yMax;
      }
    } else {
      for (alignRow=false, r=1; r<nrows; ++r) {
        for (i=r*ncols, m=i+ncols; i<m; ++i) {
          yOffset[i] += yExtent[r-1];
        }
      }
    }

    // perform horizontal grid layout
    for (x=0, i=0; i<n; ++i) {
      g = groups[i];
      px = g.x || 0;
      g.x = (x = xOffset[i] + (i % ncols ? x : 0));
      g.bounds.translate(x - px, 0);
    }

    // perform vertical grid layout
    for (c=0; c<ncols; ++c) {
      for (y=0, i=c; i<n; i += ncols) {
        g = groups[i];
        py = g.y || 0;
        g.y = (y += yOffset[i]);
        g.bounds.translate(0, y - py);
      }
    }

    // perform horizontal centering
    if (get(opt.center, Column) && nrows > 1 && alignCol) {
      for (i=0; i<n; ++i) {
        g = groups[i];
        b = alignCol === All ? xMax : xExtent[i % ncols];
        x = b - bbox(g).x2;
        if (x > 0) {
          g.x += (px = x / 2);
          g.bounds.translate(px, 0);
        }
      }
    }

    // perform vertical centering
    if (get(opt.center, Row) && ncols !== 1 && alignRow) {
      for (i=0; i<n; ++i) {
        g = groups[i];
        b = alignRow === All ? yMax : yExtent[~~(i / ncols)];
        y = b - bbox(g).y2;
        if (y > 0) {
          g.y += (py = y / 2);
          g.bounds.translate(0, py);
        }
      }
    }

    // update mark bounds, mark dirty
    for (i=0; i<n; ++i) groups[i].mark.bounds.clear();
    for (i=0; i<n; ++i) {
      g = groups[i];
      view.dirty(g);
      bounds.union(g.mark.bounds.union(g.bounds));
    }

    // -- layout grid headers and footers --

    // aggregation functions for grid margin determination
    function min(a, b) { return Math.floor(Math.min(a, b)); }
    function max(a, b) { return Math.ceil(Math.max(a, b)); }

    // bounding box calculation methods
    bbox = flush ? boundFlush : boundFull;

    // perform row header layout
    band = get(opt.headerBand, Row, null);
    x = layoutHeaders(view, views.rowheaders, groups, ncols, nrows, -get(off, 'rowHeader'),    min, 0, bbox, 'x1', 0, ncols, 1, band);

    // perform column header layout
    band = get(opt.headerBand, Column, null);
    y = layoutHeaders(view, views.colheaders, groups, ncols, ncols, -get(off, 'columnHeader'), min, 1, bbox, 'y1', 0, 1, ncols, band);

    // perform row footer layout
    band = get(opt.footerBand, Row, null);
    layoutHeaders(    view, views.rowfooters, groups, ncols, nrows,  get(off, 'rowFooter'),    max, 0, bbox, 'x2', ncols-1, ncols, 1, band);

    // perform column footer layout
    band = get(opt.footerBand, Column, null);
    layoutHeaders(    view, views.colfooters, groups, ncols, ncols,  get(off, 'columnFooter'), max, 1, bbox, 'y2', cells-ncols, 1, ncols, band);

    // perform row title layout
    if (views.rowtitle) {
      offset = x - get(off, 'rowTitle');
      band = get(opt.titleBand, Row, 0.5);
      layoutTitle(view, views.rowtitle, offset, 0, bounds, band);
    }

    // perform column title layout
    if (views.coltitle) {
      offset = y - get(off, 'columnTitle');
      band = get(opt.titleBand, Column, 0.5);
      layoutTitle(view, views.coltitle, offset, 1, bounds, band);
    }
  }

  function layoutHeaders(view, headers, groups, ncols, limit, offset, agg, isX, bound, bf, start, stride, back, band) {
    var n = groups.length,
        init = 0,
        edge = 0,
        i, j, k, m, b, h, g, x, y;

    // if no groups, early exit and return 0
    if (!n) return init;

    // compute margin
    for (i=start; i<n; i+=stride) {
      if (groups[i]) init = agg(init, bound(groups[i], bf));
    }

    // if no headers, return margin calculation
    if (!headers.length) return init;

    // check if number of headers exceeds number of rows or columns
    if (headers.length > limit) {
      view.warn('Grid headers exceed limit: ' + limit);
      headers = headers.slice(0, limit);
    }

    // apply offset
    init += offset;

    // clear mark bounds for all headers
    for (j=0, m=headers.length; j<m; ++j) {
      view.dirty(headers[j]);
      headers[j].mark.bounds.clear();
    }

    // layout each header
    for (i=start, j=0, m=headers.length; j<m; ++j, i+=stride) {
      h = headers[j];
      b = h.mark.bounds;

      // search for nearest group to align to
      // necessary if table has empty cells
      for (k=i; k >= 0 && (g = groups[k]) == null; k-=back);

      // assign coordinates and update bounds
      if (isX) {
        x = band == null ? g.x : Math.round(g.bounds.x1 + band * g.bounds.width());
        y = init;
      } else {
        x = init;
        y = band == null ? g.y : Math.round(g.bounds.y1 + band * g.bounds.height());
      }
      b.union(h.bounds.translate(x - (h.x || 0), y - (h.y || 0)));
      h.x = x;
      h.y = y;
      view.dirty(h);

      // update current edge of layout bounds
      edge = agg(edge, b[bf]);
    }

    return edge;
  }

  function layoutTitle(view, g, offset, isX, bounds, band) {
    if (!g) return;
    view.dirty(g);

    // compute title coordinates
    var x = offset, y = offset;
    isX
      ? (x = Math.round(bounds.x1 + band * bounds.width()))
      : (y = Math.round(bounds.y1 + band * bounds.height()));

    // assign coordinates and update bounds
    g.bounds.translate(x - (g.x || 0), y - (g.y || 0));
    g.mark.bounds.clear().union(g.bounds);
    g.x = x;
    g.y = y;

    // queue title for redraw
    view.dirty(g);
  }

  var AxisOffset = 0.5,
      tempBounds = new vegaScenegraph.Bounds();

  /**
   * Layout view elements such as axes and legends.
   * Also performs size adjustments.
   * @constructor
   * @param {object} params - The parameters for this operator.
   * @param {object} params.mark - Scenegraph mark of groups to layout.
   */
  function ViewLayout(params) {
    vegaDataflow.Transform.call(this, null, params);
  }

  var prototype$5 = vegaUtil.inherits(ViewLayout, vegaDataflow.Transform);

  prototype$5.transform = function(_, pulse) {
    // TODO incremental update, output?
    var view = pulse.dataflow;
    _.mark.items.forEach(function(group) {
      if (_.layout) gridLayout(view, group, _.layout);
      layoutGroup(view, group, _);
    });
    if (_.modified()) pulse.reflow();
    return pulse;
  };

  function layoutGroup(view, group, _) {
    var items = group.items,
        width = Math.max(0, group.width || 0),
        height = Math.max(0, group.height || 0),
        viewBounds = new vegaScenegraph.Bounds().set(0, 0, width, height),
        xBounds = viewBounds.clone(),
        yBounds = viewBounds.clone(),
        legends = [], title,
        mark, flow, b, i, n;

    // layout axes, gather legends, collect bounds
    for (i=0, n=items.length; i<n; ++i) {
      mark = items[i];
      switch (mark.role) {
        case AxisRole:
          b = isYAxis(mark) ? xBounds : yBounds;
          b.union(axisLayout(view, mark, width, height));
          break;
        case TitleRole:
          title = mark; break;
        case LegendRole:
          legends.push(mark); break;
        case FrameRole:
        case ScopeRole:
        case RowHeader:
        case RowFooter:
        case RowTitle:
        case ColHeader:
        case ColFooter:
        case ColTitle:
          xBounds.union(mark.bounds);
          yBounds.union(mark.bounds);
          break;
        default:
          viewBounds.union(mark.bounds);
      }
    }

    // layout legends, adjust viewBounds
    if (legends.length) {
      flow = {
        leftWidth: legendPreprocess(view, legends),
        margin: _.legendMargin || 8,
        left: 0, right: 0, top: 0, bottom: 0
      };

      for (i=0, n=legends.length; i<n; ++i) {
        b = legendLayout(view, legends[i], flow, xBounds, yBounds, width, height);
        if (_.autosize && _.autosize.type === Fit) {
          // For autosize fit, incorporate the orthogonal dimension only.
          // Legends that overrun the chart area will then be clipped;
          // otherwise the chart area gets reduced to nothing!
          var orient = legends[i].items[0].datum.orient;
          if (orient === Left || orient === Right) {
            viewBounds.add(b.x1, 0).add(b.x2, 0);
          } else if (orient === Top || orient === Bottom) {
            viewBounds.add(0, b.y1).add(0, b.y2);
          }
        } else {
          viewBounds.union(b);
        }
      }
    }

    // combine bounding boxes
    viewBounds.union(xBounds).union(yBounds);

    // layout title, adjust bounds
    if (title) {
      viewBounds.union(titleLayout(view, title, width, height, viewBounds));
    }

    // perform size adjustment
    viewSizeLayout(view, group, viewBounds, _);
  }

  function set(item, property, value) {
    if (item[property] === value) {
      return 0;
    } else {
      item[property] = value;
      return 1;
    }
  }

  function isYAxis(mark) {
    var orient = mark.items[0].datum.orient;
    return orient === Left || orient === Right;
  }

  function axisIndices(datum) {
    var index = +datum.grid;
    return [
      datum.ticks  ? index++ : -1, // ticks index
      datum.labels ? index++ : -1, // labels index
      index + (+datum.domain)      // title index
    ];
  }

  function axisLayout(view, axis, width, height) {
    var item = axis.items[0],
        datum = item.datum,
        orient = datum.orient,
        indices = axisIndices(datum),
        range = item.range,
        offset = item.offset,
        position = item.position,
        minExtent = item.minExtent,
        maxExtent = item.maxExtent,
        title = datum.title && item.items[indices[2]].items[0],
        titlePadding = item.titlePadding,
        bounds = item.bounds,
        x = 0, y = 0, i, s;

    tempBounds.clear().union(bounds);
    bounds.clear();
    if ((i=indices[0]) > -1) bounds.union(item.items[i].bounds);
    if ((i=indices[1]) > -1) bounds.union(item.items[i].bounds);

    // position axis group and title
    switch (orient) {
      case Top:
        x = position || 0;
        y = -offset;
        s = Math.max(minExtent, Math.min(maxExtent, -bounds.y1));
        if (title) s = axisTitleLayout(title, s, titlePadding, 0, -1, bounds);
        bounds.add(0, -s).add(range, 0);
        break;
      case Left:
        x = -offset;
        y = position || 0;
        s = Math.max(minExtent, Math.min(maxExtent, -bounds.x1));
        if (title) s = axisTitleLayout(title, s, titlePadding, 1, -1, bounds);
        bounds.add(-s, 0).add(0, range);
        break;
      case Right:
        x = width + offset;
        y = position || 0;
        s = Math.max(minExtent, Math.min(maxExtent, bounds.x2));
        if (title) s = axisTitleLayout(title, s, titlePadding, 1, 1, bounds);
        bounds.add(0, 0).add(s, range);
        break;
      case Bottom:
        x = position || 0;
        y = height + offset;
        s = Math.max(minExtent, Math.min(maxExtent, bounds.y2));
        if (title) s = axisTitleLayout(title, s, titlePadding, 0, 1, bounds);
        bounds.add(0, 0).add(range, s);
        break;
      default:
        x = item.x;
        y = item.y;
    }

    // update bounds
    vegaScenegraph.boundStroke(bounds.translate(x, y), item);

    if (set(item, 'x', x + AxisOffset) | set(item, 'y', y + AxisOffset)) {
      item.bounds = tempBounds;
      view.dirty(item);
      item.bounds = bounds;
      view.dirty(item);
    }

    return item.mark.bounds.clear().union(bounds);
  }

  function axisTitleLayout(title, offset, pad, isYAxis, sign, bounds) {
    var b = title.bounds, dx = 0, dy = 0;

    if (title.auto) {
      offset += pad;

      isYAxis
        ? dx = (title.x || 0) - (title.x = sign * offset)
        : dy = (title.y || 0) - (title.y = sign * offset);

      b.translate(-dx, -dy);
      title.mark.bounds.set(b.x1, b.y1, b.x2, b.y2);

      if (isYAxis) {
        bounds.add(0, b.y1).add(0, b.y2);
        offset += b.width();
      } else {
        bounds.add(b.x1, 0).add(b.x2, 0);
        offset += b.height();
      }
    } else {
      bounds.union(b);
    }

    return offset;
  }

  function titleLayout(view, title, width, height, viewBounds) {
    var item = title.items[0],
        orient = item.orient,
        frame = item.frame,
        anchor = item.anchor,
        offset = item.offset,
        bounds = item.bounds,
        vertical = (orient === Left || orient === Right),
        start = 0,
        end = vertical ? height : width,
        x = 0, y = 0, pos;

    if (frame !== Group) {
      orient === Left ? (start = viewBounds.y2, end = viewBounds.y1)
        : orient === Right ? (start = viewBounds.y1, end = viewBounds.y2)
        : (start = viewBounds.x1, end = viewBounds.x2);
    } else if (orient === Left) {
      start = height, end = 0;
    }

    pos = (anchor === Start) ? start
      : (anchor === End) ? end
      : (start + end) / 2;

    tempBounds.clear().union(bounds);

    // position title text
    switch (orient) {
      case Top:
        x = pos;
        y = viewBounds.y1 - offset;
        break;
      case Left:
        x = viewBounds.x1 - offset;
        y = pos;
        break;
      case Right:
        x = viewBounds.x2 + offset;
        y = pos;
        break;
      case Bottom:
        x = pos;
        y = viewBounds.y2 + offset;
        break;
      default:
        x = item.x;
        y = item.y;
    }

    bounds.translate(x - item.x, y - item.y);
    if (set(item, 'x', x) | set(item, 'y', y)) {
      item.bounds = tempBounds;
      view.dirty(item);
      item.bounds = bounds;
      view.dirty(item);
    }

    // update bounds
    return title.bounds.clear().union(bounds);
  }

  function legendPreprocess(view, legends) {
    return legends.reduce(function(w, legend) {
      var item = legend.items[0];

      // adjust entry to accommodate padding and title
      legendGroupLayout(view, item, item.items[0].items[0]);

      if (item.datum.orient === Left) {
        var b = tempBounds.clear();
        item.items.forEach(function(_) { b.union(_.bounds); });
        w = Math.max(w, Math.ceil(b.width() + 2 * item.padding - 1));
      }

      return w;
    }, 0);
  }

  function legendGroupLayout(view, item, entry) {
    var x = item.padding - entry.x,
        y = item.padding - entry.y;

    if (item.datum.title) {
      var title = item.items[1].items[0];
      y += item.titlePadding + title.fontSize;
    }

    if (x || y) {
      entry.x += x;
      entry.y += y;
      entry.bounds.translate(x, y);
      entry.mark.bounds.translate(x, y);
      view.dirty(entry);
    }
  }

  function legendLayout(view, legend, flow, xBounds, yBounds, width, height) {
    var item = legend.items[0],
        datum = item.datum,
        orient = datum.orient,
        offset = item.offset,
        bounds = item.bounds,
        x = 0,
        y = 0,
        w, h, axisBounds;

    if (orient === Top || orient === Bottom) {
      axisBounds = yBounds,
      x = flow[orient];
    } else if (orient === Left || orient === Right) {
      axisBounds = xBounds;
      y = flow[orient];
    }

    tempBounds.clear().union(bounds);
    bounds.clear();

    // aggregate bounds to determine size
    // shave off 1 pixel because it looks better...
    item.items.forEach(function(_) { bounds.union(_.bounds); });
    w = 2 * item.padding - 1;
    h = 2 * item.padding - 1;
    if (!bounds.empty()) {
      w = Math.ceil(bounds.width() + w);
      h = Math.ceil(bounds.height() + h);
    }

    if (datum.type === Symbols) {
      legendEntryLayout(item.items[0].items[0].items[0].items);
    }

    switch (orient) {
      case Left:
        x -= flow.leftWidth + offset - Math.floor(axisBounds.x1);
        flow.left += h + flow.margin;
        break;
      case Right:
        x += offset + Math.ceil(axisBounds.x2);
        flow.right += h + flow.margin;
        break;
      case Top:
        y -= h + offset - Math.floor(axisBounds.y1);
        flow.top += w + flow.margin;
        break;
      case Bottom:
        y += offset + Math.ceil(axisBounds.y2);
        flow.bottom += w + flow.margin;
        break;
      case TopLeft:
        x += offset;
        y += offset;
        break;
      case TopRight:
        x += width - w - offset;
        y += offset;
        break;
      case BottomLeft:
        x += offset;
        y += height - h - offset;
        break;
      case BottomRight:
        x += width - w - offset;
        y += height - h - offset;
        break;
      default:
        x = item.x;
        y = item.y;
    }

    // update bounds
    vegaScenegraph.boundStroke(bounds.set(x, y, x + w, y + h), item);

    // update legend layout
    if (set(item, 'x', x) | set(item, 'width', w) |
        set(item, 'y', y) | set(item, 'height', h)) {
      item.bounds = tempBounds;
      view.dirty(item);
      item.bounds = bounds;
      view.dirty(item);
    }

    return item.mark.bounds.clear().union(bounds);
  }

  function legendEntryLayout(entries) {
    // get max widths for each column
    var widths = entries.reduce(function(w, g) {
      w[g.column] = Math.max(g.bounds.x2 - g.x, w[g.column] || 0);
      return w;
    }, {});

    // set dimensions of legend entry groups
    entries.forEach(function(g) {
      g.width  = widths[g.column];
      g.height = g.bounds.y2 - g.y;
    });
  }

  function viewSizeLayout(view, group, viewBounds, _) {
    var auto = _.autosize || {},
        type = auto.type,
        viewWidth = view._width,
        viewHeight = view._height,
        padding = view.padding();

    if (view._autosize < 1 || !type) return;

    var width  = Math.max(0, group.width || 0),
        left   = Math.max(0, Math.ceil(-viewBounds.x1)),
        right  = Math.max(0, Math.ceil(viewBounds.x2 - width)),
        height = Math.max(0, group.height || 0),
        top    = Math.max(0, Math.ceil(-viewBounds.y1)),
        bottom = Math.max(0, Math.ceil(viewBounds.y2 - height));

    if (auto.contains === Padding) {
      viewWidth -= padding.left + padding.right;
      viewHeight -= padding.top + padding.bottom;
    }

    if (type === None) {
      left = 0;
      top = 0;
      width = viewWidth;
      height = viewHeight;
    }

    else if (type === Fit) {
      width = Math.max(0, viewWidth - left - right);
      height = Math.max(0, viewHeight - top - bottom);
    }

    else if (type === FitX) {
      width = Math.max(0, viewWidth - left - right);
      viewHeight = height + top + bottom;
    }

    else if (type === FitY) {
      viewWidth = width + left + right;
      height = Math.max(0, viewHeight - top - bottom);
    }

    else if (type === Pad) {
      viewWidth = width + left + right;
      viewHeight = height + top + bottom;
    }

    view._resizeView(
      viewWidth, viewHeight,
      width, height,
      [left, top],
      auto.resize
    );
  }

  exports.bound = Bound;
  exports.identifier = Identifier;
  exports.mark = Mark;
  exports.overlap = Overlap;
  exports.render = Render;
  exports.viewlayout = ViewLayout;

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

})));
